Source code for tvb.core.neotraits.forms

# -*- coding: utf-8 -*-
#
#
# TheVirtualBrain-Framework Package. This package holds all Data Management, and
# Web-UI helpful to run brain-simulations. To use it, you also need to download
# TheVirtualBrain-Scientific Package (for simulators). See content of the
# documentation-folder for more details. See also http://www.thevirtualbrain.org
#
# (c) 2012-2023, Baycrest Centre for Geriatric Care ("Baycrest") and others
#
# This program is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software Foundation,
# either version 3 of the License, or (at your option) any later version.
# This program 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 General Public License for more details.
# You should have received a copy of the GNU General Public License along with this
# program.  If not, see <http://www.gnu.org/licenses/>.
#
#
#   CITATION:
# When using The Virtual Brain for scientific publications, please cite it as explained here:
# https://www.thevirtualbrain.org/tvb/zwei/neuroscience-publications
#
#

import json
import uuid
from collections import namedtuple
import numpy

from tvb.core.entities.filters.chain import FilterChain
from tvb.core.neocom.h5 import REGISTRY
from tvb.basic.neotraits.ex import TraitError
from tvb.basic.neotraits.api import List, Attr, TVBEnum
# TODO: remove dependency
from tvb.core.neotraits.db import HasTraitsIndex
from tvb.core.neotraits.view_model import DataTypeGidAttr

# This setting is injected.
# The pattern might be confusing, but it is an interesting alternative to
# universal tvbprofile imports

jinja_env = None


