web/formfields.py
changeset 4159 6b2b20c73d59
parent 4158 0e97cf2cf55b
child 4160 3fbdeef9a610
--- 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,
     }