refactor form field value handling, to get a nicer api and an easier algorithm to get field's value
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Mon, 21 Dec 2009 19:45:24 +0100
changeset 4159 6b2b20c73d59
parent 4158 0e97cf2cf55b
child 4160 3fbdeef9a610
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
web/formfields.py
web/formwidgets.py
web/views/cwproperties.py
web/views/forms.py
web/views/massmailing.py
web/views/sparql.py
web/views/workflow.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'</div>')
-        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'<br/>')
             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,
     }
--- 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)
--- 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:
--- 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, <role>_<rtype>_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):
--- 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())
--- 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):
--- 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