cubicweb/web/formfields.py
changeset 11057 0b59724cb3f2
parent 10903 da30851f9706
child 11129 97095348b3ee
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
       
     1 # copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """
       
    19 The Field class and basic fields
       
    20 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       
    21 
       
    22 .. Note::
       
    23   Fields are used to control what's edited in forms. They makes the link between
       
    24   something to edit and its display in the form. Actual display is handled by a
       
    25   widget associated to the field.
       
    26 
       
    27 Let first see the base class for fields:
       
    28 
       
    29 .. autoclass:: cubicweb.web.formfields.Field
       
    30 
       
    31 Now, you usually don't use that class but one of the concrete field classes
       
    32 described below, according to what you want to edit.
       
    33 
       
    34 Basic fields
       
    35 ''''''''''''
       
    36 
       
    37 .. autoclass:: cubicweb.web.formfields.StringField()
       
    38 .. autoclass:: cubicweb.web.formfields.PasswordField()
       
    39 .. autoclass:: cubicweb.web.formfields.IntField()
       
    40 .. autoclass:: cubicweb.web.formfields.BigIntField()
       
    41 .. autoclass:: cubicweb.web.formfields.FloatField()
       
    42 .. autoclass:: cubicweb.web.formfields.BooleanField()
       
    43 .. autoclass:: cubicweb.web.formfields.DateField()
       
    44 .. autoclass:: cubicweb.web.formfields.DateTimeField()
       
    45 .. autoclass:: cubicweb.web.formfields.TimeField()
       
    46 .. autoclass:: cubicweb.web.formfields.TimeIntervalField()
       
    47 
       
    48 Compound fields
       
    49 ''''''''''''''''
       
    50 
       
    51 .. autoclass:: cubicweb.web.formfields.RichTextField()
       
    52 .. autoclass:: cubicweb.web.formfields.FileField()
       
    53 .. autoclass:: cubicweb.web.formfields.CompoundField()
       
    54 
       
    55 .. autoclass cubicweb.web.formfields.EditableFileField() XXX should be a widget
       
    56 
       
    57 Entity specific fields and function
       
    58 '''''''''''''''''''''''''''''''''''
       
    59 
       
    60 .. autoclass:: cubicweb.web.formfields.RelationField()
       
    61 .. autofunction:: cubicweb.web.formfields.guess_field
       
    62 
       
    63 """
       
    64 __docformat__ = "restructuredtext en"
       
    65 
       
    66 from warnings import warn
       
    67 from datetime import datetime, timedelta
       
    68 
       
    69 from six import PY2, text_type, string_types
       
    70 
       
    71 from logilab.mtconverter import xml_escape
       
    72 from logilab.common import nullobject
       
    73 from logilab.common.date import ustrftime
       
    74 from logilab.common.configuration import format_time
       
    75 from logilab.common.textutils import apply_units, TIME_UNITS
       
    76 
       
    77 from yams.schema import KNOWN_METAATTRIBUTES, role_name
       
    78 from yams.constraints import (SizeConstraint, StaticVocabularyConstraint,
       
    79                               FormatConstraint)
       
    80 
       
    81 from cubicweb import Binary, tags, uilib
       
    82 from cubicweb.utils import support_args
       
    83 from cubicweb.web import INTERNAL_FIELD_VALUE, ProcessFormError, eid_param, \
       
    84      formwidgets as fw
       
    85 from cubicweb.web.views import uicfg
       
    86 
       
    87 class UnmodifiedField(Exception):
       
    88     """raise this when a field has not actually been edited and you want to skip
       
    89     it
       
    90     """
       
    91 
       
    92 def normalize_filename(filename):
       
    93     return filename.split('\\')[-1]
       
    94 
       
    95 def vocab_sort(vocab):
       
    96     """sort vocabulary, considering option groups"""
       
    97     result = []
       
    98     partresult = []
       
    99     for label, value in vocab:
       
   100         if value is None: # opt group start
       
   101             if partresult:
       
   102                 result += sorted(partresult)
       
   103                 partresult = []
       
   104             result.append( (label, value) )
       
   105         else:
       
   106             partresult.append( (label, value) )
       
   107     result += sorted(partresult)
       
   108     return result
       
   109 
       
   110 _MARKER = nullobject()
       
   111 
       
   112 class Field(object):
       
   113     """This class is the abstract base class for all fields. It hold a bunch
       
   114     of attributes which may be used for fine control of the behaviour of a
       
   115     concrete field.
       
   116 
       
   117     **Attributes**
       
   118 
       
   119     All the attributes described below have sensible default value which may be
       
   120     overriden by named arguments given to field's constructor.
       
   121 
       
   122     :attr:`name`
       
   123        base name of the field (basestring). The actual input name is returned by
       
   124        the :meth:`input_name` method and may differ from that name (for instance
       
   125        if `eidparam` is true).
       
   126     :attr:`id`
       
   127        DOM identifier (default to the same value as `name`), should be unique in
       
   128        a form.
       
   129     :attr:`label`
       
   130        label of the field (default to the same value as `name`).
       
   131     :attr:`help`
       
   132        help message about this field.
       
   133     :attr:`widget`
       
   134        widget associated to the field. Each field class has a default widget
       
   135        class which may be overriden per instance.
       
   136     :attr:`value`
       
   137        field value. May be an actual value or a callable which should take the
       
   138        form as argument and return a value.
       
   139     :attr:`choices`
       
   140        static vocabulary for this field. May be a list of values, a list of
       
   141        (label, value) tuples or a callable which should take the form and field
       
   142        as arguments and return a list of values or a list of (label, value).
       
   143     :attr:`required`
       
   144        bool flag telling if the field is required or not.
       
   145     :attr:`sort`
       
   146        bool flag telling if the vocabulary (either static vocabulary specified
       
   147        in `choices` or dynamic vocabulary fetched from the form) should be
       
   148        sorted on label.
       
   149     :attr:`internationalizable`
       
   150        bool flag telling if the vocabulary labels should be translated using the
       
   151        current request language.
       
   152     :attr:`eidparam`
       
   153        bool flag telling if this field is linked to a specific entity
       
   154     :attr:`role`
       
   155        when the field is linked to an entity attribute or relation, tells the
       
   156        role of the entity in the relation (eg 'subject' or 'object'). If this is
       
   157        not an attribute or relation of the edited entity, `role` should be
       
   158        `None`.
       
   159     :attr:`fieldset`
       
   160        optional fieldset to which this field belongs to
       
   161     :attr:`order`
       
   162        key used by automatic forms to sort fields
       
   163     :attr:`ignore_req_params`
       
   164        when true, this field won't consider value potentially specified using
       
   165        request's form parameters (eg you won't be able to specify a value using for
       
   166        instance url like http://mywebsite.com/form?field=value)
       
   167 
       
   168     .. currentmodule:: cubicweb.web.formfields
       
   169 
       
   170     **Generic methods**
       
   171 
       
   172     .. automethod:: Field.input_name
       
   173     .. automethod:: Field.dom_id
       
   174     .. automethod:: Field.actual_fields
       
   175 
       
   176     **Form generation methods**
       
   177 
       
   178     .. automethod:: form_init
       
   179     .. automethod:: typed_value
       
   180 
       
   181     **Post handling methods**
       
   182 
       
   183     .. automethod:: process_posted
       
   184     .. automethod:: process_form_value
       
   185 
       
   186     """
       
   187     # default widget associated to this class of fields. May be overriden per
       
   188     # instance
       
   189     widget = fw.TextInput
       
   190     # does this field requires a multipart form
       
   191     needs_multipart = False
       
   192     # class attribute used for ordering of fields in a form
       
   193     __creation_rank = 0
       
   194 
       
   195     eidparam = False
       
   196     role = None
       
   197     id = None
       
   198     help = None
       
   199     required = False
       
   200     choices = None
       
   201     sort = True
       
   202     internationalizable = False
       
   203     fieldset = None
       
   204     order = None
       
   205     value = _MARKER
       
   206     fallback_on_none_attribute = False
       
   207     ignore_req_params = False
       
   208 
       
   209     def __init__(self, name=None, label=_MARKER, widget=None, **kwargs):
       
   210         for key, val in kwargs.items():
       
   211             assert hasattr(self.__class__, key) and not key[0] == '_', key
       
   212             setattr(self, key, val)
       
   213         self.name = name
       
   214         if label is _MARKER:
       
   215             label = name or _MARKER
       
   216         self.label = label
       
   217         # has to be done after other attributes initialization
       
   218         self.init_widget(widget)
       
   219         # ordering number for this field instance
       
   220         self.creation_rank = Field.__creation_rank
       
   221         Field.__creation_rank += 1
       
   222 
       
   223     def as_string(self, repr=True):
       
   224         l = [u'<%s' % self.__class__.__name__]
       
   225         for attr in ('name', 'eidparam', 'role', 'id', 'value'):
       
   226             value = getattr(self, attr)
       
   227             if value is not None and value is not _MARKER:
       
   228                 l.append('%s=%r' % (attr, value))
       
   229         if repr:
       
   230             l.append('@%#x' % id(self))
       
   231         return u'%s>' % ' '.join(l)
       
   232 
       
   233     def __unicode__(self):
       
   234         return self.as_string(False)
       
   235 
       
   236     if PY2:
       
   237         def __str__(self):
       
   238             return self.as_string(False).encode('UTF8')
       
   239     else:
       
   240         __str__ = __unicode__
       
   241 
       
   242     def __repr__(self):
       
   243         return self.as_string(True)
       
   244 
       
   245     def init_widget(self, widget):
       
   246         if widget is not None:
       
   247             self.widget = widget
       
   248         elif self.choices and not self.widget.vocabulary_widget:
       
   249             self.widget = fw.Select()
       
   250         if isinstance(self.widget, type):
       
   251             self.widget = self.widget()
       
   252 
       
   253     def set_name(self, name):
       
   254         """automatically set .label when name is set"""
       
   255         assert name
       
   256         self.name = name
       
   257         if self.label is _MARKER:
       
   258             self.label = name
       
   259 
       
   260     def is_visible(self):
       
   261         """return true if the field is not an hidden field"""
       
   262         return not isinstance(self.widget, fw.HiddenInput)
       
   263 
       
   264     def actual_fields(self, form):
       
   265         """Fields may be composed of other fields. For instance the
       
   266         :class:`~cubicweb.web.formfields.RichTextField` is containing a format
       
   267         field to define the text format. This method returns actual fields that
       
   268         should be considered for display / edition. It usually simply return
       
   269         self.
       
   270         """
       
   271         yield self
       
   272 
       
   273     def format_value(self, req, value):
       
   274         """return value suitable for display where value may be a list or tuple
       
   275         of values
       
   276         """
       
   277         if isinstance(value, (list, tuple)):
       
   278             return [self.format_single_value(req, val) for val in value]
       
   279         return self.format_single_value(req, value)
       
   280 
       
   281     def format_single_value(self, req, value):
       
   282         """return value suitable for display"""
       
   283         if value is None or value is False:
       
   284             return u''
       
   285         if value is True:
       
   286             return u'1'
       
   287         return text_type(value)
       
   288 
       
   289     def get_widget(self, form):
       
   290         """return the widget instance associated to this field"""
       
   291         return self.widget
       
   292 
       
   293     def input_name(self, form, suffix=None):
       
   294         """Return the 'qualified name' for this field, e.g. something suitable
       
   295         to use as HTML input name. You can specify a suffix that will be
       
   296         included in the name when widget needs several inputs.
       
   297         """
       
   298         # caching is necessary else we get some pb on entity creation :
       
   299         # entity.eid is modified from creation mark (eg 'X') to its actual eid
       
   300         # (eg 123), and then `field.input_name()` won't return the right key
       
   301         # anymore if not cached (first call to input_name done *before* eventual
       
   302         # eid affectation).
       
   303         #
       
   304         # note that you should NOT use @cached else it will create a memory leak
       
   305         # on persistent fields (eg created once for all on a form class) because
       
   306         # of the 'form' appobject argument: the cache will keep growing as new
       
   307         # form are created...
       
   308         try:
       
   309             return form.formvalues[(self, 'input_name', suffix)]
       
   310         except KeyError:
       
   311             name = self.role_name()
       
   312             if suffix is not None:
       
   313                 name += suffix
       
   314             if self.eidparam:
       
   315                 name = eid_param(name, form.edited_entity.eid)
       
   316             form.formvalues[(self, 'input_name', suffix)] = name
       
   317             return name
       
   318 
       
   319     def role_name(self):
       
   320         """return <field.name>-<field.role> if role is specified, else field.name"""
       
   321         assert self.name, 'field without a name (give it to constructor for explicitly built fields)'
       
   322         if self.role is not None:
       
   323             return role_name(self.name, self.role)
       
   324         return self.name
       
   325 
       
   326     def dom_id(self, form, suffix=None):
       
   327         """Return the HTML DOM identifier for this field, e.g. something
       
   328         suitable to use as HTML input id. You can specify a suffix that will be
       
   329         included in the name when widget needs several inputs.
       
   330         """
       
   331         id = self.id or self.role_name()
       
   332         if suffix is not None:
       
   333             id += suffix
       
   334         if self.eidparam:
       
   335             return eid_param(id, form.edited_entity.eid)
       
   336         return id
       
   337 
       
   338     def typed_value(self, form, load_bytes=False):
       
   339         """Return the correctly typed value for this field in the form context.
       
   340         """
       
   341         if self.eidparam and self.role is not None:
       
   342             entity = form.edited_entity
       
   343             if form._cw.vreg.schema.rschema(self.name).final:
       
   344                 if entity.has_eid() or self.name in entity.cw_attr_cache:
       
   345                     value = getattr(entity, self.name)
       
   346                     if value is not None or not self.fallback_on_none_attribute:
       
   347                         return value
       
   348             elif entity.has_eid() or entity.cw_relation_cached(self.name, self.role):
       
   349                 value = [r[0] for r in entity.related(self.name, self.role)]
       
   350                 if value or not self.fallback_on_none_attribute:
       
   351                     return value
       
   352         return self.initial_typed_value(form, load_bytes)
       
   353 
       
   354     def initial_typed_value(self, form, load_bytes):
       
   355         if self.value is not _MARKER:
       
   356             if callable(self.value):
       
   357                 return self.value(form, self)
       
   358             return self.value
       
   359         formattr = '%s_%s_default' % (self.role, self.name)
       
   360         if self.eidparam and self.role is not None:
       
   361             if form._cw.vreg.schema.rschema(self.name).final:
       
   362                 return form.edited_entity.e_schema.default(self.name)
       
   363             return form.linked_to.get((self.name, self.role), ())
       
   364         return None
       
   365 
       
   366     def example_format(self, req):
       
   367         """return a sample string describing what can be given as input for this
       
   368         field
       
   369         """
       
   370         return u''
       
   371 
       
   372     def render(self, form, renderer):
       
   373         """render this field, which is part of form, using the given form
       
   374         renderer
       
   375         """
       
   376         widget = self.get_widget(form)
       
   377         return widget.render(form, self, renderer)
       
   378 
       
   379     def vocabulary(self, form, **kwargs):
       
   380         """return vocabulary for this field. This method will be
       
   381         called by widgets which requires a vocabulary.
       
   382 
       
   383         It should return a list of tuple (label, value), where value
       
   384         *must be a unicode string*, not a typed value.
       
   385         """
       
   386         assert self.choices is not None
       
   387         if callable(self.choices):
       
   388             # pylint: disable=E1102
       
   389             if getattr(self.choices, '__self__', None) is self:
       
   390                 vocab = self.choices(form=form, **kwargs)
       
   391             else:
       
   392                 vocab = self.choices(form=form, field=self, **kwargs)
       
   393         else:
       
   394             vocab = self.choices
       
   395         if vocab and not isinstance(vocab[0], (list, tuple)):
       
   396             vocab = [(x, x) for x in vocab]
       
   397         if self.internationalizable:
       
   398             # the short-cirtcuit 'and' boolean operator is used here
       
   399             # to permit a valid empty string in vocabulary without
       
   400             # attempting to translate it by gettext (which can lead to
       
   401             # weird strings display)
       
   402             vocab = [(label and form._cw._(label), value)
       
   403                      for label, value in vocab]
       
   404         if self.sort:
       
   405             vocab = vocab_sort(vocab)
       
   406         return vocab
       
   407 
       
   408     # support field as argument to avoid warning when used as format field value
       
   409     # callback
       
   410     def format(self, form, field=None):
       
   411         """return MIME type used for the given (text or bytes) field"""
       
   412         if self.eidparam and self.role == 'subject':
       
   413             entity = form.edited_entity
       
   414             if entity.e_schema.has_metadata(self.name, 'format') and (
       
   415                 entity.has_eid() or '%s_format' % self.name in entity.cw_attr_cache):
       
   416                 return form.edited_entity.cw_attr_metadata(self.name, 'format')
       
   417         return form._cw.property_value('ui.default-text-format')
       
   418 
       
   419     def encoding(self, form):
       
   420         """return encoding used for the given (text) field"""
       
   421         if self.eidparam:
       
   422             entity = form.edited_entity
       
   423             if entity.e_schema.has_metadata(self.name, 'encoding') and (
       
   424                 entity.has_eid() or '%s_encoding' % self.name in entity):
       
   425                 return form.edited_entity.cw_attr_metadata(self.name, 'encoding')
       
   426         return form._cw.encoding
       
   427 
       
   428     def form_init(self, form):
       
   429         """Method called at form initialization to trigger potential field
       
   430         initialization requiring the form instance. Do nothing by default.
       
   431         """
       
   432         pass
       
   433 
       
   434     def has_been_modified(self, form):
       
   435         for field in self.actual_fields(form):
       
   436             if field._has_been_modified(form):
       
   437                 return True # XXX
       
   438         return False # not modified
       
   439 
       
   440     def _has_been_modified(self, form):
       
   441         # fields not corresponding to an entity attribute / relations
       
   442         # are considered modified
       
   443         if not self.eidparam or not self.role or not form.edited_entity.has_eid():
       
   444             return True # XXX
       
   445         try:
       
   446             if self.role == 'subject':
       
   447                 previous_value = getattr(form.edited_entity, self.name)
       
   448             else:
       
   449                 previous_value = getattr(form.edited_entity,
       
   450                                          'reverse_%s' % self.name)
       
   451         except AttributeError:
       
   452             # fields with eidparam=True but not corresponding to an actual
       
   453             # attribute or relation
       
   454             return True
       
   455         # if it's a non final relation, we need the eids
       
   456         if isinstance(previous_value, (list, tuple)):
       
   457             # widget should return a set of untyped eids
       
   458             previous_value = set(e.eid for e in previous_value)
       
   459         try:
       
   460             new_value = self.process_form_value(form)
       
   461         except ProcessFormError:
       
   462             return True
       
   463         except UnmodifiedField:
       
   464             return False # not modified
       
   465         if previous_value == new_value:
       
   466             return False # not modified
       
   467         return True
       
   468 
       
   469     def process_form_value(self, form):
       
   470         """Return the correctly typed value posted for this field."""
       
   471         try:
       
   472             return form.formvalues[(self, form)]
       
   473         except KeyError:
       
   474             value = form.formvalues[(self, form)] = self._process_form_value(form)
       
   475             return value
       
   476 
       
   477     def _process_form_value(self, form):
       
   478         widget = self.get_widget(form)
       
   479         value = widget.process_field_data(form, self)
       
   480         return self._ensure_correctly_typed(form, value)
       
   481 
       
   482     def _ensure_correctly_typed(self, form, value):
       
   483         """widget might to return date as a correctly formatted string or as
       
   484         correctly typed objects, but process_for_value must return a typed value.
       
   485         Override this method to type the value if necessary
       
   486         """
       
   487         return value or None
       
   488 
       
   489     def process_posted(self, form):
       
   490         """Return an iterator on (field, value) that has been posted for
       
   491         field returned by :meth:`~cubicweb.web.formfields.Field.actual_fields`.
       
   492         """
       
   493         for field in self.actual_fields(form):
       
   494             if field is self:
       
   495                 try:
       
   496                     value = field.process_form_value(form)
       
   497                     if field.no_value(value) and field.required:
       
   498                         raise ProcessFormError(form._cw._("required field"))
       
   499                     yield field, value
       
   500                 except UnmodifiedField:
       
   501                     continue
       
   502             else:
       
   503                 # recursive function: we might have compound fields
       
   504                 # of compound fields (of compound fields of ...)
       
   505                 for field, value in field.process_posted(form):
       
   506                     yield field, value
       
   507 
       
   508     @staticmethod
       
   509     def no_value(value):
       
   510         """return True if the value can be considered as no value for the field"""
       
   511         return value is None
       
   512 
       
   513 
       
   514 class StringField(Field):
       
   515     """Use this field to edit unicode string (`String` yams type). This field
       
   516     additionally support a `max_length` attribute that specify a maximum size for
       
   517     the string (`None` meaning no limit).
       
   518 
       
   519     Unless explicitly specified, the widget for this field will be:
       
   520 
       
   521     * :class:`~cubicweb.web.formwidgets.Select` if some vocabulary is specified
       
   522       using `choices` attribute
       
   523 
       
   524     * :class:`~cubicweb.web.formwidgets.TextInput` if maximum size is specified
       
   525       using `max_length` attribute and this length is inferior to 257.
       
   526 
       
   527     * :class:`~cubicweb.web.formwidgets.TextArea` in all other cases
       
   528     """
       
   529     widget = fw.TextArea
       
   530     size = 45
       
   531     placeholder = None
       
   532 
       
   533     def __init__(self, name=None, max_length=None, **kwargs):
       
   534         self.max_length = max_length # must be set before super call
       
   535         super(StringField, self).__init__(name=name, **kwargs)
       
   536 
       
   537     def init_widget(self, widget):
       
   538         if widget is None:
       
   539             if self.choices:
       
   540                 widget = fw.Select()
       
   541             elif self.max_length and self.max_length < 257:
       
   542                 widget = fw.TextInput()
       
   543 
       
   544         super(StringField, self).init_widget(widget)
       
   545         if isinstance(self.widget, fw.TextArea):
       
   546             self.init_text_area(self.widget)
       
   547         elif isinstance(self.widget, fw.TextInput):
       
   548             self.init_text_input(self.widget)
       
   549 
       
   550         if self.placeholder:
       
   551             self.widget.attrs.setdefault('placeholder', self.placeholder)
       
   552 
       
   553     def init_text_input(self, widget):
       
   554         if self.max_length:
       
   555             widget.attrs.setdefault('size', min(self.size, self.max_length))
       
   556             widget.attrs.setdefault('maxlength', self.max_length)
       
   557 
       
   558     def init_text_area(self, widget):
       
   559         if self.max_length and self.max_length < 513:
       
   560             widget.attrs.setdefault('cols', 60)
       
   561             widget.attrs.setdefault('rows', 5)
       
   562 
       
   563     def set_placeholder(self, placeholder):
       
   564         self.placeholder = placeholder
       
   565         if self.widget and self.placeholder:
       
   566             self.widget.attrs.setdefault('placeholder', self.placeholder)
       
   567 
       
   568 
       
   569 class PasswordField(StringField):
       
   570     """Use this field to edit password (`Password` yams type, encoded python
       
   571     string).
       
   572 
       
   573     Unless explicitly specified, the widget for this field will be
       
   574     a :class:`~cubicweb.web.formwidgets.PasswordInput`.
       
   575     """
       
   576     widget = fw.PasswordInput
       
   577     def form_init(self, form):
       
   578         if self.eidparam and form.edited_entity.has_eid():
       
   579             # see below: value is probably set but we can't retreive it. Ensure
       
   580             # the field isn't show as a required field on modification
       
   581             self.required = False
       
   582 
       
   583     def typed_value(self, form, load_bytes=False):
       
   584         if self.eidparam:
       
   585             # no way to fetch actual password value with cw
       
   586             if form.edited_entity.has_eid():
       
   587                 return ''
       
   588             return self.initial_typed_value(form, load_bytes)
       
   589         return super(PasswordField, self).typed_value(form, load_bytes)
       
   590 
       
   591 
       
   592 class RichTextField(StringField):
       
   593     """This compound field allow edition of text (unicode string) in
       
   594     a particular format. It has an inner field holding the text format,
       
   595     that can be specified using `format_field` argument. If not specified
       
   596     one will be automaticall generated.
       
   597 
       
   598     Unless explicitly specified, the widget for this field will be a
       
   599     :class:`~cubicweb.web.formwidgets.FCKEditor` or a
       
   600     :class:`~cubicweb.web.formwidgets.TextArea`. according to the field's
       
   601     format and to user's preferences.
       
   602     """
       
   603 
       
   604     widget = None
       
   605     def __init__(self, format_field=None, **kwargs):
       
   606         super(RichTextField, self).__init__(**kwargs)
       
   607         self.format_field = format_field
       
   608 
       
   609     def init_text_area(self, widget):
       
   610         pass
       
   611 
       
   612     def get_widget(self, form):
       
   613         if self.widget is None:
       
   614             if self.use_fckeditor(form):
       
   615                 return fw.FCKEditor()
       
   616             widget = fw.TextArea()
       
   617             self.init_text_area(widget)
       
   618             return widget
       
   619         return self.widget
       
   620 
       
   621     def get_format_field(self, form):
       
   622         if self.format_field:
       
   623             return self.format_field
       
   624         # we have to cache generated field since it's use as key in the
       
   625         # context dictionary
       
   626         req = form._cw
       
   627         try:
       
   628             return req.data[self]
       
   629         except KeyError:
       
   630             fkwargs = {'eidparam': self.eidparam, 'role': self.role}
       
   631             if self.use_fckeditor(form):
       
   632                 # if fckeditor is used and format field isn't explicitly
       
   633                 # deactivated, we want an hidden field for the format
       
   634                 fkwargs['widget'] = fw.HiddenInput()
       
   635                 fkwargs['value'] = 'text/html'
       
   636             else:
       
   637                 # else we want a format selector
       
   638                 fkwargs['widget'] = fw.Select()
       
   639                 fcstr = FormatConstraint()
       
   640                 fkwargs['choices'] = fcstr.vocabulary(form=form)
       
   641                 fkwargs['internationalizable'] = True
       
   642                 fkwargs['value'] = self.format
       
   643             fkwargs['eidparam'] = self.eidparam
       
   644             field = StringField(name=self.name + '_format', **fkwargs)
       
   645             req.data[self] = field
       
   646             return field
       
   647 
       
   648     def actual_fields(self, form):
       
   649         yield self
       
   650         format_field = self.get_format_field(form)
       
   651         if format_field:
       
   652             yield format_field
       
   653 
       
   654     def use_fckeditor(self, form):
       
   655         """return True if fckeditor should be used to edit entity's attribute named
       
   656         `attr`, according to user preferences
       
   657         """
       
   658         if form._cw.use_fckeditor():
       
   659             return self.format(form) == 'text/html'
       
   660         return False
       
   661 
       
   662     def render(self, form, renderer):
       
   663         format_field = self.get_format_field(form)
       
   664         if format_field:
       
   665             # XXX we want both fields to remain vertically aligned
       
   666             if format_field.is_visible():
       
   667                 format_field.widget.attrs['style'] = 'display: block'
       
   668             result = format_field.render(form, renderer)
       
   669         else:
       
   670             result = u''
       
   671         return result + self.get_widget(form).render(form, self, renderer)
       
   672 
       
   673 
       
   674 class FileField(StringField):
       
   675     """This compound field allow edition of binary stream (`Bytes` yams
       
   676     type). Three inner fields may be specified:
       
   677 
       
   678     * `format_field`, holding the file's format.
       
   679     * `encoding_field`, holding the file's content encoding.
       
   680     * `name_field`, holding the file's name.
       
   681 
       
   682     Unless explicitly specified, the widget for this field will be a
       
   683     :class:`~cubicweb.web.formwidgets.FileInput`. Inner fields, if any,
       
   684     will be added to a drop down menu at the right of the file input.
       
   685     """
       
   686     widget = fw.FileInput
       
   687     needs_multipart = True
       
   688 
       
   689     def __init__(self, format_field=None, encoding_field=None, name_field=None,
       
   690                  **kwargs):
       
   691         super(FileField, self).__init__(**kwargs)
       
   692         self.format_field = format_field
       
   693         self.encoding_field = encoding_field
       
   694         self.name_field = name_field
       
   695 
       
   696     def actual_fields(self, form):
       
   697         yield self
       
   698         if self.format_field:
       
   699             yield self.format_field
       
   700         if self.encoding_field:
       
   701             yield self.encoding_field
       
   702         if self.name_field:
       
   703             yield self.name_field
       
   704 
       
   705     def typed_value(self, form, load_bytes=False):
       
   706         if self.eidparam and self.role is not None:
       
   707             if form.edited_entity.has_eid():
       
   708                 if load_bytes:
       
   709                     return getattr(form.edited_entity, self.name)
       
   710                 # don't actually load data
       
   711                 # XXX value should reflect if some file is already attached
       
   712                 # * try to display name metadata
       
   713                 # * check length(data) / data != null
       
   714                 return True
       
   715             return False
       
   716         return super(FileField, self).typed_value(form, load_bytes)
       
   717 
       
   718     def render(self, form, renderer):
       
   719         wdgs = [self.get_widget(form).render(form, self, renderer)]
       
   720         if self.format_field or self.encoding_field:
       
   721             divid = '%s-advanced' % self.input_name(form)
       
   722             wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
       
   723                         (xml_escape(uilib.toggle_action(divid)),
       
   724                          form._cw._('show advanced fields'),
       
   725                          xml_escape(form._cw.data_url('puce_down.png')),
       
   726                          form._cw._('show advanced fields')))
       
   727             wdgs.append(u'<div id="%s" class="hidden">' % divid)
       
   728             if self.name_field:
       
   729                 wdgs.append(self.render_subfield(form, self.name_field, renderer))
       
   730             if self.format_field:
       
   731                 wdgs.append(self.render_subfield(form, self.format_field, renderer))
       
   732             if self.encoding_field:
       
   733                 wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
       
   734             wdgs.append(u'</div>')
       
   735         if not self.required and self.typed_value(form):
       
   736             # trick to be able to delete an uploaded file
       
   737             wdgs.append(u'<br/>')
       
   738             wdgs.append(tags.input(name=self.input_name(form, u'__detach'),
       
   739                                    type=u'checkbox'))
       
   740             wdgs.append(form._cw._('detach attached file'))
       
   741         return u'\n'.join(wdgs)
       
   742 
       
   743     def render_subfield(self, form, field, renderer):
       
   744         return (renderer.render_label(form, field)
       
   745                 + field.render(form, renderer)
       
   746                 + renderer.render_help(form, field)
       
   747                 + u'<br/>')
       
   748 
       
   749     def _process_form_value(self, form):
       
   750         posted = form._cw.form
       
   751         if self.input_name(form, u'__detach') in posted:
       
   752             # drop current file value on explictily asked to detach
       
   753             return None
       
   754         try:
       
   755             value = posted[self.input_name(form)]
       
   756         except KeyError:
       
   757             # raise UnmodifiedField instead of returning None, since the later
       
   758             # will try to remove already attached file if any
       
   759             raise UnmodifiedField()
       
   760         # value is a 2-uple (filename, stream) or a list of such
       
   761         # tuples (multiple files)
       
   762         try:
       
   763             if isinstance(value, list):
       
   764                 value = value[0]
       
   765                 form.warning('mutiple files provided, however '
       
   766                              'only the first will be picked')
       
   767             filename, stream = value
       
   768         except ValueError:
       
   769             raise UnmodifiedField()
       
   770         # XXX avoid in memory loading of posted files. Requires Binary handling changes...
       
   771         value = Binary(stream.read())
       
   772         if not value.getvalue(): # usually an unexistant file
       
   773             value = None
       
   774         else:
       
   775             # set filename on the Binary instance, may be used later in hooks
       
   776             value.filename = normalize_filename(filename)
       
   777         return value
       
   778 
       
   779 
       
   780 # XXX turn into a widget
       
   781 class EditableFileField(FileField):
       
   782     """This compound field allow edition of binary stream as
       
   783     :class:`~cubicweb.web.formfields.FileField` but expect that stream to
       
   784     actually contains some text.
       
   785 
       
   786     If the stream format is one of text/plain, text/html, text/rest,
       
   787     text/markdown
       
   788     then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionally
       
   789     displayed, allowing to directly the file's content when desired, instead
       
   790     of choosing a file from user's file system.
       
   791     """
       
   792     editable_formats = (
       
   793         'text/plain', 'text/html', 'text/rest', 'text/markdown')
       
   794 
       
   795     def render(self, form, renderer):
       
   796         wdgs = [super(EditableFileField, self).render(form, renderer)]
       
   797         if self.format(form) in self.editable_formats:
       
   798             data = self.typed_value(form, load_bytes=True)
       
   799             if data:
       
   800                 encoding = self.encoding(form)
       
   801                 try:
       
   802                     form.formvalues[(self, form)] = data.getvalue().decode(encoding)
       
   803                 except UnicodeError:
       
   804                     pass
       
   805                 else:
       
   806                     if not self.required:
       
   807                         msg = form._cw._(
       
   808                             'You can either submit a new file using the browse button above'
       
   809                             ', or choose to remove already uploaded file by checking the '
       
   810                             '"detach attached file" check-box, or edit file content online '
       
   811                             'with the widget below.')
       
   812                     else:
       
   813                         msg = form._cw._(
       
   814                             'You can either submit a new file using the browse button above'
       
   815                             ', or edit file content online with the widget below.')
       
   816                     wdgs.append(u'<p><b>%s</b></p>' % msg)
       
   817                     wdgs.append(fw.TextArea(setdomid=False).render(form, self, renderer))
       
   818                     # XXX restore form context?
       
   819         return '\n'.join(wdgs)
       
   820 
       
   821     def _process_form_value(self, form):
       
   822         value = form._cw.form.get(self.input_name(form))
       
   823         if isinstance(value, text_type):
       
   824             # file modified using a text widget
       
   825             return Binary(value.encode(self.encoding(form)))
       
   826         return super(EditableFileField, self)._process_form_value(form)
       
   827 
       
   828 
       
   829 class BigIntField(Field):
       
   830     """Use this field to edit big integers (`BigInt` yams type). This field
       
   831     additionally support `min` and `max` attributes that specify a minimum and/or
       
   832     maximum value for the integer (`None` meaning no boundary).
       
   833 
       
   834     Unless explicitly specified, the widget for this field will be a
       
   835     :class:`~cubicweb.web.formwidgets.TextInput`.
       
   836     """
       
   837     default_text_input_size = 10
       
   838 
       
   839     def __init__(self, min=None, max=None, **kwargs):
       
   840         super(BigIntField, self).__init__(**kwargs)
       
   841         self.min = min
       
   842         self.max = max
       
   843 
       
   844     def init_widget(self, widget):
       
   845         super(BigIntField, self).init_widget(widget)
       
   846         if isinstance(self.widget, fw.TextInput):
       
   847             self.widget.attrs.setdefault('size', self.default_text_input_size)
       
   848 
       
   849     def _ensure_correctly_typed(self, form, value):
       
   850         if isinstance(value, string_types):
       
   851             value = value.strip()
       
   852             if not value:
       
   853                 return None
       
   854             try:
       
   855                 return int(value)
       
   856             except ValueError:
       
   857                 raise ProcessFormError(form._cw._('an integer is expected'))
       
   858         return value
       
   859 
       
   860 
       
   861 class IntField(BigIntField):
       
   862     """Use this field to edit integers (`Int` yams type). Similar to
       
   863     :class:`~cubicweb.web.formfields.BigIntField` but set max length when text
       
   864     input widget is used (the default).
       
   865     """
       
   866     default_text_input_size = 5
       
   867 
       
   868     def init_widget(self, widget):
       
   869         super(IntField, self).init_widget(widget)
       
   870         if isinstance(self.widget, fw.TextInput):
       
   871             self.widget.attrs.setdefault('maxlength', 15)
       
   872 
       
   873 
       
   874 class BooleanField(Field):
       
   875     """Use this field to edit booleans (`Boolean` yams type).
       
   876 
       
   877     Unless explicitly specified, the widget for this field will be a
       
   878     :class:`~cubicweb.web.formwidgets.Radio` with yes/no values. You
       
   879     can change that values by specifing `choices`.
       
   880     """
       
   881     widget = fw.Radio
       
   882 
       
   883     def __init__(self, allow_none=False, **kwargs):
       
   884         super(BooleanField, self).__init__(**kwargs)
       
   885         self.allow_none = allow_none
       
   886 
       
   887     def vocabulary(self, form):
       
   888         if self.choices:
       
   889             return super(BooleanField, self).vocabulary(form)
       
   890         if self.allow_none:
       
   891             return [(form._cw._('indifferent'), ''),
       
   892                     (form._cw._('yes'), '1'),
       
   893                     (form._cw._('no'), '0')]
       
   894         # XXX empty string for 'no' in that case for bw compat
       
   895         return [(form._cw._('yes'), '1'), (form._cw._('no'), '')]
       
   896 
       
   897     def format_single_value(self, req, value):
       
   898         """return value suitable for display"""
       
   899         if self.allow_none:
       
   900             if value is None:
       
   901                 return u''
       
   902             if value is False:
       
   903                 return '0'
       
   904         return super(BooleanField, self).format_single_value(req, value)
       
   905 
       
   906     def _ensure_correctly_typed(self, form, value):
       
   907         if self.allow_none:
       
   908             if value:
       
   909                 return bool(int(value))
       
   910             return None
       
   911         return bool(value)
       
   912 
       
   913 
       
   914 class FloatField(IntField):
       
   915     """Use this field to edit floats (`Float` yams type). This field additionally
       
   916     support `min` and `max` attributes as the
       
   917     :class:`~cubicweb.web.formfields.IntField`.
       
   918 
       
   919     Unless explicitly specified, the widget for this field will be a
       
   920     :class:`~cubicweb.web.formwidgets.TextInput`.
       
   921     """
       
   922     def format_single_value(self, req, value):
       
   923         formatstr = req.property_value('ui.float-format')
       
   924         if value is None:
       
   925             return u''
       
   926         return formatstr % float(value)
       
   927 
       
   928     def render_example(self, req):
       
   929         return self.format_single_value(req, 1.234)
       
   930 
       
   931     def _ensure_correctly_typed(self, form, value):
       
   932         if isinstance(value, string_types):
       
   933             value = value.strip()
       
   934             if not value:
       
   935                 return None
       
   936             try:
       
   937                 return float(value)
       
   938             except ValueError:
       
   939                 raise ProcessFormError(form._cw._('a float is expected'))
       
   940         return None
       
   941 
       
   942 
       
   943 class TimeIntervalField(StringField):
       
   944     """Use this field to edit time interval (`Interval` yams type).
       
   945 
       
   946     Unless explicitly specified, the widget for this field will be a
       
   947     :class:`~cubicweb.web.formwidgets.TextInput`.
       
   948     """
       
   949     widget = fw.TextInput
       
   950 
       
   951     def format_single_value(self, req, value):
       
   952         if value:
       
   953             value = format_time(value.days * 24 * 3600 + value.seconds)
       
   954             return text_type(value)
       
   955         return u''
       
   956 
       
   957     def example_format(self, req):
       
   958         """return a sample string describing what can be given as input for this
       
   959         field
       
   960         """
       
   961         return u'20s, 10min, 24h, 4d'
       
   962 
       
   963     def _ensure_correctly_typed(self, form, value):
       
   964         if isinstance(value, string_types):
       
   965             value = value.strip()
       
   966             if not value:
       
   967                 return None
       
   968             try:
       
   969                 value = apply_units(value, TIME_UNITS)
       
   970             except ValueError:
       
   971                 raise ProcessFormError(form._cw._('a number (in seconds) or 20s, 10min, 24h or 4d are expected'))
       
   972         return timedelta(0, value)
       
   973 
       
   974 
       
   975 class DateField(StringField):
       
   976     """Use this field to edit date (`Date` yams type).
       
   977 
       
   978     Unless explicitly specified, the widget for this field will be a
       
   979     :class:`~cubicweb.web.formwidgets.JQueryDatePicker`.
       
   980     """
       
   981     widget = fw.JQueryDatePicker
       
   982     format_prop = 'ui.date-format'
       
   983     etype = 'Date'
       
   984 
       
   985     def format_single_value(self, req, value):
       
   986         if value:
       
   987             return ustrftime(value, req.property_value(self.format_prop))
       
   988         return u''
       
   989 
       
   990     def render_example(self, req):
       
   991         return self.format_single_value(req, datetime.now())
       
   992 
       
   993     def _ensure_correctly_typed(self, form, value):
       
   994         if isinstance(value, string_types):
       
   995             value = value.strip()
       
   996             if not value:
       
   997                 return None
       
   998             try:
       
   999                 value = form._cw.parse_datetime(value, self.etype)
       
  1000             except ValueError as ex:
       
  1001                 raise ProcessFormError(text_type(ex))
       
  1002         return value
       
  1003 
       
  1004 
       
  1005 class DateTimeField(DateField):
       
  1006     """Use this field to edit datetime (`Datetime` yams type).
       
  1007 
       
  1008     Unless explicitly specified, the widget for this field will be a
       
  1009     :class:`~cubicweb.web.formwidgets.JQueryDateTimePicker`.
       
  1010     """
       
  1011     widget = fw.JQueryDateTimePicker
       
  1012     format_prop = 'ui.datetime-format'
       
  1013     etype = 'Datetime'
       
  1014 
       
  1015 
       
  1016 class TimeField(DateField):
       
  1017     """Use this field to edit time (`Time` yams type).
       
  1018 
       
  1019     Unless explicitly specified, the widget for this field will be a
       
  1020     :class:`~cubicweb.web.formwidgets.JQueryTimePicker`.
       
  1021     """
       
  1022     widget = fw.JQueryTimePicker
       
  1023     format_prop = 'ui.time-format'
       
  1024     etype = 'Time'
       
  1025 
       
  1026 
       
  1027 # XXX use cases where we don't actually want a better widget?
       
  1028 class CompoundField(Field):
       
  1029     """This field shouldn't be used directly, it's designed to hold inner
       
  1030     fields that should be conceptually groupped together.
       
  1031     """
       
  1032     def __init__(self, fields, *args, **kwargs):
       
  1033         super(CompoundField, self).__init__(*args, **kwargs)
       
  1034         self.fields = fields
       
  1035 
       
  1036     def subfields(self, form):
       
  1037         return self.fields
       
  1038 
       
  1039     def actual_fields(self, form):
       
  1040         # don't add [self] to actual fields, compound field is usually kinda
       
  1041         # virtual, all interesting values are in subfield. Skipping it may avoid
       
  1042         # error when processed by the editcontroller : it may be marked as required
       
  1043         # while it has no value, hence generating a false error.
       
  1044         return list(self.fields)
       
  1045 
       
  1046     @property
       
  1047     def needs_multipart(self):
       
  1048         return any(f.needs_multipart for f in self.fields)
       
  1049 
       
  1050 
       
  1051 class RelationField(Field):
       
  1052     """Use this field to edit a relation of an entity.
       
  1053 
       
  1054     Unless explicitly specified, the widget for this field will be a
       
  1055     :class:`~cubicweb.web.formwidgets.Select`.
       
  1056     """
       
  1057 
       
  1058     @staticmethod
       
  1059     def fromcardinality(card, **kwargs):
       
  1060         kwargs.setdefault('widget', fw.Select(multiple=card in '*+'))
       
  1061         return RelationField(**kwargs)
       
  1062 
       
  1063     def choices(self, form, limit=None):
       
  1064         """Take care, choices function for relation field instance should take
       
  1065         an extra 'limit' argument, with default to None.
       
  1066 
       
  1067         This argument is used by the 'unrelateddivs' view (see in autoform) and
       
  1068         when it's specified (eg not None), vocabulary returned should:
       
  1069         * not include already related entities
       
  1070         * have a max size of `limit` entities
       
  1071         """
       
  1072         entity = form.edited_entity
       
  1073         # first see if its specified by __linkto form parameters
       
  1074         if limit is None:
       
  1075             linkedto = self.relvoc_linkedto(form)
       
  1076             if linkedto:
       
  1077                 return linkedto
       
  1078             # it isn't, search more vocabulary
       
  1079             vocab = self.relvoc_init(form)
       
  1080         else:
       
  1081             vocab = []
       
  1082         vocab += self.relvoc_unrelated(form, limit)
       
  1083         if self.sort:
       
  1084             vocab = vocab_sort(vocab)
       
  1085         return vocab
       
  1086 
       
  1087     def relvoc_linkedto(self, form):
       
  1088         linkedto = form.linked_to.get((self.name, self.role))
       
  1089         if linkedto:
       
  1090             buildent = form._cw.entity_from_eid
       
  1091             return [(buildent(eid).view('combobox'), text_type(eid))
       
  1092                     for eid in linkedto]
       
  1093         return []
       
  1094 
       
  1095     def relvoc_init(self, form):
       
  1096         entity, rtype, role = form.edited_entity, self.name, self.role
       
  1097         vocab = []
       
  1098         if not self.required:
       
  1099             vocab.append(('', INTERNAL_FIELD_VALUE))
       
  1100         # vocabulary doesn't include current values, add them
       
  1101         if form.edited_entity.has_eid():
       
  1102             rset = form.edited_entity.related(self.name, self.role)
       
  1103             vocab += [(e.view('combobox'), text_type(e.eid))
       
  1104                       for e in rset.entities()]
       
  1105         return vocab
       
  1106 
       
  1107     def relvoc_unrelated(self, form, limit=None):
       
  1108         entity = form.edited_entity
       
  1109         rtype = entity._cw.vreg.schema.rschema(self.name)
       
  1110         if entity.has_eid():
       
  1111             done = set(row[0] for row in entity.related(rtype, self.role))
       
  1112         else:
       
  1113             done = None
       
  1114         result = []
       
  1115         rsetsize = None
       
  1116         for objtype in rtype.targets(entity.e_schema, self.role):
       
  1117             if limit is not None:
       
  1118                 rsetsize = limit - len(result)
       
  1119             result += self._relvoc_unrelated(form, objtype, rsetsize, done)
       
  1120             if limit is not None and len(result) >= limit:
       
  1121                 break
       
  1122         return result
       
  1123 
       
  1124     def _relvoc_unrelated(self, form, targettype, limit, done):
       
  1125         """return unrelated entities for a given relation and target entity type
       
  1126         for use in vocabulary
       
  1127         """
       
  1128         if done is None:
       
  1129             done = set()
       
  1130         res = []
       
  1131         entity = form.edited_entity
       
  1132         for entity in entity.unrelated(self.name, targettype, self.role, limit,
       
  1133                                        lt_infos=form.linked_to).entities():
       
  1134             if entity.eid in done:
       
  1135                 continue
       
  1136             done.add(entity.eid)
       
  1137             res.append((entity.view('combobox'), text_type(entity.eid)))
       
  1138         return res
       
  1139 
       
  1140     def format_single_value(self, req, value):
       
  1141         return text_type(value)
       
  1142 
       
  1143     def process_form_value(self, form):
       
  1144         """process posted form and return correctly typed value"""
       
  1145         try:
       
  1146             return form.formvalues[(self, form)]
       
  1147         except KeyError:
       
  1148             value = self._process_form_value(form)
       
  1149             # if value is None, there are some remaining pending fields, we'll
       
  1150             # have to recompute this later -> don't cache in formvalues
       
  1151             if value is not None:
       
  1152                 form.formvalues[(self, form)] = value
       
  1153             return value
       
  1154 
       
  1155     def _process_form_value(self, form):
       
  1156         """process posted form and return correctly typed value"""
       
  1157         widget = self.get_widget(form)
       
  1158         values = widget.process_field_data(form, self)
       
  1159         if values is None:
       
  1160             values = ()
       
  1161         elif not isinstance(values, list):
       
  1162             values = (values,)
       
  1163         eids = set()
       
  1164         rschema = form._cw.vreg.schema.rschema(self.name)
       
  1165         for eid in values:
       
  1166             if not eid or eid == INTERNAL_FIELD_VALUE:
       
  1167                 continue
       
  1168             typed_eid = form.actual_eid(eid)
       
  1169             # if entity doesn't exist yet
       
  1170             if typed_eid is None:
       
  1171                 # inlined relations of to-be-created **subject entities** have
       
  1172                 # to be handled separatly
       
  1173                 if self.role == 'object' and rschema.inlined:
       
  1174                     form._cw.data['pending_inlined'][eid].add( (form, self) )
       
  1175                 else:
       
  1176                     form._cw.data['pending_others'].add( (form, self) )
       
  1177                 return None
       
  1178             eids.add(typed_eid)
       
  1179         return eids
       
  1180 
       
  1181     @staticmethod
       
  1182     def no_value(value):
       
  1183         """return True if the value can be considered as no value for the field"""
       
  1184         # value is None is the 'not yet ready value, consider the empty set
       
  1185         return value is not None and not value
       
  1186 
       
  1187 
       
  1188 _AFF_KWARGS = uicfg.autoform_field_kwargs
       
  1189 
       
  1190 def guess_field(eschema, rschema, role='subject', req=None, **kwargs):
       
  1191     """This function return the most adapted field to edit the given relation
       
  1192     (`rschema`) where the given entity type (`eschema`) is the subject or object
       
  1193     (`role`).
       
  1194 
       
  1195     The field is initialized according to information found in the schema,
       
  1196     though any value can be explicitly specified using `kwargs`.
       
  1197     """
       
  1198     fieldclass = None
       
  1199     rdef = eschema.rdef(rschema, role)
       
  1200     if role == 'subject':
       
  1201         targetschema = rdef.object
       
  1202         if rschema.final:
       
  1203             if rdef.get('internationalizable'):
       
  1204                 kwargs.setdefault('internationalizable', True)
       
  1205     else:
       
  1206         targetschema = rdef.subject
       
  1207     card = rdef.role_cardinality(role)
       
  1208     kwargs['name'] = rschema.type
       
  1209     kwargs['role'] = role
       
  1210     kwargs['eidparam'] = True
       
  1211     kwargs.setdefault('required', card in '1+')
       
  1212     if role == 'object':
       
  1213         kwargs.setdefault('label', (eschema.type, rschema.type + '_object'))
       
  1214     else:
       
  1215         kwargs.setdefault('label', (eschema.type, rschema.type))
       
  1216     kwargs.setdefault('help', rdef.description)
       
  1217     if rschema.final:
       
  1218         fieldclass = FIELDS[targetschema]
       
  1219         if fieldclass is StringField:
       
  1220             if eschema.has_metadata(rschema, 'format'):
       
  1221                 # use RichTextField instead of StringField if the attribute has
       
  1222                 # a "format" metadata. But getting information from constraints
       
  1223                 # may be useful anyway...
       
  1224                 for cstr in rdef.constraints:
       
  1225                     if isinstance(cstr, StaticVocabularyConstraint):
       
  1226                         raise Exception('rich text field with static vocabulary')
       
  1227                 return RichTextField(**kwargs)
       
  1228             # init StringField parameters according to constraints
       
  1229             for cstr in rdef.constraints:
       
  1230                 if isinstance(cstr, StaticVocabularyConstraint):
       
  1231                     kwargs.setdefault('choices', cstr.vocabulary)
       
  1232                     break
       
  1233             for cstr in rdef.constraints:
       
  1234                 if isinstance(cstr, SizeConstraint) and cstr.max is not None:
       
  1235                     kwargs['max_length'] = cstr.max
       
  1236             return StringField(**kwargs)
       
  1237         if fieldclass is FileField:
       
  1238             if req:
       
  1239                 aff_kwargs = req.vreg['uicfg'].select('autoform_field_kwargs', req)
       
  1240             else:
       
  1241                 aff_kwargs = _AFF_KWARGS
       
  1242             for metadata in KNOWN_METAATTRIBUTES:
       
  1243                 metaschema = eschema.has_metadata(rschema, metadata)
       
  1244                 if metaschema is not None:
       
  1245                     metakwargs = aff_kwargs.etype_get(eschema, metaschema, 'subject')
       
  1246                     kwargs['%s_field' % metadata] = guess_field(eschema, metaschema,
       
  1247                                                                 req=req, **metakwargs)
       
  1248         return fieldclass(**kwargs)
       
  1249     return RelationField.fromcardinality(card, **kwargs)
       
  1250 
       
  1251 
       
  1252 FIELDS = {
       
  1253     'String' :  StringField,
       
  1254     'Bytes':    FileField,
       
  1255     'Password': PasswordField,
       
  1256 
       
  1257     'Boolean':  BooleanField,
       
  1258     'Int':      IntField,
       
  1259     'BigInt':   BigIntField,
       
  1260     'Float':    FloatField,
       
  1261     'Decimal':  StringField,
       
  1262 
       
  1263     'Date':       DateField,
       
  1264     'Datetime':   DateTimeField,
       
  1265     'TZDatetime': DateTimeField,
       
  1266     'Time':       TimeField,
       
  1267     'TZTime':     TimeField,
       
  1268     'Interval':   TimeIntervalField,
       
  1269     }