web/formfields.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
--- a/web/formfields.py	Mon Jan 04 18:40:30 2016 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1269 +0,0 @@
-# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
-# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
-#
-# This file is part of CubicWeb.
-#
-# CubicWeb is free software: you can redistribute it and/or modify it under the
-# terms of the GNU Lesser General Public License as published by the Free
-# Software Foundation, either version 2.1 of the License, or (at your option)
-# any later version.
-#
-# CubicWeb is distributed in the hope that it will be useful, but WITHOUT
-# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-# FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
-# details.
-#
-# You should have received a copy of the GNU Lesser General Public License along
-# with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
-"""
-The Field class and basic fields
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-.. Note::
-  Fields are used to control what's edited in forms. They makes the link between
-  something to edit and its display in the form. Actual display is handled by a
-  widget associated to the field.
-
-Let first see the base class for fields:
-
-.. autoclass:: cubicweb.web.formfields.Field
-
-Now, you usually don't use that class but one of the concrete field classes
-described below, according to what you want to edit.
-
-Basic fields
-''''''''''''
-
-.. autoclass:: cubicweb.web.formfields.StringField()
-.. autoclass:: cubicweb.web.formfields.PasswordField()
-.. autoclass:: cubicweb.web.formfields.IntField()
-.. autoclass:: cubicweb.web.formfields.BigIntField()
-.. autoclass:: cubicweb.web.formfields.FloatField()
-.. autoclass:: cubicweb.web.formfields.BooleanField()
-.. autoclass:: cubicweb.web.formfields.DateField()
-.. autoclass:: cubicweb.web.formfields.DateTimeField()
-.. autoclass:: cubicweb.web.formfields.TimeField()
-.. autoclass:: cubicweb.web.formfields.TimeIntervalField()
-
-Compound fields
-''''''''''''''''
-
-.. autoclass:: cubicweb.web.formfields.RichTextField()
-.. autoclass:: cubicweb.web.formfields.FileField()
-.. autoclass:: cubicweb.web.formfields.CompoundField()
-
-.. autoclass cubicweb.web.formfields.EditableFileField() XXX should be a widget
-
-Entity specific fields and function
-'''''''''''''''''''''''''''''''''''
-
-.. autoclass:: cubicweb.web.formfields.RelationField()
-.. autofunction:: cubicweb.web.formfields.guess_field
-
-"""
-__docformat__ = "restructuredtext en"
-
-from warnings import warn
-from datetime import datetime, timedelta
-
-from six import PY2, text_type, string_types
-
-from logilab.mtconverter import xml_escape
-from logilab.common import nullobject
-from logilab.common.date import ustrftime
-from logilab.common.configuration import format_time
-from logilab.common.textutils import apply_units, TIME_UNITS
-
-from yams.schema import KNOWN_METAATTRIBUTES, role_name
-from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
-                              FormatConstraint)
-
-from cubicweb import Binary, tags, uilib
-from cubicweb.utils import support_args
-from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \
-     formwidgets as fw
-from cubicweb.web.views import uicfg
-
-class UnmodifiedField(Exception):
-    """raise this when a field has not actually been edited and you want to skip
-    it
-    """
-
-def normalize_filename(filename):
-    return filename.split('\\')[-1]
-
-def vocab_sort(vocab):
-    """sort vocabulary, considering option groups"""
-    result = []
-    partresult = []
-    for label, value in vocab:
-        if value is None: # opt group start
-            if partresult:
-                result += sorted(partresult)
-                partresult = []
-            result.append( (label, value) )
-        else:
-            partresult.append( (label, value) )
-    result += sorted(partresult)
-    return result
-
-_MARKER = nullobject()
-
-class Field(object):
-    """This class is the abstract base class for all fields. It hold a bunch
-    of attributes which may be used for fine control of the behaviour of a
-    concrete field.
-
-    **Attributes**
-
-    All the attributes described below have sensible default value which may be
-    overriden by named arguments given to field's constructor.
-
-    :attr:`name`
-       base name of the field (basestring). The actual input name is returned by
-       the :meth:`input_name` method and may differ from that name (for instance
-       if `eidparam` is true).
-    :attr:`id`
-       DOM identifier (default to the same value as `name`), should be unique in
-       a form.
-    :attr:`label`
-       label of the field (default to the same value as `name`).
-    :attr:`help`
-       help message about this field.
-    :attr:`widget`
-       widget associated to the field. Each field class has a default widget
-       class which may be overriden per instance.
-    :attr:`value`
-       field value. May be an actual value or a callable which should take the
-       form as argument and return a value.
-    :attr:`choices`
-       static vocabulary for this field. May be a list of values, a list of
-       (label, value) tuples or a callable which should take the form and field
-       as arguments and return a list of values or a list of (label, value).
-    :attr:`required`
-       bool flag telling if the field is required or not.
-    :attr:`sort`
-       bool flag telling if the vocabulary (either static vocabulary specified
-       in `choices` or dynamic vocabulary fetched from the form) should be
-       sorted on label.
-    :attr:`internationalizable`
-       bool flag telling if the vocabulary labels should be translated using the
-       current request language.
-    :attr:`eidparam`
-       bool flag telling if this field is linked to a specific entity
-    :attr:`role`
-       when the field is linked to an entity attribute or relation, tells the
-       role of the entity in the relation (eg 'subject' or 'object'). If this is
-       not an attribute or relation of the edited entity, `role` should be
-       `None`.
-    :attr:`fieldset`
-       optional fieldset to which this field belongs to
-    :attr:`order`
-       key used by automatic forms to sort fields
-    :attr:`ignore_req_params`
-       when true, this field won't consider value potentially specified using
-       request's form parameters (eg you won't be able to specify a value using for
-       instance url like http://mywebsite.com/form?field=value)
-
-    .. currentmodule:: cubicweb.web.formfields
-
-    **Generic methods**
-
-    .. automethod:: Field.input_name
-    .. automethod:: Field.dom_id
-    .. automethod:: Field.actual_fields
-
-    **Form generation methods**
-
-    .. automethod:: form_init
-    .. automethod:: typed_value
-
-    **Post handling methods**
-
-    .. automethod:: process_posted
-    .. automethod:: process_form_value
-
-    """
-    # default widget associated to this class of fields. May be overriden per
-    # instance
-    widget = fw.TextInput
-    # does this field requires a multipart form
-    needs_multipart = False
-    # class attribute used for ordering of fields in a form
-    __creation_rank = 0
-
-    eidparam = False
-    role = None
-    id = None
-    help = None
-    required = False
-    choices = None
-    sort = True
-    internationalizable = False
-    fieldset = None
-    order = None
-    value = _MARKER
-    fallback_on_none_attribute = False
-    ignore_req_params = False
-
-    def __init__(self, name=None, label=_MARKER, widget=None, **kwargs):
-        for key, val in kwargs.items():
-            assert hasattr(self.__class__, key) and not key[0] == '_', key
-            setattr(self, key, val)
-        self.name = name
-        if label is _MARKER:
-            label = name or _MARKER
-        self.label = label
-        # has to be done after other attributes initialization
-        self.init_widget(widget)
-        # ordering number for this field instance
-        self.creation_rank = Field.__creation_rank
-        Field.__creation_rank += 1
-
-    def as_string(self, repr=True):
-        l = [u'<%s' % self.__class__.__name__]
-        for attr in ('name', 'eidparam', 'role', 'id', 'value'):
-            value = getattr(self, attr)
-            if value is not None and value is not _MARKER:
-                l.append('%s=%r' % (attr, value))
-        if repr:
-            l.append('@%#x' % id(self))
-        return u'%s>' % ' '.join(l)
-
-    def __unicode__(self):
-        return self.as_string(False)
-
-    if PY2:
-        def __str__(self):
-            return self.as_string(False).encode('UTF8')
-    else:
-        __str__ = __unicode__
-
-    def __repr__(self):
-        return self.as_string(True)
-
-    def init_widget(self, widget):
-        if widget is not None:
-            self.widget = widget
-        elif self.choices and not self.widget.vocabulary_widget:
-            self.widget = fw.Select()
-        if isinstance(self.widget, type):
-            self.widget = self.widget()
-
-    def set_name(self, name):
-        """automatically set .label when name is set"""
-        assert name
-        self.name = name
-        if self.label is _MARKER:
-            self.label = name
-
-    def is_visible(self):
-        """return true if the field is not an hidden field"""
-        return not isinstance(self.widget, fw.HiddenInput)
-
-    def actual_fields(self, form):
-        """Fields may be composed of other fields. For instance the
-        :class:`~cubicweb.web.formfields.RichTextField` is containing a format
-        field to define the text format. This method returns actual fields that
-        should be considered for display / edition. It usually simply return
-        self.
-        """
-        yield self
-
-    def format_value(self, req, value):
-        """return value suitable for display where value may be a list or tuple
-        of values
-        """
-        if isinstance(value, (list, tuple)):
-            return [self.format_single_value(req, val) for val in value]
-        return self.format_single_value(req, value)
-
-    def format_single_value(self, req, value):
-        """return value suitable for display"""
-        if value is None or value is False:
-            return u''
-        if value is True:
-            return u'1'
-        return text_type(value)
-
-    def get_widget(self, form):
-        """return the widget instance associated to this field"""
-        return self.widget
-
-    def input_name(self, form, suffix=None):
-        """Return the 'qualified name' for this field, e.g. something suitable
-        to use as HTML input name. You can specify a suffix that will be
-        included in the name when widget needs several inputs.
-        """
-        # caching is necessary else we get some pb on entity creation :
-        # entity.eid is modified from creation mark (eg 'X') to its actual eid
-        # (eg 123), and then `field.input_name()` won't return the right key
-        # anymore if not cached (first call to input_name done *before* eventual
-        # eid affectation).
-        #
-        # note that you should NOT use @cached else it will create a memory leak
-        # on persistent fields (eg created once for all on a form class) because
-        # of the 'form' appobject argument: the cache will keep growing as new
-        # form are created...
-        try:
-            return form.formvalues[(self, 'input_name', suffix)]
-        except KeyError:
-            name = self.role_name()
-            if suffix is not None:
-                name += suffix
-            if self.eidparam:
-                name = eid_param(name, form.edited_entity.eid)
-            form.formvalues[(self, 'input_name', suffix)] = name
-            return name
-
-    def role_name(self):
-        """return <field.name>-<field.role> if role is specified, else field.name"""
-        assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
-        if self.role is not None:
-            return role_name(self.name, self.role)
-        return self.name
-
-    def dom_id(self, form, suffix=None):
-        """Return the HTML DOM identifier for this field, e.g. something
-        suitable to use as HTML input id. You can specify a suffix that will be
-        included in the name when widget needs several inputs.
-        """
-        id = self.id or self.role_name()
-        if suffix is not None:
-            id += suffix
-        if self.eidparam:
-            return eid_param(id, form.edited_entity.eid)
-        return id
-
-    def typed_value(self, form, load_bytes=False):
-        """Return the correctly typed value for this field in the form context.
-        """
-        if self.eidparam and self.role is not None:
-            entity = form.edited_entity
-            if form._cw.vreg.schema.rschema(self.name).final:
-                if entity.has_eid() or self.name in entity.cw_attr_cache:
-                    value = getattr(entity, self.name)
-                    if value is not None or not self.fallback_on_none_attribute:
-                        return value
-            elif entity.has_eid() or entity.cw_relation_cached(self.name, self.role):
-                value = [r[0] for r in entity.related(self.name, self.role)]
-                if value or not self.fallback_on_none_attribute:
-                    return value
-        return self.initial_typed_value(form, load_bytes)
-
-    def initial_typed_value(self, form, load_bytes):
-        if self.value is not _MARKER:
-            if callable(self.value):
-                return self.value(form, self)
-            return self.value
-        formattr = '%s_%s_default' % (self.role, self.name)
-        if self.eidparam and self.role is not None:
-            if form._cw.vreg.schema.rschema(self.name).final:
-                return form.edited_entity.e_schema.default(self.name)
-            return form.linked_to.get((self.name, self.role), ())
-        return None
-
-    def example_format(self, req):
-        """return a sample string describing what can be given as input for this
-        field
-        """
-        return u''
-
-    def render(self, form, renderer):
-        """render this field, which is part of form, using the given form
-        renderer
-        """
-        widget = self.get_widget(form)
-        return widget.render(form, self, renderer)
-
-    def vocabulary(self, form, **kwargs):
-        """return vocabulary for this field. This method will be
-        called by widgets which requires a vocabulary.
-
-        It should return a list of tuple (label, value), where value
-        *must be a unicode string*, not a typed value.
-        """
-        assert self.choices is not None
-        if callable(self.choices):
-            # pylint: disable=E1102
-            if getattr(self.choices, '__self__', None) is self:
-                vocab = self.choices(form=form, **kwargs)
-            else:
-                vocab = self.choices(form=form, field=self, **kwargs)
-        else:
-            vocab = self.choices
-        if vocab and not isinstance(vocab[0], (list, tuple)):
-            vocab = [(x, x) for x in vocab]
-        if self.internationalizable:
-            # the short-cirtcuit 'and' boolean operator is used here
-            # to permit a valid empty string in vocabulary without
-            # attempting to translate it by gettext (which can lead to
-            # weird strings display)
-            vocab = [(label and form._cw._(label), value)
-                     for label, value in vocab]
-        if self.sort:
-            vocab = vocab_sort(vocab)
-        return vocab
-
-    # support field as argument to avoid warning when used as format field value
-    # callback
-    def format(self, form, field=None):
-        """return MIME type used for the given (text or bytes) field"""
-        if self.eidparam and self.role == 'subject':
-            entity = form.edited_entity
-            if entity.e_schema.has_metadata(self.name, 'format') and (
-                entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache):
-                return form.edited_entity.cw_attr_metadata(self.name, 'format')
-        return form._cw.property_value('ui.default-text-format')
-
-    def encoding(self, form):
-        """return encoding used for the given (text) field"""
-        if self.eidparam:
-            entity = form.edited_entity
-            if entity.e_schema.has_metadata(self.name, 'encoding') and (
-                entity.has_eid() or '%s_encoding' % self.name in entity):
-                return form.edited_entity.cw_attr_metadata(self.name, 'encoding')
-        return form._cw.encoding
-
-    def form_init(self, form):
-        """Method called at form initialization to trigger potential field
-        initialization requiring the form instance. Do nothing by default.
-        """
-        pass
-
-    def has_been_modified(self, form):
-        for field in self.actual_fields(form):
-            if field._has_been_modified(form):
-                return True # XXX
-        return False # not modified
-
-    def _has_been_modified(self, form):
-        # fields not corresponding to an entity attribute / relations
-        # are considered modified
-        if not self.eidparam or not self.role or not form.edited_entity.has_eid():
-            return True # XXX
-        try:
-            if self.role == 'subject':
-                previous_value = getattr(form.edited_entity, self.name)
-            else:
-                previous_value = getattr(form.edited_entity,
-                                         'reverse_%s' % self.name)
-        except AttributeError:
-            # fields with eidparam=True but not corresponding to an actual
-            # attribute or relation
-            return True
-        # if it's a non final relation, we need the eids
-        if isinstance(previous_value, (list, tuple)):
-            # widget should return a set of untyped eids
-            previous_value = set(e.eid for e in previous_value)
-        try:
-            new_value = self.process_form_value(form)
-        except ProcessFormError:
-            return True
-        except UnmodifiedField:
-            return False # not modified
-        if previous_value == new_value:
-            return False # not modified
-        return True
-
-    def process_form_value(self, form):
-        """Return the correctly typed value posted for this field."""
-        try:
-            return form.formvalues[(self, form)]
-        except KeyError:
-            value = form.formvalues[(self, form)] = self._process_form_value(form)
-            return value
-
-    def _process_form_value(self, form):
-        widget = self.get_widget(form)
-        value = widget.process_field_data(form, self)
-        return self._ensure_correctly_typed(form, value)
-
-    def _ensure_correctly_typed(self, form, value):
-        """widget might to return date as a correctly formatted string or as
-        correctly typed objects, but process_for_value must return a typed value.
-        Override this method to type the value if necessary
-        """
-        return value or None
-
-    def process_posted(self, form):
-        """Return an iterator on (field, value) that has been posted for
-        field returned by :meth:`~cubicweb.web.formfields.Field.actual_fields`.
-        """
-        for field in self.actual_fields(form):
-            if field is self:
-                try:
-                    value = field.process_form_value(form)
-                    if field.no_value(value) and field.required:
-                        raise ProcessFormError(form._cw._("required field"))
-                    yield field, value
-                except UnmodifiedField:
-                    continue
-            else:
-                # recursive function: we might have compound fields
-                # of compound fields (of compound fields of ...)
-                for field, value in field.process_posted(form):
-                    yield field, value
-
-    @staticmethod
-    def no_value(value):
-        """return True if the value can be considered as no value for the field"""
-        return value is None
-
-
-class StringField(Field):
-    """Use this field to edit unicode string (`String` yams type). This field
-    additionally support a `max_length` attribute that specify a maximum size for
-    the string (`None` meaning no limit).
-
-    Unless explicitly specified, the widget for this field will be:
-
-    * :class:`~cubicweb.web.formwidgets.Select` if some vocabulary is specified
-      using `choices` attribute
-
-    * :class:`~cubicweb.web.formwidgets.TextInput` if maximum size is specified
-      using `max_length` attribute and this length is inferior to 257.
-
-    * :class:`~cubicweb.web.formwidgets.TextArea` in all other cases
-    """
-    widget = fw.TextArea
-    size = 45
-    placeholder = None
-
-    def __init__(self, name=None, max_length=None, **kwargs):
-        self.max_length = max_length # must be set before super call
-        super(StringField, self).__init__(name=name, **kwargs)
-
-    def init_widget(self, widget):
-        if widget is None:
-            if self.choices:
-                widget = fw.Select()
-            elif self.max_length and self.max_length < 257:
-                widget = fw.TextInput()
-
-        super(StringField, self).init_widget(widget)
-        if isinstance(self.widget, fw.TextArea):
-            self.init_text_area(self.widget)
-        elif isinstance(self.widget, fw.TextInput):
-            self.init_text_input(self.widget)
-
-        if self.placeholder:
-            self.widget.attrs.setdefault('placeholder', self.placeholder)
-
-    def init_text_input(self, widget):
-        if self.max_length:
-            widget.attrs.setdefault('size', min(self.size, self.max_length))
-            widget.attrs.setdefault('maxlength', self.max_length)
-
-    def init_text_area(self, widget):
-        if self.max_length and self.max_length < 513:
-            widget.attrs.setdefault('cols', 60)
-            widget.attrs.setdefault('rows', 5)
-
-    def set_placeholder(self, placeholder):
-        self.placeholder = placeholder
-        if self.widget and self.placeholder:
-            self.widget.attrs.setdefault('placeholder', self.placeholder)
-
-
-class PasswordField(StringField):
-    """Use this field to edit password (`Password` yams type, encoded python
-    string).
-
-    Unless explicitly specified, the widget for this field will be
-    a :class:`~cubicweb.web.formwidgets.PasswordInput`.
-    """
-    widget = fw.PasswordInput
-    def form_init(self, form):
-        if self.eidparam and form.edited_entity.has_eid():
-            # see below: value is probably set but we can't retreive it. Ensure
-            # the field isn't show as a required field on modification
-            self.required = False
-
-    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 ''
-            return self.initial_typed_value(form, load_bytes)
-        return super(PasswordField, self).typed_value(form, load_bytes)
-
-
-class RichTextField(StringField):
-    """This compound field allow edition of text (unicode string) in
-    a particular format. It has an inner field holding the text format,
-    that can be specified using `format_field` argument. If not specified
-    one will be automaticall generated.
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.FCKEditor` or a
-    :class:`~cubicweb.web.formwidgets.TextArea`. according to the field's
-    format and to user's preferences.
-    """
-
-    widget = None
-    def __init__(self, format_field=None, **kwargs):
-        super(RichTextField, self).__init__(**kwargs)
-        self.format_field = format_field
-
-    def init_text_area(self, widget):
-        pass
-
-    def get_widget(self, form):
-        if self.widget is None:
-            if self.use_fckeditor(form):
-                return fw.FCKEditor()
-            widget = fw.TextArea()
-            self.init_text_area(widget)
-            return widget
-        return self.widget
-
-    def get_format_field(self, form):
-        if self.format_field:
-            return self.format_field
-        # we have to cache generated field since it's use as key in the
-        # context dictionary
-        req = form._cw
-        try:
-            return req.data[self]
-        except KeyError:
-            fkwargs = {'eidparam': self.eidparam, 'role': self.role}
-            if self.use_fckeditor(form):
-                # if fckeditor is used and format field isn't explicitly
-                # deactivated, we want an hidden field for the format
-                fkwargs['widget'] = fw.HiddenInput()
-                fkwargs['value'] = 'text/html'
-            else:
-                # else we want a format selector
-                fkwargs['widget'] = fw.Select()
-                fcstr = FormatConstraint()
-                fkwargs['choices'] = fcstr.vocabulary(form=form)
-                fkwargs['internationalizable'] = True
-                fkwargs['value'] = self.format
-            fkwargs['eidparam'] = self.eidparam
-            field = StringField(name=self.name + '_format', **fkwargs)
-            req.data[self] = field
-            return field
-
-    def actual_fields(self, form):
-        yield self
-        format_field = self.get_format_field(form)
-        if format_field:
-            yield format_field
-
-    def use_fckeditor(self, form):
-        """return True if fckeditor should be used to edit entity's attribute named
-        `attr`, according to user preferences
-        """
-        if form._cw.use_fckeditor():
-            return self.format(form) == 'text/html'
-        return False
-
-    def render(self, form, renderer):
-        format_field = self.get_format_field(form)
-        if format_field:
-            # XXX we want both fields to remain vertically aligned
-            if format_field.is_visible():
-                format_field.widget.attrs['style'] = 'display: block'
-            result = format_field.render(form, renderer)
-        else:
-            result = u''
-        return result + self.get_widget(form).render(form, self, renderer)
-
-
-class FileField(StringField):
-    """This compound field allow edition of binary stream (`Bytes` yams
-    type). Three inner fields may be specified:
-
-    * `format_field`, holding the file's format.
-    * `encoding_field`, holding the file's content encoding.
-    * `name_field`, holding the file's name.
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.FileInput`. Inner fields, if any,
-    will be added to a drop down menu at the right of the file input.
-    """
-    widget = fw.FileInput
-    needs_multipart = True
-
-    def __init__(self, format_field=None, encoding_field=None, name_field=None,
-                 **kwargs):
-        super(FileField, self).__init__(**kwargs)
-        self.format_field = format_field
-        self.encoding_field = encoding_field
-        self.name_field = name_field
-
-    def actual_fields(self, form):
-        yield self
-        if self.format_field:
-            yield self.format_field
-        if self.encoding_field:
-            yield self.encoding_field
-        if self.name_field:
-            yield self.name_field
-
-    def typed_value(self, form, load_bytes=False):
-        if self.eidparam and self.role is not None:
-            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:
-            divid = '%s-advanced' % self.input_name(form)
-            wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
-                        (xml_escape(uilib.toggle_action(divid)),
-                         form._cw._('show advanced fields'),
-                         xml_escape(form._cw.data_url('puce_down.png')),
-                         form._cw._('show advanced fields')))
-            wdgs.append(u'<div id="%s" class="hidden">' % divid)
-            if self.name_field:
-                wdgs.append(self.render_subfield(form, self.name_field, renderer))
-            if self.format_field:
-                wdgs.append(self.render_subfield(form, self.format_field, renderer))
-            if self.encoding_field:
-                wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
-            wdgs.append(u'</div>')
-        if not self.required and self.typed_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'),
-                                   type=u'checkbox'))
-            wdgs.append(form._cw._('detach attached file'))
-        return u'\n'.join(wdgs)
-
-    def render_subfield(self, form, field, renderer):
-        return (renderer.render_label(form, field)
-                + field.render(form, renderer)
-                + renderer.render_help(form, field)
-                + u'<br/>')
-
-    def _process_form_value(self, form):
-        posted = form._cw.form
-        if self.input_name(form, u'__detach') in posted:
-            # drop current file value on explictily asked to detach
-            return None
-        try:
-            value = posted[self.input_name(form)]
-        except KeyError:
-            # raise UnmodifiedField instead of returning None, since the later
-            # will try to remove already attached file if any
-            raise UnmodifiedField()
-        # value is a 2-uple (filename, stream) or a list of such
-        # tuples (multiple files)
-        try:
-            if isinstance(value, list):
-                value = value[0]
-                form.warning('mutiple files provided, however '
-                             'only the first will be picked')
-            filename, stream = value
-        except ValueError:
-            raise UnmodifiedField()
-        # XXX avoid in memory loading of posted files. Requires Binary handling changes...
-        value = Binary(stream.read())
-        if not value.getvalue(): # usually an unexistant file
-            value = None
-        else:
-            # set filename on the Binary instance, may be used later in hooks
-            value.filename = normalize_filename(filename)
-        return value
-
-
-# XXX turn into a widget
-class EditableFileField(FileField):
-    """This compound field allow edition of binary stream as
-    :class:`~cubicweb.web.formfields.FileField` but expect that stream to
-    actually contains some text.
-
-    If the stream format is one of text/plain, text/html, text/rest,
-    text/markdown
-    then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionally
-    displayed, allowing to directly the file's content when desired, instead
-    of choosing a file from user's file system.
-    """
-    editable_formats = (
-        'text/plain', 'text/html', 'text/rest', 'text/markdown')
-
-    def render(self, form, renderer):
-        wdgs = [super(EditableFileField, self).render(form, renderer)]
-        if self.format(form) in self.editable_formats:
-            data = self.typed_value(form, load_bytes=True)
-            if data:
-                encoding = self.encoding(form)
-                try:
-                    form.formvalues[(self, form)] = data.getvalue().decode(encoding)
-                except UnicodeError:
-                    pass
-                else:
-                    if not self.required:
-                        msg = form._cw._(
-                            'You can either submit a new file using the browse button above'
-                            ', or choose to remove already uploaded file by checking the '
-                            '"detach attached file" check-box, or edit file content online '
-                            'with the widget below.')
-                    else:
-                        msg = form._cw._(
-                            'You can either submit a new file using the browse button above'
-                            ', or edit file content online with the widget below.')
-                    wdgs.append(u'<p><b>%s</b></p>' % msg)
-                    wdgs.append(fw.TextArea(setdomid=False).render(form, self, renderer))
-                    # XXX restore form context?
-        return '\n'.join(wdgs)
-
-    def _process_form_value(self, form):
-        value = form._cw.form.get(self.input_name(form))
-        if isinstance(value, text_type):
-            # file modified using a text widget
-            return Binary(value.encode(self.encoding(form)))
-        return super(EditableFileField, self)._process_form_value(form)
-
-
-class BigIntField(Field):
-    """Use this field to edit big integers (`BigInt` yams type). This field
-    additionally support `min` and `max` attributes that specify a minimum and/or
-    maximum value for the integer (`None` meaning no boundary).
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.TextInput`.
-    """
-    default_text_input_size = 10
-
-    def __init__(self, min=None, max=None, **kwargs):
-        super(BigIntField, self).__init__(**kwargs)
-        self.min = min
-        self.max = max
-
-    def init_widget(self, widget):
-        super(BigIntField, self).init_widget(widget)
-        if isinstance(self.widget, fw.TextInput):
-            self.widget.attrs.setdefault('size', self.default_text_input_size)
-
-    def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
-            value = value.strip()
-            if not value:
-                return None
-            try:
-                return int(value)
-            except ValueError:
-                raise ProcessFormError(form._cw._('an integer is expected'))
-        return value
-
-
-class IntField(BigIntField):
-    """Use this field to edit integers (`Int` yams type). Similar to
-    :class:`~cubicweb.web.formfields.BigIntField` but set max length when text
-    input widget is used (the default).
-    """
-    default_text_input_size = 5
-
-    def init_widget(self, widget):
-        super(IntField, self).init_widget(widget)
-        if isinstance(self.widget, fw.TextInput):
-            self.widget.attrs.setdefault('maxlength', 15)
-
-
-class BooleanField(Field):
-    """Use this field to edit booleans (`Boolean` yams type).
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.Radio` with yes/no values. You
-    can change that values by specifing `choices`.
-    """
-    widget = fw.Radio
-
-    def __init__(self, allow_none=False, **kwargs):
-        super(BooleanField, self).__init__(**kwargs)
-        self.allow_none = allow_none
-
-    def vocabulary(self, form):
-        if self.choices:
-            return super(BooleanField, self).vocabulary(form)
-        if self.allow_none:
-            return [(form._cw._('indifferent'), ''),
-                    (form._cw._('yes'), '1'),
-                    (form._cw._('no'), '0')]
-        # XXX empty string for 'no' in that case for bw compat
-        return [(form._cw._('yes'), '1'), (form._cw._('no'), '')]
-
-    def format_single_value(self, req, value):
-        """return value suitable for display"""
-        if self.allow_none:
-            if value is None:
-                return u''
-            if value is False:
-                return '0'
-        return super(BooleanField, self).format_single_value(req, value)
-
-    def _ensure_correctly_typed(self, form, value):
-        if self.allow_none:
-            if value:
-                return bool(int(value))
-            return None
-        return bool(value)
-
-
-class FloatField(IntField):
-    """Use this field to edit floats (`Float` yams type). This field additionally
-    support `min` and `max` attributes as the
-    :class:`~cubicweb.web.formfields.IntField`.
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.TextInput`.
-    """
-    def format_single_value(self, req, value):
-        formatstr = req.property_value('ui.float-format')
-        if value is None:
-            return u''
-        return formatstr % float(value)
-
-    def render_example(self, req):
-        return self.format_single_value(req, 1.234)
-
-    def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
-            value = value.strip()
-            if not value:
-                return None
-            try:
-                return float(value)
-            except ValueError:
-                raise ProcessFormError(form._cw._('a float is expected'))
-        return None
-
-
-class TimeIntervalField(StringField):
-    """Use this field to edit time interval (`Interval` yams type).
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.TextInput`.
-    """
-    widget = fw.TextInput
-
-    def format_single_value(self, req, value):
-        if value:
-            value = format_time(value.days * 24 * 3600 + value.seconds)
-            return text_type(value)
-        return u''
-
-    def example_format(self, req):
-        """return a sample string describing what can be given as input for this
-        field
-        """
-        return u'20s, 10min, 24h, 4d'
-
-    def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
-            value = value.strip()
-            if not value:
-                return None
-            try:
-                value = apply_units(value, TIME_UNITS)
-            except ValueError:
-                raise ProcessFormError(form._cw._('a number (in seconds) or 20s, 10min, 24h or 4d are expected'))
-        return timedelta(0, value)
-
-
-class DateField(StringField):
-    """Use this field to edit date (`Date` yams type).
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.JQueryDatePicker`.
-    """
-    widget = fw.JQueryDatePicker
-    format_prop = 'ui.date-format'
-    etype = 'Date'
-
-    def format_single_value(self, req, value):
-        if value:
-            return ustrftime(value, req.property_value(self.format_prop))
-        return u''
-
-    def render_example(self, req):
-        return self.format_single_value(req, datetime.now())
-
-    def _ensure_correctly_typed(self, form, value):
-        if isinstance(value, string_types):
-            value = value.strip()
-            if not value:
-                return None
-            try:
-                value = form._cw.parse_datetime(value, self.etype)
-            except ValueError as ex:
-                raise ProcessFormError(text_type(ex))
-        return value
-
-
-class DateTimeField(DateField):
-    """Use this field to edit datetime (`Datetime` yams type).
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.JQueryDateTimePicker`.
-    """
-    widget = fw.JQueryDateTimePicker
-    format_prop = 'ui.datetime-format'
-    etype = 'Datetime'
-
-
-class TimeField(DateField):
-    """Use this field to edit time (`Time` yams type).
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.JQueryTimePicker`.
-    """
-    widget = fw.JQueryTimePicker
-    format_prop = 'ui.time-format'
-    etype = 'Time'
-
-
-# XXX use cases where we don't actually want a better widget?
-class CompoundField(Field):
-    """This field shouldn't be used directly, it's designed to hold inner
-    fields that should be conceptually groupped together.
-    """
-    def __init__(self, fields, *args, **kwargs):
-        super(CompoundField, self).__init__(*args, **kwargs)
-        self.fields = fields
-
-    def subfields(self, form):
-        return self.fields
-
-    def actual_fields(self, form):
-        # don't add [self] to actual fields, compound field is usually kinda
-        # virtual, all interesting values are in subfield. Skipping it may avoid
-        # error when processed by the editcontroller : it may be marked as required
-        # while it has no value, hence generating a false error.
-        return list(self.fields)
-
-    @property
-    def needs_multipart(self):
-        return any(f.needs_multipart for f in self.fields)
-
-
-class RelationField(Field):
-    """Use this field to edit a relation of an entity.
-
-    Unless explicitly specified, the widget for this field will be a
-    :class:`~cubicweb.web.formwidgets.Select`.
-    """
-
-    @staticmethod
-    def fromcardinality(card, **kwargs):
-        kwargs.setdefault('widget', fw.Select(multiple=card in '*+'))
-        return RelationField(**kwargs)
-
-    def choices(self, form, limit=None):
-        """Take care, choices function for relation field instance should take
-        an extra 'limit' argument, with default to None.
-
-        This argument is used by the 'unrelateddivs' view (see in autoform) and
-        when it's specified (eg not None), vocabulary returned should:
-        * not include already related entities
-        * have a max size of `limit` entities
-        """
-        entity = form.edited_entity
-        # first see if its specified by __linkto form parameters
-        if limit is None:
-            linkedto = self.relvoc_linkedto(form)
-            if linkedto:
-                return linkedto
-            # it isn't, search more vocabulary
-            vocab = self.relvoc_init(form)
-        else:
-            vocab = []
-        vocab += self.relvoc_unrelated(form, limit)
-        if self.sort:
-            vocab = vocab_sort(vocab)
-        return vocab
-
-    def relvoc_linkedto(self, form):
-        linkedto = form.linked_to.get((self.name, self.role))
-        if linkedto:
-            buildent = form._cw.entity_from_eid
-            return [(buildent(eid).view('combobox'), text_type(eid))
-                    for eid in linkedto]
-        return []
-
-    def relvoc_init(self, form):
-        entity, rtype, role = form.edited_entity, self.name, self.role
-        vocab = []
-        if not self.required:
-            vocab.append(('', INTERNAL_FIELD_VALUE))
-        # vocabulary doesn't include current values, add them
-        if form.edited_entity.has_eid():
-            rset = form.edited_entity.related(self.name, self.role)
-            vocab += [(e.view('combobox'), text_type(e.eid))
-                      for e in rset.entities()]
-        return vocab
-
-    def relvoc_unrelated(self, form, limit=None):
-        entity = form.edited_entity
-        rtype = entity._cw.vreg.schema.rschema(self.name)
-        if entity.has_eid():
-            done = set(row[0] for row in entity.related(rtype, self.role))
-        else:
-            done = None
-        result = []
-        rsetsize = None
-        for objtype in rtype.targets(entity.e_schema, self.role):
-            if limit is not None:
-                rsetsize = limit - len(result)
-            result += self._relvoc_unrelated(form, objtype, rsetsize, done)
-            if limit is not None and len(result) >= limit:
-                break
-        return result
-
-    def _relvoc_unrelated(self, form, targettype, limit, done):
-        """return unrelated entities for a given relation and target entity type
-        for use in vocabulary
-        """
-        if done is None:
-            done = set()
-        res = []
-        entity = form.edited_entity
-        for entity in entity.unrelated(self.name, targettype, self.role, limit,
-                                       lt_infos=form.linked_to).entities():
-            if entity.eid in done:
-                continue
-            done.add(entity.eid)
-            res.append((entity.view('combobox'), text_type(entity.eid)))
-        return res
-
-    def format_single_value(self, req, value):
-        return text_type(value)
-
-    def process_form_value(self, form):
-        """process posted form and return correctly typed value"""
-        try:
-            return form.formvalues[(self, form)]
-        except KeyError:
-            value = self._process_form_value(form)
-            # if value is None, there are some remaining pending fields, we'll
-            # have to recompute this later -> don't cache in formvalues
-            if value is not None:
-                form.formvalues[(self, form)] = value
-            return value
-
-    def _process_form_value(self, form):
-        """process posted form and return correctly typed value"""
-        widget = self.get_widget(form)
-        values = widget.process_field_data(form, self)
-        if values is None:
-            values = ()
-        elif not isinstance(values, list):
-            values = (values,)
-        eids = set()
-        rschema = form._cw.vreg.schema.rschema(self.name)
-        for eid in values:
-            if not eid or eid == INTERNAL_FIELD_VALUE:
-                continue
-            typed_eid = form.actual_eid(eid)
-            # if entity doesn't exist yet
-            if typed_eid is None:
-                # inlined relations of to-be-created **subject entities** have
-                # to be handled separatly
-                if self.role == 'object' and rschema.inlined:
-                    form._cw.data['pending_inlined'][eid].add( (form, self) )
-                else:
-                    form._cw.data['pending_others'].add( (form, self) )
-                return None
-            eids.add(typed_eid)
-        return eids
-
-    @staticmethod
-    def no_value(value):
-        """return True if the value can be considered as no value for the field"""
-        # value is None is the 'not yet ready value, consider the empty set
-        return value is not None and not value
-
-
-_AFF_KWARGS = uicfg.autoform_field_kwargs
-
-def guess_field(eschema, rschema, role='subject', req=None, **kwargs):
-    """This function return the most adapted field to edit the given relation
-    (`rschema`) where the given entity type (`eschema`) is the subject or object
-    (`role`).
-
-    The field is initialized according to information found in the schema,
-    though any value can be explicitly specified using `kwargs`.
-    """
-    fieldclass = None
-    rdef = eschema.rdef(rschema, role)
-    if role == 'subject':
-        targetschema = rdef.object
-        if rschema.final:
-            if rdef.get('internationalizable'):
-                kwargs.setdefault('internationalizable', True)
-    else:
-        targetschema = rdef.subject
-    card = rdef.role_cardinality(role)
-    kwargs['name'] = rschema.type
-    kwargs['role'] = role
-    kwargs['eidparam'] = True
-    kwargs.setdefault('required', card in '1+')
-    if role == 'object':
-        kwargs.setdefault('label', (eschema.type, rschema.type + '_object'))
-    else:
-        kwargs.setdefault('label', (eschema.type, rschema.type))
-    kwargs.setdefault('help', rdef.description)
-    if rschema.final:
-        fieldclass = FIELDS[targetschema]
-        if fieldclass is StringField:
-            if eschema.has_metadata(rschema, 'format'):
-                # use RichTextField instead of StringField if the attribute has
-                # a "format" metadata. But getting information from constraints
-                # may be useful anyway...
-                for cstr in rdef.constraints:
-                    if isinstance(cstr, StaticVocabularyConstraint):
-                        raise Exception('rich text field with static vocabulary')
-                return RichTextField(**kwargs)
-            # init StringField parameters according to constraints
-            for cstr in rdef.constraints:
-                if isinstance(cstr, StaticVocabularyConstraint):
-                    kwargs.setdefault('choices', cstr.vocabulary)
-                    break
-            for cstr in rdef.constraints:
-                if isinstance(cstr, SizeConstraint) and cstr.max is not None:
-                    kwargs['max_length'] = cstr.max
-            return StringField(**kwargs)
-        if fieldclass is FileField:
-            if req:
-                aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req)
-            else:
-                aff_kwargs = _AFF_KWARGS
-            for metadata in KNOWN_METAATTRIBUTES:
-                metaschema = eschema.has_metadata(rschema, metadata)
-                if metaschema is not None:
-                    metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject')
-                    kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
-                                                                req=req, **metakwargs)
-        return fieldclass(**kwargs)
-    return RelationField.fromcardinality(card, **kwargs)
-
-
-FIELDS = {
-    'String' :  StringField,
-    'Bytes':    FileField,
-    'Password': PasswordField,
-
-    'Boolean':  BooleanField,
-    'Int':      IntField,
-    'BigInt':   BigIntField,
-    'Float':    FloatField,
-    'Decimal':  StringField,
-
-    'Date':       DateField,
-    'Datetime':   DateTimeField,
-    'TZDatetime': DateTimeField,
-    'Time':       TimeField,
-    'TZTime':     TimeField,
-    'Interval':   TimeIntervalField,
-    }