author | sylvain.thenault@logilab.fr |
Tue, 14 Apr 2009 11:26:50 +0200 | |
branch | tls-sprint |
changeset 1340 | 12e46a39f3f2 |
parent 1319 | 2fe3df4e1e60 |
child 1362 | decb7507fa91 |
permissions | -rw-r--r-- |
1147 | 1 |
"""form renderers, responsible to layout a form to html |
2 |
||
3 |
:organization: Logilab |
|
4 |
:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
5 |
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 |
""" |
|
7 |
__docformat__ = "restructuredtext en" |
|
8 |
||
9 |
from logilab.common import dictattr |
|
10 |
from logilab.mtconverter import html_escape |
|
11 |
||
12 |
from simplejson import dumps |
|
13 |
||
14 |
from cubicweb.common import tags |
|
15 |
from cubicweb.web import eid_param |
|
16 |
from cubicweb.web import formwidgets as fwdgs |
|
17 |
from cubicweb.web.widgets import checkbox |
|
18 |
||
1319 | 19 |
|
1147 | 20 |
class FormRenderer(object): |
21 |
"""basic renderer displaying fields in a two columns table label | value |
|
22 |
""" |
|
23 |
button_bar_class = u'formButtonBar' |
|
24 |
||
25 |
def __init__(self, display_fields=None, display_label=True, |
|
26 |
display_help=True, button_bar_class=None): |
|
27 |
self.display_fields = display_fields # None -> all fields |
|
28 |
self.display_label = display_label |
|
29 |
self.display_help = display_help |
|
30 |
if button_bar_class is not None: |
|
31 |
self.button_bar_class = button_bar_class |
|
32 |
||
33 |
# renderer interface ###################################################### |
|
34 |
||
35 |
def render(self, form, values): |
|
36 |
form.add_media() |
|
37 |
data = [] |
|
38 |
w = data.append |
|
39 |
w(self.open_form(form, values)) |
|
40 |
w(u'<div id="progress">%s</div>' % form.req._('validating...')) |
|
41 |
w(u'<fieldset>') |
|
42 |
w(tags.input(type=u'hidden', name=u'__form_id', |
|
43 |
value=values.get('formvid', form.id))) |
|
44 |
if form.redirect_path: |
|
45 |
w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path)) |
|
46 |
self.render_fields(w, form, values) |
|
47 |
self.render_buttons(w, form) |
|
48 |
w(u'</fieldset>') |
|
49 |
w(u'</form>') |
|
50 |
errormsg = self.error_message(form) |
|
51 |
if errormsg: |
|
52 |
data.insert(0, errormsg) |
|
53 |
return '\n'.join(data) |
|
54 |
||
55 |
def render_label(self, form, field): |
|
56 |
label = form.req._(field.label) |
|
57 |
attrs = {'for': form.context[field]['id']} |
|
58 |
if field.required: |
|
59 |
attrs['class'] = 'required' |
|
60 |
return tags.label(label, **attrs) |
|
61 |
||
62 |
def render_help(self, form, field): |
|
63 |
help = [ u'<br/>' ] |
|
64 |
descr = field.help |
|
65 |
if descr: |
|
66 |
help.append('<span class="helper">%s</span>' % form.req._(descr)) |
|
67 |
example = field.example_format(form.req) |
|
68 |
if example: |
|
69 |
help.append('<span class="helper">(%s: %s)</span>' |
|
70 |
% (form.req._('sample format'), example)) |
|
71 |
return u' '.join(help) |
|
72 |
||
73 |
# specific methods (mostly to ease overriding) ############################# |
|
74 |
||
75 |
def error_message(self, form): |
|
76 |
"""return formatted error message |
|
77 |
||
78 |
This method should be called once inlined field errors has been consumed |
|
79 |
""" |
|
80 |
req = form.req |
|
81 |
errex = req.data.get('formerrors') |
|
82 |
# get extra errors |
|
83 |
if errex is not None: |
|
84 |
errormsg = req._('please correct the following errors:') |
|
85 |
displayed = req.data['displayederrors'] |
|
86 |
errors = sorted((field, err) for field, err in errex.errors.items() |
|
87 |
if not field in displayed) |
|
88 |
if errors: |
|
89 |
if len(errors) > 1: |
|
90 |
templstr = '<li>%s</li>\n' |
|
91 |
else: |
|
92 |
templstr = ' %s\n' |
|
93 |
for field, err in errors: |
|
94 |
if field is None: |
|
95 |
errormsg += templstr % err |
|
96 |
else: |
|
97 |
errormsg += templstr % '%s: %s' % (req._(field), err) |
|
98 |
if len(errors) > 1: |
|
99 |
errormsg = '<ul>%s</ul>' % errormsg |
|
100 |
return u'<div class="errorMessage">%s</div>' % errormsg |
|
101 |
return u'' |
|
102 |
||
103 |
def open_form(self, form, values): |
|
104 |
if form.form_needs_multipart: |
|
105 |
enctype = 'multipart/form-data' |
|
106 |
else: |
|
107 |
enctype = 'application/x-www-form-urlencoded' |
|
108 |
if form.action is None: |
|
109 |
action = form.req.build_url('edit') |
|
110 |
else: |
|
111 |
action = form.action |
|
1274
b730932a79b7
we don't want dom id systematically
sylvain.thenault@logilab.fr
parents:
1181
diff
changeset
|
112 |
tag = ('<form action="%s" method="post" enctype="%s"' % ( |
b730932a79b7
we don't want dom id systematically
sylvain.thenault@logilab.fr
parents:
1181
diff
changeset
|
113 |
html_escape(action or '#'), enctype)) |
b730932a79b7
we don't want dom id systematically
sylvain.thenault@logilab.fr
parents:
1181
diff
changeset
|
114 |
if form.domid: |
b730932a79b7
we don't want dom id systematically
sylvain.thenault@logilab.fr
parents:
1181
diff
changeset
|
115 |
tag += ' id="%s"' % form.domid |
1147 | 116 |
if form.onsubmit: |
117 |
tag += ' onsubmit="%s"' % html_escape(form.onsubmit % dictattr(form)) |
|
118 |
if form.cssstyle: |
|
119 |
tag += ' style="%s"' % html_escape(form.cssstyle) |
|
120 |
if form.cssclass: |
|
121 |
tag += ' class="%s"' % html_escape(form.cssclass) |
|
122 |
if form.cwtarget: |
|
123 |
tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget) |
|
124 |
return tag + '>' |
|
125 |
||
126 |
def display_field(self, form, field): |
|
127 |
return (self.display_fields is None |
|
128 |
or field.name in self.display_fields |
|
129 |
or field.name in form.internal_fields) |
|
130 |
||
131 |
def render_fields(self, w, form, values): |
|
132 |
form.form_build_context(values) |
|
133 |
fields = self._render_hidden_fields(w, form) |
|
134 |
if fields: |
|
135 |
self._render_fields(fields, w, form, values) |
|
136 |
self.render_child_forms(w, form, values) |
|
137 |
||
138 |
def render_child_forms(self, w, form, values): |
|
139 |
# render |
|
140 |
for childform in getattr(form, 'forms', []): |
|
141 |
self.render_fields(w, childform, values) |
|
142 |
||
143 |
def _render_hidden_fields(self, w, form): |
|
144 |
fields = form.fields[:] |
|
145 |
for field in form.fields: |
|
146 |
if not self.display_field(form, field): |
|
147 |
fields.remove(field) |
|
148 |
elif not field.is_visible(): |
|
149 |
w(field.render(form, self)) |
|
150 |
fields.remove(field) |
|
151 |
return fields |
|
152 |
||
153 |
def _render_fields(self, fields, w, form, values): |
|
1340
12e46a39f3f2
missing interpolation. Until proven we need an id on attribute form table, remove it
sylvain.thenault@logilab.fr
parents:
1319
diff
changeset
|
154 |
w(u'<table class="attributeForm" style="width:100%;">') |
1147 | 155 |
for field in fields: |
156 |
w(u'<tr>') |
|
157 |
if self.display_label: |
|
158 |
w(u'<th class="labelCol">%s</th>' % self.render_label(form, field)) |
|
159 |
error = form.form_field_error(field) |
|
160 |
if error: |
|
161 |
w(u'<td class="error" style="width:100%;">') |
|
162 |
w(error) |
|
163 |
else: |
|
164 |
w(u'<td style="width:100%;">') |
|
165 |
w(field.render(form, self)) |
|
166 |
if self.display_help: |
|
167 |
w(self.render_help(form, field)) |
|
168 |
w(u'</td></tr>') |
|
169 |
w(u'</table>') |
|
170 |
||
171 |
def render_buttons(self, w, form): |
|
172 |
w(u'<table class="%s">\n<tr>\n' % self.button_bar_class) |
|
1304 | 173 |
for button in form.form_buttons: |
174 |
w(u'<td>%s</td>\n' % button.render(form)) |
|
1147 | 175 |
w(u'</tr></table>') |
176 |
||
177 |
||
178 |
||
179 |
class EntityCompositeFormRenderer(FormRenderer): |
|
180 |
"""specific renderer for multiple entities edition form (muledit)""" |
|
181 |
def render_fields(self, w, form, values): |
|
182 |
if not form.is_subform: |
|
183 |
w(u'<table class="listing">') |
|
184 |
super(EntityCompositeFormRenderer, self).render_fields(w, form, values) |
|
185 |
if not form.is_subform: |
|
186 |
w(u'</table>') |
|
187 |
||
188 |
def _render_fields(self, fields, w, form, values): |
|
189 |
if form.is_subform: |
|
190 |
entity = form.edited_entity |
|
191 |
values = form.req.data.get('formvalues', ()) |
|
192 |
qeid = eid_param('eid', entity.eid) |
|
193 |
cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % html_escape(dumps(entity.eid)) |
|
194 |
w(u'<tr class="%s">' % (entity.row % 2 and u'even' or u'odd')) |
|
195 |
# XXX turn this into a widget used on the eid field |
|
196 |
w(u'<td>%s</td>' % checkbox('eid', entity.eid, checked=qeid in values)) |
|
197 |
for field in fields: |
|
198 |
error = form.form_field_error(field) |
|
199 |
if error: |
|
200 |
w(u'<td class="error">') |
|
201 |
w(error) |
|
202 |
else: |
|
203 |
w(u'<td>') |
|
204 |
if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, fwdgs.Radio)): |
|
205 |
field.widget.attrs['onchange'] = cbsetstate |
|
206 |
elif isinstance(field.widget, fwdgs.Input): |
|
207 |
field.widget.attrs['onkeypress'] = cbsetstate |
|
208 |
w(u'<div>%s</div>' % field.render(form, self)) |
|
209 |
w(u'/<td>') |
|
210 |
else: |
|
211 |
# main form, display table headers |
|
212 |
w(u'<tr class="header">') |
|
213 |
w(u'<th align="left">%s</th>' |
|
214 |
% tags.input(type='checkbox', title=form.req._('toggle check boxes'), |
|
215 |
onclick="setCheckboxesState('eid', this.checked)")) |
|
216 |
for field in self.forms[0].fields: |
|
217 |
if self.display_field(form, field) and field.is_visible(): |
|
218 |
w(u'<th>%s</th>' % form.req._(field.label)) |
|
219 |
w(u'</tr>') |
|
220 |
||
221 |
||
222 |
||
223 |
class EntityFormRenderer(FormRenderer): |
|
224 |
"""specific renderer for entity edition form (edition)""" |
|
225 |
||
1181
620ec8e6ae19
cleanup, various fix to get something working
sylvain.thenault@logilab.fr
parents:
1147
diff
changeset
|
226 |
def render(self, form, values): |
620ec8e6ae19
cleanup, various fix to get something working
sylvain.thenault@logilab.fr
parents:
1147
diff
changeset
|
227 |
rendered = super(EntityFormRenderer, self).render(form, values) |
620ec8e6ae19
cleanup, various fix to get something working
sylvain.thenault@logilab.fr
parents:
1147
diff
changeset
|
228 |
return rendered + u'</div>' # close extra div introducted by open_form |
620ec8e6ae19
cleanup, various fix to get something working
sylvain.thenault@logilab.fr
parents:
1147
diff
changeset
|
229 |
|
1147 | 230 |
def open_form(self, form, values): |
231 |
attrs_fs_label = ('<div class="iformTitle"><span>%s</span></div>' |
|
232 |
% form.req._('main informations')) |
|
233 |
attrs_fs_label += '<div class="formBody">' |
|
1181
620ec8e6ae19
cleanup, various fix to get something working
sylvain.thenault@logilab.fr
parents:
1147
diff
changeset
|
234 |
return attrs_fs_label + super(EntityFormRenderer, self).open_form(form, values) |
1147 | 235 |
|
236 |
def render_fields(self, w, form, values): |
|
237 |
super(EntityFormRenderer, self).render_fields(w, form, values) |
|
238 |
self.inline_entities_form(w, form) |
|
239 |
if form.edited_entity.has_eid(): |
|
240 |
self.relations_form(w, form) |
|
241 |
||
242 |
def _render_fields(self, fields, w, form, values): |
|
243 |
if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'): |
|
244 |
super(EntityFormRenderer, self)._render_fields(fields, w, form, values) |
|
245 |
||
246 |
def render_buttons(self, w, form): |
|
1304 | 247 |
if len(form.form_buttons) == 3: |
1147 | 248 |
w("""<table width="100%%"> |
249 |
<tbody> |
|
250 |
<tr><td align="center"> |
|
251 |
%s |
|
252 |
</td><td style="align: right; width: 50%%;"> |
|
253 |
%s |
|
254 |
%s |
|
255 |
</td></tr> |
|
256 |
</tbody> |
|
1304 | 257 |
</table>""" % tuple(button.render(form) for button in form.form_buttons)) |
1147 | 258 |
else: |
259 |
super(EntityFormRenderer, self).render_buttons(w, form) |
|
260 |
||
261 |
def relations_form(self, w, form): |
|
262 |
srels_by_cat = form.srelations_by_category(('generic', 'metadata'), 'add') |
|
263 |
if not srels_by_cat: |
|
264 |
return u'' |
|
265 |
req = form.req |
|
266 |
_ = req._ |
|
267 |
label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize() |
|
268 |
eid = form.edited_entity.eid |
|
269 |
w(u'<fieldset class="subentity">') |
|
270 |
w(u'<legend class="iformTitle">%s</legend>' % label) |
|
271 |
w(u'<table id="relatedEntities">') |
|
272 |
for rschema, target, related in form.relations_table(): |
|
273 |
# already linked entities |
|
274 |
if related: |
|
275 |
w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, target)) |
|
276 |
w(u'<td>') |
|
277 |
w(u'<ul>') |
|
278 |
for viewparams in related: |
|
279 |
w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>' |
|
280 |
% (viewparams[1], viewparams[0], viewparams[2], viewparams[3])) |
|
281 |
if not form.force_display and form.maxrelitems < len(related): |
|
282 |
link = (u'<span class="invisible">' |
|
283 |
'[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]' |
|
284 |
'</span>' % form.req._('view all')) |
|
285 |
w(u'<li class="invisible">%s</li>' % link) |
|
286 |
w(u'</ul>') |
|
287 |
w(u'</td>') |
|
288 |
w(u'</tr>') |
|
289 |
pendings = list(form.restore_pending_inserts()) |
|
290 |
if not pendings: |
|
291 |
w(u'<tr><th> </th><td> </td></tr>') |
|
292 |
else: |
|
293 |
for row in pendings: |
|
294 |
# soon to be linked to entities |
|
295 |
w(u'<tr id="tr%s">' % row[1]) |
|
296 |
w(u'<th>%s</th>' % row[3]) |
|
297 |
w(u'<td>') |
|
298 |
w(u'<a class="handle" title="%s" href="%s">[x]</a>' % |
|
299 |
(_('cancel this insert'), row[2])) |
|
300 |
w(u'<a id="a%s" class="editionPending" href="%s">%s</a>' |
|
301 |
% (row[1], row[4], html_escape(row[5]))) |
|
302 |
w(u'</td>') |
|
303 |
w(u'</tr>') |
|
304 |
w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid) |
|
305 |
w(u'<th class="labelCol">') |
|
306 |
w(u'<span>%s</span>' % _('add relation')) |
|
1305
395ef7f2b95b
cleanup, remove some unnecessary (sometime buggy) stuff
sylvain.thenault@logilab.fr
parents:
1304
diff
changeset
|
307 |
w(u'<select id="relationSelector_%s" tabindex="%s" ' |
395ef7f2b95b
cleanup, remove some unnecessary (sometime buggy) stuff
sylvain.thenault@logilab.fr
parents:
1304
diff
changeset
|
308 |
'onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">' |
1147 | 309 |
% (eid, req.next_tabindex(), html_escape(dumps(eid)))) |
310 |
w(u'<option value="">%s</option>' % _('select a relation')) |
|
311 |
for i18nrtype, rschema, target in srels_by_cat: |
|
312 |
# more entities to link to |
|
313 |
w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype)) |
|
314 |
w(u'</select>') |
|
315 |
w(u'</th>') |
|
316 |
w(u'<td id="unrelatedDivs_%s"></td>' % eid) |
|
317 |
w(u'</tr>') |
|
318 |
w(u'</table>') |
|
319 |
w(u'</fieldset>') |
|
320 |
||
321 |
def inline_entities_form(self, w, form): |
|
322 |
"""create a form to edit entity's inlined relations""" |
|
323 |
entity = form.edited_entity |
|
324 |
__ = form.req.__ |
|
1291
22b4d300d18d
inlineview is no more a 'category', needs specific handling
sylvain.thenault@logilab.fr
parents:
1274
diff
changeset
|
325 |
for rschema, targettypes, role in form.inlined_relations(): |
1147 | 326 |
# show inline forms only if there's one possible target type |
327 |
# for rschema |
|
328 |
if len(targettypes) != 1: |
|
329 |
self.warning('entity related by the %s relation should have ' |
|
330 |
'inlined form but there is multiple target types, ' |
|
331 |
'dunno what to do', rschema) |
|
332 |
continue |
|
333 |
targettype = targettypes[0].type |
|
334 |
if form.should_inline_relation_form(rschema, targettype, role): |
|
335 |
w(u'<div id="inline%sslot">' % rschema) |
|
336 |
existant = entity.has_eid() and entity.related(rschema) |
|
337 |
if existant: |
|
338 |
# display inline-edition view for all existing related entities |
|
1292
18a86192c9c4
inline_creation_form doesn't take ptype argument anymore
sylvain.thenault@logilab.fr
parents:
1291
diff
changeset
|
339 |
w(form.view('inline-edition', existant, rtype=rschema, role=role, |
18a86192c9c4
inline_creation_form doesn't take ptype argument anymore
sylvain.thenault@logilab.fr
parents:
1291
diff
changeset
|
340 |
ptype=entity.e_schema, peid=entity.eid)) |
1147 | 341 |
if role == 'subject': |
342 |
card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0] |
|
343 |
else: |
|
344 |
card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1] |
|
345 |
# there is no related entity and we need at least one: we need to |
|
346 |
# display one explicit inline-creation view |
|
347 |
if form.should_display_inline_creation_form(rschema, existant, card): |
|
1319 | 348 |
w(form.view('inline-creation', None, etype=targettype, |
1147 | 349 |
peid=entity.eid, ptype=entity.e_schema, |
1319 | 350 |
rtype=rschema, role=role)) |
1147 | 351 |
# we can create more than one related entity, we thus display a link |
352 |
# to add new related entities |
|
353 |
if form.should_display_add_new_relation_link(rschema, existant, card): |
|
354 |
divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid) |
|
355 |
w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">' |
|
356 |
% divid) |
|
1292
18a86192c9c4
inline_creation_form doesn't take ptype argument anymore
sylvain.thenault@logilab.fr
parents:
1291
diff
changeset
|
357 |
js = "addInlineCreationForm('%s', '%s', '%s', '%s')" % ( |
18a86192c9c4
inline_creation_form doesn't take ptype argument anymore
sylvain.thenault@logilab.fr
parents:
1291
diff
changeset
|
358 |
entity.eid, targettype, rschema, role) |
1147 | 359 |
if card in '1?': |
360 |
js = "toggleVisibility('%s'); %s" % (divid, js) |
|
361 |
w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>' |
|
362 |
% (rschema, entity.eid, js, __('add a %s' % targettype))) |
|
363 |
w(u'</div>') |
|
364 |
w(u'<div class="trame_grise"> </div>') |
|
365 |
w(u'</div>') |
|
366 |
||
367 |
||
368 |
class EntityInlinedFormRenderer(EntityFormRenderer): |
|
369 |
"""specific renderer for entity inlined edition form |
|
370 |
(inline-[creation|edition]) |
|
371 |
""" |
|
372 |
def render(self, form, values): |
|
373 |
form.add_media() |
|
374 |
data = [] |
|
375 |
w = data.append |
|
376 |
try: |
|
377 |
w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values) |
|
378 |
except KeyError: |
|
379 |
w(u'<div id="div-%(divid)s">' % values) |
|
380 |
else: |
|
381 |
w(u'<div id="notice-%s" class="notice">%s</div>' % ( |
|
382 |
values['divid'], form.req._('click on the box to cancel the deletion'))) |
|
383 |
w(u'<div class="iformBody">') |
|
384 |
values['removemsg'] = form.req.__('remove this %s' % form.edited_entity.e_schema) |
|
385 |
w(u'<div class="iformTitle"><span>%(title)s</span> ' |
|
386 |
'#<span class="icounter">1</span> ' |
|
387 |
'[<a href="javascript: %(removejs)s;noop();">%(removemsg)s</a>]</div>' |
|
388 |
% values) |
|
389 |
self.render_fields(w, form, values) |
|
1293 | 390 |
w(u'</div></div>') |
1147 | 391 |
return '\n'.join(data) |
392 |
||
393 |
def render_fields(self, w, form, values): |
|
394 |
form.form_build_context(values) |
|
395 |
w(u'<fieldset id="fs-%(divid)s">' % values) |
|
396 |
fields = self._render_hidden_fields(w, form) |
|
397 |
w(u'</fieldset>') |
|
398 |
w(u'<fieldset class="subentity">') |
|
399 |
if fields: |
|
400 |
self._render_fields(fields, w, form, values) |
|
401 |
self.render_child_forms(w, form, values) |
|
402 |
self.inline_entities_form(w, form) |
|
403 |
w(u'</fieldset>') |
|
404 |