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