Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add DenominationValidator to validate dosage fields in Prescription model #1716

Merged
merged 6 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -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()],
),
),
]
46 changes: 44 additions & 2 deletions care/facility/models/prescription.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,44 @@
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

Check warning on line 47 in care/facility/models/prescription.py

View check run for this annotation

Codecov / codecov/patch

care/facility/models/prescription.py#L47

Added line #L47 was not covered by tests
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, [], {})

Check warning on line 75 in care/facility/models/prescription.py

View check run for this annotation

Codecov / codecov/patch

care/facility/models/prescription.py#L74-L75

Added lines #L74 - L75 were not covered by tests


class MedibaseMedicineType(enum.Enum):
BRAND = "brand"
GENERIC = "generic"
Expand Down Expand Up @@ -92,7 +130,9 @@
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)

Expand All @@ -107,7 +147,9 @@

# 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading