# HG changeset patch # User Sylvain Thénault # Date 1261421124 -3600 # Node ID 6b2b20c73d5959531e485b5ed11cac79b4d4a695 # Parent 0e97cf2cf55b80cc1fcadc562ebbd1e0600b0dc2 refactor form field value handling, to get a nicer api and an easier algorithm to get field's value Details: * new .typed_value / .display_value on fields * droped form_field_value on form * .value attribute of field instead of .initial * nicer field's __init__, allowing to give a lambda as value's value diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/formfields.py --- a/web/formfields.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/formfields.py Mon Dec 21 19:45:24 2009 +0100 @@ -39,6 +39,7 @@ result += sorted(partresult) return result +_MARKER = object() class Field(object): """field class is introduced to control what's displayed in forms. It makes @@ -64,8 +65,8 @@ class which may be overriden per instance. :required: bool flag telling if the field is required or not. - :initial: - initial value, used when no value specified by other means. + :value: + field's value, used when no value specified by other means. XXX explain :choices: static vocabulary for this field. May be a list of values or a list of (label, value) tuples if specified. @@ -95,32 +96,38 @@ # class attribute used for ordering of fields in a form __creation_rank = 0 - def __init__(self, name=None, id=None, label=None, help=None, - widget=None, required=False, initial=None, - choices=None, sort=True, internationalizable=False, - eidparam=False, role='subject', fieldset=None, order=None): + eidparam = False + role = None + id = None + help = None + required = False + choices = None + sort = True + internationalizable = False + fieldset = None + order = None + value = _MARKER + + def __init__(self, name=None, label=None, widget=None, **kwargs): + for key, val in kwargs.items(): + if key == 'initial': + warn('[3.6] use value instead of initial', DeprecationWarning, + stacklevel=3) + key = 'value' + assert hasattr(self.__class__, key) and not key[0] == '_', key + setattr(self, key, val) self.name = name - self.id = id or name self.label = label or name - self.help = help - self.required = required - self.initial = initial - self.choices = choices - self.sort = sort - self.internationalizable = internationalizable - self.eidparam = eidparam - self.role = role - self.fieldset = fieldset + # has to be done after other attributes initialization self.init_widget(widget) - self.order = order # ordering number for this field instance self.creation_rank = Field.__creation_rank Field.__creation_rank += 1 def __unicode__(self): - return u'<%s name=%r label=%r id=%r initial=%r visible=%r @%x>' % ( - self.__class__.__name__, self.name, self.label, - self.id, self.initial, self.is_visible(), id(self)) + return u'<%s name=%r eidparam=%s role=%r id=%r value=%r visible=%r @%x>' % ( + self.__class__.__name__, self.name, self.eidparam, self.role, + self.id, self.value, self.is_visible(), id(self)) def __repr__(self): return self.__unicode__().encode('utf-8') @@ -134,11 +141,9 @@ self.widget = self.widget() def set_name(self, name): - """automatically set .id and .label when name is set""" + """automatically set .label when name is set""" assert name self.name = name - if not self.id: - self.id = name if not self.label: self.label = name @@ -200,6 +205,62 @@ return eid_param(id, form.edited_entity.eid) return id + def display_value(self, form): + """return field's *string* value to use for display + + looks in + 1. previously submitted form values if any (eg on validation error) + 2. req.form + 3. extra form args given to render_form + 4. field's typed value + + values found in 1. and 2. are expected te be already some 'display' + value while those found in 3. and 4. are expected to be correctly typed. + """ + qname = self.input_name(form) + if qname in form.form_previous_values: + return form.form_previous_values[qname] + if qname in form._cw.form: + return form._cw.form[qname] + if self.name != qname and self.name in form._cw.form: + return form._cw.form[self.name] + for key in (self, qname): + try: + value = form.formvalues[key] + break + except: + continue + else: + if self.name != qname and self.name in form.formvalues: + value = form.formvalues[self.name] + else: + value = self.typed_value(form) + if value != INTERNAL_FIELD_VALUE: + value = self.format_value(form._cw, value) + return value + + def typed_value(self, form, load_bytes=False): + if self.value is not _MARKER: + if callable(self.value): + return self.value(form) + return self.value + return self._typed_value(form, load_bytes) + + def _typed_value(self, form, load_bytes=False): + if self.eidparam: + assert form._cw.vreg.schema.rschema(self.name).final + entity = form.edited_entity + if entity.has_eid() or self.name in entity: + return getattr(entity, self.name) + formattr = '%s_%s_default' % (self.role, self.name) + if hasattr(form, formattr): + warn('[3.6] %s.%s deprecated, use field.value' % ( + form.__class__.__name__, formattr), DeprecationWarning) + return getattr(form, formattr)() + if self.eidparam: + return entity.e_schema.default(self.name) + return None + def example_format(self, req): """return a sample string describing what can be given as input for this field @@ -292,6 +353,18 @@ widget.attrs.setdefault('rows', 5) +class PasswordField(StringField): + widget = PasswordInput + + def _typed_value(self, form, load_bytes=False): + if self.eidparam: + # no way to fetch actual password value with cw + if form.edited_entity.has_eid(): + return INTERNAL_FIELD_VALUE + return form.edited_entity.e_schema.default(self.name) + return super(PasswordField, self)._typed_value(form, load_bytes) + + class RichTextField(StringField): widget = None def __init__(self, format_field=None, **kwargs): @@ -324,14 +397,14 @@ # if fckeditor is used and format field isn't explicitly # deactivated, we want an hidden field for the format fkwargs['widget'] = HiddenInput() - fkwargs['initial'] = 'text/html' + fkwargs['value'] = 'text/html' else: # else we want a format selector fkwargs['widget'] = Select() fcstr = FormatConstraint() fkwargs['choices'] = fcstr.vocabulary(form=form) fkwargs['internationalizable'] = True - fkwargs['initial'] = lambda f: f.form_field_format(self) + fkwargs['value'] = self.format fkwargs['eidparam'] = self.eidparam field = StringField(name=self.name + '_format', **fkwargs) req.data[self] = field @@ -383,6 +456,19 @@ if self.name_field: yield self.name_field + def _typed_value(self, form, load_bytes=False): + if self.eidparam: + if form.edited_entity.has_eid(): + if load_bytes: + return getattr(form.edited_entity, self.name) + # don't actually load data + # XXX value should reflect if some file is already attached + # * try to display name metadata + # * check length(data) / data != null + return True + return False + return super(FileField, self)._typed_value(form, load_bytes) + def render(self, form, renderer): wdgs = [self.get_widget(form).render(form, self, renderer)] if self.format_field or self.encoding_field: @@ -400,7 +486,7 @@ if self.encoding_field: wdgs.append(self.render_subfield(form, self.encoding_field, renderer)) wdgs.append(u'') - if not self.required and form.context[self]['value']: + if not self.required and self.display_value(form): # trick to be able to delete an uploaded file wdgs.append(u'
') wdgs.append(tags.input(name=self.input_name(form, u'__detach'), @@ -439,12 +525,12 @@ def render(self, form, renderer): wdgs = [super(EditableFileField, self).render(form, renderer)] - if form.form_field_format(self) in self.editable_formats: - data = form.form_field_value(self, load_bytes=True) + if self.format(form) in self.editable_formats: + data = self.typed_value(form, load_bytes=True) if data: encoding = form.form_field_encoding(self) try: - form.context[self]['value'] = unicode(data.getvalue(), encoding) + form.formvalues[self] = unicode(data.getvalue(), encoding) except UnicodeError: pass else: @@ -592,6 +678,25 @@ vocab = vocab_sort(vocab) return vocab + def form_init(self, form): + if not self.display_value(form): + value = form.edited_entity.linked_to(self.name, self.role) + if value: + searchedvalues = ['%s:%s:%s' % (self.name, eid, self.role) + for eid in value] + # remove associated __linkto hidden fields + for field in form.root_form.fields_by_name('__linkto'): + if field.value in searchedvalues: + form.root_form.remove_field(field) + form.formvalues[self] = value + + def _typed_value(self, form, load_bytes=False): + entity = form.edited_entity + # non final relation field + if entity.has_eid() or entity.relation_cached(self.name, self.role): + return [r[0] for r in entity.related(self.name, self.role)] + return () + def format_single_value(self, req, value): return value @@ -628,9 +733,6 @@ if rschema.final: if rdef.get('internationalizable'): kwargs.setdefault('internationalizable', True) - def get_default(form, es=eschema, rs=rschema): - return es.default(rs) - kwargs.setdefault('initial', get_default) else: targetschema = rdef.subject card = rdef.role_cardinality(role) @@ -647,10 +749,6 @@ return None fieldclass = FIELDS[targetschema] if fieldclass is StringField: - if targetschema == 'Password': - # special case for Password field: specific PasswordInput widget - kwargs.setdefault('widget', PasswordInput()) - return StringField(**kwargs) if eschema.has_metadata(rschema, 'format'): # use RichTextField instead of StringField if the attribute has # a "format" metadata. But getting information from constraints @@ -687,7 +785,7 @@ 'Int': IntField, 'Float': FloatField, 'Decimal': StringField, - 'Password': StringField, + 'Password': PasswordField, 'String' : StringField, 'Time': TimeField, } diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/formwidgets.py --- a/web/formwidgets.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/formwidgets.py Mon Dec 21 19:45:24 2009 +0100 @@ -345,7 +345,7 @@ return txtwidget + cal_button def _render_calendar_popup(self, form, field): - value = form.form_field_value(field) + value = field.typed_value(form) if not value: value = date.today() inputid = field.dom_id(form) diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/views/cwproperties.py --- a/web/views/cwproperties.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/views/cwproperties.py Mon Dec 21 19:45:24 2009 +0100 @@ -335,7 +335,7 @@ self.widget = NotEditableWidget(entity.printable_value('value'), msg) # XXX race condition when used from CWPropertyForm, should not rely on # instance attributes - self.initial = pdef['default'] + self.value = pdef['default'] self.help = pdef['help'] vocab = pdef['vocabulary'] if vocab is not None: diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/views/forms.py --- a/web/views/forms.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/views/forms.py Mon Dec 21 19:45:24 2009 +0100 @@ -12,6 +12,7 @@ from logilab.common.compat import any from logilab.common.deprecation import deprecated +from cubicweb import typed_eid from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param from cubicweb.web import form, formwidgets as fwdgs @@ -113,7 +114,7 @@ def form_add_hidden(self, name, value=None, **kwargs): """add an hidden field to the form""" kwargs.setdefault('widget', fwdgs.HiddenInput) - field = StringField(name=name, initial=value, **kwargs) + field = StringField(name=name, value=value, **kwargs) if 'id' in kwargs: # by default, hidden input don't set id attribute. If one is # explicitly specified, ensure it will be set @@ -142,77 +143,21 @@ self._cw, rset=self.cw_rset, row=self.cw_row, col=self.cw_col) - def build_context(self, rendervalues=None): + formvalues = None + def build_context(self, formvalues=None): """build form context values (the .context attribute which is a dictionary with field instance as key associated to a dictionary containing field 'name' (qualified), 'id', 'value' (for display, always a string). - - rendervalues is an optional dictionary containing extra form values - given to render() """ - if self.context is not None: + if self.formvalues is not None: return # already built - self.context = context = {} - # ensure rendervalues is a dict - if rendervalues is None: - rendervalues = {} + self.formvalues = formvalues or {} # use a copy in case fields are modified while context is build (eg # __linkto handling for instance) for field in self.fields[:]: for field in field.actual_fields(self): field.form_init(self) - value = self.form_field_display_value(field, rendervalues) - context[field] = {'value': value, - 'name': self.form_field_name(field), - 'id': self.form_field_id(field), - } - - def form_field_display_value(self, field, rendervalues, load_bytes=False): - """return field's *string* value to use for display - - looks in - 1. previously submitted form values if any (eg on validation error) - 2. req.form - 3. extra kw args given to render_form - 4. field's typed value - - values found in 1. and 2. are expected te be already some 'display' - value while those found in 3. and 4. are expected to be correctly typed. - """ - value = self._req_display_value(field) - if value is None: - if field.name in rendervalues: - value = rendervalues[field.name] - elif field.name in self.cw_extra_kwargs: - value = self.cw_extra_kwargs[field.name] - else: - value = self.form_field_value(field, load_bytes) - if callable(value): - value = value(self) - if value != INTERNAL_FIELD_VALUE: - value = field.format_value(self._cw, value) - return value - - def _req_display_value(self, field): - qname = self.form_field_name(field) - if qname in self.form_previous_values: - return self.form_previous_values[qname] - if qname in self._cw.form: - return self._cw.form[qname] - if field.name in self._cw.form: - return self._cw.form[field.name] - return None - - def form_field_value(self, field, load_bytes=False): - """return field's *typed* value""" - myattr = '%s_%s_default' % (field.role, field.name) - if hasattr(self, myattr): - return getattr(self, myattr)() - value = field.initial - if callable(value): - value = value(self) - return value def form_field_error(self, field): """return validation error for widget's field, if any""" @@ -297,6 +242,15 @@ return '%s#%s' % (self.edited_entity.absolute_url(), self.domid) return '%s#%s' % (self.req.url(), self.domid) + def build_context(self, formvalues=None): + super(EntityFieldsForm, self).build_context(formvalues) + edited = set() + for field in self.fields: + if field.eidparam: + edited.add(field.role_name()) + self.add_hidden('_cw_edited_fields', u','.join(edited), + eidparam=True) + def _field_has_error(self, field): """return true if the field has some error in given validation exception """ @@ -353,43 +307,7 @@ self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row, col=self.cw_col, entity=self.edited_entity) - def form_field_value(self, field, load_bytes=False): - """return field's *typed* value - overriden to deal with - * special eid / __type - * lookup for values on edited entities - """ - attr = field.name - entity = self.edited_entity - if attr == 'eid': - return entity.eid - if not field.eidparam: - return super(EntityFieldsForm, self).form_field_value(field, load_bytes) - if attr == '__type': - return entity.__regid__ - if self._cw.vreg.schema.rschema(attr).final: - attrtype = entity.e_schema.destination(attr) - if attrtype == 'Password': - return entity.has_eid() and INTERNAL_FIELD_VALUE or '' - if attrtype == 'Bytes': - if entity.has_eid(): - if load_bytes: - return getattr(entity, attr) - # XXX value should reflect if some file is already attached - return True - return False - if entity.has_eid() or attr in entity: - value = getattr(entity, attr) - else: - value = self._form_field_default_value(field, load_bytes) - return value - # non final relation field - if entity.has_eid() or entity.relation_cached(attr, field.role): - value = [r[0] for r in entity.related(attr, field.role)] - else: - value = self._form_field_default_value(field, load_bytes) - return value def form_field_format(self, field): """return MIME type used for the given (text or bytes) field""" @@ -406,28 +324,21 @@ entity.has_eid() or '%s_encoding' % field.name in entity): return self.edited_entity.attr_metadata(field.name, 'encoding') return super(EntityFieldsForm, self).form_field_encoding(field) - - def form_field_name(self, field): - """return qualified name for the given field""" - if field.eidparam: - return eid_param(field.name, self.edited_entity.eid) - return field.name - - def form_field_id(self, field): - """return dom id for the given field""" - if field.eidparam: - return eid_param(field.id, self.edited_entity.eid) - return field.id - # XXX all this vocabulary handling should be on the field, no? def form_field_vocabulary(self, field, limit=None): """return vocabulary for the given field""" role, rtype = field.role, field.name method = '%s_%s_vocabulary' % (role, rtype) + def actual_eid(self, eid): + # should be either an int (existant entity) or a variable (to be + # created entity) + assert eid or eid == 0, repr(eid) # 0 is a valid eid try: vocabfunc = getattr(self, method) except AttributeError: + return typed_eid(eid) + except ValueError: try: # XXX bw compat, __vocabulary on the entity vocabfunc = getattr(self.edited_entity, method) @@ -511,6 +422,10 @@ if limit is not None and len(result) >= limit: break return result + return self._cw.data['eidmap'][eid] + except KeyError: + self._cw.data['eidmap'][eid] = None + return None def editable_relations(self): return () @@ -533,10 +448,10 @@ subform.parent_form = self self.forms.append(subform) - def build_context(self, rendervalues=None): - super(CompositeFormMixIn, self).build_context(rendervalues) + def build_context(self, formvalues=None): + super(CompositeFormMixIn, self).build_context(formvalues) for form in self.forms: - form.build_context(rendervalues) + form.build_context(formvalues) class CompositeForm(CompositeFormMixIn, FieldsForm): diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/views/massmailing.py --- a/web/views/massmailing.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/views/massmailing.py Mon Dec 21 19:45:24 2009 +0100 @@ -39,8 +39,11 @@ __regid__ = 'massmailing' sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}), - label=_('From:')) - recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:')) + label=_('From:'), + value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email())) + recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'), + choices=recipient_vocabulary, + value= lambda f: [entity.eid for entity in f.cw_rset.entities() if entity.get_email()]) subject = ff.StringField(label=_('Subject:'), max_length=256) mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField', inputid='mailbody')) @@ -57,12 +60,10 @@ return [(label, value) for label, value in vocab if label] return super(MassMailingForm, self).form_field_vocabulary(field) - def form_field_value(self, field, values): - if field.name == 'recipient': - return [entity.eid for entity in self.cw_rset.entities() if entity.get_email()] - elif field.name == 'mailbody': - field.widget.attrs['cubicweb:variables'] = ','.join(self.get_allowed_substitutions()) - return super(MassMailingForm, self).form_field_value(field, values) + def __init__(self, *args, **kwargs): + super(MassMailingForm, self).__init__(*args, **kwargs) + field = self.field_by_name('mailbody') + field.widget.attrs['cubicweb:variables'] = ','.join(self.get_allowed_substitutions()) def get_allowed_substitutions(self): attrs = [] @@ -126,5 +127,5 @@ req.add_css('cubicweb.mailform.css') from_addr = '%s <%s>' % (req.user.dc_title(), req.user.get_email()) form = self._cw.vreg['forms'].select('massmailing', self._cw, rset=self.cw_rset, - action='sendmail', domid='sendmail') - self.w(form.render(formvalues=dict(sender=from_addr))) + action='sendmail', domid='sendmail') + self.w(form.render()) diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/views/sparql.py --- a/web/views/sparql.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/views/sparql.py Mon Dec 21 19:45:24 2009 +0100 @@ -28,7 +28,7 @@ resultvid = formfields.StringField(choices=((_('table'), 'table'), (_('sparql xml'), 'sparqlxml')), widget=fwdgs.Radio, - initial='table') + value='table') form_buttons = [fwdgs.SubmitButton()] @property def action(self): diff -r 0e97cf2cf55b -r 6b2b20c73d59 web/views/workflow.py --- a/web/views/workflow.py Mon Dec 21 19:25:07 2009 +0100 +++ b/web/views/workflow.py Mon Dec 21 19:45:24 2009 +0100 @@ -86,8 +86,11 @@ trinfo = self._cw.vreg['etypes'].etype_class('TrInfo')(self._cw) trinfo.eid = self._cw.varmaker.next() subform = self._cw.vreg['forms'].select('edition', self._cw, entity=trinfo, - mainform=False) - subform.field_by_name('by_transition').widget = fwdgs.HiddenInput() + mainform=False) + subform.field_by_name('wf_info_for', 'subject').value = entity.eid + trfield = subform.field_by_name('by_transition', 'subject') + trfield.widget = fwdgs.HiddenInput() + trfield.value = transition.eid form.add_subform(subform) return form