Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/src/models/fhir_immunization_pre_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ def pre_validate_recorded(self, values: dict) -> dict:
"""
try:
recorded = values["recorded"]
PreValidation.for_date_time(recorded, "recorded")
PreValidation.for_date_time(recorded, "recorded",strict_timezone=False)
except KeyError:
pass

Expand Down
10 changes: 6 additions & 4 deletions backend/src/models/utils/pre_validator_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ def for_date(field_value: str, field_location: str):
) from value_error

@staticmethod
def for_date_time(field_value: str, field_location: str):
def for_date_time(field_value: str, field_location: str, strict_timezone: bool = True):
"""
Apply pre-validation to a datetime field to ensure that it is a string (JSON dates must be written as strings)
containing a valid datetime. Note that partial dates are valid for FHIR, but are not allowed for this API.
Expand All @@ -120,9 +120,11 @@ def for_date_time(field_value: str, field_location: str):
"- 'YYYY-MM-DDThh:mm:ss.f' — Full date and time with milliseconds (any level of precision)"
"- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)"
"- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone"
"Only '+00:00' and '+01:00' are accepted as valid timezone offsets."
f"Note that partial dates are not allowed for {field_location} in this service."
)

if strict_timezone:
error_message += "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n"
error_message += f"Note that partial dates are not allowed for {field_location} in this service."

allowed_suffixes = {"+00:00", "+01:00", "+0000", "+0100",}

Expand All @@ -136,7 +138,7 @@ def for_date_time(field_value: str, field_location: str):
try:
fhir_date = datetime.strptime(field_value, fmt)

if fhir_date.tzinfo is not None:
if strict_timezone and fhir_date.tzinfo is not None:
if not any(field_value.endswith(suffix) for suffix in allowed_suffixes):
raise ValueError(error_message)
return fhir_date.isoformat()
Expand Down
2 changes: 1 addition & 1 deletion backend/tests/test_immunization_pre_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ def test_pre_validate_practitioner_name_family(self):

def test_pre_validate_recorded(self):
"""Test pre_validate_recorded accepts valid values and rejects invalid values"""
ValidatorModelTests.test_date_time_value(self, field_location="recorded")
ValidatorModelTests.test_date_time_value(self, field_location="recorded", is_occurrence_date_time=False)

