2016-10-25 14:53:31 +01:00
|
|
|
import json
|
2024-05-23 13:59:51 -07:00
|
|
|
from datetime import timedelta
|
2018-02-15 13:34:06 +00:00
|
|
|
from uuid import UUID
|
2016-11-09 14:56:54 +00:00
|
|
|
|
2021-03-10 13:55:06 +00:00
|
|
|
from iso8601 import ParseError, iso8601
|
|
|
|
|
from jsonschema import Draft7Validator, FormatChecker, ValidationError
|
2024-05-16 10:17:45 -04:00
|
|
|
|
2024-05-23 13:59:51 -07:00
|
|
|
from app.utils import utc_now
|
2021-03-10 13:55:06 +00:00
|
|
|
from notifications_utils.recipients import (
|
|
|
|
|
InvalidEmailError,
|
|
|
|
|
InvalidPhoneError,
|
|
|
|
|
validate_email_address,
|
|
|
|
|
validate_phone_number,
|
|
|
|
|
)
|
2016-10-25 14:53:31 +01:00
|
|
|
|
2019-01-10 17:31:32 +00:00
|
|
|
format_checker = FormatChecker()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@format_checker.checks("validate_uuid", raises=Exception)
|
|
|
|
|
def validate_uuid(instance):
|
|
|
|
|
if isinstance(instance, str):
|
|
|
|
|
UUID(instance)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
@format_checker.checks("phone_number", raises=InvalidPhoneError)
|
2019-01-10 17:31:32 +00:00
|
|
|
def validate_schema_phone_number(instance):
|
|
|
|
|
if isinstance(instance, str):
|
|
|
|
|
validate_phone_number(instance, international=True)
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
@format_checker.checks("email_address", raises=InvalidEmailError)
|
2019-01-10 17:31:32 +00:00
|
|
|
def validate_schema_email_address(instance):
|
|
|
|
|
if isinstance(instance, str):
|
|
|
|
|
validate_email_address(instance)
|
|
|
|
|
return True
|
2017-05-15 15:02:38 +01:00
|
|
|
|
2019-01-10 17:31:32 +00:00
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
@format_checker.checks("datetime_within_next_day", raises=ValidationError)
|
2019-01-10 17:31:32 +00:00
|
|
|
def validate_schema_date_with_hour(instance):
|
|
|
|
|
if isinstance(instance, str):
|
|
|
|
|
try:
|
|
|
|
|
dt = iso8601.parse_date(instance).replace(tzinfo=None)
|
2024-05-23 13:59:51 -07:00
|
|
|
if dt < utc_now():
|
2019-01-10 17:31:32 +00:00
|
|
|
raise ValidationError("datetime can not be in the past")
|
2024-05-23 13:59:51 -07:00
|
|
|
if dt > utc_now() + timedelta(hours=24):
|
2019-01-10 17:31:32 +00:00
|
|
|
raise ValidationError("datetime can only be 24 hours in the future")
|
|
|
|
|
except ParseError:
|
2023-08-29 14:54:30 -07:00
|
|
|
raise ValidationError(
|
|
|
|
|
"datetime format is invalid. It must be a valid ISO8601 date time format, "
|
|
|
|
|
"https://en.wikipedia.org/wiki/ISO_8601"
|
|
|
|
|
)
|
2019-01-10 17:31:32 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2023-08-29 14:54:30 -07:00
|
|
|
@format_checker.checks("datetime", raises=ValidationError)
|
2020-07-09 12:59:09 +01:00
|
|
|
def validate_schema_datetime(instance):
|
|
|
|
|
if isinstance(instance, str):
|
|
|
|
|
try:
|
|
|
|
|
iso8601.parse_date(instance)
|
|
|
|
|
except ParseError:
|
2023-08-29 14:54:30 -07:00
|
|
|
raise ValidationError(
|
|
|
|
|
"datetime format is invalid. It must be a valid ISO8601 date time format, "
|
|
|
|
|
"https://en.wikipedia.org/wiki/ISO_8601"
|
|
|
|
|
)
|
2020-07-09 12:59:09 +01:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
2019-01-10 17:31:32 +00:00
|
|
|
def validate(json_to_validate, schema):
|
2018-11-02 16:00:22 +00:00
|
|
|
validator = Draft7Validator(schema, format_checker=format_checker)
|
2016-10-25 14:53:31 +01:00
|
|
|
errors = list(validator.iter_errors(json_to_validate))
|
|
|
|
|
if errors.__len__() > 0:
|
2016-11-16 17:25:00 +00:00
|
|
|
raise ValidationError(build_error_message(errors))
|
2016-10-25 14:53:31 +01:00
|
|
|
return json_to_validate
|
|
|
|
|
|
|
|
|
|
|
2016-11-16 17:25:00 +00:00
|
|
|
def build_error_message(errors):
|
2016-10-25 14:53:31 +01:00
|
|
|
fields = []
|
|
|
|
|
for e in errors:
|
2017-01-09 16:22:27 +00:00
|
|
|
field = (
|
2023-08-29 14:54:30 -07:00
|
|
|
"{} {}".format(e.path[0], e.schema["validationMessage"])
|
|
|
|
|
if "validationMessage" in e.schema
|
|
|
|
|
else __format_message(e)
|
2017-01-09 16:22:27 +00:00
|
|
|
)
|
2016-11-16 17:25:00 +00:00
|
|
|
fields.append({"error": "ValidationError", "message": field})
|
2023-08-29 14:54:30 -07:00
|
|
|
message = {"status_code": 400, "errors": unique_errors(fields)}
|
2016-10-25 14:53:31 +01:00
|
|
|
|
2016-10-25 18:04:03 +01:00
|
|
|
return json.dumps(message)
|
2016-11-16 17:25:00 +00:00
|
|
|
|
|
|
|
|
|
2017-06-15 11:32:51 +01:00
|
|
|
def unique_errors(dups):
|
|
|
|
|
unique = []
|
|
|
|
|
for x in dups:
|
|
|
|
|
if x not in unique:
|
|
|
|
|
unique.append(x)
|
|
|
|
|
return unique
|
|
|
|
|
|
|
|
|
|
|
2016-11-16 17:25:00 +00:00
|
|
|
def __format_message(e):
|
2016-12-15 16:35:27 +00:00
|
|
|
def get_path(e):
|
|
|
|
|
error_path = None
|
|
|
|
|
try:
|
2016-12-16 12:41:19 +00:00
|
|
|
error_path = e.path.popleft()
|
|
|
|
|
# no need to catch IndexError exception explicity as
|
|
|
|
|
# error_path is None if e.path has no items
|
2022-08-19 14:32:11 +00:00
|
|
|
except IndexError:
|
2020-12-22 15:46:31 +00:00
|
|
|
pass
|
|
|
|
|
return error_path
|
2016-12-15 16:35:27 +00:00
|
|
|
|
|
|
|
|
def get_error_message(e):
|
2017-01-09 16:22:27 +00:00
|
|
|
# e.cause is an exception (such as InvalidPhoneError). if it's not present it was a standard jsonschema error
|
|
|
|
|
# such as a required field not being present
|
|
|
|
|
error_message = str(e.cause) if e.cause else e.message
|
2023-08-29 14:54:30 -07:00
|
|
|
return error_message.replace("'", "")
|
2016-12-15 16:35:27 +00:00
|
|
|
|
|
|
|
|
path = get_path(e)
|
|
|
|
|
message = get_error_message(e)
|
|
|
|
|
if path:
|
|
|
|
|
return "{} {}".format(path, message)
|
|
|
|
|
else:
|
|
|
|
|
return "{}".format(message)
|