"""Set up custom fields for marshmallow.""" from dateutil import tz import dateutil.parser from flask_marshmallow import Marshmallow from marshmallow.validate import OneOf, Range from royalty_common.constants import error from royalty_common.constants.constants import DATE_FORMAT, DATETIME_FORMAT from royalty_common.marshalling.custom_validations import ( check_decimal_precision, not_blank) from royalty_common.marshalling.helpers import create_custom_field from royalty_common.utils.dates import is_iso_datetime ma = Marshmallow() ma.Field.default_error_messages['required'] = error.ERROR_FIELD_MISSING def create_enum(options, **kwargs): """Create a custom enum field.""" return create_custom_field( base_class=ma.String, custom_validations=[OneOf(options, error=error.ERROR_INVALID_OPTION)] )(**kwargs) class FormattedDate(ma.Date): """Represents a date.""" default_error_messages = {'invalid': error.ERROR_INVALID_DATE} def __init__(self, **options): """Init method.""" super().__init__( **options, format=DATE_FORMAT ) def _serialize(self, value, attr, obj, **kwargs): if value is None: return None return value.strftime(DATE_FORMAT) class FormattedDateTime(ma.DateTime): """Represents a datetime.""" default_error_messages = {'invalid': error.ERROR_INVALID_DATETIME} def __init__(self, **options): """Init method.""" super().__init__( **options, format=DATETIME_FORMAT ) def _serialize(self, value, attr, obj, **kwargs): if value is None: return None return value.strftime(DATETIME_FORMAT) def _deserialize(self, value, attr, data, **kwargs): if value and is_iso_datetime(value): datetime_value = dateutil.parser.parse( value) if not datetime_value.tzinfo: datetime_value = datetime_value.replace(tzinfo=tz.tzutc()) value = datetime_value.strftime(DATETIME_FORMAT) return super()._deserialize(value, attr, data, **kwargs) ma.Enum = create_enum ma.FormattedDate = FormattedDate ma.FormattedDateTime = FormattedDateTime ma.MoneyAmount = create_custom_field( base_class=ma.Number, custom_validations=[check_decimal_precision], error_messages={'invalid': error.ERROR_INVALID_DECIMAL} ) ma.NonemptyString = create_custom_field( base_class=ma.String, custom_validations=[not_blank] ) ma.NonnegativeInteger = create_custom_field( base_class=ma.Int, custom_validations=[Range(min=0)], error_messages={'invalid': error.ERROR_MUST_BE_INT} ) ma.IntegerId = ma.NonnegativeInteger ma.Percentage = create_custom_field( base_class=ma.Number, custom_validations=[ Range(0, 100, error=error.ERROR_PERCENTAGE_NOT_IN_RANGE), check_decimal_precision ], error_messages={'invalid': error.ERROR_INVALID_DECIMAL} )