210 class FieldNotFound(Exception): |
200 class FieldNotFound(Exception): |
211 """raised by field_by_name when a field with the given name has not been |
201 """raised by field_by_name when a field with the given name has not been |
212 found |
202 found |
213 """ |
203 """ |
214 |
204 |
215 class FieldsForm(FormMixIn, AppRsetObject): |
205 class Form(FormMixIn, AppRsetObject): |
216 __metaclass__ = metafieldsform |
206 __metaclass__ = metafieldsform |
217 __registry__ = 'forms' |
207 __registry__ = 'forms' |
218 __select__ = yes() |
|
219 |
|
220 is_subform = False |
|
221 |
|
222 # attributes overrideable through __init__ |
|
223 internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS |
|
224 needs_js = ('cubicweb.ajax.js', 'cubicweb.edition.js',) |
|
225 needs_css = ('cubicweb.form.css',) |
|
226 domid = 'form' |
|
227 title = None |
|
228 action = None |
|
229 onsubmit = "return freezeFormButtons('%(domid)s');" |
|
230 cssclass = None |
|
231 cssstyle = None |
|
232 cwtarget = None |
|
233 redirect_path = None |
|
234 set_error_url = True |
|
235 copy_nav_params = False |
|
236 form_buttons = None # form buttons (button widgets instances) |
|
237 form_renderer_id = 'default' |
|
238 |
|
239 def __init__(self, req, rset=None, row=None, col=None, submitmsg=None, |
|
240 **kwargs): |
|
241 super(FieldsForm, self).__init__(req, rset, row=row, col=col) |
|
242 self.fields = list(self.__class__._fields_) |
|
243 for key, val in kwargs.items(): |
|
244 if key in NAV_FORM_PARAMETERS: |
|
245 self.form_add_hidden(key, val) |
|
246 else: |
|
247 assert hasattr(self.__class__, key) and not key[0] == '_', key |
|
248 setattr(self, key, val) |
|
249 if self.set_error_url: |
|
250 self.form_add_hidden('__errorurl', self.session_key()) |
|
251 if self.copy_nav_params: |
|
252 for param in NAV_FORM_PARAMETERS: |
|
253 if not param in kwargs: |
|
254 value = req.form.get(param) |
|
255 if value: |
|
256 self.form_add_hidden(param, value) |
|
257 if submitmsg is not None: |
|
258 self.form_add_hidden('__message', submitmsg) |
|
259 self.context = None |
|
260 if 'domid' in kwargs:# session key changed |
|
261 self.restore_previous_post(self.session_key()) |
|
262 |
|
263 @iclassmethod |
|
264 def _fieldsattr(cls_or_self): |
|
265 if isinstance(cls_or_self, type): |
|
266 fields = cls_or_self._fields_ |
|
267 else: |
|
268 fields = cls_or_self.fields |
|
269 return fields |
|
270 |
|
271 @iclassmethod |
|
272 def field_by_name(cls_or_self, name, role='subject'): |
|
273 """return field with the given name and role. |
|
274 Raise FieldNotFound if the field can't be found. |
|
275 """ |
|
276 for field in cls_or_self._fieldsattr(): |
|
277 if field.name == name and field.role == role: |
|
278 return field |
|
279 raise FieldNotFound(name) |
|
280 |
|
281 @iclassmethod |
|
282 def fields_by_name(cls_or_self, name, role='subject'): |
|
283 """return a list of fields with the given name and role""" |
|
284 return [field for field in cls_or_self._fieldsattr() |
|
285 if field.name == name and field.role == role] |
|
286 |
|
287 @iclassmethod |
|
288 def remove_field(cls_or_self, field): |
|
289 """remove a field from form class or instance""" |
|
290 cls_or_self._fieldsattr().remove(field) |
|
291 |
|
292 @iclassmethod |
|
293 def append_field(cls_or_self, field): |
|
294 """append a field to form class or instance""" |
|
295 cls_or_self._fieldsattr().append(field) |
|
296 |
|
297 @iclassmethod |
|
298 def insert_field_before(cls_or_self, new_field, name, role='subject'): |
|
299 field = cls_or_self.field_by_name(name, role) |
|
300 fields = cls_or_self._fieldsattr() |
|
301 fields.insert(fields.index(field), new_field) |
|
302 |
|
303 @iclassmethod |
|
304 def insert_field_after(cls_or_self, new_field, name, role='subject'): |
|
305 field = cls_or_self.field_by_name(name, role) |
|
306 fields = cls_or_self._fieldsattr() |
|
307 fields.insert(fields.index(field)+1, new_field) |
|
308 |
|
309 @property |
|
310 def form_needs_multipart(self): |
|
311 """true if the form needs enctype=multipart/form-data""" |
|
312 return any(field.needs_multipart for field in self.fields) |
|
313 |
|
314 def form_add_hidden(self, name, value=None, **kwargs): |
|
315 """add an hidden field to the form""" |
|
316 field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value, |
|
317 **kwargs) |
|
318 if 'id' in kwargs: |
|
319 # by default, hidden input don't set id attribute. If one is |
|
320 # explicitly specified, ensure it will be set |
|
321 field.widget.setdomid = True |
|
322 self.append_field(field) |
|
323 return field |
|
324 |
|
325 def add_media(self): |
|
326 """adds media (CSS & JS) required by this widget""" |
|
327 if self.needs_js: |
|
328 self.req.add_js(self.needs_js) |
|
329 if self.needs_css: |
|
330 self.req.add_css(self.needs_css) |
|
331 |
|
332 def form_render(self, **values): |
|
333 """render this form, using the renderer given in args or the default |
|
334 FormRenderer() |
|
335 """ |
|
336 renderer = values.pop('renderer', None) |
|
337 if renderer is None: |
|
338 renderer = self.form_default_renderer() |
|
339 return renderer.render(self, values) |
|
340 |
|
341 def form_default_renderer(self): |
|
342 return self.vreg.select_object('formrenderers', self.form_renderer_id, |
|
343 self.req, self.rset, |
|
344 row=self.row, col=self.col) |
|
345 |
|
346 def form_build_context(self, rendervalues=None): |
|
347 """build form context values (the .context attribute which is a |
|
348 dictionary with field instance as key associated to a dictionary |
|
349 containing field 'name' (qualified), 'id', 'value' (for display, always |
|
350 a string). |
|
351 |
|
352 rendervalues is an optional dictionary containing extra kwargs given to |
|
353 form_render() |
|
354 """ |
|
355 self.context = context = {} |
|
356 # ensure rendervalues is a dict |
|
357 if rendervalues is None: |
|
358 rendervalues = {} |
|
359 # use a copy in case fields are modified while context is build (eg |
|
360 # __linkto handling for instance) |
|
361 for field in self.fields[:]: |
|
362 for field in field.actual_fields(self): |
|
363 field.form_init(self) |
|
364 value = self.form_field_display_value(field, rendervalues) |
|
365 context[field] = {'value': value, |
|
366 'name': self.form_field_name(field), |
|
367 'id': self.form_field_id(field), |
|
368 } |
|
369 |
|
370 def form_field_display_value(self, field, rendervalues, load_bytes=False): |
|
371 """return field's *string* value to use for display |
|
372 |
|
373 looks in |
|
374 1. previously submitted form values if any (eg on validation error) |
|
375 2. req.form |
|
376 3. extra kw args given to render_form |
|
377 4. field's typed value |
|
378 |
|
379 values found in 1. and 2. are expected te be already some 'display' |
|
380 value while those found in 3. and 4. are expected to be correctly typed. |
|
381 """ |
|
382 value = self._req_display_value(field) |
|
383 if value is None: |
|
384 if field.name in rendervalues: |
|
385 value = rendervalues[field.name] |
|
386 else: |
|
387 value = self.form_field_value(field, load_bytes) |
|
388 if callable(value): |
|
389 value = value(self) |
|
390 if value != INTERNAL_FIELD_VALUE: |
|
391 value = field.format_value(self.req, value) |
|
392 return value |
|
393 |
|
394 def _req_display_value(self, field): |
|
395 qname = self.form_field_name(field) |
|
396 if qname in self.form_previous_values: |
|
397 return self.form_previous_values[qname] |
|
398 if qname in self.req.form: |
|
399 return self.req.form[qname] |
|
400 if field.name in self.req.form: |
|
401 return self.req.form[field.name] |
|
402 return None |
|
403 |
|
404 def form_field_value(self, field, load_bytes=False): |
|
405 """return field's *typed* value""" |
|
406 myattr = '%s_%s_default' % (field.role, field.name) |
|
407 if hasattr(self, myattr): |
|
408 return getattr(self, myattr)() |
|
409 value = field.initial |
|
410 if callable(value): |
|
411 value = value(self) |
|
412 return value |
|
413 |
|
414 def form_field_error(self, field): |
|
415 """return validation error for widget's field, if any""" |
|
416 if self._field_has_error(field): |
|
417 self.form_displayed_errors.add(field.name) |
|
418 return u'<span class="error">%s</span>' % self.form_valerror.errors[field.name] |
|
419 return u'' |
|
420 |
|
421 def form_field_format(self, field): |
|
422 """return MIME type used for the given (text or bytes) field""" |
|
423 return self.req.property_value('ui.default-text-format') |
|
424 |
|
425 def form_field_encoding(self, field): |
|
426 """return encoding used for the given (text) field""" |
|
427 return self.req.encoding |
|
428 |
|
429 def form_field_name(self, field): |
|
430 """return qualified name for the given field""" |
|
431 return field.name |
|
432 |
|
433 def form_field_id(self, field): |
|
434 """return dom id for the given field""" |
|
435 return field.id |
|
436 |
|
437 def form_field_vocabulary(self, field, limit=None): |
|
438 """return vocabulary for the given field. Should be overriden in |
|
439 specific forms using fields which requires some vocabulary |
|
440 """ |
|
441 raise NotImplementedError |
|
442 |
|
443 def _field_has_error(self, field): |
|
444 """return true if the field has some error in given validation exception |
|
445 """ |
|
446 return self.form_valerror and field.name in self.form_valerror.errors |
|
447 |
|
448 |
|
449 class EntityFieldsForm(FieldsForm): |
|
450 __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity())) |
|
451 |
|
452 internal_fields = FieldsForm.internal_fields + ('__type', 'eid', '__maineid') |
|
453 domid = 'entityForm' |
|
454 |
|
455 def __init__(self, *args, **kwargs): |
|
456 self.edited_entity = kwargs.pop('entity', None) |
|
457 msg = kwargs.pop('submitmsg', None) |
|
458 super(EntityFieldsForm, self).__init__(*args, **kwargs) |
|
459 if self.edited_entity is None: |
|
460 self.edited_entity = self.complete_entity(self.row or 0, self.col or 0) |
|
461 self.form_add_hidden('__type', eidparam=True) |
|
462 self.form_add_hidden('eid') |
|
463 if msg: |
|
464 # If we need to directly attach the new object to another one |
|
465 self.form_add_hidden('__message', msg) |
|
466 if not self.is_subform: |
|
467 for linkto in self.req.list_form_param('__linkto'): |
|
468 self.form_add_hidden('__linkto', linkto) |
|
469 msg = '%s %s' % (msg, self.req._('and linked')) |
|
470 # in case of direct instanciation |
|
471 self.schema = self.edited_entity.schema |
|
472 self.vreg = self.edited_entity.vreg |
|
473 |
|
474 def _field_has_error(self, field): |
|
475 """return true if the field has some error in given validation exception |
|
476 """ |
|
477 return super(EntityFieldsForm, self)._field_has_error(field) \ |
|
478 and self.form_valerror.eid == self.edited_entity.eid |
|
479 |
|
480 def _relation_vocabulary(self, rtype, targettype, role, |
|
481 limit=None, done=None): |
|
482 """return unrelated entities for a given relation and target entity type |
|
483 for use in vocabulary |
|
484 """ |
|
485 if done is None: |
|
486 done = set() |
|
487 rset = self.edited_entity.unrelated(rtype, targettype, role, limit) |
|
488 res = [] |
|
489 for entity in rset.entities(): |
|
490 if entity.eid in done: |
|
491 continue |
|
492 done.add(entity.eid) |
|
493 res.append((entity.view('combobox'), entity.eid)) |
|
494 return res |
|
495 |
|
496 def _req_display_value(self, field): |
|
497 value = super(EntityFieldsForm, self)._req_display_value(field) |
|
498 if value is None: |
|
499 value = self.edited_entity.linked_to(field.name, field.role) |
|
500 if value: |
|
501 searchedvalues = ['%s:%s:%s' % (field.name, eid, field.role) |
|
502 for eid in value] |
|
503 # remove associated __linkto hidden fields |
|
504 for field in self.fields_by_name('__linkto'): |
|
505 if field.initial in searchedvalues: |
|
506 self.remove_field(field) |
|
507 else: |
|
508 value = None |
|
509 return value |
|
510 |
|
511 def _form_field_default_value(self, field, load_bytes): |
|
512 defaultattr = 'default_%s' % field.name |
|
513 if hasattr(self.edited_entity, defaultattr): |
|
514 # XXX bw compat, default_<field name> on the entity |
|
515 warn('found %s on %s, should be set on a specific form' |
|
516 % (defaultattr, self.edited_entity.id), DeprecationWarning) |
|
517 value = getattr(self.edited_entity, defaultattr) |
|
518 if callable(value): |
|
519 value = value() |
|
520 else: |
|
521 value = super(EntityFieldsForm, self).form_field_value(field, |
|
522 load_bytes) |
|
523 return value |
|
524 |
|
525 def form_default_renderer(self): |
|
526 return self.vreg.select_object('formrenderers', self.form_renderer_id, |
|
527 self.req, self.rset, |
|
528 row=self.row, col=self.col, |
|
529 entity=self.edited_entity) |
|
530 |
|
531 def form_build_context(self, values=None): |
|
532 """overriden to add edit[s|o] hidden fields and to ensure schema fields |
|
533 have eidparam set to True |
|
534 |
|
535 edit[s|o] hidden fields are used to indicate the value for the |
|
536 associated field before the (potential) modification made when |
|
537 submitting the form. |
|
538 """ |
|
539 eschema = self.edited_entity.e_schema |
|
540 for field in self.fields[:]: |
|
541 for field in field.actual_fields(self): |
|
542 fieldname = field.name |
|
543 if fieldname != 'eid' and ( |
|
544 (eschema.has_subject_relation(fieldname) or |
|
545 eschema.has_object_relation(fieldname))): |
|
546 field.eidparam = True |
|
547 self.fields.append(HiddenInitialValueField(field)) |
|
548 return super(EntityFieldsForm, self).form_build_context(values) |
|
549 |
|
550 def form_field_value(self, field, load_bytes=False): |
|
551 """return field's *typed* value |
|
552 |
|
553 overriden to deal with |
|
554 * special eid / __type / edits- / edito- fields |
|
555 * lookup for values on edited entities |
|
556 """ |
|
557 attr = field.name |
|
558 entity = self.edited_entity |
|
559 if attr == 'eid': |
|
560 return entity.eid |
|
561 if not field.eidparam: |
|
562 return super(EntityFieldsForm, self).form_field_value(field, load_bytes) |
|
563 if attr.startswith('edits-') or attr.startswith('edito-'): |
|
564 # edit[s|o]- fieds must have the actual value stored on the entity |
|
565 assert hasattr(field, 'visible_field') |
|
566 vfield = field.visible_field |
|
567 assert vfield.eidparam |
|
568 if entity.has_eid(): |
|
569 return self.form_field_value(vfield) |
|
570 return INTERNAL_FIELD_VALUE |
|
571 if attr == '__type': |
|
572 return entity.id |
|
573 if self.schema.rschema(attr).is_final(): |
|
574 attrtype = entity.e_schema.destination(attr) |
|
575 if attrtype == 'Password': |
|
576 return entity.has_eid() and INTERNAL_FIELD_VALUE or '' |
|
577 if attrtype == 'Bytes': |
|
578 if entity.has_eid(): |
|
579 if load_bytes: |
|
580 return getattr(entity, attr) |
|
581 # XXX value should reflect if some file is already attached |
|
582 return True |
|
583 return False |
|
584 if entity.has_eid() or attr in entity: |
|
585 value = getattr(entity, attr) |
|
586 else: |
|
587 value = self._form_field_default_value(field, load_bytes) |
|
588 return value |
|
589 # non final relation field |
|
590 if entity.has_eid() or entity.relation_cached(attr, field.role): |
|
591 value = [r[0] for r in entity.related(attr, field.role)] |
|
592 else: |
|
593 value = self._form_field_default_value(field, load_bytes) |
|
594 return value |
|
595 |
|
596 def form_field_format(self, field): |
|
597 """return MIME type used for the given (text or bytes) field""" |
|
598 entity = self.edited_entity |
|
599 if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and ( |
|
600 entity.has_eid() or '%s_format' % field.name in entity): |
|
601 return self.edited_entity.attr_metadata(field.name, 'format') |
|
602 return self.req.property_value('ui.default-text-format') |
|
603 |
|
604 def form_field_encoding(self, field): |
|
605 """return encoding used for the given (text) field""" |
|
606 entity = self.edited_entity |
|
607 if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and ( |
|
608 entity.has_eid() or '%s_encoding' % field.name in entity): |
|
609 return self.edited_entity.attr_metadata(field.name, 'encoding') |
|
610 return super(EntityFieldsForm, self).form_field_encoding(field) |
|
611 |
|
612 def form_field_name(self, field): |
|
613 """return qualified name for the given field""" |
|
614 if field.eidparam: |
|
615 return eid_param(field.name, self.edited_entity.eid) |
|
616 return field.name |
|
617 |
|
618 def form_field_id(self, field): |
|
619 """return dom id for the given field""" |
|
620 if field.eidparam: |
|
621 return eid_param(field.id, self.edited_entity.eid) |
|
622 return field.id |
|
623 |
|
624 def form_field_vocabulary(self, field, limit=None): |
|
625 """return vocabulary for the given field""" |
|
626 role, rtype = field.role, field.name |
|
627 method = '%s_%s_vocabulary' % (role, rtype) |
|
628 try: |
|
629 vocabfunc = getattr(self, method) |
|
630 except AttributeError: |
|
631 try: |
|
632 # XXX bw compat, <role>_<rtype>_vocabulary on the entity |
|
633 vocabfunc = getattr(self.edited_entity, method) |
|
634 except AttributeError: |
|
635 vocabfunc = getattr(self, '%s_relation_vocabulary' % role) |
|
636 else: |
|
637 warn('found %s on %s, should be set on a specific form' |
|
638 % (method, self.edited_entity.id), DeprecationWarning) |
|
639 # NOTE: it is the responsibility of `vocabfunc` to sort the result |
|
640 # (direclty through RQL or via a python sort). This is also |
|
641 # important because `vocabfunc` might return a list with |
|
642 # couples (label, None) which act as separators. In these |
|
643 # cases, it doesn't make sense to sort results afterwards. |
|
644 return vocabfunc(rtype, limit) |
|
645 |
|
646 def subject_relation_vocabulary(self, rtype, limit=None): |
|
647 """defaut vocabulary method for the given relation, looking for |
|
648 relation's object entities (i.e. self is the subject) |
|
649 """ |
|
650 entity = self.edited_entity |
|
651 if isinstance(rtype, basestring): |
|
652 rtype = entity.schema.rschema(rtype) |
|
653 done = None |
|
654 assert not rtype.is_final(), rtype |
|
655 if entity.has_eid(): |
|
656 done = set(e.eid for e in getattr(entity, str(rtype))) |
|
657 result = [] |
|
658 rsetsize = None |
|
659 for objtype in rtype.objects(entity.e_schema): |
|
660 if limit is not None: |
|
661 rsetsize = limit - len(result) |
|
662 result += self._relation_vocabulary(rtype, objtype, 'subject', |
|
663 rsetsize, done) |
|
664 if limit is not None and len(result) >= limit: |
|
665 break |
|
666 return result |
|
667 |
|
668 def object_relation_vocabulary(self, rtype, limit=None): |
|
669 """defaut vocabulary method for the given relation, looking for |
|
670 relation's subject entities (i.e. self is the object) |
|
671 """ |
|
672 entity = self.edited_entity |
|
673 if isinstance(rtype, basestring): |
|
674 rtype = entity.schema.rschema(rtype) |
|
675 done = None |
|
676 if entity.has_eid(): |
|
677 done = set(e.eid for e in getattr(entity, 'reverse_%s' % rtype)) |
|
678 result = [] |
|
679 rsetsize = None |
|
680 for subjtype in rtype.subjects(entity.e_schema): |
|
681 if limit is not None: |
|
682 rsetsize = limit - len(result) |
|
683 result += self._relation_vocabulary(rtype, subjtype, 'object', |
|
684 rsetsize, done) |
|
685 if limit is not None and len(result) >= limit: |
|
686 break |
|
687 return result |
|
688 |
|
689 def subject_in_state_vocabulary(self, rtype, limit=None): |
|
690 """vocabulary method for the in_state relation, looking for relation's |
|
691 object entities (i.e. self is the subject) according to initial_state, |
|
692 state_of and next_state relation |
|
693 """ |
|
694 entity = self.edited_entity |
|
695 if not entity.has_eid() or not entity.in_state: |
|
696 # get the initial state |
|
697 rql = 'Any S where S state_of ET, ET name %(etype)s, ET initial_state S' |
|
698 rset = self.req.execute(rql, {'etype': str(entity.e_schema)}) |
|
699 if rset: |
|
700 return [(rset.get_entity(0, 0).view('combobox'), rset[0][0])] |
|
701 return [] |
|
702 results = [] |
|
703 for tr in entity.in_state[0].transitions(entity): |
|
704 state = tr.destination_state[0] |
|
705 results.append((state.view('combobox'), state.eid)) |
|
706 return sorted(results) |
|
707 |
|
708 |
|
709 class CompositeForm(FieldsForm): |
|
710 """form composed for sub-forms""" |
|
711 form_renderer_id = 'composite' |
|
712 |
|
713 def __init__(self, *args, **kwargs): |
|
714 super(CompositeForm, self).__init__(*args, **kwargs) |
|
715 self.forms = [] |
|
716 |
|
717 def form_add_subform(self, subform): |
|
718 """mark given form as a subform and append it""" |
|
719 subform.is_subform = True |
|
720 self.forms.append(subform) |
|