diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6f3af91..5a1e4da 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ repos: - "-x *test*.py" - repo: https://github.com/psf/black - rev: 23.9.1 + rev: 23.10.1 hooks: - id: black language_version: python3.11 @@ -28,7 +28,7 @@ repos: - id: isort - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.4.0 + rev: v4.5.0 hooks: - id: requirements-txt-fixer files: requirements/.*\.txt$ diff --git a/edc_adverse_event/form_validator_mixins/death_report_form_validator.py b/edc_adverse_event/form_validator_mixins/death_report_form_validator.py index 5b3b9d9..9a30296 100644 --- a/edc_adverse_event/form_validator_mixins/death_report_form_validator.py +++ b/edc_adverse_event/form_validator_mixins/death_report_form_validator.py @@ -1,4 +1,6 @@ -from typing import Any, Optional +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Optional from zoneinfo import ZoneInfo from django import forms @@ -8,6 +10,9 @@ from edc_form_validators.base_form_validator import INVALID_ERROR from edc_utils import convert_php_dateformat +if TYPE_CHECKING: + from datetime import date + class DeathReportFormValidatorMixin: @@ -23,6 +28,13 @@ def cause_of_death_model_cls(self): def clean(self: Any) -> None: self.validate_study_day_with_death_report_date() + self.date_is_after_or_raise( + field="report_datetime", + reference_value=self.death_report_date, + inclusive=True, + extra_msg="(on or after date of death)", + ) + cause_of_death = self.cause_of_death_model_cls.objects.get(name=OTHER) self.validate_other_specify( @@ -36,7 +48,7 @@ def clean(self: Any) -> None: ) @property - def death_report_date(self: Any) -> None: + def death_report_date(self: Any) -> date: try: return self.cleaned_data.get(self.death_report_date_field).date() except AttributeError: diff --git a/edc_adverse_event/model_mixins/death_report/death_report_model_mixin.py b/edc_adverse_event/model_mixins/death_report/death_report_model_mixin.py index 7666df5..44ab29a 100644 --- a/edc_adverse_event/model_mixins/death_report/death_report_model_mixin.py +++ b/edc_adverse_event/model_mixins/death_report/death_report_model_mixin.py @@ -11,7 +11,10 @@ from edc_model.models import HistoricalRecords from edc_model.validators import date_not_future, datetime_not_future from edc_model_fields.fields.other_charfield import OtherCharField -from edc_protocol.validators import datetime_not_before_study_start +from edc_protocol.validators import ( + date_not_before_study_start, + datetime_not_before_study_start, +) from edc_sites.models import SiteModelMixin from edc_utils import get_utcnow @@ -39,14 +42,14 @@ class DeathReportModelMixin( ) death_datetime = models.DateTimeField( - validators=[datetime_not_future], + validators=[datetime_not_before_study_start, datetime_not_future], verbose_name="Date and Time of Death", null=True, blank=False, ) death_date = models.DateField( - validators=[date_not_future], + validators=[date_not_before_study_start, date_not_future], verbose_name="Date of Death", null=True, blank=False, diff --git a/edc_adverse_event/tests/tests/test_death_report_form_validator.py b/edc_adverse_event/tests/tests/test_death_report_form_validator.py new file mode 100644 index 0000000..a9e9640 --- /dev/null +++ b/edc_adverse_event/tests/tests/test_death_report_form_validator.py @@ -0,0 +1,134 @@ +from dateutil.relativedelta import relativedelta +from django import forms +from django.test import TestCase +from edc_constants.constants import OTHER, UNKNOWN +from edc_constants.disease_constants import BACTERAEMIA +from edc_form_validators import FormValidatorTestCaseMixin +from edc_form_validators.tests.mixins import FormValidatorTestMixin +from edc_utils import get_utcnow + +from edc_adverse_event.form_validators import DeathReportFormValidator as Base + + +class DeathReportFormValidator(FormValidatorTestMixin, Base): + pass + + +class TestHospitalizationFormValidation(FormValidatorTestCaseMixin, TestCase): + @staticmethod + def get_cleaned_data() -> dict: + report_datetime = get_utcnow() + return { + "report_datetime": report_datetime, + "death_datetime": report_datetime - relativedelta(days=3), + "cause_of_death": BACTERAEMIA, + "cause_of_death_other": "", + "narrative": "Narrative around death", + } + + def test_cleaned_data_ok(self): + cleaned_data = self.get_cleaned_data() + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + try: + form_validator.validate() + except forms.ValidationError as e: + self.fail(f"ValidationError unexpectedly raised. Got {e}") + + def test_death_datetime_after_report_datetime_raises(self): + for death_report_date_field in ["death_date", "death_datetime"]: + for days_after in [1, 3, 14]: + with self.subTest( + death_report_date_field=death_report_date_field, days_after=days_after + ): + report_datetime = get_utcnow() + death_datetime = report_datetime + relativedelta(days=days_after) + cleaned_data = { + "report_datetime": get_utcnow(), + death_report_date_field: ( + death_datetime + if death_report_date_field == "death_datetime" + else death_datetime.date() + ), + } + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + form_validator.death_report_date_field = death_report_date_field + with self.assertRaises(forms.ValidationError) as cm: + form_validator.validate() + self.assertIn("report_datetime", cm.exception.error_dict) + self.assertIn( + "Invalid. Expected a date on or after", + str(cm.exception.error_dict.get("report_datetime")), + ) + self.assertIn( + "(on or after date of death)", + str(cm.exception.error_dict.get("report_datetime")), + ) + + def test_death_datetime_on_or_before_report_datetime_datetime_ok(self): + for death_report_date_field in ["death_date", "death_datetime"]: + for days_before in [0, 1, 2, 14]: + with self.subTest( + death_report_date_field=death_report_date_field, days_before=days_before + ): + report_datetime = get_utcnow() + death_datetime = report_datetime - relativedelta(days=days_before) + cleaned_data = { + "report_datetime": report_datetime, + death_report_date_field: death_datetime + if death_report_date_field == "death_datetime" + else death_datetime.date(), + } + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + form_validator.death_report_date_field = death_report_date_field + try: + form_validator.validate() + except forms.ValidationError as e: + self.fail(f"ValidationError unexpectedly raised. Got {e}") + + def test_cause_of_death_other_required_if_cause_of_death_other(self): + cleaned_data = self.get_cleaned_data() + cleaned_data.update( + { + "cause_of_death": OTHER, + "cause_of_death_other": "", + } + ) + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + with self.assertRaises(forms.ValidationError) as cm: + form_validator.validate() + self.assertIn("cause_of_death_other", cm.exception.error_dict) + self.assertIn( + "This field is required.", + str(cm.exception.error_dict.get("cause_of_death_other")), + ) + + cleaned_data.update({"cause_of_death_other": "Some other cause of death..."}) + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + try: + form_validator.validate() + except forms.ValidationError as e: + self.fail(f"ValidationError unexpectedly raised. Got {e}") + + def test_cause_of_death_other_not_required_if_cause_of_death_not_other(self): + cleaned_data = self.get_cleaned_data() + cleaned_data.update( + { + "cause_of_death": UNKNOWN, + "cause_of_death_other": "Some other cause of death", + } + ) + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + with self.assertRaises(forms.ValidationError) as cm: + form_validator.validate() + self.assertIn("cause_of_death_other", cm.exception.error_dict) + self.assertIn( + "This field is not required.", + str(cm.exception.error_dict.get("cause_of_death_other")), + ) + + cleaned_data.update({"cause_of_death_other": ""}) + form_validator = DeathReportFormValidator(cleaned_data=cleaned_data) + try: + form_validator.validate() + except forms.ValidationError as e: + self.fail(f"ValidationError unexpectedly raised. Got {e}")