From da914b99b76ebbc7b94e7b8e14ad5b7042c81887 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 15 Nov 2023 23:44:39 +0530 Subject: [PATCH] Add DosageValidator to Prescription model --- ...0394_alter_prescription_dosage_and_more.py | 37 ++++++ care/facility/models/prescription.py | 46 ++++++- ...istrations_api.py => test_medicine_api.py} | 113 ++++++++++++++++++ 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 care/facility/migrations/0394_alter_prescription_dosage_and_more.py rename care/facility/tests/{test_medicine_administrations_api.py => test_medicine_api.py} (51%) diff --git a/care/facility/migrations/0394_alter_prescription_dosage_and_more.py b/care/facility/migrations/0394_alter_prescription_dosage_and_more.py new file mode 100644 index 0000000000..8aad309e46 --- /dev/null +++ b/care/facility/migrations/0394_alter_prescription_dosage_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.7 on 2023-11-15 18:13 + +from django.db import migrations, models + +import care.facility.models.prescription + + +class Migration(migrations.Migration): + dependencies = [ + ( + "facility", + "0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more", + ), + ] + + operations = [ + migrations.AlterField( + model_name="prescription", + name="dosage", + field=models.CharField( + blank=True, + max_length=100, + null=True, + validators=[care.facility.models.prescription.DosageValidator()], + ), + ), + migrations.AlterField( + model_name="prescription", + name="max_dosage", + field=models.CharField( + blank=True, + max_length=100, + null=True, + validators=[care.facility.models.prescription.DosageValidator()], + ), + ), + ] diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index fc05467f4e..4bcfd1ca67 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -37,6 +37,44 @@ def generate_choices(enum_class): return [(tag.name, tag.value) for tag in enum_class] +class DosageValidator: + min_dosage = 0.0001 + max_dosage = 5000 + allowed_units = {"mg", "g", "ml", "drop(s)", "ampule(s)", "tsp"} + + def __call__(self, value: str): + if not value: + return + try: + value, unit = value.split(" ", maxsplit=1) + if unit not in self.allowed_units: + raise ValidationError( + f"Unit must be one of {', '.join(self.allowed_units)}" + ) + + value_num: int | float = float(value) + if value_num.is_integer(): + value_num = int(value_num) + elif len(str(value_num).split(".")[1]) > 4: + raise ValidationError( + "Dosage amount must have at most 4 decimal places" + ) + + if len(value) != len(str(value_num)): + raise ValidationError("Dosage amount must be a valid number") + + if self.min_dosage > value_num or value_num > self.max_dosage: + raise ValidationError( + f"Dosage amount must be between {self.min_dosage} and {self.max_dosage}" + ) + except ValueError: + raise ValidationError("Invalid dosage") + + def deconstruct(self): + path = "%s.%s" % (self.__class__.__module__, self.__class__.__name__) + return (path, [], {}) + + class MedibaseMedicineType(enum.Enum): BRAND = "brand" GENERIC = "generic" @@ -92,7 +130,9 @@ class Prescription(BaseModel): blank=True, null=True, ) - dosage = models.CharField(max_length=100, blank=True, null=True) + dosage = models.CharField( + max_length=100, blank=True, null=True, validators=[DosageValidator()] + ) is_prn = models.BooleanField(default=False) @@ -107,7 +147,9 @@ class Prescription(BaseModel): # prn fields indicator = models.TextField(blank=True, null=True) - max_dosage = models.CharField(max_length=100, blank=True, null=True) + max_dosage = models.CharField( + max_length=100, blank=True, null=True, validators=[DosageValidator()] + ) min_hours_between_doses = models.IntegerField(blank=True, null=True) notes = models.TextField(default="", blank=True) diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_api.py similarity index 51% rename from care/facility/tests/test_medicine_administrations_api.py rename to care/facility/tests/test_medicine_api.py index dd99877261..520309768a 100644 --- a/care/facility/tests/test_medicine_administrations_api.py +++ b/care/facility/tests/test_medicine_api.py @@ -6,6 +6,119 @@ from care.utils.tests.test_utils import TestUtils +class MedicinePrescriptionApiTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls) -> None: + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility) + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.consultation = cls.create_consultation(cls.patient, cls.facility) + meds = MedibaseMedicine.objects.all().values_list("external_id", flat=True)[:2] + cls.medicine1 = str(meds[0]) + + def setUp(self) -> None: + super().setUp() + + def prescription_data(self, **kwargs): + data = { + "medicine": self.medicine1, + "prescription_type": "REGULAR", + "dosage": "1 mg", + "frequency": "OD", + "is_prn": False, + } + return {**data, **kwargs} + + def test_invalid_dosage(self): + data = self.prescription_data(dosage="abc") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(res.json()["dosage"][0], "Invalid dosage") + + def test_dosage_out_of_range(self): + data = self.prescription_data(dosage="10000 mg") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + res.json()["dosage"][0], + "Dosage amount must be between 0.0001 and 5000", + ) + + data = self.prescription_data(dosage="-1 mg") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + res.json()["dosage"][0], + "Dosage amount must be between 0.0001 and 5000", + ) + + def test_dosage_precision(self): + data = self.prescription_data(dosage="0.300003 mg") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + res.json()["dosage"][0], + "Dosage amount must have at most 4 decimal places", + ) + + def test_dosage_unit_invalid(self): + data = self.prescription_data(dosage="1 abc") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertTrue(res.json()["dosage"][0].startswith("Unit must be one of")) + + def test_dosage_leading_zero(self): + data = self.prescription_data(dosage="01 mg") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + res.json()["dosage"][0], "Dosage amount must be a valid number" + ) + + def test_dosage_trailing_zero(self): + data = self.prescription_data(dosage="1.0 mg") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + res.json()["dosage"][0], "Dosage amount must be a valid number" + ) + + def test_valid_dosage(self): + data = self.prescription_data(dosage="1 mg") + res = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data, + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + class MedicineAdministrationsApiTestCase(TestUtils, APITestCase): @classmethod def setUpTestData(cls) -> None: