# Copyright 2020 Karlsruhe Institute of Technology
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import timezone
from flask import json
from flask_babel import gettext as _
from flask_babel import lazy_gettext as _l
from flask_wtf import FlaskForm
from flask_wtf.i18n import translations as wtf_translations
from markupsafe import escape
from werkzeug.datastructures import FileStorage
from wtforms import BooleanField as _BooleanField
from wtforms import DateTimeField
from wtforms import Field
from wtforms import FileField as _FileField
from wtforms import IntegerField as _IntegerField
from wtforms import PasswordField as _PasswordField
from wtforms import SelectField as _SelectField
from wtforms import SelectMultipleField
from wtforms import StringField as _StringField
from wtforms import SubmitField as _SubmitField
from wtforms import TextAreaField
from wtforms.validators import Length
from wtforms.validators import StopValidation
from kadi.lib.config.core import MISSING
from kadi.lib.config.core import get_sys_config
from kadi.lib.config.core import set_sys_config
from kadi.lib.utils import compact_json
from .validation import validate_identifier as _validate_identifier
from .validation import validate_iri as _validate_iri
from .validation import validate_mimetype as _validate_mimetype
from .validation import validate_username as _validate_username
from .validation import validator
SCHEMA_FORM_ERRORS = {
"Field may not be null.": "This field is required.",
}
[docs]
class CustomFieldMixin:
"""Mixin class for all custom fields and for wrapping existing fields.
Adds a common dictionary conversion to all inheriting fields and also handles some
common corner cases.
"""
def __call__(self):
"""Convert this field into a JSON representation for use in HTML templates.
:return: An escaped markup object representing the dictionary returned by
:meth:`to_dict` serialized as JSON.
"""
return escape(compact_json(self.to_dict(), ensure_ascii=True, sort_keys=False))
def process_formdata(self, valuelist):
# pylint: disable=missing-function-docstring
if valuelist:
# The file should always be listed first.
value = valuelist[0]
is_file = isinstance(value, FileStorage)
is_file_field = isinstance(self, FileField)
# Workaround to handle corner cases where a file is expected but a string is
# supplied and vice versa.
if is_file != is_file_field:
raise ValueError("Incorrect value format.")
super().process_formdata(valuelist)
[docs]
def to_dict(self):
"""Convert this field into a dictionary representation."""
data = {
"id": self.id,
"name": self.name,
"label": str(self.label.text),
"description": str(self.description),
"errors": self.errors or [],
"data": self.data if self.data is not None else "",
"validation": {
"required": self.flags.required or False,
},
}
for validator in self.validators:
if isinstance(validator, Length):
if validator.min != -1:
data["validation"]["min"] = validator.min
if validator.max != -1:
data["validation"]["max"] = validator.max
return data
[docs]
class BooleanField(CustomFieldMixin, _BooleanField):
"""Regular boolean field inheriting from :class:`CustomFieldMixin`."""
[docs]
class IntegerField(CustomFieldMixin, _IntegerField):
"""Regular integer field inheriting from :class:`CustomFieldMixin`."""
[docs]
class StringField(CustomFieldMixin, _StringField):
"""Regular string field inheriting from :class:`CustomFieldMixin`."""
[docs]
class SubmitField(CustomFieldMixin, _SubmitField):
"""Regular submit field inheriting from :class:`CustomFieldMixin`."""
[docs]
class PasswordField(CustomFieldMixin, _PasswordField):
"""Regular password field inheriting from :class:`CustomFieldMixin`."""
[docs]
def to_dict(self):
data = super().to_dict()
data["data"] = ""
return data
[docs]
class FileField(CustomFieldMixin, _FileField):
"""Custom file field."""
[docs]
def to_dict(self):
data = super().to_dict()
data["data"] = None
return data
[docs]
class LFTextAreaField(CustomFieldMixin, TextAreaField):
"""Custom text area field that converts *CRLF* to *LF*."""
[docs]
def process_formdata(self, valuelist):
super().process_formdata(valuelist)
if valuelist:
self.data = self.data.replace("\r\n", "\n")
[docs]
class UTCDateTimeField(CustomFieldMixin, DateTimeField):
"""Custom timezone aware DateTimeField using UTC.
:param date_format: (optional) The date format to use for parsing and serializing.
"""
def __init__(self, *args, date_format="%Y-%m-%dT%H:%M:%S.%fZ", **kwargs):
kwargs["format"] = date_format
super().__init__(*args, **kwargs)
[docs]
def to_dict(self):
data = super().to_dict()
data["data"] = self._value()
return data
[docs]
class SelectField(CustomFieldMixin, _SelectField):
"""Custom select field."""
[docs]
def to_dict(self):
data = super().to_dict()
if self.choices is not None:
data["choices"] = [(val, str(title)) for val, title in self.choices]
else:
data["choices"] = []
return data
[docs]
class DynamicSelectField(CustomFieldMixin, _SelectField):
"""Custom select field for dynamically generated selections.
Note that this field automatically replaces empty strings in the form data with its
default value (``None``) instead of trying to coerce them.
In addition, the instance variable ``initial`` can be used to specify an initial
value to prefill the selection with by setting it to a tuple containing the actual
value and a corresponding text to be displayed within the selection.
"""
def __init__(self, *args, **kwargs):
self.initial = None
kwargs["default"] = None
kwargs["choices"] = []
kwargs["validate_choice"] = False
super().__init__(*args, **kwargs)
[docs]
def to_dict(self):
data = super().to_dict()
data["data"] = self.initial
return data
[docs]
class DynamicMultiSelectField(CustomFieldMixin, SelectMultipleField):
"""Custom multi select field for dynamically generated selections.
The instance variable ``initial`` can be used to set a list of initial values to
prefill the selection with. See also :class:`DynamicSelectField`.
"""
def __init__(self, *args, **kwargs):
self.initial = []
kwargs["default"] = []
kwargs["choices"] = []
kwargs["validate_choice"] = False
super().__init__(*args, **kwargs)
[docs]
def to_dict(self):
data = super().to_dict()
data["data"] = self.initial
return data
[docs]
class JSONField(CustomFieldMixin, Field):
"""Custom field that processes its data as JSON."""
def _value(self):
return self.data
[docs]
def to_dict(self):
data = super().to_dict()
data["data"] = self._value()
return data
[docs]
@validator(StopValidation)
def validate_identifier(form, field):
"""Validate an identifier in a form field.
Uses :func:`kadi.lib.validation.validate_identifier`.
:param form: The form object.
:param field: The field object.
"""
if field.data is not None:
_validate_identifier(field.data)
[docs]
@validator(StopValidation)
def validate_mimetype(form, field):
"""Validate a MIME type in a form field.
Uses :func:`kadi.lib.validation.validate_mimetype`.
:param form: The form object.
:param field: The field object.
"""
if field.data is not None:
_validate_mimetype(field.data)
[docs]
@validator(StopValidation)
def validate_username(form, field):
"""Validate a local username in a form field.
Uses :func:`kadi.lib.validation.validate_username`.
:param form: The form object.
:param field: The field object.
"""
if field.data is not None:
_validate_username(field.data)
[docs]
@validator(StopValidation)
def validate_iri(form, field):
"""Validate an IRI in a form field.
Uses :func:`kadi.lib.validation.validate_iri`.
:param form: The form object.
:param field: The field object.
"""
if field.data is not None:
_validate_iri(field.data)
[docs]
def convert_schema_validation_msg(msg, **interpolations):
r"""Convert a schema validation message to a corresponding form field message.
This is mainly useful when using a schema for validation in a field in order to
better match the usual form validation messages. Note that, if possible, the
messages will be translated using the translations provided by WTForms.
:param msg: The validation message to convert.
:param \**interpolations: Additional keyword arguments to provide interpolation
values for the converted validation message.
:return: The converted validation message or the original message if it could not be
converted.
"""
# Avoids the strings from being extracted, as we don't want to translate them
# ourselves.
ngettext_func = wtf_translations.ngettext
if msg.startswith("Longer than maximum length"):
max_value = interpolations.get("max", -1)
msg = ngettext_func(
"Field cannot be longer than %(max)d character.",
"Field cannot be longer than %(max)d characters.",
max_value,
)
return msg % {"max": max_value}
msg = SCHEMA_FORM_ERRORS.get(msg, msg)
return wtf_translations.gettext(msg)