# -*- 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