def test_pre_validate_primary_source(self):
"""Test pre_validate_primary_source accepts valid values and rejects invalid values"""
Expand Down
33 changes: 20 additions & 13 deletions backend/tests/utils/pre_validation_test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,11 +328,29 @@ def test_date_time_value(
* Invalid date time string formats
* Invalid date-times
"""
expected_error_message = (
f"{field_location} must be a valid datetime in one of the following formats:"
"- 'YYYY-MM-DD' — Full date only"
"- 'YYYY-MM-DDThh:mm:ss' — Full date and time without milliseconds"
"- 'YYYY-MM-DDThh:mm:ss.f' — Full date and time with milliseconds (any level of precision)"
"- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)"
"- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone"
)

if is_occurrence_date_time:
expected_error_message += "Only '+00:00' and '+01:00' are accepted as valid timezone offsets.\n"
expected_error_message += f"Note that partial dates are not allowed for {field_location} in this service."
valid_datetime_formats = ValidValues.for_date_times_strict_timezones
invalid_datetime_formats = InvalidValues.for_date_time_string_formats_for_strict_timezone
else:
# For recorded, skip values that are valid ISO with non-restricted timezone
valid_datetime_formats = ValidValues.for_date_times_relaxed_timezones
invalid_datetime_formats = InvalidValues.for_date_time_string_formats_for_relaxed_timezone

valid_json_data = deepcopy(test_instance.json_data)

# Test that valid data is accepted
test_valid_values_accepted(test_instance, valid_json_data, field_location, ValidValues.for_date_times)
test_valid_values_accepted(test_instance, valid_json_data, field_location, valid_datetime_formats)

# Set list of invalid data types to test
invalid_data_types_for_strings = InvalidDataTypes.for_strings
Expand All @@ -349,19 +367,8 @@ def test_date_time_value(
expected_error_message=f"{field_location} must be a string",
)

expected_error_message = (
f"{field_location} must be a valid datetime in one of the following formats:"
"- 'YYYY-MM-DD' — Full date only"
"- 'YYYY-MM-DDThh:mm:ss' — Full date and time without milliseconds"
"- 'YYYY-MM-DDThh:mm:ss.f' — Full date and time with milliseconds (any level of precision)"
"- 'YYYY-MM-DDThh:mm:ss%z' — Full date and time with timezone (e.g. +00:00 or +01:00)"
"- 'YYYY-MM-DDThh:mm:ss.f%z' — Full date and time with milliseconds and timezone"
"Only '+00:00' and '+01:00' are accepted as valid timezone offsets."
f"Note that partial dates are not allowed for {field_location} in this service."
)

# Test invalid date time string formats
for invalid_occurrence_date_time in InvalidValues.for_date_time_string_formats:
for invalid_occurrence_date_time in invalid_datetime_formats:
test_invalid_values_rejected(
test_instance,
valid_json_data,
Expand Down
23 changes: 16 additions & 7 deletions backend/tests/utils/values_for_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class ValidValues:

nhs_number = "9990548609"

for_date_times = [
for_date_times_strict_timezones = [
"2000-01-01", # Full date only
"2000-01-01T00:00:00+00:00", # Time and offset all zeroes
"2000-01-01T10:34:27", # Date with Time only
Expand All @@ -44,6 +44,12 @@ class ValidValues:
"1933-12-31T11:11:11.111111+00:00", # DateTime with milliseconds to 6 decimal places
]

for_date_times_relaxed_timezones = for_date_times_strict_timezones + [
"2000-01-01T00:00:00+05:00", # Time and offset all zeroes
"1933-12-31T11:11:11-01:00", # Negative offset (with hours and minutes not 0)
"1933-12-31T11:11:11.1-05:00", # DateTime with milliseconds to 1 decimal place
]

for_strings_with_any_length_chars = (
"This is a really long string with more than 100 characters to test whether the validator is working well!! "
)
Expand Down Expand Up @@ -281,7 +287,7 @@ class InvalidValues:
]

# Strings which are not in acceptable date time format
for_date_time_string_formats = [
for_date_time_string_formats_for_relaxed_timezone = [
"", # Empty string
"invalid", # Invalid format
"20000101", # Date digits only (i.e. without hypens)
Expand All @@ -290,10 +296,6 @@ class InvalidValues:
"2000", # Year only
"2000-01", # Year and month only
"2000-01-01T00:00:00+00", # Date and time with GMT timezone offset only in hours
"2000-01-01T00:00:00-00:00", # Date and time with negative GMT timezone offset
"2000-01-01T00:00:00-01:00", # Date and time with negative GMT timezone offset
"2000-01-01T00:00:00-05:00", # Date and time with negative offset asides from GMT and BST
"2000-01-01T00:00:00+05:00", # Date and time with offset asides from GMT and BST
"2000-01-01T00:00:00+01", # Date and time with BST timezone offset only in hours
"12000-01-01T00:00:00+00:00", # Extra character at start of string
"2000-01-01T00:00:00+00:001", # Extra character at end of string
Expand All @@ -302,7 +304,6 @@ class InvalidValues:
"2000-01-0122:22:22+00:00.000", # Missing T (with milliseconds)
"2000-01-01T222222+00:00", # Missing time colons
"2000-01-01T22:22:2200:00", # Missing timezone indicator
"2000-01-01T22:22:22-0100", # Missing timezone colon
"2000-01-01T22:22:22-01", # Timezone hours only
"99-01-01T00:00:00+00:00", # Missing century (i.e. only 2 digits for year)
"01-01-2000T00:00:00+00:00", # Date in wrong order (DD-MM-YYYY)
Expand All @@ -323,6 +324,14 @@ class InvalidValues:
"2000-01-01T00:00:00+00:60", # Timezone minute 60
]

for_date_time_string_formats_for_strict_timezone = for_date_time_string_formats_for_relaxed_timezone + [
"2000-01-01T22:22:22-0100", # Missing timezone colon
"2000-01-01T00:00:00-01:00", # Date and time with negative GMT timezone offset
"2000-01-01T00:00:00-05:00", # Date and time with negative offset asides from GMT and BST
"2000-01-01T00:00:00+05:00", # Date and time with offset asides from GMT and BST
"2000-01-01T00:00:00-00:00", # Date and time with negative GMT timezone offset
]

for_lists_of_strings_of_length_1 = [[1], [False], [["Test1"]]]

for_lists_of_dicts_of_length_1 = [[1], [False], [["Invalid"]], ["Invalid"]]
Expand Down
Loading