[docs] class Field(object): template = None def __init__(self, name, disabled=False, required=False, label='', doc='', default=None): # type: (str, bool, bool, str, str, object) -> None self.name = name self.disabled = disabled self.required = required self.label = label self.doc = doc self.label_classes = [] if required: self.label_classes.append('field-mandatory') # keeps the deserialized data self.data = None # keeps user input, even if wrong, we have to redisplay it self.unvalidated_data = default self.errors = []
[docs] def fill_from_post(self, post_data): """ deserialize form a post dictionary """ self.unvalidated_data = post_data.get(self.name) try: self._from_post() except ValueError as ex: self.errors.append(ex)
[docs] def validate(self): """ validation besides the deserialization from post""" return not self.errors
def _from_post(self): if self.required and (self.unvalidated_data is None or len(str(self.unvalidated_data).strip()) == 0): raise ValueError('Field required') self.data = self.unvalidated_data @property def value(self): if self.data is not None: return self.data return self.unvalidated_data def __repr__(self): return '<{}>(name={})'.format(type(self).__name__, self.name) def __str__(self): return jinja_env.get_template(self.template).render(field=self)
[docs] class TraitField(Field): def __init__(self, trait_attribute, name=None, disabled=False): # type: (Attr, str, bool) -> None self.trait_attribute = trait_attribute # type: Attr name = name or trait_attribute.field_name label = trait_attribute.label or name super(TraitField, self).__init__( name, disabled, trait_attribute.required, label, trait_attribute.doc, trait_attribute.default )
[docs] def from_trait(self, trait, f_name): self.data = getattr(trait, f_name)
TEMPORARY_PREFIX = ".tmp"
[docs] class TraitUploadField(TraitField): template = 'form_fields/upload_field.html' def __init__(self, traited_attribute, required_type, name, disabled=False): super(TraitUploadField, self).__init__(traited_attribute, name, disabled) self.required_type = required_type
[docs] class TraitDataTypeSelectField(TraitField): template = 'form_fields/datatype_select_field.html' missing_value = 'explicit-None-value' def __init__(self, trait_attribute, name=None, conditions=None, draw_dynamic_conditions_buttons=True, has_all_option=False, show_only_all_option=False): super(TraitDataTypeSelectField, self).__init__(trait_attribute, name) if issubclass(type(trait_attribute), DataTypeGidAttr): type_to_query = trait_attribute.linked_datatype else: type_to_query = trait_attribute.field_type if issubclass(type_to_query, HasTraitsIndex): self.datatype_index = type_to_query else: self.datatype_index = REGISTRY.get_index_for_datatype(type_to_query) self.conditions = conditions self.draw_dynamic_conditions_buttons = draw_dynamic_conditions_buttons self.has_all_option = has_all_option self.show_only_all_option = show_only_all_option self.datatype_options = []
[docs] def from_trait(self, trait, f_name): if hasattr(trait, f_name): self.data = getattr(trait, f_name) if isinstance(self.data, uuid.UUID): self.data = self.data.hex
@property def get_dynamic_filters(self): return FilterChain().get_filters_for_type(self.datatype_index) @property def get_form_filters(self): return self.conditions
[docs] def options(self): if not self.required: choice = None yield Option( id='{}_{}'.format(self.name, None), value=self.missing_value, label=str(choice).title(), checked=self.data is None ) if not self.show_only_all_option: for i, dt_opt in enumerate(self.datatype_options): yield Option( id='{}_{}'.format(self.name, i), value=dt_opt[0][2], label=dt_opt[1], checked=self.data == dt_opt[0][2] ) if self.has_all_option: all_values = '' for fdt in self.datatype_options: all_values += str(fdt[0][2]) + ',' choice = "All" yield Option( id='{}_{}'.format(self.name, choice), value=all_values[:-1], label=choice, checked=self.data is choice )
def _from_post(self): if self.unvalidated_data == self.missing_value: self.unvalidated_data = None if self.required and not self.unvalidated_data: raise ValueError('Field required') # TODO: ensure is in choices try: if self.unvalidated_data is None: self.data = None else: self.data = uuid.UUID(self.unvalidated_data) except ValueError: raise ValueError('The chosen entity does not have a proper GID')
[docs] class StrField(TraitField): template = 'form_fields/str_field.html'
[docs] class UserSessionStrField(TraitField): template = 'form_fields/str_field.html' def __init__(self, trait_attribute, name=None, disabled=False, key=None): # type: (Attr, str, bool, str) -> None self.key = key # Key in session super(UserSessionStrField, self).__init__(trait_attribute, name, disabled)
[docs] class BoolField(TraitField): template = 'form_fields/bool_field.html' def _from_post(self): self.data = self.unvalidated_data is not None
[docs] class IntField(TraitField): template = 'form_fields/number_field.html' min = None max = None def _from_post(self): super(IntField, self)._from_post() if len(self.unvalidated_data.strip()) == 0: self.data = None else: self.data = int(self.unvalidated_data)
[docs] class FloatField(TraitField): template = 'form_fields/number_field.html' min = None max = None step = 'any' def _from_post(self): super(FloatField, self)._from_post() if len(self.unvalidated_data.strip()) == 0: self.data = None else: self.data = float(self.unvalidated_data)
[docs] class ArrayField(TraitField): template = 'form_fields/str_field.html' def _from_post(self): super(ArrayField, self)._from_post() self.data = None if len(self.unvalidated_data.strip()) == 0: self.data = None else: data = json.loads(self.unvalidated_data) self.data = numpy.array(data, dtype=self.trait_attribute.dtype) @property def value(self): if self.data is None: # this None means self.data is missing, either not set or unset cause of validation error return self.unvalidated_data try: if self.data.size > 100: data_to_display = self.data[:100] else: data_to_display = self.data return json.dumps(data_to_display.tolist()) except (TypeError, ValueError): return self.unvalidated_data
Option = namedtuple('Option', ['id', 'value', 'label', 'checked'])
[docs] class SelectField(TraitField): template = 'form_fields/radio_field.html' missing_value = 'explicit-None-value' subform_prefix = 'subform_' def _prepare_template(self, choices): if len(choices) > 4: self.template = 'form_fields/select_field.html' def __init__(self, trait_attribute, name=None, disabled=False, display_none_choice=True, subform=None, display_subform=True, ui_values=None, session_key=None, form_key=None): super(SelectField, self).__init__(trait_attribute, name, disabled) self.choices = list(trait_attribute.choices) if not self.choices: raise ValueError('no choices for field') self.display_none_choice = display_none_choice self.subform_field = None if subform: self.subform_field = FormField(subform, self.subform_prefix + self.name) self.display_subform = display_subform self._prepare_template(self.choices) self.ui_values = ui_values self.session_key = session_key self.form_key = form_key @property def value(self): if self.data is None and not self.trait_attribute.required: return self.data return super(SelectField, self).value
[docs] def options(self): """ to be used from template, assumes self.data is set """ if self.display_none_choice: if not self.trait_attribute.required: choice = None yield Option( id='{}_{}'.format(self.name, None), value=self.missing_value, label=str(choice).title(), checked=self.data is None ) choices = self.choices if self.ui_values is None else self.ui_values for i, choice in enumerate(self.choices): yield Option( id='{}_{}'.format(self.name, i), value=choice, label=str(choices[i]).title(), checked=self.value == choice or (self.value == choice.value if hasattr(choice, 'value') else False) )
def _from_post(self): super(SelectField, self)._from_post() data_as_enum = TVBEnum.string_to_enum(self.choices, self.unvalidated_data) if self.unvalidated_data != self.missing_value and data_as_enum is None \ and (self.unvalidated_data is not None or self.display_none_choice is False): raise ValueError("the entered value is not among the choices for this field!") self.data = data_as_enum
[docs] class DynamicSelectField(TraitField): template = 'form_fields/radio_field.html' missing_value = 'explicit-None-value' subform_prefix = 'subform_' def _prepare_template(self, choices): if len(choices) > 4: self.template = 'form_fields/select_field.html' def __init__(self, trait_attribute, name=None, disabled=False, choices=None, display_none_choice=True, subform=None, display_subform=True, ui_values=None): super(DynamicSelectField, self).__init__(trait_attribute, name, disabled) self.choices = choices if not self.choices: raise ValueError('no choices for field') self.display_none_choice = display_none_choice self.subform_field = None if subform: self.subform_field = FormField(subform, self.subform_prefix + self.name) self.display_subform = display_subform self._prepare_template(self.choices) self.ui_values = ui_values @property def value(self): if self.data is None and not self.trait_attribute.required: return self.data return super(DynamicSelectField, self).value
[docs] def options(self): """ to be used from template, assumes self.data is set """ if self.display_none_choice: if not self.trait_attribute.required: choice = None yield Option( id='{}_{}'.format(self.name, None), value=self.missing_value, label=str(choice).title(), checked=self.data is None ) choices = self.choices if self.ui_values is None else self.ui_values for i, choice in enumerate(self.choices): yield Option( id='{}_{}'.format(self.name, i), value=str(choice), label=str(choices[i]), checked=self.value == choice )
def _from_post(self): super(DynamicSelectField, self)._from_post() data_as_object = self.__string_to_object() if self.unvalidated_data != self.missing_value and data_as_object is None \ and (self.unvalidated_data is not None or self.display_none_choice is False): raise ValueError("the entered value is not among the choices for this field!") self.data = data_as_object def __string_to_object(self): for choice in self.choices: if self.unvalidated_data == str(choice): return choice return None
[docs] class MultiSelectField(TraitField): template = 'form_fields/checkbox_field.html' def __init__(self, trait_attribute, name=None, disabled=False): super(MultiSelectField, self).__init__(trait_attribute, name, disabled) if not isinstance(trait_attribute, List): raise NotImplementedError('only List in multi select for now')
[docs] def options(self): """ to be used from template, assumes self.data is set """ for i, choice in enumerate(self.trait_attribute.element_choices): yield Option( id='{}_{}'.format(self.name, i), value=choice, label=str(choice).title(), checked=self.data is not None and choice in self.data )
def _from_post(self): if self.unvalidated_data is None: if self.required: raise ValueError('Field required') else: return None if not isinstance(self.unvalidated_data, list): selected = [self.unvalidated_data] else: selected = self.unvalidated_data data = [] # don't mutate self.data until we know all values converted ok for s in selected: converted_s = self.trait_attribute.element_type(s) if converted_s not in self.trait_attribute.element_choices: raise ValueError('must be one of {}'.format(self.trait_attribute.element_choices)) data.append(converted_s) self.data = data
[docs] class HiddenField(TraitField): template = 'form_fields/hidden_field.html' def __init__(self, trait_attribute, name=None, disabled=False): super(HiddenField, self).__init__(trait_attribute, name, disabled) self.label = ''
[docs] class LabelField(TraitField): template = 'form_fields/label_field.html' def __init__(self, trait_attribute, label_message=None, name=None): super(LabelField, self).__init__(trait_attribute, name, disabled=True) if label_message is None: self.label_message = trait_attribute.label else: self.label_message = label_message
[docs] class FormField(Field): template = 'form_fields/form_field.html' def __init__(self, form_class, name, label='', doc=''): super(FormField, self).__init__(name, False, False, label, doc) self.form = form_class()
[docs] def fill_from_post(self, post_data): self.form.fill_from_post(post_data) self.errors = self.form.errors
[docs] def validate(self): return self.form.validate()
def __str__(self): return jinja_env.get_template(self.template).render(adapter_form=self.form)
[docs] class Form(object): def __init__(self): self.errors = []
[docs] @staticmethod def get_subform_key(): """ If the current form can be used as subform, this method should return the proper value. """ raise NotImplementedError
@property def fields(self): for field in self.__dict__.values(): if isinstance(field, Field): yield field @property def trait_fields(self): for field in self.__dict__.values(): if isinstance(field, TraitField): yield field
[docs] def validate(self): valid = True for field in self.fields: if not field.validate(): valid = False return valid
[docs] def get_errors_dict(self): result = {} for field in self.fields: if not field.validate(): result[field.name] = field.errors[0] return result
[docs] def fill_from_trait(self, trait): """ Sets data for all traited fields from a trait instance. Note that FormFields are not TraitFields, so this does not work recursively Override to fill in sub-forms """ for field in self.trait_fields: f_name = field.trait_attribute.field_name if f_name is None: # skipp attribute that does not seem to belong to a traited type continue field.from_trait(trait, f_name)
[docs] def fill_trait(self, datatype): """ Copies the value of the TraitFields to the corresponding Attr-ibutes of the given trait instance Note that FormFields are not TraitFields, so this does not work recursively Override to fill in sub-forms """ for field in self.trait_fields: f_name = field.trait_attribute.field_name if f_name is None: # skipp attribute that does not seem to belong to a traited type continue try: setattr(datatype, f_name, field.data) except TraitError as ex: # as field.data is clearly tainted set it to None so that field.unvalidated_data # will render and the user can fix the typo's field.data = None field.errors.append(ex) raise
[docs] def fill_trait_partially(self, datatype, fields=None): for field in self.trait_fields: f_name = field.trait_attribute.field_name if f_name is None or \ fields is not None and f_name not in fields: # skipp attribute that does not seem to belong to a traited type continue try: setattr(datatype, f_name, field.data) except TraitError as ex: # as field.data is clearly tainted set it to None so that field.unvalidated_data # will render and the user can fix the typo's field.data = None field.errors.append(ex) raise
[docs] def fill_from_post(self, form_data): for field in self.fields: field.fill_from_post(form_data)
[docs] def fill_from_single_post_param(self, **param): param_key = list(param)[0] field = getattr(self, param_key) field.fill_from_post(param)