web/formfields.py
author Sylvain Thénault <sylvain.thenault@logilab.fr>
Wed, 27 Jan 2010 09:53:48 +0100
changeset 4387 4aacd6492ef4
parent 4386 cf8842b69379
child 4388 15c6607c4bda
permissions -rw-r--r--
reorganize code: move everything related to inlined forms and generic relation from editviews/editforms where there are used, eg in autoforms

"""field classes for form construction

:organization: Logilab
:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2.
:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
"""
__docformat__ = "restructuredtext en"

from warnings import warn
from datetime import datetime

from logilab.mtconverter import xml_escape
from logilab.common.decorators import cached

from yams.schema import KNOWN_METAATTRIBUTES
from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
                              FormatConstraint)

from cubicweb import Binary, tags, uilib, utils
from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \
     formwidgets as fw


class UnmodifiedField(Exception):
    """raise this when a field has not actually been edited and you want to skip
    it
    """


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 = object()

class Field(object):
    """field class is introduced to control what's displayed in forms. It makes
    the link between something to edit and its display in the form. Actual
    display is handled by a widget associated to the field.

    Attributes
    ----------
    all the attributes described below have sensible default value which may be
    overriden by value given to field's constructor.

    :name:
       name of the field (basestring), should be unique in a form.
    :id:
       dom identifier (default to the same value as `name`), should be unique in
       a form.
    :label:
       label of the field (default to the same value as `name`).
    :help:
       help message about this field.
    :widget:
       widget associated to the field. Each field class has a default widget
       class which may be overriden per instance.
    :required:
       bool flag telling if the field is required or not.
    :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.
    :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.
    :internationalizable:
       bool flag telling if the vocabulary labels should be translated using the
       current request language.
    :eidparam:
       bool flag telling if this field is linked to a specific entity
    :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')
    :fieldset:
       optional fieldset to which this field belongs to
    :order:
       key used by automatic forms to sort fields

    """
    # 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

    def __init__(self, name=None, label=_MARKER, 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
        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 __unicode__(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')

    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):
        """return actual fields composing this field in case of a compound
        field, 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 unicode(value)

    def get_widget(self, form):
        """return the widget instance associated to this field"""
        return self.widget

    # cached 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).
    @cached
    def input_name(self, form, suffix=None):
        """return 'qualified name' for this field"""
        name = self.role_name()
        if suffix is not None:
            name += suffix
        if self.eidparam:
            return eid_param(name, form.edited_entity.eid)
        return name

    def role_name(self):
        """return <field.name>-<field.role> if role is specified, else field.name"""
        if self.role is not None:
            return '%s-%s' % (self.name, self.role)
        return self.name

    def dom_id(self, form, suffix=None):
        """return an html dom identifier for this field"""
        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):
        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:
                    return getattr(entity, self.name)
            elif entity.has_eid() or entity.relation_cached(self.name, self.role):
                return [r[0] for r in entity.related(self.name, self.role)]
        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)
            return self.value
        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 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 ()
        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):
        """return vocabulary for this field. This method will be called by
        widgets which requires a vocabulary.
        """
        assert self.choices is not None
        if callable(self.choices):
            try:
                if getattr(self.choices, 'im_self', None) is self:
                    vocab = self.choices(form=form)
                else:
                    vocab = self.choices(form=form, field=self)
            except TypeError:
                warn('[3.6]  %s: choices should now take '
                     'the form and field as named arguments' % self,
                     DeprecationWarning)
                try:
                    vocab = self.choices(form=form)
                except TypeError:
                    warn('[3.3]  %s: choices should now take '
                         'the form and field as named arguments' % self,
                         DeprecationWarning)
                    vocab = self.choices(req=form._cw)
        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

    def format(self, form):
        """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):
                return form.edited_entity.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.attr_metadata(self.name, 'encoding')
        return form._cw.encoding

    def form_init(self, form):
        """method called before by build_context to trigger potential field
        initialization requiring the form instance
        """
        pass

    def has_been_modified(self, form):
        if self.is_visible():
            # 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, tuple):
                # widget should return a set of untyped eids
                previous_value = set(unicode(e.eid) for e in previous_value)
            try:
                new_value = self.process_form_value(form)
            except ProcessFormError:
                return True
            except UnmodifiedField:
                return False
            if form.edited_entity.has_eid() and previous_value == new_value:
                return False # not modified
            return True
        return False

    def process_form_value(self, form):
        """process posted form and return correctly typed value"""
        try:
            return form.formvalues[self]
        except KeyError:
            value = form.formvalues[self] = 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

    def process_posted(self, form):
        for field in self.actual_fields(form):
            if field is self:
                try:
                    yield field, field.process_form_value(form)
                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


