|
1 """widgets for entity edition |
|
2 |
|
3 those are in cubicweb.common since we need to know available widgets at schema |
|
4 serialization time |
|
5 |
|
6 :organization: Logilab |
|
7 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
8 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
9 """ |
|
10 __docformat__ = "restructuredtext en" |
|
11 |
|
12 from simplejson import dumps |
|
13 from mx.DateTime import now, today |
|
14 |
|
15 from logilab.mtconverter import html_escape |
|
16 |
|
17 from yams.constraints import SizeConstraint, StaticVocabularyConstraint |
|
18 |
|
19 from cubicweb.common.uilib import toggle_action |
|
20 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param |
|
21 |
|
22 def _format_attrs(kwattrs): |
|
23 """kwattrs is the dictionary of the html attributes available for |
|
24 the edited element |
|
25 """ |
|
26 # sort for predictability (required for tests) |
|
27 return u' '.join(sorted(u'%s="%s"' % item for item in kwattrs.iteritems())) |
|
28 |
|
29 def _value_from_values(values): |
|
30 # take care, value may be 0, 0.0... |
|
31 if values: |
|
32 value = values[0] |
|
33 if value is None: |
|
34 value = u'' |
|
35 else: |
|
36 value = u'' |
|
37 return value |
|
38 |
|
39 def _eclass_eschema(eschema_or_eclass): |
|
40 try: |
|
41 return eschema_or_eclass, eschema_or_eclass.e_schema |
|
42 except AttributeError: |
|
43 return None, eschema_or_eclass |
|
44 |
|
45 def checkbox(name, value, attrs='', checked=None): |
|
46 if checked is None: |
|
47 checked = value |
|
48 checked = checked and 'checked="checked"' or '' |
|
49 return u'<input type="checkbox" name="%s" value="%s" %s %s />' % ( |
|
50 name, value, checked, attrs) |
|
51 |
|
52 def widget(vreg, subjschema, rschema, objschema, role='object'): |
|
53 """get a widget to edit the given relation""" |
|
54 if rschema == 'eid': |
|
55 # return HiddenWidget(vreg, subjschema, rschema, objschema) |
|
56 return EidWidget(vreg, _eclass_eschema(subjschema)[1], rschema, objschema) |
|
57 return widget_factory(vreg, subjschema, rschema, objschema, role=role) |
|
58 |
|
59 |
|
60 class Widget(object): |
|
61 """abstract widget class""" |
|
62 need_multipart = False |
|
63 # generate the "id" attribute with the same value as the "name" (html) attribute |
|
64 autoid = True |
|
65 html_attributes = set(('id', 'class', 'tabindex', 'accesskey', 'onchange', 'onkeypress')) |
|
66 cubicwebns_attributes = set() |
|
67 |
|
68 def __init__(self, vreg, subjschema, rschema, objschema, |
|
69 role='subject', description=None, |
|
70 **kwattrs): |
|
71 self.vreg = vreg |
|
72 self.rschema = rschema |
|
73 self.subjtype = subjschema |
|
74 self.objtype = objschema |
|
75 self.role = role |
|
76 self.name = rschema.type |
|
77 self.description = description |
|
78 self.attrs = kwattrs |
|
79 # XXX accesskey may not be unique |
|
80 kwattrs['accesskey'] = self.name[0] |
|
81 |
|
82 def copy(self): |
|
83 """shallow copy (useful when you need to modify self.attrs |
|
84 because widget instances are cached) |
|
85 """ |
|
86 # brute force copy (subclasses don't have the |
|
87 # same __init__ prototype) |
|
88 widget = self.__new__(self.__class__) |
|
89 widget.__dict__ = dict(self.__dict__) |
|
90 widget.attrs = dict(widget.attrs) |
|
91 return widget |
|
92 |
|
93 @staticmethod |
|
94 def size_constraint_attrs(attrs, maxsize): |
|
95 """set html attributes in the attrs dict to consider maxsize""" |
|
96 pass |
|
97 |
|
98 def format_attrs(self): |
|
99 """return a string with html attributes available for the edit input""" |
|
100 # sort for predictability (required for tests) |
|
101 attrs = [] |
|
102 for name, value in self.attrs.iteritems(): |
|
103 # namespace attributes have priority over standard xhtml ones |
|
104 if name in self.cubicwebns_attributes: |
|
105 attrs.append(u'cubicweb:%s="%s"' % (name, value)) |
|
106 elif name in self.html_attributes: |
|
107 attrs.append(u'%s="%s"' % (name, value)) |
|
108 return u' '.join(sorted(attrs)) |
|
109 |
|
110 def required(self, entity): |
|
111 """indicates if the widget needs a value to be filled in""" |
|
112 card = self.rschema.cardinality(self.subjtype, self.objtype, self.role) |
|
113 return card in '1+' |
|
114 |
|
115 def input_id(self, entity): |
|
116 try: |
|
117 return self.rname |
|
118 except AttributeError: |
|
119 return eid_param(self.name, entity.eid) |
|
120 |
|
121 def render_label(self, entity, label=None): |
|
122 """render widget's label""" |
|
123 label = label or self.rschema.display_name(entity.req, self.role) |
|
124 forid = self.input_id(entity) |
|
125 if forid: |
|
126 forattr = ' for="%s"' % forid |
|
127 else: |
|
128 forattr = '' |
|
129 if self.required(entity): |
|
130 label = u'<label class="required"%s>%s</label>' % (forattr, label) |
|
131 else: |
|
132 label = u'<label%s>%s</label>' % (forattr, label) |
|
133 return label |
|
134 |
|
135 def render_error(self, entity): |
|
136 """return validation error for widget's field of the given entity, if |
|
137 any |
|
138 """ |
|
139 errex = entity.req.data.get('formerrors') |
|
140 if errex and errex.eid == entity.eid and self.name in errex.errors: |
|
141 entity.req.data['displayederrors'].add(self.name) |
|
142 return u'<span class="error">%s</span>' % errex.errors[self.name] |
|
143 return u'' |
|
144 |
|
145 def render_help(self, entity): |
|
146 """render a help message about the (edited) field""" |
|
147 req = entity.req |
|
148 help = [u'<br/>'] |
|
149 descr = self.description or self.rschema.rproperty(self.subjtype, self.objtype, 'description') |
|
150 if descr: |
|
151 help.append(u'<span class="helper">%s</span>' % req._(descr)) |
|
152 example = self.render_example(req) |
|
153 if example: |
|
154 help.append(u'<span class="helper">(%s: %s)</span>' |
|
155 % (req._('sample format'), example)) |
|
156 return u' '.join(help) |
|
157 |
|
158 def render_example(self, req): |
|
159 return u'' |
|
160 |
|
161 def render(self, entity): |
|
162 """render the widget for a simple view""" |
|
163 if not entity.has_eid(): |
|
164 return u'' |
|
165 return entity.printable_value(self.name) |
|
166 |
|
167 def edit_render(self, entity, tabindex=None, |
|
168 includehelp=False, useid=None, **kwargs): |
|
169 """render the widget for edition""" |
|
170 # this is necessary to handle multiple edition |
|
171 self.rname = eid_param(self.name, entity.eid) |
|
172 if useid: |
|
173 self.attrs['id'] = useid |
|
174 elif self.autoid: |
|
175 self.attrs['id'] = self.rname |
|
176 if tabindex is not None: |
|
177 self.attrs['tabindex'] = tabindex |
|
178 else: |
|
179 self.attrs['tabindex'] = entity.req.next_tabindex() |
|
180 output = self._edit_render(entity, **kwargs) |
|
181 if includehelp: |
|
182 output += self.render_help(entity) |
|
183 return output |
|
184 |
|
185 def _edit_render(self, entity): |
|
186 """do the actual job to render the widget for edition""" |
|
187 raise NotImplementedError |
|
188 |
|
189 def current_values(self, entity): |
|
190 """return the value of the field associated to this widget on the given |
|
191 entity. always return a list of values, which'll have size equal to 1 |
|
192 if the field is monovalued (like all attribute fields, but not all non |
|
193 final relation fields |
|
194 """ |
|
195 if self.rschema.is_final(): |
|
196 return entity.attribute_values(self.name) |
|
197 elif entity.has_eid(): |
|
198 return [row[0] for row in entity.related(self.name, self.role)] |
|
199 return () |
|
200 |
|
201 def current_value(self, entity): |
|
202 return _value_from_values(self.current_values(entity)) |
|
203 |
|
204 def current_display_values(self, entity): |
|
205 """same as .current_values but consider values stored in session in case |
|
206 of validation error |
|
207 """ |
|
208 values = entity.req.data.get('formvalues') |
|
209 if values is None: |
|
210 return self.current_values(entity) |
|
211 cdvalues = values.get(self.rname) |
|
212 if cdvalues is None: |
|
213 return self.current_values(entity) |
|
214 if not isinstance(cdvalues, (list, tuple)): |
|
215 cdvalues = (cdvalues,) |
|
216 return cdvalues |
|
217 |
|
218 def current_display_value(self, entity): |
|
219 """same as .current_value but consider values stored in session in case |
|
220 of validation error |
|
221 """ |
|
222 return _value_from_values(self.current_display_values(entity)) |
|
223 |
|
224 def hidden_input(self, entity, qvalue): |
|
225 """return an hidden field which |
|
226 1. indicates that a field is edited |
|
227 2. hold the old value to easily detect if the field has been modified |
|
228 |
|
229 `qvalue` is the html quoted old value |
|
230 """ |
|
231 if self.role == 'subject': |
|
232 editmark = 'edits' |
|
233 else: |
|
234 editmark = 'edito' |
|
235 if qvalue is None or not entity.has_eid(): |
|
236 qvalue = INTERNAL_FIELD_VALUE |
|
237 return u'<input type="hidden" name="%s-%s" value="%s"/>\n' % ( |
|
238 editmark, self.rname, qvalue) |
|
239 |
|
240 class InputWidget(Widget): |
|
241 """abstract class for input generating a <input> tag""" |
|
242 input_type = None |
|
243 html_attributes = Widget.html_attributes | set(('type', 'name', 'value')) |
|
244 |
|
245 def _edit_render(self, entity): |
|
246 value = self.current_value(entity) |
|
247 dvalue = self.current_display_value(entity) |
|
248 if isinstance(value, basestring): |
|
249 value = html_escape(value) |
|
250 if isinstance(dvalue, basestring): |
|
251 dvalue = html_escape(dvalue) |
|
252 return u'%s<input type="%s" name="%s" value="%s" %s/>' % ( |
|
253 self.hidden_input(entity, value), self.input_type, |
|
254 self.rname, dvalue, self.format_attrs()) |
|
255 |
|
256 class HiddenWidget(InputWidget): |
|
257 input_type = 'hidden' |
|
258 autoid = False |
|
259 def __init__(self, vreg, subjschema, rschema, objschema, |
|
260 role='subject', **kwattrs): |
|
261 InputWidget.__init__(self, vreg, subjschema, rschema, objschema, |
|
262 role='subject', |
|
263 **kwattrs) |
|
264 # disable access key |
|
265 del self.attrs['accesskey'] |
|
266 |
|
267 def current_value(self, entity): |
|
268 value = InputWidget.current_value(self, entity) |
|
269 return value or INTERNAL_FIELD_VALUE |
|
270 |
|
271 def current_display_value(self, entity): |
|
272 value = InputWidget.current_display_value(self, entity) |
|
273 return value or INTERNAL_FIELD_VALUE |
|
274 |
|
275 def render_label(self, entity, label=None): |
|
276 """render widget's label""" |
|
277 return u'' |
|
278 |
|
279 def render_help(self, entity): |
|
280 return u'' |
|
281 |
|
282 def hidden_input(self, entity, value): |
|
283 """no hidden input for hidden input""" |
|
284 return '' |
|
285 |
|
286 |
|
287 class EidWidget(HiddenWidget): |
|
288 |
|
289 def _edit_render(self, entity): |
|
290 return u'<input type="hidden" name="eid" value="%s" />' % entity.eid |
|
291 |
|
292 |
|
293 class StringWidget(InputWidget): |
|
294 input_type = 'text' |
|
295 html_attributes = InputWidget.html_attributes | set(('size', 'maxlength')) |
|
296 @staticmethod |
|
297 def size_constraint_attrs(attrs, maxsize): |
|
298 """set html attributes in the attrs dict to consider maxsize""" |
|
299 attrs['size'] = min(maxsize, 40) |
|
300 attrs['maxlength'] = maxsize |
|
301 |
|
302 |
|
303 class AutoCompletionWidget(StringWidget): |
|
304 cubicwebns_attributes = (StringWidget.cubicwebns_attributes | |
|
305 set(('accesskey', 'size', 'maxlength'))) |
|
306 attrs = () |
|
307 |
|
308 wdgtype = 'SuggestField' |
|
309 |
|
310 def current_value(self, entity): |
|
311 value = StringWidget.current_value(self, entity) |
|
312 return value or INTERNAL_FIELD_VALUE |
|
313 |
|
314 def _get_url(self, entity): |
|
315 return entity.req.build_url('json', fname=entity.autocomplete_initfuncs[self.rschema], |
|
316 pageid=entity.req.pageid, mode='remote') |
|
317 |
|
318 def _edit_render(self, entity): |
|
319 req = entity.req |
|
320 req.add_js( ('cubicweb.widgets.js', 'jquery.autocomplete.js') ) |
|
321 req.add_css('jquery.autocomplete.css') |
|
322 value = self.current_value(entity) |
|
323 dvalue = self.current_display_value(entity) |
|
324 if isinstance(value, basestring): |
|
325 value = html_escape(value) |
|
326 if isinstance(dvalue, basestring): |
|
327 dvalue = html_escape(dvalue) |
|
328 iid = self.attrs.pop('id') |
|
329 if self.required(entity): |
|
330 cssclass = u' required' |
|
331 else: |
|
332 cssclass = u'' |
|
333 dataurl = self._get_url(entity) |
|
334 return (u'%(hidden)s<input type="text" name="%(iid)s" value="%(value)s" cubicweb:dataurl="%(url)s" class="widget%(required)s" id="%(iid)s" ' |
|
335 u'tabindex="%(tabindex)s" cubicweb:loadtype="auto" cubicweb:wdgtype="%(wdgtype)s" %(attrs)s />' % { |
|
336 'iid': iid, |
|
337 'hidden': self.hidden_input(entity, value), |
|
338 'wdgtype': self.wdgtype, |
|
339 'url': html_escape(dataurl), |
|
340 'tabindex': self.attrs.pop('tabindex'), |
|
341 'value': dvalue, |
|
342 'attrs': self.format_attrs(), |
|
343 'required' : cssclass, |
|
344 }) |
|
345 |
|
346 class StaticFileAutoCompletionWidget(AutoCompletionWidget): |
|
347 wdgtype = 'StaticFileSuggestField' |
|
348 |
|
349 def _get_url(self, entity): |
|
350 return entity.req.datadir_url + entity.autocomplete_initfuncs[self.rschema] |
|
351 |
|
352 class RestrictedAutoCompletionWidget(AutoCompletionWidget): |
|
353 wdgtype = 'RestrictedSuggestField' |
|
354 |
|
355 |
|
356 class PasswordWidget(InputWidget): |
|
357 input_type = 'password' |
|
358 |
|
359 def required(self, entity): |
|
360 if InputWidget.required(self, entity) and not entity.has_eid(): |
|
361 return True |
|
362 return False |
|
363 |
|
364 def current_values(self, entity): |
|
365 # on existant entity, show password field has non empty (we don't have |
|
366 # the actual value |
|
367 if entity.has_eid(): |
|
368 return (INTERNAL_FIELD_VALUE,) |
|
369 return super(PasswordWidget, self).current_values(entity) |
|
370 |
|
371 def _edit_render(self, entity): |
|
372 html = super(PasswordWidget, self)._edit_render(entity) |
|
373 name = eid_param(self.name + '-confirm', entity.eid) |
|
374 return u'%s<br/>\n<input type="%s" name="%s" id="%s" tabindex="%s"/> <span class="emphasis">(%s)</span>' % ( |
|
375 html, self.input_type, name, name, entity.req.next_tabindex(), |
|
376 entity.req._('confirm password')) |
|
377 |
|
378 |
|
379 class TextWidget(Widget): |
|
380 html_attributes = Widget.html_attributes | set(('rows', 'cols')) |
|
381 |
|
382 @staticmethod |
|
383 def size_constraint_attrs(attrs, maxsize): |
|
384 """set html attributes in the attrs dict to consider maxsize""" |
|
385 if 256 < maxsize < 513: |
|
386 attrs['cols'], attrs['rows'] = 60, 5 |
|
387 else: |
|
388 attrs['cols'], attrs['rows'] = 80, 10 |
|
389 |
|
390 def render(self, entity): |
|
391 if not entity.has_eid(): |
|
392 return u'' |
|
393 return entity.printable_value(self.name) |
|
394 |
|
395 def add_fckeditor_info(self, req): |
|
396 req.add_js('fckeditor.js') |
|
397 req.fckeditor_config() |
|
398 |
|
399 def _edit_render(self, entity, with_format=True): |
|
400 req = entity.req |
|
401 editor = self._edit_render_textarea(entity, with_format) |
|
402 value = self.current_value(entity) |
|
403 if isinstance(value, basestring): |
|
404 value = html_escape(value) |
|
405 return u'%s%s' % (self.hidden_input(entity, value), editor) |
|
406 |
|
407 def _edit_render_textarea(self, entity, with_format): |
|
408 self.attrs.setdefault('cols', 80) |
|
409 self.attrs.setdefault('rows', 20) |
|
410 dvalue = self.current_display_value(entity) |
|
411 if isinstance(dvalue, basestring): |
|
412 dvalue = html_escape(dvalue) |
|
413 if entity.use_fckeditor(self.name): |
|
414 self.add_fckeditor_info(entity.req) |
|
415 if with_format: |
|
416 if entity.has_eid(): |
|
417 format = entity.format(self.name) |
|
418 else: |
|
419 format = '' |
|
420 frname = eid_param(self.name + '_format', entity.eid) |
|
421 hidden = u'<input type="hidden" name="edits-%s" value="%s"/>\n'\ |
|
422 '<input type="hidden" name="%s" value="text/html"/>\n' % ( |
|
423 frname, format, frname) |
|
424 return u'%s<textarea cubicweb:type="wysiwyg" onkeypress="autogrow(this)" name="%s" %s>%s</textarea>' % ( |
|
425 hidden, self.rname, self.format_attrs(), dvalue) |
|
426 if with_format and entity.has_format(self.name): |
|
427 fmtwdg = entity.get_widget(self.name + '_format') |
|
428 fmtwdgstr = fmtwdg.edit_render(entity, tabindex=self.attrs['tabindex']) |
|
429 self.attrs['tabindex'] = entity.req.next_tabindex() |
|
430 else: |
|
431 fmtwdgstr = '' |
|
432 return u'%s<br/><textarea onkeypress="autogrow(this)" name="%s" %s>%s</textarea>' % ( |
|
433 fmtwdgstr, self.rname, self.format_attrs(), dvalue) |
|
434 |
|
435 |
|
436 class CheckBoxWidget(Widget): |
|
437 html_attributes = Widget.html_attributes | set(('checked', )) |
|
438 def _edit_render(self, entity): |
|
439 value = self.current_value(entity) |
|
440 dvalue = self.current_display_value(entity) |
|
441 return self.hidden_input(entity, value) + checkbox(self.rname, 'checked', self.format_attrs(), dvalue) |
|
442 |
|
443 def render(self, entity): |
|
444 if not entity.has_eid(): |
|
445 return u'' |
|
446 if getattr(entity, self.name): |
|
447 return entity.req._('yes') |
|
448 return entity.req._('no') |
|
449 |
|
450 |
|
451 class YesNoRadioWidget(CheckBoxWidget): |
|
452 |
|
453 def _edit_render(self, entity): |
|
454 value = self.current_value(entity) |
|
455 dvalue = self.current_display_value(entity) |
|
456 attrs1 = self.format_attrs() |
|
457 del self.attrs['id'] # avoid duplicate id for xhtml compliance |
|
458 attrs2 = self.format_attrs() |
|
459 if dvalue: |
|
460 attrs1 += ' checked="checked"' |
|
461 else: |
|
462 attrs2 += ' checked="checked"' |
|
463 wdgs = [self.hidden_input(entity, value), |
|
464 u'<input type="radio" name="%s" value="1" %s/>%s<br/>' % (self.rname, attrs1, entity.req._('yes')), |
|
465 u'<input type="radio" name="%s" value="" %s/>%s<br/>' % (self.rname, attrs2, entity.req._('no'))] |
|
466 return '\n'.join(wdgs) |
|
467 |
|
468 |
|
469 class FileWidget(Widget): |
|
470 need_multipart = True |
|
471 def _file_wdg(self, entity): |
|
472 wdgs = [u'<input type="file" name="%s" %s/>' % (self.rname, self.format_attrs())] |
|
473 req = entity.req |
|
474 if entity.has_format(self.name) or entity.has_text_encoding(self.name): |
|
475 divid = '%s-%s-advanced' % (self.name, entity.eid) |
|
476 wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' % |
|
477 (html_escape(toggle_action(divid)), |
|
478 req._('show advanced fields'), |
|
479 html_escape(req.build_url('data/puce_down.png')), |
|
480 req._('show advanced fields'))) |
|
481 wdgs.append(u'<div id="%s" class="hidden">' % divid) |
|
482 for extraattr in ('_format', '_encoding'): |
|
483 if entity.e_schema.has_subject_relation('%s%s' % (self.name, extraattr)): |
|
484 ewdg = entity.get_widget(self.name + extraattr) |
|
485 wdgs.append(ewdg.render_label(entity)) |
|
486 wdgs.append(ewdg.edit_render(entity, includehelp=True)) |
|
487 wdgs.append(u'<br/>') |
|
488 wdgs.append(u'</div>') |
|
489 if entity.has_eid() and not self.required(entity): |
|
490 # trick to be able to delete an uploaded file |
|
491 wdgs.append(u'<br/>') |
|
492 wdgs.append(checkbox(eid_param('__%s_detach' % self.rname, entity.eid), False)) |
|
493 wdgs.append(req._('detach attached file')) |
|
494 return '\n'.join(wdgs) |
|
495 |
|
496 def _edit_render(self, entity): |
|
497 return self.hidden_input(entity, None) + self._file_wdg(entity) |
|
498 |
|
499 |
|
500 class TextFileWidget(FileWidget): |
|
501 def _edit_msg(self, entity): |
|
502 if entity.has_eid() and not self.required(entity): |
|
503 msg = entity.req._( |
|
504 'You can either submit a new file using the browse button above' |
|
505 ', or choose to remove already uploaded file by checking the ' |
|
506 '"detach attached file" check-box, or edit file content online ' |
|
507 'with the widget below.') |
|
508 else: |
|
509 msg = entity.req._( |
|
510 'You can either submit a new file using the browse button above' |
|
511 ', or edit file content online with the widget below.') |
|
512 return msg |
|
513 |
|
514 def _edit_render(self, entity): |
|
515 wdgs = [self._file_wdg(entity)] |
|
516 if entity.format(self.name) in ('text/plain', 'text/html', 'text/rest'): |
|
517 msg = self._edit_msg(entity) |
|
518 wdgs.append(u'<p><b>%s</b></p>' % msg) |
|
519 twdg = TextWidget(self.vreg, self.subjtype, self.rschema, self.objtype) |
|
520 twdg.rname = self.rname |
|
521 data = getattr(entity, self.name) |
|
522 if data: |
|
523 encoding = entity.text_encoding(self.name) |
|
524 try: |
|
525 entity[self.name] = unicode(data.getvalue(), encoding) |
|
526 except UnicodeError: |
|
527 pass |
|
528 else: |
|
529 wdgs.append(twdg.edit_render(entity, with_format=False)) |
|
530 entity[self.name] = data # restore Binary value |
|
531 wdgs.append(u'<br/>') |
|
532 return '\n'.join(wdgs) |
|
533 |
|
534 |
|
535 class ComboBoxWidget(Widget): |
|
536 html_attributes = Widget.html_attributes | set(('multiple', 'size')) |
|
537 |
|
538 def __init__(self, vreg, subjschema, rschema, objschema, |
|
539 multiple=False, **kwattrs): |
|
540 super(ComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema, |
|
541 **kwattrs) |
|
542 if multiple: |
|
543 self.attrs['multiple'] = 'multiple' |
|
544 if not 'size' in self.attrs: |
|
545 self.attrs['size'] = '5' |
|
546 # disable access key (dunno why but this is not allowed by xhtml 1.0) |
|
547 del self.attrs['accesskey'] |
|
548 |
|
549 def vocabulary(self, entity): |
|
550 raise NotImplementedError() |
|
551 |
|
552 def form_value(self, entity, value, values): |
|
553 if value in values: |
|
554 flag = 'selected="selected"' |
|
555 else: |
|
556 flag = '' |
|
557 return value, flag |
|
558 |
|
559 def _edit_render(self, entity): |
|
560 values = self.current_values(entity) |
|
561 if values: |
|
562 res = [self.hidden_input(entity, v) for v in values] |
|
563 else: |
|
564 res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)] |
|
565 dvalues = self.current_display_values(entity) |
|
566 res.append(u'<select name="%s" %s>' % (self.rname, self.format_attrs())) |
|
567 for label, value in self.vocabulary(entity): |
|
568 if value is None: |
|
569 # handle separator |
|
570 res.append(u'<optgroup label="%s"/>' % (label or '')) |
|
571 else: |
|
572 value, flag = self.form_value(entity, value, dvalues) |
|
573 res.append(u'<option value="%s" %s>%s</option>' % (value, flag, html_escape(label))) |
|
574 res.append(u'</select>') |
|
575 return '\n'.join(res) |
|
576 |
|
577 |
|
578 class StaticComboBoxWidget(ComboBoxWidget): |
|
579 |
|
580 def __init__(self, vreg, subjschema, rschema, objschema, |
|
581 vocabfunc, multiple=False, sort=False, **kwattrs): |
|
582 super(StaticComboBoxWidget, self).__init__(vreg, subjschema, rschema, objschema, |
|
583 multiple, **kwattrs) |
|
584 self.sort = sort |
|
585 self.vocabfunc = vocabfunc |
|
586 |
|
587 def vocabulary(self, entity): |
|
588 choices = self.vocabfunc(entity) |
|
589 if self.sort: |
|
590 choices = sorted(choices) |
|
591 if self.rschema.rproperty(self.subjtype, self.objtype, 'internationalizable'): |
|
592 return zip((entity.req._(v) for v in choices), choices) |
|
593 return zip(choices, choices) |
|
594 |
|
595 |
|
596 class EntityLinkComboBoxWidget(ComboBoxWidget): |
|
597 """to be used be specific forms""" |
|
598 |
|
599 def current_values(self, entity): |
|
600 if entity.has_eid(): |
|
601 return [r[0] for r in entity.related(self.name, self.role)] |
|
602 defaultmeth = 'default_%s_%s' % (self.role, self.name) |
|
603 if hasattr(entity, defaultmeth): |
|
604 return getattr(entity, defaultmeth)() |
|
605 return () |
|
606 |
|
607 def vocabulary(self, entity): |
|
608 return [('', INTERNAL_FIELD_VALUE)] + entity.vocabulary(self.rschema, self.role) |
|
609 |
|
610 |
|
611 class RawDynamicComboBoxWidget(EntityLinkComboBoxWidget): |
|
612 |
|
613 def vocabulary(self, entity, limit=None): |
|
614 req = entity.req |
|
615 # first see if its specified by __linkto form parameters |
|
616 linkedto = entity.linked_to(self.name, self.role) |
|
617 if linkedto: |
|
618 entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto) |
|
619 return [(entity.view('combobox'), entity.eid) for entity in entities] |
|
620 # it isn't, check if the entity provides a method to get correct values |
|
621 if not self.required(entity): |
|
622 res = [('', INTERNAL_FIELD_VALUE)] |
|
623 else: |
|
624 res = [] |
|
625 # vocabulary doesn't include current values, add them |
|
626 if entity.has_eid(): |
|
627 rset = entity.related(self.name, self.role) |
|
628 relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()] |
|
629 else: |
|
630 relatedvocab = [] |
|
631 return res + entity.vocabulary(self.rschema, self.role) + relatedvocab |
|
632 |
|
633 |
|
634 class DynamicComboBoxWidget(RawDynamicComboBoxWidget): |
|
635 |
|
636 def vocabulary(self, entity, limit=None): |
|
637 return sorted(super(DynamicComboBoxWidget, self).vocabulary(entity, limit)) |
|
638 |
|
639 |
|
640 class AddComboBoxWidget(DynamicComboBoxWidget): |
|
641 def _edit_render(self, entity): |
|
642 req = entity.req |
|
643 req.add_js( ('cubicweb.ajax.js', 'jquery.js', 'cubicweb.widgets.js') ) |
|
644 values = self.current_values(entity) |
|
645 if values: |
|
646 res = [self.hidden_input(entity, v) for v in values] |
|
647 else: |
|
648 res = [self.hidden_input(entity, INTERNAL_FIELD_VALUE)] |
|
649 dvalues = self.current_display_values(entity) |
|
650 etype_from = entity.e_schema.subject_relation(self.name).objects(entity.e_schema)[0] |
|
651 res.append(u'<select class="widget" cubicweb:etype_to="%s" cubicweb:etype_from="%s" cubicweb:loadtype="auto" cubicweb:wdgtype="AddComboBox" name="%s" %s>' |
|
652 % (entity.e_schema, etype_from, self.rname, self.format_attrs())) |
|
653 for label, value in self.vocabulary(entity): |
|
654 if value is None: |
|
655 # handle separator |
|
656 res.append(u'<optgroup label="%s"/>' % (label or '')) |
|
657 else: |
|
658 value, flag = self.form_value(entity, value, dvalues) |
|
659 res.append(u'<option value="%s" %s>%s</option>' % (value, flag, html_escape(label))) |
|
660 res.append(u'</select>') |
|
661 res.append(u'<div id="newvalue">') |
|
662 res.append(u'<input type="text" id="newopt" />') |
|
663 res.append(u'<a href="javascript:noop()" id="add_newopt"> </a></div>') |
|
664 return '\n'.join(res) |
|
665 |
|
666 class IntegerWidget(StringWidget): |
|
667 def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs): |
|
668 kwattrs['size'] = 5 |
|
669 kwattrs['maxlength'] = 15 |
|
670 StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs) |
|
671 |
|
672 def render_example(self, req): |
|
673 return '23' |
|
674 |
|
675 |
|
676 |
|
677 class FloatWidget(StringWidget): |
|
678 def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs): |
|
679 kwattrs['size'] = 5 |
|
680 kwattrs['maxlength'] = 15 |
|
681 StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs) |
|
682 |
|
683 def render_example(self, req): |
|
684 formatstr = req.property_value('ui.float-format') |
|
685 return formatstr % 1.23 |
|
686 |
|
687 def current_values(self, entity): |
|
688 values = entity.attribute_values(self.name) |
|
689 if values: |
|
690 formatstr = entity.req.property_value('ui.float-format') |
|
691 value = values[0] |
|
692 if value is not None: |
|
693 value = float(value) |
|
694 else: |
|
695 return () |
|
696 return [formatstr % value] |
|
697 return () |
|
698 |
|
699 class DecimalWidget(StringWidget): |
|
700 def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs): |
|
701 kwattrs['size'] = 5 |
|
702 kwattrs['maxlength'] = 15 |
|
703 StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs) |
|
704 |
|
705 def render_example(self, req): |
|
706 return '345.0300' |
|
707 |
|
708 |
|
709 |
|
710 class DateWidget(StringWidget): |
|
711 format_key = 'ui.date-format' |
|
712 monthnames = ("january", "february", "march", "april", |
|
713 "may", "june", "july", "august", |
|
714 "september", "october", "november", "december") |
|
715 |
|
716 daynames = ("monday", "tuesday", "wednesday", "thursday", |
|
717 "friday", "saturday", "sunday") |
|
718 |
|
719 def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs): |
|
720 kwattrs.setdefault('size', 10) |
|
721 kwattrs.setdefault('maxlength', 10) |
|
722 StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs) |
|
723 |
|
724 def current_values(self, entity): |
|
725 values = entity.attribute_values(self.name) |
|
726 if values and hasattr(values[0], 'strftime'): |
|
727 formatstr = entity.req.property_value(self.format_key) |
|
728 return [values[0].strftime(formatstr)] |
|
729 return values |
|
730 |
|
731 def render_example(self, req): |
|
732 formatstr = req.property_value(self.format_key) |
|
733 return now().strftime(formatstr) |
|
734 |
|
735 def add_localized_infos(self, req): |
|
736 """inserts JS variables defining localized months and days""" |
|
737 # import here to avoid dependancy from cubicweb-common to simplejson |
|
738 _ = req._ |
|
739 monthnames = [_(mname) for mname in self.monthnames] |
|
740 daynames = [_(dname) for dname in self.daynames] |
|
741 req.html_headers.define_var('MONTHNAMES', monthnames) |
|
742 req.html_headers.define_var('DAYNAMES', daynames) |
|
743 |
|
744 |
|
745 def _edit_render(self, entity): |
|
746 wdg = super(DateWidget, self)._edit_render(entity) |
|
747 cal_button = self.render_calendar_popup(entity) |
|
748 return wdg+cal_button |
|
749 |
|
750 def render_help(self, entity): |
|
751 """calendar popup widget""" |
|
752 req = entity.req |
|
753 help = [ u'<br/>' ] |
|
754 descr = self.rschema.rproperty(self.subjtype, self.objtype, 'description') |
|
755 if descr: |
|
756 help.append('<span class="helper">%s</span>' % req._(descr)) |
|
757 example = self.render_example(req) |
|
758 if example: |
|
759 help.append('<span class="helper">(%s: %s)</span>' |
|
760 % (req._('sample format'), example)) |
|
761 return u' '.join(help) |
|
762 |
|
763 def render_calendar_popup(self, entity): |
|
764 """calendar popup widget""" |
|
765 req = entity.req |
|
766 self.add_localized_infos(req) |
|
767 req.add_js(('cubicweb.ajax.js', 'cubicweb.calendar.js',)) |
|
768 req.add_css(('cubicweb.calendar_popup.css',)) |
|
769 inputid = self.attrs.get('id', self.rname) |
|
770 helperid = "%shelper" % inputid |
|
771 _today = today() |
|
772 year = int(req.form.get('year', _today.year)) |
|
773 month = int(req.form.get('month', _today.month)) |
|
774 |
|
775 return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper"> |
|
776 <img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>""" |
|
777 % (helperid, inputid, year, month, |
|
778 req.external_resource('CALENDAR_ICON'), req._('calendar'), helperid) ) |
|
779 |
|
780 class DateTimeWidget(DateWidget): |
|
781 format_key = 'ui.datetime-format' |
|
782 |
|
783 def render_example(self, req): |
|
784 formatstr1 = req.property_value('ui.datetime-format') |
|
785 formatstr2 = req.property_value('ui.date-format') |
|
786 return req._('%s, or without time: %s') % (now().strftime(formatstr1), |
|
787 now().strftime(formatstr2)) |
|
788 |
|
789 |
|
790 |
|
791 |
|
792 def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs): |
|
793 kwattrs['size'] = 16 |
|
794 kwattrs['maxlength'] = 16 |
|
795 DateWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs) |
|
796 |
|
797 |
|
798 class TimeWidget(StringWidget): |
|
799 format_key = 'ui.time-format' |
|
800 def __init__(self, vreg, subjschema, rschema, objschema, **kwattrs): |
|
801 kwattrs['size'] = 5 |
|
802 kwattrs['maxlength'] = 5 |
|
803 StringWidget.__init__(self, vreg, subjschema, rschema, objschema, **kwattrs) |
|
804 |
|
805 |
|
806 class EmailWidget(StringWidget): |
|
807 |
|
808 def render(self, entity): |
|
809 email = getattr(entity, self.name) |
|
810 if not email: |
|
811 return u'' |
|
812 return u'<a href="mailto:%s">%s</a>' % (email, email) |
|
813 |
|
814 class URLWidget(StringWidget): |
|
815 |
|
816 def render(self, entity): |
|
817 url = getattr(entity, self.name) |
|
818 if not url: |
|
819 return u'' |
|
820 url = html_escape(url) |
|
821 return u'<a href="%s">%s</a>' % (url, url) |
|
822 |
|
823 class EmbededURLWidget(StringWidget): |
|
824 |
|
825 def render(self, entity): |
|
826 url = getattr(entity, self.name) |
|
827 if not url: |
|
828 return u'' |
|
829 aurl = html_escape(entity.build_url('embed', url=url)) |
|
830 return u'<a href="%s">%s</a>' % (aurl, url) |
|
831 |
|
832 |
|
833 |
|
834 class PropertyKeyWidget(ComboBoxWidget): |
|
835 """specific widget for EProperty.pkey field to set the value widget according to |
|
836 the selected key |
|
837 """ |
|
838 |
|
839 def _edit_render(self, entity): |
|
840 entity.req.add_js( ('cubicweb.ajax.js', 'cubicweb.edition.js') ) |
|
841 vtabindex = self.attrs.get('tabindex', 0) + 1 |
|
842 self.attrs['onchange'] = "javascript:setPropValueWidget('%s', %s)" % ( |
|
843 entity.eid, vtabindex) |
|
844 # limit size |
|
845 if not entity.has_eid(): |
|
846 self.attrs['size'] = 10 |
|
847 else: |
|
848 self.attrs['size'] = 1 |
|
849 return super(PropertyKeyWidget, self)._edit_render(entity) |
|
850 |
|
851 def vocabulary(self, entity): |
|
852 _ = entity.req._ |
|
853 if entity.has_eid(): |
|
854 return [(_(entity.pkey), entity.pkey)] |
|
855 # key beginning with 'system.' should usually not be edited by hand |
|
856 choices = entity.vreg.user_property_keys() |
|
857 return sorted(zip((_(v) for v in choices), choices)) |
|
858 |
|
859 |
|
860 class PropertyValueWidget(Widget): |
|
861 """specific widget for EProperty.value field which will be different according to |
|
862 the selected key type and vocabulary information |
|
863 """ |
|
864 |
|
865 def render_help(self, entity): |
|
866 return u'' |
|
867 |
|
868 def render(self, entity): |
|
869 assert entity.has_eid() |
|
870 w = self.vreg.property_value_widget(entity.pkey, req=entity.req, **self.attrs) |
|
871 return w.render(entity) |
|
872 |
|
873 def _edit_render(self, entity): |
|
874 if not entity.has_eid(): |
|
875 # no key set yet, just include an empty div which will be filled |
|
876 # on key selection |
|
877 # empty span as well else html validation fail (label is refering to this id) |
|
878 return u'<div id="div:%s"><span id="%s"/></div>' % (self.rname, self.attrs.get('id')) |
|
879 w = self.vreg.property_value_widget(entity.pkey, req=entity.req, **self.attrs) |
|
880 if entity.pkey.startswith('system.'): |
|
881 value = '<span class="value" id="%s">%s</span>' % (self.attrs.get('id'), w.render(entity)) |
|
882 msg = entity.req._('value associated to this key is not editable manually') |
|
883 return value + '<div>%s</div>' % msg |
|
884 return w.edit_render(entity, self.attrs.get('tabindex'), includehelp=True) |
|
885 |
|
886 |
|
887 def widget_factory(vreg, subjschema, rschema, objschema, role='subject', |
|
888 **kwargs): |
|
889 """return the most adapated widget to edit the relation |
|
890 'subjschema rschema objschema' according to information found in the schema |
|
891 """ |
|
892 if role == 'subject': |
|
893 eclass, subjschema = _eclass_eschema(subjschema) |
|
894 else: |
|
895 eclass, objschema = _eclass_eschema(objschema) |
|
896 if eclass is not None and rschema in eclass.widgets: |
|
897 wcls = WIDGETS[eclass.widgets[rschema]] |
|
898 elif not rschema.is_final(): |
|
899 card = rschema.rproperty(subjschema, objschema, 'cardinality') |
|
900 if role == 'object': |
|
901 multiple = card[1] in '+*' |
|
902 else: #if role == 'subject': |
|
903 multiple = card[0] in '+*' |
|
904 return DynamicComboBoxWidget(vreg, subjschema, rschema, objschema, |
|
905 role=role, multiple=multiple) |
|
906 else: |
|
907 wcls = None |
|
908 factory = FACTORIES.get(objschema, _default_widget_factory) |
|
909 return factory(vreg, subjschema, rschema, objschema, wcls=wcls, |
|
910 role=role, **kwargs) |
|
911 |
|
912 |
|
913 # factories to find the most adapated widget according to a type and other constraints |
|
914 |
|
915 def _string_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs): |
|
916 w = None |
|
917 for c in rschema.rproperty(subjschema, objschema, 'constraints'): |
|
918 if isinstance(c, StaticVocabularyConstraint): |
|
919 # may have been set by a previous SizeConstraint but doesn't make sense |
|
920 # here (even doesn't have the same meaning on a combobox actually) |
|
921 kwargs.pop('size', None) |
|
922 return (wcls or StaticComboBoxWidget)(vreg, subjschema, rschema, objschema, |
|
923 vocabfunc=c.vocabulary, **kwargs) |
|
924 if isinstance(c, SizeConstraint) and c.max is not None: |
|
925 # don't return here since a StaticVocabularyConstraint may |
|
926 # follow |
|
927 if wcls is None: |
|
928 if c.max < 257: |
|
929 _wcls = StringWidget |
|
930 else: |
|
931 _wcls = TextWidget |
|
932 else: |
|
933 _wcls = wcls |
|
934 _wcls.size_constraint_attrs(kwargs, c.max) |
|
935 w = _wcls(vreg, subjschema, rschema, objschema, **kwargs) |
|
936 if w is None: |
|
937 w = (wcls or TextWidget)(vreg, subjschema, rschema, objschema, **kwargs) |
|
938 return w |
|
939 |
|
940 def _default_widget_factory(vreg, subjschema, rschema, objschema, wcls=None, **kwargs): |
|
941 if wcls is None: |
|
942 wcls = _WFACTORIES[objschema] |
|
943 return wcls(vreg, subjschema, rschema, objschema, **kwargs) |
|
944 |
|
945 FACTORIES = { |
|
946 'String' : _string_widget_factory, |
|
947 'Boolean': _default_widget_factory, |
|
948 'Bytes': _default_widget_factory, |
|
949 'Date': _default_widget_factory, |
|
950 'Datetime': _default_widget_factory, |
|
951 'Float': _default_widget_factory, |
|
952 'Decimal': _default_widget_factory, |
|
953 'Int': _default_widget_factory, |
|
954 'Password': _default_widget_factory, |
|
955 'Time': _default_widget_factory, |
|
956 } |
|
957 |
|
958 # default widget by entity's type |
|
959 _WFACTORIES = { |
|
960 'Boolean': YesNoRadioWidget, |
|
961 'Bytes': FileWidget, |
|
962 'Date': DateWidget, |
|
963 'Datetime': DateTimeWidget, |
|
964 'Int': IntegerWidget, |
|
965 'Float': FloatWidget, |
|
966 'Decimal': DecimalWidget, |
|
967 'Password': PasswordWidget, |
|
968 'String' : StringWidget, |
|
969 'Time': TimeWidget, |
|
970 } |
|
971 |
|
972 # widgets registry |
|
973 WIDGETS = {} |
|
974 def register(widget_list): |
|
975 for obj in widget_list: |
|
976 if isinstance(obj, type) and issubclass(obj, Widget): |
|
977 if obj is Widget or obj is ComboBoxWidget: |
|
978 continue |
|
979 WIDGETS[obj.__name__] = obj |
|
980 |
|
981 register(globals().values()) |