''' ```eval_rst
This module contains :class:`appyter.fields.Field`, the base class for all fields
defined in :mod:`appyter.profiles.default.fields`.
``` '''
from flask import Markup
from appyter.ext.flask import request_get
[docs]class PartialField:
''' Partial instantiation of a field
Replaces a decorator so that we can still identify it as
a callable which will produce a field.
'''
def __init__(self, field, **kwargs):
self._field = field
self._kwargs = kwargs
def __call__(self, name=None, value=None, **kwargs):
kwargs = dict(self._kwargs, **kwargs)
value = kwargs.pop('context').get(name) if value is None else value
return self._field(name=name, value=value, **kwargs)
[docs]def build_fields(fields, context={}, env=None):
''' INTERNAL: Build a dictionary of Field instances
'''
return {
field_name: PartialField(field, context=context, _env=env)
for field_name, field in fields.items()
}
[docs]class FieldConstraintException(Exception):
def __init__(self, field, field_name, value, message=None):
self.field = field
self.field_name = field_name
self.value = value
if message is None: message = "{}[{}]: {} does not satisfy constraints".format(field, field_name, repr(value))
self.message = message
super().__init__(message)
[docs] def as_dict(self):
return dict(
field=self.field,
field_name=self.field_name,
value=self.value,
message=self.message,
)
[docs]class Field(dict):
''' Base field for which all fields derive
```eval_rst
Base class for all Field objects representing a value that will later be provided via a front-end form.
See :mod:`appyter.profiles.default.fields` for the actual fields.
``` '''
def __init__(self,
name=None,
label=None,
description=None,
choices=[],
required=False,
default=None,
value=None,
section=None,
_env=None,
**kwargs):
'''
:param name: (str) A name that will be used to refer to the object as a variable and in the HTML form.
:param label: (str) A human readable label for the field for the HTML form
:param description: (Optional[str]) A long human readable description for the field for the HTML form
:param choices: (Optional[Union[List[str], Dict[str, str]]]) A set of choices that are available for this field or lookup table mapping from choice label to resulting value
:param required: (Optional[bool]) Whether or not this field is required (defaults to false)
:param default: (Any) A default value as an example and for use during prototyping
:param section: (Optional[str]) The name of a SectionField for which to nest this field under, defaults to a root SectionField
:param value: (INTERNAL Any) The raw value of the field (from the form for instance)
:param \**kwargs: Additional keyword arguments used by other fields
'''
super().__init__(
field=self.field,
args=dict(
name=name,
label=label,
description=description,
choices=choices,
required=required,
default=default,
value=value if value is not None else default,
section=section,
**kwargs,
)
)
assert name is not None, "Name should be defined and unique"
assert not name.startswith('_'), "Names with _ prefix are reserved"
assert len(name) > 1, "Names should use more than 1 character"
self._env = _env
@property
def args(self):
''' Get the raw args, the values used to initialize this field
'''
return self['args']
[docs] def prepare(self, req):
''' Given a flask request, capture relevant variables for this field
'''
value = request_get(req, self.args['name'], self.args.get('default'))
#
self.args['value'] = value
return { self.args['name']: value }
[docs] def constraint(self):
''' Return true if the received args.value satisfies constraints.
Should be overridden by subclasses.
'''
return (self.raw_value is None and not self.args.get('required')) or (self.raw_value in self.choices)
[docs] def render(self, **kwargs):
''' Return a rendered version of the field (form)
:param \**kwargs: The instance values of the form e.g. `Field.render(**field.args)`
'''
return Markup(
self._env.get_template(
self.template
).render(dict(**kwargs, this=self))
)
[docs] def to_jsonschema(self):
schema = {'type': 'string'}
if self.args.get('label'): schema['title'] = self.args['label']
if self.args.get('description'): schema['description'] = self.args['description']
if self.args.get('choices'): schema['enum'] = list(self.args['choices'])
if self.args.get('default'): schema['default'] = self.args['default']
return schema
[docs] def to_cwl(self):
schema = {
'id': self.args['name'],
'inputBinding': {
'prefix': f"--{self.args['name']}=",
'separate': False,
'shellQuote': True,
}
}
if self.args.get('choices'):
if self.args.get('required') == True:
schema['type'] = {
'type': 'enum', 'symbols': list(self.args['choices'])
}
else:
schema['type'] = ['null', {
'type': 'enum', 'symbols': list(self.args['choices'])
}]
else:
schema['type'] = f"string{'' if self.args.get('required') == True else '?'}"
#
if self.args.get('label'): schema['label'] = self.args['label']
if self.args.get('description'): schema['doc'] = self.args['description']
if self.args.get('default'): schema['default'] = self.to_cwl_value()
return schema
[docs] def to_cwl_value(self):
return self.raw_value
[docs] def to_click(self):
import click
args = (f"--{self.args['name']}",)
kwargs = dict()
#
if self.args.get('required') == True:
kwargs['required'] = True
#
if self.args.get('choices'):
kwargs['type'] = click.Choice(list(self.args['choices']))
else:
kwargs['type'] = click.STRING
#
if self.args.get('label'): kwargs['help'] = self.args['label']
if self.args.get('description'):
if 'help' in kwargs: kwargs['help'] += ': ' + self.args['description']
else: kwargs['help'] = self.args['description']
if self.args.get('default'): kwargs['default'] = self.args['default']
return args, kwargs
@property
def field(self):
''' Field name
'''
return self.__class__.__name__
@property
def template(self):
''' Template to use for rendering field
'''
return '/'.join(['fields', self.field + '.j2'])
@property
def js_url(self):
''' Template to use for rendering field
'''
from appyter.profiles.default.filters.url_for import url_for
return url_for('static', filename='js/fields/' + self.field + '.js')
@property
def choices(self):
''' Potential values to choose from
'''
return self.args['choices']
@property
def raw_value(self):
''' (UNSAFE) Raw value of the field
'''
return self.args['value']
@property
def value(self):
''' (SEMI-SAFE) Effective raw value of the field when parsed and constraints are asserted.
When instantiating code, you should use safe_value.
'''
choices = self.choices
if self.raw_value is None:
if not self.args.get('required'):
return None
else:
raise FieldConstraintException(
field=self.field,
field_name=self.args['name'],
value=self.raw_value,
message='{}[{}] is required'.format(self.field, self.args['name']),
)
elif type(choices) == dict:
if self.raw_value in choices:
return choices[self.raw_value]
else:
raise FieldConstraintException(
field=self.field,
field_name=self.args['name'],
value=self.raw_value,
)
elif not self.constraint():
raise FieldConstraintException(
field=self.field,
field_name=self.args['name'],
value=self.raw_value,
)
else:
return self.raw_value
@property
def safe_value(self):
''' (SAFE) Effective safe value for use in code, we use `repr` to escape values as necessary
'''
return repr(self.value)
@property
def render_value(self):
''' (SAFE) Effective safe value ready to be displayed, specifically for Markdown output.
'''
return Markup(self.value)
def __str__(self):
''' (SAFE) The default str(Field) is just safe_value
'''
return self.safe_value