From 4163d745cebdc273699cd758ad701666fabb7e48 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Wed, 5 Jul 2023 09:12:07 +0000 Subject: [PATCH] Adds validation to consulation date fields (`admission_date`, `discharge_date`, `death_datetime`) (#1415) * validation: disallow admision_date to be future * validation: death_datetime not future or before admission date * validation: disallow discharge_date before admission or future * clean * add tests, draft * fix tests * fix user test --------- Co-authored-by: Vignesh Hari --- .vscode/launch.json | 9 ++ .../api/serializers/patient_consultation.py | 43 +++-- .../tests/test_patient_consultation_api.py | 150 ++++++++++++++++++ care/users/tests/test_api.py | 5 +- care/utils/tests/test_base.py | 10 +- 5 files changed, 200 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 67165ab0a1..cba23bf2dc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -26,5 +26,14 @@ "django": true, "justMyCode": false }, + { + "name": "Python: Django test", + "type": "python", + "request": "launch", + "program": "${workspaceFolder}/manage.py", + "args": ["test", "--keepdb"], + "django": true, + "justMyCode": false + }, ] } diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 2be3c1d7c3..97ebc55220 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -328,18 +328,17 @@ def validate(self, attrs): validated["referred_to"] = None elif validated.get("referred_to"): validated["referred_to_external"] = None - if ( - validated["suggestion"] is SuggestionChoices.A - and validated.get("admitted") - and not validated.get("admission_date") - ): - raise ValidationError( - { - "admission_date": [ - "This field is required as the patient has been admitted." - ] - } - ) + if validated["suggestion"] is SuggestionChoices.A: + if not validated.get("admission_date"): + raise ValidationError( + { + "admission_date": "This field is required as the patient has been admitted." + } + ) + if validated["admission_date"] > now(): + raise ValidationError( + {"admission_date": "This field value cannot be in the future."} + ) if "action" in validated: if validated["action"] == PatientRegistration.ActionEnum.REVIEW: @@ -432,6 +431,16 @@ def validate(self, attrs): if attrs.get("discharge_reason") == "EXP": if not attrs.get("death_datetime"): raise ValidationError({"death_datetime": "This field is required"}) + if attrs.get("death_datetime") > now(): + raise ValidationError( + {"death_datetime": "This field value cannot be in the future."} + ) + if attrs.get("death_datetime") < self.instance.admission_date: + raise ValidationError( + { + "death_datetime": "This field value cannot be before the admission date." + } + ) if not attrs.get("death_confirmed_doctor"): raise ValidationError( {"death_confirmed_doctor": "This field is required"} @@ -439,6 +448,16 @@ def validate(self, attrs): attrs["discharge_date"] = attrs["death_datetime"] elif not attrs.get("discharge_date"): raise ValidationError({"discharge_date": "This field is required"}) + elif attrs.get("discharge_date") > now(): + raise ValidationError( + {"discharge_date": "This field value cannot be in the future."} + ) + elif attrs.get("discharge_date") < self.instance.admission_date: + raise ValidationError( + { + "discharge_date": "This field value cannot be before the admission date." + } + ) return attrs def save(self, **kwargs): diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index e69de29bb2..82e6a914ab 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -0,0 +1,150 @@ +import datetime + +from django.utils.timezone import make_aware +from rest_framework import status +from rest_framework.test import APIRequestFactory, APITestCase + +from care.facility.api.viewsets.patient_consultation import PatientConsultationViewSet +from care.facility.models.patient_consultation import ( + CATEGORY_CHOICES, + PatientConsultation, +) +from care.facility.tests.mixins import TestClassMixin +from care.utils.tests.test_base import TestBase + + +class TestPatientConsultation(TestBase, TestClassMixin, APITestCase): + default_data = { + "symptoms": [1], + "category": CATEGORY_CHOICES[0][0], + "examination_details": "examination_details", + "history_of_present_illness": "history_of_present_illness", + "prescribed_medication": "prescribed_medication", + "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], + } + + def setUp(self): + self.factory = APIRequestFactory() + self.consultation = self.create_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + + def create_admission_consultation(self, patient=None, **kwargs): + patient = ( + self.create_patient(facility_id=self.facility.id) + if not patient + else patient + ) + data = self.default_data.copy() + kwargs.update( + { + "patient": patient.external_id, + "facility": self.facility.external_id, + } + ) + data.update(kwargs) + res = self.new_request( + (self.get_url(), data, "json"), + {"post": "create"}, + PatientConsultationViewSet, + self.state_admin, + {}, + ) + return PatientConsultation.objects.get(external_id=res.data["id"]) + + def get_url(self, consultation=None): + if consultation: + return f"/api/v1/consultation/{consultation.external_id}" + return "/api/v1/consultation" + + def discharge(self, consultation, **kwargs): + return self.new_request( + (f"{self.get_url(consultation)}/discharge_patient", kwargs, "json"), + {"post": "discharge_patient"}, + PatientConsultationViewSet, + self.state_admin, + {"external_id": consultation.external_id}, + ) + + def test_discharge_as_recovered_preadmission(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="REC", + discharge_date="2002-04-01T16:30:00Z", + discharge_notes="Discharge as recovered before admission", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_discharge_as_recovered_future(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="REC", + discharge_date="2319-04-01T15:30:00Z", + discharge_notes="Discharge as recovered in the future", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_discharge_as_recovered_after_admission(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="REC", + discharge_date="2020-04-02T15:30:00Z", + discharge_notes="Discharge as recovered after admission before future", + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_discharge_as_expired_pre_admission(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="EXP", + death_datetime="2002-04-01T16:30:00Z", + discharge_notes="Death before admission", + death_confirmed_doctor="Dr. Test", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_discharge_as_expired_future(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="EXP", + death_datetime="2319-04-01T15:30:00Z", + discharge_notes="Death in the future", + death_confirmed_doctor="Dr. Test", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_discharge_as_expired_after_admission(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="EXP", + death_datetime="2020-04-02T15:30:00Z", + discharge_notes="Death after admission before future", + death_confirmed_doctor="Dr. Test", + discharge_date="2319-04-01T15:30:00Z", + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) diff --git a/care/users/tests/test_api.py b/care/users/tests/test_api.py index bcd06db2c2..fb3ba702c7 100644 --- a/care/users/tests/test_api.py +++ b/care/users/tests/test_api.py @@ -108,7 +108,8 @@ def setUpClass(cls) -> None: """ Runs once per class method - Create 2 users + Create 3 users + - 2 users initialized by setUpClass of TestBase - 1 will be used to check if they can tinker attributes of the other """ super(TestUser, cls).setUpClass() @@ -136,7 +137,7 @@ def test_user_can_read_all(self): self.assertEqual(response.status_code, status.HTTP_200_OK) res_data_json = response.json() # test total user count - self.assertEqual(res_data_json["count"], 2) # 2 existing, plus the new one + self.assertEqual(res_data_json["count"], 3) # 3 existing, plus the new one results = res_data_json["results"] # test presence of usernames self.assertIn(self.user.id, {r["id"] for r in results}) diff --git a/care/utils/tests/test_base.py b/care/utils/tests/test_base.py index 1edd441567..84c73bd0fd 100644 --- a/care/utils/tests/test_base.py +++ b/care/utils/tests/test_base.py @@ -98,6 +98,7 @@ def create_patient(cls, **kwargs): patient_data.update( { + "facility": cls.facility, "district_id": district_id, "state_id": state_id, "disease_status": getattr( @@ -218,6 +219,12 @@ def setUpClass(cls) -> None: cls.super_user = cls.create_super_user(district=cls.district) cls.facility = cls.create_facility(cls.district) cls.patient = cls.create_patient() + cls.state_admin = cls.create_user( + cls.district, + username="state-admin", + user_type=User.TYPE_VALUE_MAP["StateAdmin"], + home_facility=cls.facility, + ) cls.user_data = cls.get_user_data(cls.district, cls.user_type) cls.facility_data = cls.get_facility_data(cls.district) @@ -394,13 +401,10 @@ def get_consultation_data(cls): "prescribed_medication": "prescribed_medication", "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], "referred_to": None, - "admitted": False, "admission_date": None, "discharge_date": None, "consultation_notes": "", "course_in_facility": "", - "discharge_advice": {}, - "prescriptions": {}, "created_date": mock_equal, "modified_date": mock_equal, }