class StringField(Field):
    widget = fw.TextArea
    size = 45

    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)

    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 < 513:
            widget.attrs.setdefault('cols', 60)
            widget.attrs.setdefault('rows', 5)


class PasswordField(StringField):
    widget = fw.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 self.initial_typed_value(form, load_bytes)
        return super(PasswordField, self).typed_value(form, load_bytes)


class RichTextField(StringField):
    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 dictionnary
        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):
    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.build_url('data/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()
        # skip browser submitted mime type
        filename, _, stream = value
        # value is a  3-uple (filename, mimetype, stream)
        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 = filename
        return value


class EditableFileField(FileField):
    editable_formats = ('text/plain', 'text/html', 'text/rest')

    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] = unicode(data.getvalue(), 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, unicode):
            # file modified using a text widget
            return Binary(value.encode(self.encoding(form)))
        return super(EditableFileField, self)._process_form_value(form)


class IntField(Field):
    def __init__(self, min=None, max=None, **kwargs):
        super(IntField, self).__init__(**kwargs)
        self.min = min
        self.max = max
        if isinstance(self.widget, fw.TextInput):
            self.widget.attrs.setdefault('size', 5)
            self.widget.attrs.setdefault('maxlength', 15)

    def _ensure_correctly_typed(self, form, value):
        if isinstance(value, basestring):
            try:
                return int(value)
            except ValueError:
                raise ProcessFormError(form._cw._('an integer is expected'))
        return value


class BooleanField(Field):
    widget = fw.Radio

    def vocabulary(self, form):
        if self.choices:
            return super(BooleanField, self).vocabulary(form)
        return [(form._cw._('yes'), '1'), (form._cw._('no'), '')]

    def _ensure_correctly_typed(self, form, value):
        if value is not None:
            return bool(value)
        return value


class FloatField(IntField):
    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, basestring):
            try:
                return float(value)
            except ValueError:
                raise ProcessFormError(form._cw._('a float is expected'))
        return None


class DateField(StringField):
    widget = fw.JQueryDatePicker
    format_prop = 'ui.date-format'
    etype = 'Date'

    def format_single_value(self, req, value):
        if value:
            return utils.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, basestring):
            try:
                value = form._cw.parse_datetime(value, self.etype)
            except ValueError, ex:
                raise ProcessFormError(unicode(ex))
        return value


class DateTimeField(DateField):
    widget = fw.JQueryDateTimePicker
    format_prop = 'ui.datetime-format'
    etype = 'Datetime'


class TimeField(DateField):
    widget = fw.JQueryTimePicker
    format_prop = 'ui.time-format'
    etype = 'Time'


# relation vocabulary helper functions #########################################

def relvoc_linkedto(entity, rtype, role):
    # first see if its specified by __linkto form parameters
    linkedto = entity.linked_to(rtype, role)
    if linkedto:
        buildent = entity._cw.entity_from_eid
        return [(buildent(eid).view('combobox'), eid) for eid in linkedto]
    return []

def relvoc_init(entity, rtype, role, required=False):
    # it isn't, check if the entity provides a method to get correct values
    vocab = []
    if not required:
        vocab.append(('', INTERNAL_FIELD_VALUE))
    # vocabulary doesn't include current values, add them
    if entity.has_eid():
        rset = entity.related(rtype, role)
        vocab += [(e.view('combobox'), e.eid) for e in rset.entities()]
    return vocab

