|
1 """form renderers, responsible to layout a form to html |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2009 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. |
|
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses |
|
7 """ |
|
8 __docformat__ = "restructuredtext en" |
|
9 |
|
10 from logilab.common import dictattr |
|
11 from logilab.mtconverter import html_escape |
|
12 |
|
13 from simplejson import dumps |
|
14 |
|
15 from cubicweb.common import tags |
|
16 from cubicweb.appobject import AppRsetObject |
|
17 from cubicweb.selectors import entity_implements, yes |
|
18 from cubicweb.web import eid_param |
|
19 from cubicweb.web import formwidgets as fwdgs |
|
20 from cubicweb.web.widgets import checkbox |
|
21 from cubicweb.web.formfields import HiddenInitialValueField |
|
22 |
|
23 |
|
24 class FormRenderer(AppRsetObject): |
|
25 """basic renderer displaying fields in a two columns table label | value |
|
26 |
|
27 +--------------+--------------+ |
|
28 | field1 label | field1 input | |
|
29 +--------------+--------------+ |
|
30 | field1 label | field2 input | |
|
31 +--------------+--------------+ |
|
32 +---------+ |
|
33 | buttons | |
|
34 +---------+ |
|
35 """ |
|
36 __registry__ = 'formrenderers' |
|
37 id = 'default' |
|
38 |
|
39 _options = ('display_fields', 'display_label', 'display_help', |
|
40 'display_progress_div', 'table_class', 'button_bar_class', |
|
41 # add entity since it may be given to select the renderer |
|
42 'entity') |
|
43 display_fields = None # None -> all fields |
|
44 display_label = True |
|
45 display_help = True |
|
46 display_progress_div = True |
|
47 table_class = u'attributeForm' |
|
48 button_bar_class = u'formButtonBar' |
|
49 |
|
50 def __init__(self, req=None, rset=None, row=None, col=None, **kwargs): |
|
51 super(FormRenderer, self).__init__(req, rset, row, col) |
|
52 if self._set_options(kwargs): |
|
53 raise ValueError('unconsumed arguments %s' % kwargs) |
|
54 |
|
55 def _set_options(self, kwargs): |
|
56 for key in self._options: |
|
57 try: |
|
58 setattr(self, key, kwargs.pop(key)) |
|
59 except KeyError: |
|
60 continue |
|
61 return kwargs |
|
62 |
|
63 # renderer interface ###################################################### |
|
64 |
|
65 def render(self, form, values): |
|
66 self._set_options(values) |
|
67 form.add_media() |
|
68 data = [] |
|
69 w = data.append |
|
70 w(self.open_form(form, values)) |
|
71 if self.display_progress_div: |
|
72 w(u'<div id="progress">%s</div>' % form.req._('validating...')) |
|
73 w(u'<fieldset>') |
|
74 w(tags.input(type=u'hidden', name=u'__form_id', |
|
75 value=values.get('formvid', form.id))) |
|
76 if form.redirect_path: |
|
77 w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path)) |
|
78 self.render_fields(w, form, values) |
|
79 self.render_buttons(w, form) |
|
80 w(u'</fieldset>') |
|
81 w(u'</form>') |
|
82 errormsg = self.error_message(form) |
|
83 if errormsg: |
|
84 data.insert(0, errormsg) |
|
85 return '\n'.join(data) |
|
86 |
|
87 def render_label(self, form, field): |
|
88 label = form.req._(field.label) |
|
89 attrs = {'for': form.context[field]['id']} |
|
90 if field.required: |
|
91 attrs['class'] = 'required' |
|
92 return tags.label(label, **attrs) |
|
93 |
|
94 def render_help(self, form, field): |
|
95 help = [] |
|
96 descr = field.help |
|
97 if descr: |
|
98 help.append('<div class="helper">%s</div>' % form.req._(descr)) |
|
99 example = field.example_format(form.req) |
|
100 if example: |
|
101 help.append('<div class="helper">(%s: %s)</div>' |
|
102 % (form.req._('sample format'), example)) |
|
103 return u' '.join(help) |
|
104 |
|
105 # specific methods (mostly to ease overriding) ############################# |
|
106 |
|
107 def error_message(self, form): |
|
108 """return formatted error message |
|
109 |
|
110 This method should be called once inlined field errors has been consumed |
|
111 """ |
|
112 req = form.req |
|
113 errex = form.form_valerror |
|
114 # get extra errors |
|
115 if errex is not None: |
|
116 errormsg = req._('please correct the following errors:') |
|
117 displayed = form.form_displayed_errors |
|
118 errors = sorted((field, err) for field, err in errex.errors.items() |
|
119 if not field in displayed) |
|
120 if errors: |
|
121 if len(errors) > 1: |
|
122 templstr = '<li>%s</li>\n' |
|
123 else: |
|
124 templstr = ' %s\n' |
|
125 for field, err in errors: |
|
126 if field is None: |
|
127 errormsg += templstr % err |
|
128 else: |
|
129 errormsg += templstr % '%s: %s' % (req._(field), err) |
|
130 if len(errors) > 1: |
|
131 errormsg = '<ul>%s</ul>' % errormsg |
|
132 return u'<div class="errorMessage">%s</div>' % errormsg |
|
133 return u'' |
|
134 |
|
135 def open_form(self, form, values): |
|
136 if form.form_needs_multipart: |
|
137 enctype = 'multipart/form-data' |
|
138 else: |
|
139 enctype = 'application/x-www-form-urlencoded' |
|
140 if form.action is None: |
|
141 action = form.req.build_url('edit') |
|
142 else: |
|
143 action = form.action |
|
144 tag = ('<form action="%s" method="post" enctype="%s"' % ( |
|
145 html_escape(action or '#'), enctype)) |
|
146 if form.domid: |
|
147 tag += ' id="%s"' % form.domid |
|
148 if form.onsubmit: |
|
149 tag += ' onsubmit="%s"' % html_escape(form.onsubmit % dictattr(form)) |
|
150 if form.cssstyle: |
|
151 tag += ' style="%s"' % html_escape(form.cssstyle) |
|
152 if form.cssclass: |
|
153 tag += ' class="%s"' % html_escape(form.cssclass) |
|
154 if form.cwtarget: |
|
155 tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget) |
|
156 return tag + '>' |
|
157 |
|
158 def display_field(self, form, field): |
|
159 if isinstance(field, HiddenInitialValueField): |
|
160 field = field.visible_field |
|
161 return (self.display_fields is None |
|
162 or field.name in form.internal_fields |
|
163 or (field.name, field.role) in self.display_fields |
|
164 or (field.name, field.role) in form.internal_fields) |
|
165 |
|
166 def render_fields(self, w, form, values): |
|
167 form.form_build_context(values) |
|
168 fields = self._render_hidden_fields(w, form) |
|
169 if fields: |
|
170 self._render_fields(fields, w, form) |
|
171 self.render_child_forms(w, form, values) |
|
172 |
|
173 def render_child_forms(self, w, form, values): |
|
174 # render |
|
175 for childform in getattr(form, 'forms', []): |
|
176 self.render_fields(w, childform, values) |
|
177 |
|
178 def _render_hidden_fields(self, w, form): |
|
179 fields = form.fields[:] |
|
180 for field in form.fields: |
|
181 if not self.display_field(form, field): |
|
182 fields.remove(field) |
|
183 elif not field.is_visible(): |
|
184 w(field.render(form, self)) |
|
185 fields.remove(field) |
|
186 return fields |
|
187 |
|
188 def _render_fields(self, fields, w, form): |
|
189 w(u'<table class="%s">' % self.table_class) |
|
190 for field in fields: |
|
191 w(u'<tr>') |
|
192 if self.display_label: |
|
193 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field)) |
|
194 error = form.form_field_error(field) |
|
195 if error: |
|
196 w(u'<td class="error">') |
|
197 w(error) |
|
198 else: |
|
199 w(u'<td>') |
|
200 w(field.render(form, self)) |
|
201 if self.display_help: |
|
202 w(self.render_help(form, field)) |
|
203 w(u'</td></tr>') |
|
204 w(u'</table>') |
|
205 |
|
206 def render_buttons(self, w, form): |
|
207 w(u'<table class="%s">\n<tr>\n' % self.button_bar_class) |
|
208 for button in form.form_buttons: |
|
209 w(u'<td>%s</td>\n' % button.render(form)) |
|
210 w(u'</tr></table>') |
|
211 |
|
212 |
|
213 class HTableFormRenderer(FormRenderer): |
|
214 """display fields horizontally in a table |
|
215 |
|
216 +--------------+--------------+---------+ |
|
217 | field1 label | field2 label | | |
|
218 +--------------+--------------+---------+ |
|
219 | field1 input | field2 input | buttons |
|
220 +--------------+--------------+---------+ |
|
221 """ |
|
222 id = 'htable' |
|
223 |
|
224 display_help = False |
|
225 def _render_fields(self, fields, w, form): |
|
226 w(u'<table border="0">') |
|
227 w(u'<tr>') |
|
228 for field in fields: |
|
229 if self.display_label: |
|
230 w(u'<th class="labelCol">%s</th>' % self.render_label(form, field)) |
|
231 if self.display_help: |
|
232 w(self.render_help(form, field)) |
|
233 # empty slot for buttons |
|
234 w(u'<th class="labelCol"> </th>') |
|
235 w(u'</tr>') |
|
236 w(u'<tr>') |
|
237 for field in fields: |
|
238 error = form.form_field_error(field) |
|
239 if error: |
|
240 w(u'<td class="error">') |
|
241 w(error) |
|
242 else: |
|
243 w(u'<td>') |
|
244 w(field.render(form, self)) |
|
245 w(u'</td>') |
|
246 w(u'<td>') |
|
247 for button in form.form_buttons: |
|
248 w(button.render(form)) |
|
249 w(u'</td>') |
|
250 w(u'</tr>') |
|
251 w(u'</table>') |
|
252 |
|
253 def render_buttons(self, w, form): |
|
254 pass |
|
255 |
|
256 |
|
257 class EntityCompositeFormRenderer(FormRenderer): |
|
258 """specific renderer for multiple entities edition form (muledit)""" |
|
259 id = 'composite' |
|
260 |
|
261 def render_fields(self, w, form, values): |
|
262 if not form.is_subform: |
|
263 w(u'<table class="listing">') |
|
264 super(EntityCompositeFormRenderer, self).render_fields(w, form, values) |
|
265 if not form.is_subform: |
|
266 w(u'</table>') |
|
267 |
|
268 def _render_fields(self, fields, w, form): |
|
269 if form.is_subform: |
|
270 entity = form.edited_entity |
|
271 values = form.form_previous_values |
|
272 qeid = eid_param('eid', entity.eid) |
|
273 cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % html_escape(dumps(entity.eid)) |
|
274 w(u'<tr class="%s">' % (entity.row % 2 and u'even' or u'odd')) |
|
275 # XXX turn this into a widget used on the eid field |
|
276 w(u'<td>%s</td>' % checkbox('eid', entity.eid, checked=qeid in values)) |
|
277 for field in fields: |
|
278 error = form.form_field_error(field) |
|
279 if error: |
|
280 w(u'<td class="error">') |
|
281 w(error) |
|
282 else: |
|
283 w(u'<td>') |
|
284 if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, fwdgs.Radio)): |
|
285 field.widget.attrs['onchange'] = cbsetstate |
|
286 elif isinstance(field.widget, fwdgs.Input): |
|
287 field.widget.attrs['onkeypress'] = cbsetstate |
|
288 w(u'<div>%s</div>' % field.render(form, self)) |
|
289 w(u'</td>') |
|
290 else: |
|
291 # main form, display table headers |
|
292 w(u'<tr class="header">') |
|
293 w(u'<th align="left">%s</th>' |
|
294 % tags.input(type='checkbox', title=form.req._('toggle check boxes'), |
|
295 onclick="setCheckboxesState('eid', this.checked)")) |
|
296 for field in self.forms[0].fields: |
|
297 if self.display_field(form, field) and field.is_visible(): |
|
298 w(u'<th>%s</th>' % form.req._(field.label)) |
|
299 w(u'</tr>') |
|
300 |
|
301 class BaseFormRenderer(FormRenderer): |
|
302 """use form_renderer_id = 'base' if you don't want adaptation by selection |
|
303 """ |
|
304 id = 'base' |
|
305 |
|
306 class EntityFormRenderer(FormRenderer): |
|
307 """specific renderer for entity edition form (edition)""" |
|
308 __select__ = entity_implements('Any') & yes() |
|
309 |
|
310 _options = FormRenderer._options + ('display_relations_form',) |
|
311 display_relations_form = True |
|
312 |
|
313 def render(self, form, values): |
|
314 rendered = super(EntityFormRenderer, self).render(form, values) |
|
315 return rendered + u'</div>' # close extra div introducted by open_form |
|
316 |
|
317 def open_form(self, form, values): |
|
318 attrs_fs_label = ('<div class="iformTitle"><span>%s</span></div>' |
|
319 % form.req._('main informations')) |
|
320 attrs_fs_label += '<div class="formBody">' |
|
321 return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values) |
|
322 |
|
323 def render_fields(self, w, form, values): |
|
324 super(EntityFormRenderer, self).render_fields(w, form, values) |
|
325 self.inline_entities_form(w, form) |
|
326 if form.edited_entity.has_eid() and self.display_relations_form: |
|
327 self.relations_form(w, form) |
|
328 |
|
329 def _render_fields(self, fields, w, form): |
|
330 if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'): |
|
331 super(EntityFormRenderer, self)._render_fields(fields, w, form) |
|
332 |
|
333 def render_buttons(self, w, form): |
|
334 if len(form.form_buttons) == 3: |
|
335 w("""<table width="100%%"> |
|
336 <tbody> |
|
337 <tr><td align="center"> |
|
338 %s |
|
339 </td><td style="align: right; width: 50%%;"> |
|
340 %s |
|
341 %s |
|
342 </td></tr> |
|
343 </tbody> |
|
344 </table>""" % tuple(button.render(form) for button in form.form_buttons)) |
|
345 else: |
|
346 super(EntityFormRenderer, self).render_buttons(w, form) |
|
347 |
|
348 def relations_form(self, w, form): |
|
349 srels_by_cat = form.srelations_by_category('generic', 'add') |
|
350 if not srels_by_cat: |
|
351 return u'' |
|
352 req = form.req |
|
353 _ = req._ |
|
354 label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize() |
|
355 eid = form.edited_entity.eid |
|
356 w(u'<fieldset class="subentity">') |
|
357 w(u'<legend class="iformTitle">%s</legend>' % label) |
|
358 w(u'<table id="relatedEntities">') |
|
359 for rschema, target, related in form.relations_table(): |
|
360 # already linked entities |
|
361 if related: |
|
362 w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, target)) |
|
363 w(u'<td>') |
|
364 w(u'<ul>') |
|
365 for viewparams in related: |
|
366 w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>' |
|
367 % (viewparams[1], viewparams[0], viewparams[2], viewparams[3])) |
|
368 if not form.force_display and form.maxrelitems < len(related): |
|
369 link = (u'<span class="invisible">' |
|
370 '[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]' |
|
371 '</span>' % form.req._('view all')) |
|
372 w(u'<li class="invisible">%s</li>' % link) |
|
373 w(u'</ul>') |
|
374 w(u'</td>') |
|
375 w(u'</tr>') |
|
376 pendings = list(form.restore_pending_inserts()) |
|
377 if not pendings: |
|
378 w(u'<tr><th> </th><td> </td></tr>') |
|
379 else: |
|
380 for row in pendings: |
|
381 # soon to be linked to entities |
|
382 w(u'<tr id="tr%s">' % row[1]) |
|
383 w(u'<th>%s</th>' % row[3]) |
|
384 w(u'<td>') |
|
385 w(u'<a class="handle" title="%s" href="%s">[x]</a>' % |
|
386 (_('cancel this insert'), row[2])) |
|
387 w(u'<a id="a%s" class="editionPending" href="%s">%s</a>' |
|
388 % (row[1], row[4], html_escape(row[5]))) |
|
389 w(u'</td>') |
|
390 w(u'</tr>') |
|
391 w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid) |
|
392 w(u'<th class="labelCol">') |
|
393 w(u'<span>%s</span>' % _('add relation')) |
|
394 w(u'<select id="relationSelector_%s" tabindex="%s" ' |
|
395 'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">' |
|
396 % (eid, req.next_tabindex(), html_escape(dumps(eid)))) |
|
397 w(u'<option value="">%s</option>' % _('select a relation')) |
|
398 for i18nrtype, rschema, target in srels_by_cat: |
|
399 # more entities to link to |
|
400 w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype)) |
|
401 w(u'</select>') |
|
402 w(u'</th>') |
|
403 w(u'<td id="unrelatedDivs_%s"></td>' % eid) |
|
404 w(u'</tr>') |
|
405 w(u'</table>') |
|
406 w(u'</fieldset>') |
|
407 |
|
408 def inline_entities_form(self, w, form): |
|
409 """create a form to edit entity's inlined relations""" |
|
410 if not hasattr(form, 'inlined_relations'): |
|
411 return |
|
412 entity = form.edited_entity |
|
413 __ = form.req.__ |
|
414 for rschema, targettypes, role in form.inlined_relations(): |
|
415 # show inline forms only if there's one possible target type |
|
416 # for rschema |
|
417 if len(targettypes) != 1: |
|
418 self.warning('entity related by the %s relation should have ' |
|
419 'inlined form but there is multiple target types, ' |
|
420 'dunno what to do', rschema) |
|
421 continue |
|
422 targettype = targettypes[0].type |
|
423 if form.should_inline_relation_form(rschema, targettype, role): |
|
424 w(u'<div id="inline%sslot">' % rschema) |
|
425 existant = entity.has_eid() and entity.related(rschema) |
|
426 if existant: |
|
427 # display inline-edition view for all existing related entities |
|
428 w(form.view('inline-edition', existant, rtype=rschema, role=role, |
|
429 ptype=entity.e_schema, peid=entity.eid)) |
|
430 if role == 'subject': |
|
431 card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0] |
|
432 else: |
|
433 card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1] |
|
434 # there is no related entity and we need at least one: we need to |
|
435 # display one explicit inline-creation view |
|
436 if form.should_display_inline_creation_form(rschema, existant, card): |
|
437 w(form.view('inline-creation', None, etype=targettype, |
|
438 peid=entity.eid, ptype=entity.e_schema, |
|
439 rtype=rschema, role=role)) |
|
440 # we can create more than one related entity, we thus display a link |
|
441 # to add new related entities |
|
442 if form.should_display_add_new_relation_link(rschema, existant, card): |
|
443 divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid) |
|
444 w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">' |
|
445 % divid) |
|
446 js = "addInlineCreationForm('%s', '%s', '%s', '%s')" % ( |
|
447 entity.eid, targettype, rschema, role) |
|
448 if card in '1?': |
|
449 js = "toggleVisibility('%s'); %s" % (divid, js) |
|
450 w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>' |
|
451 % (rschema, entity.eid, js, __('add a %s' % targettype))) |
|
452 w(u'</div>') |
|
453 w(u'<div class="trame_grise"> </div>') |
|
454 w(u'</div>') |
|
455 |
|
456 |
|
457 class EntityInlinedFormRenderer(EntityFormRenderer): |
|
458 """specific renderer for entity inlined edition form |
|
459 (inline-[creation|edition]) |
|
460 """ |
|
461 id = 'inline' |
|
462 |
|
463 def render(self, form, values): |
|
464 form.add_media() |
|
465 data = [] |
|
466 w = data.append |
|
467 try: |
|
468 w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values) |
|
469 except KeyError: |
|
470 w(u'<div id="div-%(divid)s">' % values) |
|
471 else: |
|
472 w(u'<div id="notice-%s" class="notice">%s</div>' % ( |
|
473 values['divid'], form.req._('click on the box to cancel the deletion'))) |
|
474 w(u'<div class="iformBody">') |
|
475 values['removemsg'] = form.req.__('remove this %s' % form.edited_entity.e_schema) |
|
476 w(u'<div class="iformTitle"><span>%(title)s</span> ' |
|
477 '#<span class="icounter">1</span> ' |
|
478 '[<a href="javascript: %(removejs)s;noop();">%(removemsg)s</a>]</div>' |
|
479 % values) |
|
480 # cleanup values |
|
481 for key in ('title', 'removejs', 'removemsg'): |
|
482 values.pop(key) |
|
483 self.render_fields(w, form, values) |
|
484 w(u'</div></div>') |
|
485 return '\n'.join(data) |
|
486 |
|
487 def render_fields(self, w, form, values): |
|
488 form.form_build_context(values) |
|
489 w(u'<fieldset id="fs-%(divid)s">' % values) |
|
490 fields = self._render_hidden_fields(w, form) |
|
491 w(u'</fieldset>') |
|
492 w(u'<fieldset class="subentity">') |
|
493 if fields: |
|
494 self._render_fields(fields, w, form) |
|
495 self.render_child_forms(w, form, values) |
|
496 self.inline_entities_form(w, form) |
|
497 w(u'</fieldset>') |
|
498 |