|
1 """some base form classes for CubicWeb web client |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2001-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 warnings import warn |
|
11 |
|
12 from logilab.common.compat import any |
|
13 from logilab.common.decorators import iclassmethod |
|
14 |
|
15 from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset |
|
16 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param |
|
17 from cubicweb.web import form, formwidgets as fwdgs |
|
18 from cubicweb.web.controller import NAV_FORM_PARAMETERS |
|
19 from cubicweb.web.formfields import HiddenInitialValueField, StringField |
|
20 |
|
21 |
|
22 class FieldsForm(form.Form): |
|
23 id = 'base' |
|
24 |
|
25 is_subform = False |
|
26 |
|
27 # attributes overrideable through __init__ |
|
28 internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS |
|
29 needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',) |
|
30 needs_css = ('cubicweb.form.css',) |
|
31 domid = 'form' |
|
32 title = None |
|
33 action = None |
|
34 onsubmit = "return freezeFormButtons('%(domid)s');" |
|
35 cssclass = None |
|
36 cssstyle = None |
|
37 cwtarget = None |
|
38 redirect_path = None |
|
39 set_error_url = True |
|
40 copy_nav_params = False |
|
41 form_buttons = None # form buttons (button widgets instances) |
|
42 form_renderer_id = 'default' |
|
43 |
|
44 def __init__(self, req, rset=None, row=None, col=None, submitmsg=None, |
|
45 **kwargs): |
|
46 super(FieldsForm, self).__init__(req, rset, row=row, col=col) |
|
47 self.fields = list(self.__class__._fields_) |
|
48 for key, val in kwargs.items(): |
|
49 if key in NAV_FORM_PARAMETERS: |
|
50 self.form_add_hidden(key, val) |
|
51 else: |
|
52 assert hasattr(self.__class__, key) and not key[0] == '_', key |
|
53 setattr(self, key, val) |
|
54 if self.set_error_url: |
|
55 self.form_add_hidden('__errorurl', self.session_key()) |
|
56 if self.copy_nav_params: |
|
57 for param in NAV_FORM_PARAMETERS: |
|
58 if not param in kwargs: |
|
59 value = req.form.get(param) |
|
60 if value: |
|
61 self.form_add_hidden(param, value) |
|
62 if submitmsg is not None: |
|
63 self.form_add_hidden('__message', submitmsg) |
|
64 self.context = None |
|
65 if 'domid' in kwargs:# session key changed |
|
66 self.restore_previous_post(self.session_key()) |
|
67 |
|
68 @iclassmethod |
|
69 def _fieldsattr(cls_or_self): |
|
70 if isinstance(cls_or_self, type): |
|
71 fields = cls_or_self._fields_ |
|
72 else: |
|
73 fields = cls_or_self.fields |
|
74 return fields |
|
75 |
|
76 @iclassmethod |
|
77 def field_by_name(cls_or_self, name, role='subject'): |
|
78 """return field with the given name and role. |
|
79 Raise FieldNotFound if the field can't be found. |
|
80 """ |
|
81 for field in cls_or_self._fieldsattr(): |
|
82 if field.name == name and field.role == role: |
|
83 return field |
|
84 raise form.FieldNotFound(name) |
|
85 |
|
86 @iclassmethod |
|
87 def fields_by_name(cls_or_self, name, role='subject'): |
|
88 """return a list of fields with the given name and role""" |
|
89 return [field for field in cls_or_self._fieldsattr() |
|
90 if field.name == name and field.role == role] |
|
91 |
|
92 @iclassmethod |
|
93 def remove_field(cls_or_self, field): |
|
94 """remove a field from form class or instance""" |
|
95 cls_or_self._fieldsattr().remove(field) |
|
96 |
|
97 @iclassmethod |
|
98 def append_field(cls_or_self, field): |
|
99 """append a field to form class or instance""" |
|
100 cls_or_self._fieldsattr().append(field) |
|
101 |
|
102 @iclassmethod |
|
103 def insert_field_before(cls_or_self, new_field, name, role='subject'): |
|
104 field = cls_or_self.field_by_name(name, role) |
|
105 fields = cls_or_self._fieldsattr() |
|
106 fields.insert(fields.index(field), new_field) |
|
107 |
|
108 @iclassmethod |
|
109 def insert_field_after(cls_or_self, new_field, name, role='subject'): |
|
110 field = cls_or_self.field_by_name(name, role) |
|
111 fields = cls_or_self._fieldsattr() |
|
112 fields.insert(fields.index(field)+1, new_field) |
|
113 |
|
114 @property |
|
115 def form_needs_multipart(self): |
|
116 """true if the form needs enctype=multipart/form-data""" |
|
117 return any(field.needs_multipart for field in self.fields) |
|
118 |
|
119 def form_add_hidden(self, name, value=None, **kwargs): |
|
120 """add an hidden field to the form""" |
|
121 field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value, |
|
122 **kwargs) |
|
123 if 'id' in kwargs: |
|
124 # by default, hidden input don't set id attribute. If one is |
|
125 # explicitly specified, ensure it will be set |
|
126 field.widget.setdomid = True |
|
127 self.append_field(field) |
|
128 return field |
|
129 |
|
130 def add_media(self): |
|
131 """adds media (CSS & JS) required by this widget""" |
|
132 if self.needs_js: |
|
133 self.req.add_js(self.needs_js) |
|
134 if self.needs_css: |
|
135 self.req.add_css(self.needs_css) |
|
136 |
|
137 def form_render(self, **values): |
|
138 """render this form, using the renderer given in args or the default |
|
139 FormRenderer() |
|
140 """ |
|
141 renderer = values.pop('renderer', None) |
|
142 if renderer is None: |
|
143 renderer = self.form_default_renderer() |
|
144 return renderer.render(self, values) |
|
145 |
|
146 def form_default_renderer(self): |
|
147 return self.vreg.select_object('formrenderers', self.form_renderer_id, |
|
148 self.req, self.rset, |
|
149 row=self.row, col=self.col) |
|
150 |
|
151 def form_build_context(self, rendervalues=None): |
|
152 """build form context values (the .context attribute which is a |
|
153 dictionary with field instance as key associated to a dictionary |
|
154 containing field 'name' (qualified), 'id', 'value' (for display, always |
|
155 a string). |
|
156 |
|
157 rendervalues is an optional dictionary containing extra kwargs given to |
|
158 form_render() |
|
159 """ |
|
160 self.context = context = {} |
|
161 # ensure rendervalues is a dict |
|
162 if rendervalues is None: |
|
163 rendervalues = {} |
|
164 # use a copy in case fields are modified while context is build (eg |
|
165 # __linkto handling for instance) |
|
166 for field in self.fields[:]: |
|
167 for field in field.actual_fields(self): |
|
168 field.form_init(self) |
|
169 value = self.form_field_display_value(field, rendervalues) |
|
170 context[field] = {'value': value, |
|
171 'name': self.form_field_name(field), |
|
172 'id': self.form_field_id(field), |
|
173 } |
|
174 |
|
175 def form_field_display_value(self, field, rendervalues, load_bytes=False): |
|
176 """return field's *string* value to use for display |
|
177 |
|
178 looks in |
|
179 1. previously submitted form values if any (eg on validation error) |
|
180 2. req.form |
|
181 3. extra kw args given to render_form |
|
182 4. field's typed value |
|
183 |
|
184 values found in 1. and 2. are expected te be already some 'display' |
|
185 value while those found in 3. and 4. are expected to be correctly typed. |
|
186 """ |
|
187 value = self._req_display_value(field) |
|
188 if value is None: |
|
189 if field.name in rendervalues: |
|
190 value = rendervalues[field.name] |
|
191 else: |
|
192 value = self.form_field_value(field, load_bytes) |
|
193 if callable(value): |
|
194 value = value(self) |
|
195 if value != INTERNAL_FIELD_VALUE: |
|
196 value = field.format_value(self.req, value) |
|
197 return value |
|
198 |
|
199 def _req_display_value(self, field): |
|
200 qname = self.form_field_name(field) |
|
201 if qname in self.form_previous_values: |
|
202 return self.form_previous_values[qname] |
|
203 if qname in self.req.form: |
|
204 return self.req.form[qname] |
|
205 if field.name in self.req.form: |
|
206 return self.req.form[field.name] |
|
207 return None |
|
208 |
|
209 def form_field_value(self, field, load_bytes=False): |
|
210 """return field's *typed* value""" |
|
211 myattr = '%s_%s_default' % (field.role, field.name) |
|
212 if hasattr(self, myattr): |
|
213 return getattr(self, myattr)() |
|
214 value = field.initial |
|
215 if callable(value): |
|
216 value = value(self) |
|
217 return value |
|
218 |
|
219 def form_field_error(self, field): |
|
220 """return validation error for widget's field, if any""" |
|
221 if self._field_has_error(field): |
|
222 self.form_displayed_errors.add(field.name) |
|
223 return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name] |
|
224 return u'' |
|
225 |
|
226 def form_field_format(self, field): |
|
227 """return MIME type used for the given (text or bytes) field""" |
|
228 return self.req.property_value('ui.default-text-format') |
|
229 |
|
230 def form_field_encoding(self, field): |
|
231 """return encoding used for the given (text) field""" |
|
232 return self.req.encoding |
|
233 |
|
234 def form_field_name(self, field): |
|
235 """return qualified name for the given field""" |
|
236 return field.name |
|
237 |
|
238 def form_field_id(self, field): |
|
239 """return dom id for the given field""" |
|
240 return field.id |
|
241 |
|
242 def form_field_vocabulary(self, field, limit=None): |
|
243 """return vocabulary for the given field. Should be overriden in |
|
244 specific forms using fields which requires some vocabulary |
|
245 """ |
|
246 raise NotImplementedError |
|
247 |
|
248 def _field_has_error(self, field): |
|
249 """return true if the field has some error in given validation exception |
|
250 """ |
|
251 return self.form_valerror and field.name in self.form_valerror.errors |
|
252 |
|
253 |
|
254 class EntityFieldsForm(FieldsForm): |
|
255 id = 'base' |
|
256 __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity())) |
|
257 |
|
258 internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid') |
|
259 domid = 'entityForm' |
|
260 |
|
261 def __init__(self, *args, **kwargs): |
|
262 self.edited_entity = kwargs.pop('entity', None) |
|
263 msg = kwargs.pop('submitmsg', None) |
|
264 super(EntityFieldsForm, self).__init__(*args, **kwargs) |
|
265 if self.edited_entity is None: |
|
266 self.edited_entity = self.complete_entity(self.row or 0, self.col or 0) |
|
267 self.form_add_hidden('__type', eidparam=True) |
|
268 self.form_add_hidden('eid') |
|
269 if msg: |
|
270 # If we need to directly attach the new object to another one |
|
271 self.form_add_hidden('__message', msg) |
|
272 if not self.is_subform: |
|
273 for linkto in self.req.list_form_param('__linkto'): |
|
274 self.form_add_hidden('__linkto', linkto) |
|
275 msg = '%s %s' % (msg, self.req._('and linked')) |
|
276 |
|
277 def _field_has_error(self, field): |
|
278 """return true if the field has some error in given validation exception |
|
279 """ |
|
280 return super(EntityFieldsForm, self)._field_has_error(field) \ |
|
281 and self.form_valerror.eid == self.edited_entity.eid |
|
282 |
|
283 def _relation_vocabulary(self, rtype, targettype, role, |
|
284 limit=None, done=None): |
|
285 """return unrelated entities for a given relation and target entity type |
|
286 for use in vocabulary |
|
287 """ |
|
288 if done is None: |
|
289 done = set() |
|
290 rset = self.edited_entity.unrelated(rtype, targettype, role, limit) |
|
291 res = [] |
|
292 for entity in rset.entities(): |
|
293 if entity.eid in done: |
|
294 continue |
|
295 done.add(entity.eid) |
|
296 res.append((entity.view('combobox'), entity.eid)) |
|
297 return res |
|
298 |
|
299 def _req_display_value(self, field): |
|
300 value = super(EntityFieldsForm, self)._req_display_value(field) |
|
301 if value is None: |
|
302 value = self.edited_entity.linked_to(field.name, field.role) |
|
303 if value: |
|
304 searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role) |
|
305 for eid in value] |
|
306 # remove associated __linkto hidden fields |
|
307 for field in self.fields_by_name('__linkto'): |
|
308 if field.initial in searchedvalues: |
|
309 self.remove_field(field) |
|
310 else: |
|
311 value = None |
|
312 return value |
|
313 |
|
314 def _form_field_default_value(self, field, load_bytes): |
|
315 defaultattr = 'default_%s' % field.name |
|
316 if hasattr(self.edited_entity, defaultattr): |
|
317 # XXX bw compat, default_<field name> on the entity |
|
318 warn('found %s on %s, should be set on a specific form' |
|
319 % (defaultattr, self.edited_entity.id), DeprecationWarning) |
|
320 value = getattr(self.edited_entity, defaultattr) |
|
321 if callable(value): |
|
322 value = value() |
|
323 else: |
|
324 value = super(EntityFieldsForm, self).form_field_value(field, |
|
325 load_bytes) |
|
326 return value |
|
327 |
|
328 def form_default_renderer(self): |
|
329 return self.vreg.select_object('formrenderers', self.form_renderer_id, |
|
330 self.req, self.rset, |
|
331 row=self.row, col=self.col, |
|
332 entity=self.edited_entity) |
|
333 |
|
334 def form_build_context(self, values=None): |
|
335 """overriden to add edit[s|o] hidden fields and to ensure schema fields |
|
336 have eidparam set to True |
|
337 |
|
338 edit[s|o] hidden fields are used to indicate the value for the |
|
339 associated field before the (potential) modification made when |
|
340 submitting the form. |
|
341 """ |
|
342 eschema = self.edited_entity.e_schema |
|
343 for field in self.fields[:]: |
|
344 for field in field.actual_fields(self): |
|
345 fieldname = field.name |
|
346 if fieldname != 'eid' and ( |
|
347 (eschema.has_subject_relation(fieldname) or |
|
348 eschema.has_object_relation(fieldname))): |
|
349 field.eidparam = True |
|
350 self.fields.append(HiddenInitialValueField(field)) |
|
351 return super(EntityFieldsForm, self).form_build_context(values) |
|
352 |
|
353 def form_field_value(self, field, load_bytes=False): |
|
354 """return field's *typed* value |
|
355 |
|
356 overriden to deal with |
|
357 * special eid / __type / edits- / edito- fields |
|
358 * lookup for values on edited entities |
|
359 """ |
|
360 attr = field.name |
|
361 entity = self.edited_entity |
|
362 if attr == 'eid': |
|
363 return entity.eid |
|
364 if not field.eidparam: |
|
365 return super(EntityFieldsForm, self).form_field_value(field, load_bytes) |
|
366 if attr.startswith('edits-') or attr.startswith('edito-'): |
|
367 # edit[s|o]- fieds must have the actual value stored on the entity |
|
368 assert hasattr(field, 'visible_field') |
|
369 vfield = field.visible_field |
|
370 assert vfield.eidparam |
|
371 if entity.has_eid(): |
|
372 return self.form_field_value(vfield) |
|
373 return INTERNAL_FIELD_VALUE |
|
374 if attr == '__type': |
|
375 return entity.id |
|
376 if self.schema.rschema(attr).is_final(): |
|
377 attrtype = entity.e_schema.destination(attr) |
|
378 if attrtype == 'Password': |
|
379 return entity.has_eid() and INTERNAL_FIELD_VALUE or '' |
|
380 if attrtype == 'Bytes': |
|
381 if entity.has_eid(): |
|
382 if load_bytes: |
|
383 return getattr(entity, attr) |
|
384 # XXX value should reflect if some file is already attached |
|
385 return True |
|
386 return False |
|
387 if entity.has_eid() or attr in entity: |
|
388 value = getattr(entity, attr) |
|
389 else: |
|
390 value = self._form_field_default_value(field, load_bytes) |
|
391 return value |
|
392 # non final relation field |
|
393 if entity.has_eid() or entity.relation_cached(attr, field.role): |
|
394 value = [r[0] for r in entity.related(attr, field.role)] |
|
395 else: |
|
396 value = self._form_field_default_value(field, load_bytes) |
|
397 return value |
|
398 |
|
399 def form_field_format(self, field): |
|
400 """return MIME type used for the given (text or bytes) field""" |
|
401 entity = self.edited_entity |
|
402 if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and ( |
|
403 entity.has_eid() or '%s_format' % field.name in entity): |
|
404 return self.edited_entity.attr_metadata(field.name, 'format') |
|
405 return self.req.property_value('ui.default-text-format') |
|
406 |
|
407 def form_field_encoding(self, field): |
|
408 """return encoding used for the given (text) field""" |
|
409 entity = self.edited_entity |
|
410 if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and ( |
|
411 entity.has_eid() or '%s_encoding' % field.name in entity): |
|
412 return self.edited_entity.attr_metadata(field.name, 'encoding') |
|
413 return super(EntityFieldsForm, self).form_field_encoding(field) |
|
414 |
|
415 def form_field_name(self, field): |
|
416 """return qualified name for the given field""" |
|
417 if field.eidparam: |
|
418 return eid_param(field.name, self.edited_entity.eid) |
|
419 return field.name |
|
420 |
|
421 def form_field_id(self, field): |
|
422 """return dom id for the given field""" |
|
423 if field.eidparam: |
|
424 return eid_param(field.id, self.edited_entity.eid) |
|
425 return field.id |
|
426 |
|
427 def form_field_vocabulary(self, field, limit=None): |
|
428 """return vocabulary for the given field""" |
|
429 role, rtype = field.role, field.name |
|
430 method = '%s_%s_vocabulary' % (role, rtype) |
|
431 try: |
|
432 vocabfunc = getattr(self, method) |
|
433 except AttributeError: |
|
434 try: |
|
435 # XXX bw compat, <role>_<rtype>_vocabulary on the entity |
|
436 vocabfunc = getattr(self.edited_entity, method) |
|
437 except AttributeError: |
|
438 vocabfunc = getattr(self, '%s_relation_vocabulary' % role) |
|
439 else: |
|
440 warn('found %s on %s, should be set on a specific form' |
|
441 % (method, self.edited_entity.id), DeprecationWarning) |
|
442 # NOTE: it is the responsibility of `vocabfunc` to sort the result |
|
443 # (direclty through RQL or via a python sort). This is also |
|
444 # important because `vocabfunc` might return a list with |
|
445 # couples (label, None) which act as separators. In these |
|
446 # cases, it doesn't make sense to sort results afterwards. |
|
447 return vocabfunc(rtype, limit) |
|
448 |
|
449 def subject_relation_vocabulary(self, rtype, limit=None): |
|
450 """defaut vocabulary method for the given relation, looking for |
|
451 relation's object entities (i.e. self is the subject) |
|
452 """ |
|
453 entity = self.edited_entity |
|
454 if isinstance(rtype, basestring): |
|
455 rtype = entity.schema.rschema(rtype) |
|
456 done = None |
|
457 assert not rtype.is_final(), rtype |
|
458 if entity.has_eid(): |
|
459 done = set(e.eid for e in getattr(entity, str(rtype))) |
|
460 result = [] |
|
461 rsetsize = None |
|
462 for objtype in rtype.objects(entity.e_schema): |
|
463 if limit is not None: |
|
464 rsetsize = limit - len(result) |
|
465 result += self._relation_vocabulary(rtype, objtype, 'subject', |
|
466 rsetsize, done) |
|
467 if limit is not None and len(result) >= limit: |
|
468 break |
|
469 return result |
|
470 |
|
471 def object_relation_vocabulary(self, rtype, limit=None): |
|
472 """defaut vocabulary method for the given relation, looking for |
|
473 relation's subject entities (i.e. self is the object) |
|
474 """ |
|
475 entity = self.edited_entity |
|
476 if isinstance(rtype, basestring): |
|
477 rtype = entity.schema.rschema(rtype) |
|
478 done = None |
|
479 if entity.has_eid(): |
|
480 done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype)) |
|
481 result = [] |
|
482 rsetsize = None |
|
483 for subjtype in rtype.subjects(entity.e_schema): |
|
484 if limit is not None: |
|
485 rsetsize = limit - len(result) |
|
486 result += self._relation_vocabulary(rtype, subjtype, 'object', |
|
487 rsetsize, done) |
|
488 if limit is not None and len(result) >= limit: |
|
489 break |
|
490 return result |
|
491 |
|
492 def subject_in_state_vocabulary(self, rtype, limit=None): |
|
493 """vocabulary method for the in_state relation, looking for relation's |
|
494 object entities (i.e. self is the subject) according to initial_state, |
|
495 state_of and next_state relation |
|
496 """ |
|
497 entity = self.edited_entity |
|
498 if not entity.has_eid() or not entity.in_state: |
|
499 # get the initial state |
|
500 rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S' |
|
501 rset = self.req.execute(rql, {'etype': str(entity.e_schema)}) |
|
502 if rset: |
|
503 return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])] |
|
504 return [] |
|
505 results = [] |
|
506 for tr in entity.in_state[0].transitions(entity): |
|
507 state = tr.destination_state[0] |
|
508 results.append((state.view('combobox'), state.eid)) |
|
509 return sorted(results) |
|
510 |
|
511 |
|
512 class CompositeForm(FieldsForm): |
|
513 """form composed for sub-forms""" |
|
514 id = 'composite' |
|
515 form_renderer_id = id |
|
516 |
|
517 def __init__(self, *args, **kwargs): |
|
518 super(CompositeForm, self).__init__(*args, **kwargs) |
|
519 self.forms = [] |
|
520 |
|
521 def form_add_subform(self, subform): |
|
522 """mark given form as a subform and append it""" |
|
523 subform.is_subform = True |
|
524 self.forms.append(subform) |