def relvoc_unrelated(entity, rtype, role, limit=None):
    if isinstance(rtype, basestring):
        rtype = entity._cw.vreg.schema.rschema(rtype)
    if entity.has_eid():
        done = set(row[0] for row in entity.related(rtype, role))
    else:
        done = None
    result = []
    rsetsize = None
    for objtype in rtype.targets(entity.e_schema, role):
        if limit is not None:
            rsetsize = limit - len(result)
        result += _relvoc_unrelated(entity, rtype, objtype, role, rsetsize, done)
        if limit is not None and len(result) >= limit:
            break
    return result

def _relvoc_unrelated(entity, rtype, targettype, role, limit, done):
    """return unrelated entities for a given relation and target entity type
    for use in vocabulary
    """
    if done is None:
        done = set()
    res = []
    for entity in entity.unrelated(rtype, targettype, role, limit).entities():
        if entity.eid in done:
            continue
        done.add(entity.eid)
        res.append((entity.view('combobox'), entity.eid))
    return res


class RelationField(Field):

    @staticmethod
    def fromcardinality(card, **kwargs):
        kwargs.setdefault('widget', fw.Select(multiple=card in '*+'))
        return RelationField(**kwargs)

    def choices(self, form, limit=None):
        entity = form.edited_entity
        # first see if its specified by __linkto form parameters
        linkedto = relvoc_linkedto(entity, self.name, self.role)
        if linkedto:
            return linkedto
        # it isn't, check if the entity provides a method to get correct values
        vocab = relvoc_init(entity, self.name, self.role, self.required)
        method = '%s_%s_vocabulary' % (self.role, self.name)
        try:
            vocab += getattr(form, method)(self.name, limit)
            warn('[3.6] found %s on %s, should override field.choices instead (need tweaks)'
                 % (method, form), DeprecationWarning)
        except AttributeError:
            vocab += relvoc_unrelated(entity, self.name, self.role, limit)
        if self.sort:
            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 format_single_value(self, req, 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()
        for eid in values:
            # XXX 'not eid' for AutoCompletionWidget, deal with this in the widget
            if not eid or eid == INTERNAL_FIELD_VALUE:
                continue
            typed_eid = form.actual_eid(eid)
            if typed_eid is None:
                form._cw.data['pendingfields'].add( (form, self) )
                return None
            eids.add(typed_eid)
        return eids


class CompoundField(Field):
    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):
        return [self] + list(self.fields)


def guess_field(eschema, rschema, role='subject', skip_meta_attr=True, **kwargs):
    """return the most adapated widget to edit the relation
    'subjschema rschema objschema' according to information found in the schema
    """
    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['required'] = card in '1+'
    kwargs['name'] = rschema.type
    kwargs['role'] = role
    if role == 'object':
        kwargs.setdefault('label', (eschema.type, rschema.type + '_object'))
    else:
        kwargs.setdefault('label', (eschema.type, rschema.type))
    kwargs['eidparam'] = True
    kwargs.setdefault('help', rdef.description)
    if rschema.final:
        if skip_meta_attr and rschema in eschema.meta_attributes():
            return None
        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:
            for metadata in KNOWN_METAATTRIBUTES:
                metaschema = eschema.has_metadata(rschema, metadata)
                if metaschema is not None:
                    kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
                                                                skip_meta_attr=False)
        return fieldclass(**kwargs)
    return RelationField.fromcardinality(card, **kwargs)


FIELDS = {
    'Boolean':  BooleanField,
    'Bytes':    FileField,
    'Date':     DateField,
    'Datetime': DateTimeField,
    'Int':      IntField,
    'Float':    FloatField,
    'Decimal':  StringField,
    'Password': PasswordField,
    'String' :  StringField,
    'Time':     TimeField,
    }