diff -r 058bb3dc685f -r 0b59724cb3f2 web/formfields.py --- 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 . -""" -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 - 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'%s' % - (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'') - if not self.required and self.typed_value(form): - # trick to be able to delete an uploaded file - wdgs.append(u'
') - 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'
') - - 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'

%s

' % 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, - }