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