From 0aee002b4ef5000c38ea1172d2517df9eda11cb5 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 7 May 2024 17:20:57 +0530 Subject: [PATCH 1/8] Disallow writes to prescription from non-home facility user --- care/facility/models/prescription.py | 10 +- .../test_medicine_administrations_api.py | 142 +++++++++ care/facility/tests/test_medicine_api.py | 298 ------------------ care/facility/tests/test_prescriptions_api.py | 120 ++++++- 4 files changed, 267 insertions(+), 303 deletions(-) create mode 100644 care/facility/tests/test_medicine_administrations_api.py delete mode 100644 care/facility/tests/test_medicine_api.py diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index 3c9bdedba2..5f3eca9e09 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -171,7 +171,10 @@ def last_administration(self): return self.administrations.order_by("-administered_date").first() def has_object_write_permission(self, request): - return ConsultationRelatedPermissionMixin.has_write_permission(request) + return ( + ConsultationRelatedPermissionMixin.has_write_permission(request) + and self.consultation.facility == request.user.home_facility + ) def __str__(self): return self.medicine + " - " + self.consultation.patient.name @@ -213,7 +216,10 @@ def get_related_consultation(self): return self.prescription.consultation def has_object_write_permission(self, request): - return ConsultationRelatedPermissionMixin.has_write_permission(request) + return ( + ConsultationRelatedPermissionMixin.has_write_permission(request) + and self.prescription.consultation.facility == request.user.home_facility + ) def validate(self) -> None: if self.prescription.discontinued: diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_administrations_api.py new file mode 100644 index 0000000000..13fd8b928b --- /dev/null +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -0,0 +1,142 @@ +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models import MedibaseMedicine, Prescription, PrescriptionDosageType +from care.utils.tests.test_utils import TestUtils + + +class MedicineAdministrationsApiTestCase(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("nurse1", cls.district, home_facility=cls.facility) + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.remote_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.remote_user = cls.create_user( + "remote-nurse", cls.district, home_facility=cls.remote_facility + ) + cls.discharged_patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.discharged_consultation = cls.create_consultation( + cls.discharged_patient, cls.facility, discharge_date="2024-01-04T00:00:00Z" + ) + + def setUp(self) -> None: + super().setUp() + self.normal_prescription = self.create_prescription() + + def create_prescription(self, **kwargs): + patient = kwargs.pop("patient", self.patient) + data = { + "consultation": self.create_consultation(patient, self.facility), + "medicine": MedibaseMedicine.objects.first(), + "prescription_type": "REGULAR", + "base_dosage": "1 mg", + "frequency": "OD", + "dosage_type": kwargs.get( + "dosage_type", PrescriptionDosageType.REGULAR.value + ), + } + return Prescription.objects.create( + **{**data, **kwargs, "prescribed_by": self.user} + ) + + def test_administer_and_archive(self): + # test administer + prescription = self.normal_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes"}, + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + administration_id = res.data["id"] + + # test archive + archive_path = f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/{administration_id}/archive/" + res = self.client.post(archive_path, {}) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + # test archive again + res = self.client.post(archive_path, {}) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + # test list administrations + res = self.client.get( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue( + any([administration_id == x["id"] for x in res.data["results"]]) + ) + + # test archived list administrations + res = self.client.get( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=true" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue( + any([administration_id == x["id"] for x in res.data["results"]]) + ) + + # test archived list administrations + res = self.client.get( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=false" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertFalse( + any([administration_id == x["id"] for x in res.data["results"]]) + ) + + def test_administer_in_future(self): + prescription = self.normal_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes", "administered_date": "2300-09-01T16:34"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_in_past(self): + prescription = self.normal_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes", "administered_date": "2019-09-01T16:34"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_discontinued(self): + prescription = self.create_prescription( + discontinued=True, discontinued_date=timezone.now() + ) + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_titrated_dosage(self): + prescription = self.create_prescription( + dosage_type=PrescriptionDosageType.TITRATED.value, target_dosage="10 mg" + ) + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes", "dosage": "1 mg"}, + ) + + self.assertEqual(res.status_code, status.HTTP_201_CREATED) diff --git a/care/facility/tests/test_medicine_api.py b/care/facility/tests/test_medicine_api.py deleted file mode 100644 index b683a6c22e..0000000000 --- a/care/facility/tests/test_medicine_api.py +++ /dev/null @@ -1,298 +0,0 @@ -from django.utils import timezone -from rest_framework import status -from rest_framework.test import APITestCase - -from care.facility.models import MedibaseMedicine, Prescription, PrescriptionDosageType -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", - "base_dosage": "1 mg", - "frequency": "OD", - "dosage_type": kwargs.get( - "dosage_type", PrescriptionDosageType.REGULAR.value - ), - } - return {**data, **kwargs} - - def test_invalid_dosage(self): - data = self.prescription_data(base_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()["base_dosage"][0], - "Invalid Input, must be in the format: ", - ) - - def test_dosage_out_of_range(self): - data = self.prescription_data(base_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()["base_dosage"][0], - "Input amount must be between 0.0001 and 5000", - ) - - data = self.prescription_data(base_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()["base_dosage"][0], - "Input amount must be between 0.0001 and 5000", - ) - - def test_dosage_precision(self): - data = self.prescription_data(base_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()["base_dosage"][0], - "Input amount must have at most 4 decimal places", - ) - - def test_dosage_unit_invalid(self): - data = self.prescription_data(base_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()["base_dosage"][0].startswith("Unit must be one of")) - - def test_dosage_leading_zero(self): - data = self.prescription_data(base_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()["base_dosage"][0], - "Input amount must be a valid number without leading or trailing zeroes", - ) - - def test_dosage_trailing_zero(self): - data = self.prescription_data(base_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()["base_dosage"][0], - "Input amount must be a valid number without leading or trailing zeroes", - ) - - def test_dosage_validator_clean(self): - data = self.prescription_data(base_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) - - def test_valid_dosage(self): - data = self.prescription_data(base_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) - - def test_create_titrated_prescription(self): - titrated_prescription_data = self.prescription_data( - dosage_type=PrescriptionDosageType.TITRATED.value, - target_dosage="2 mg", - instruction_on_titration="Test Instruction", - ) - response = self.client.post( - f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", - titrated_prescription_data, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - titrated_prescription_data = self.prescription_data( - dosage_type=PrescriptionDosageType.TITRATED.value, - ) - response = self.client.post( - f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", - titrated_prescription_data, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - def test_create_prn_prescription(self): - prn_prescription_data = self.prescription_data( - dosage_type=PrescriptionDosageType.PRN.value, - indicator="Test Indicator", - ) - response = self.client.post( - f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", - prn_prescription_data, - ) - self.assertEqual(response.status_code, status.HTTP_201_CREATED) - - prn_prescription_data = self.prescription_data( - dosage_type=PrescriptionDosageType.PRN.value, - ) - response = self.client.post( - f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", - prn_prescription_data, - ) - self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) - - -class MedicineAdministrationsApiTestCase(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 - ) - - def setUp(self) -> None: - super().setUp() - self.normal_prescription = self.create_prescription() - - def create_prescription(self, **kwargs): - data = { - "consultation": self.create_consultation(self.patient, self.facility), - "medicine": MedibaseMedicine.objects.first(), - "prescription_type": "REGULAR", - "base_dosage": "1 mg", - "frequency": "OD", - "dosage_type": kwargs.get( - "dosage_type", PrescriptionDosageType.REGULAR.value - ), - } - return Prescription.objects.create( - **{**data, **kwargs, "prescribed_by": self.user} - ) - - def test_administer_and_archive(self): - # test administer - prescription = self.normal_prescription - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes"}, - ) - self.assertEqual(res.status_code, status.HTTP_201_CREATED) - - administration_id = res.data["id"] - - # test archive - archive_path = f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/{administration_id}/archive/" - res = self.client.post(archive_path, {}) - self.assertEqual(res.status_code, status.HTTP_200_OK) - - # test archive again - res = self.client.post(archive_path, {}) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - # test list administrations - res = self.client.get( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) - - # test archived list administrations - res = self.client.get( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=true" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) - - # test archived list administrations - res = self.client.get( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=false" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertFalse( - any([administration_id == x["id"] for x in res.data["results"]]) - ) - - def test_administer_in_future(self): - prescription = self.normal_prescription - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes", "administered_date": "2300-09-01T16:34"}, - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_administer_in_past(self): - prescription = self.normal_prescription - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes", "administered_date": "2019-09-01T16:34"}, - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_administer_discontinued(self): - prescription = self.create_prescription( - discontinued=True, discontinued_date=timezone.now() - ) - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes"}, - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_administer_titrated_dosage(self): - prescription = self.create_prescription( - dosage_type=PrescriptionDosageType.TITRATED.value, target_dosage="10 mg" - ) - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes"}, - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes", "dosage": "1 mg"}, - ) - - self.assertEqual(res.status_code, status.HTTP_201_CREATED) diff --git a/care/facility/tests/test_prescriptions_api.py b/care/facility/tests/test_prescriptions_api.py index 87d5fa3c1d..cd5e4ab14b 100644 --- a/care/facility/tests/test_prescriptions_api.py +++ b/care/facility/tests/test_prescriptions_api.py @@ -13,14 +13,19 @@ def setUpTestData(cls) -> None: 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.user = cls.create_user("nurse1", cls.district, home_facility=cls.facility) + cls.remote_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.remote_user = cls.create_user( + "remote-nurse", cls.district, home_facility=cls.remote_facility + ) cls.patient = cls.create_patient(cls.district, cls.facility) + cls.consultation = cls.create_consultation(cls.patient, cls.facility) def setUp(self) -> None: super().setUp() - self.consultation = self.create_consultation(self.patient, self.facility) self.medicine = MedibaseMedicine.objects.first() - self.normal_prescription_data = { "medicine": self.medicine.external_id, "prescription_type": "REGULAR", @@ -29,6 +34,11 @@ def setUp(self) -> None: "dosage_type": "REGULAR", } + def prescription_data(self, **kwargs): + data = self.normal_prescription_data + data.update(**kwargs) + return data + def test_create_normal_prescription(self): response = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", @@ -36,6 +46,110 @@ def test_create_normal_prescription(self): ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_create_prescription_non_home_facility(self): + self.client.force_authenticate(self.remote_user) + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_invalid_dosage(self): + data = self.prescription_data(base_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()["base_dosage"][0], + "Invalid Input, must be in the format: ", + ) + + def test_dosage_out_of_range(self): + data = self.prescription_data(base_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()["base_dosage"][0], + "Input amount must be between 0.0001 and 5000", + ) + + data = self.prescription_data(base_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()["base_dosage"][0], + "Input amount must be between 0.0001 and 5000", + ) + + def test_dosage_precision(self): + data = self.prescription_data(base_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()["base_dosage"][0], + "Input amount must have at most 4 decimal places", + ) + + def test_dosage_unit_invalid(self): + data = self.prescription_data(base_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()["base_dosage"][0].startswith("Unit must be one of")) + + def test_dosage_leading_zero(self): + data = self.prescription_data(base_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()["base_dosage"][0], + "Input amount must be a valid number without leading or trailing zeroes", + ) + + def test_dosage_trailing_zero(self): + data = self.prescription_data(base_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()["base_dosage"][0], + "Input amount must be a valid number without leading or trailing zeroes", + ) + + def test_dosage_validator_clean(self): + data = self.prescription_data(base_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) + + def test_valid_dosage(self): + data = self.prescription_data(base_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) + def test_prescribe_duplicate_active_medicine_and_discontinue(self): """ 1. Creates a prescription with Medicine A From e348e09d3413c131d4fa92881283757f277b508b Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 10 May 2024 19:26:23 +0530 Subject: [PATCH 2/8] disallow create prescription for discharged consultations --- care/facility/api/serializers/prescription.py | 8 +++++++- care/facility/tests/test_prescriptions_api.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index ed68e772a7..4e001d609e 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -149,4 +149,10 @@ def validate(self, attrs): attrs.pop("target_dosage", None) return super().validate(attrs) - # TODO: Ensure that this medicine is not already prescribed to the same patient and is currently active. + + def create(self, validated_data): + if validated_data["consultation"].discharge_date: + raise serializers.ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) + return super().create(validated_data) diff --git a/care/facility/tests/test_prescriptions_api.py b/care/facility/tests/test_prescriptions_api.py index cd5e4ab14b..c1602312bc 100644 --- a/care/facility/tests/test_prescriptions_api.py +++ b/care/facility/tests/test_prescriptions_api.py @@ -22,6 +22,10 @@ def setUpTestData(cls) -> None: ) cls.patient = cls.create_patient(cls.district, cls.facility) cls.consultation = cls.create_consultation(cls.patient, cls.facility) + cls.discharged_patient = cls.create_patient(cls.district, cls.facility) + cls.discharged_consultation = cls.create_consultation( + cls.patient, cls.facility, discharge_date="2002-04-01T16:30:00Z" + ) def setUp(self) -> None: super().setUp() @@ -54,6 +58,13 @@ def test_create_prescription_non_home_facility(self): ) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + def test_create_prescription_on_discharged_consultation(self): + response = self.client.post( + f"/api/v1/consultation/{self.discharged_consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_invalid_dosage(self): data = self.prescription_data(base_dosage="abc") res = self.client.post( From 207cd40c9d1bb3c528d67ae3f0eb83468cea54a7 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 10 May 2024 20:01:28 +0530 Subject: [PATCH 3/8] disallow administer and archive for discharged consultations --- care/facility/api/serializers/prescription.py | 7 ++++ care/facility/api/viewsets/prescription.py | 5 +++ .../test_medicine_administrations_api.py | 36 +++++++++++++++++-- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index 4e001d609e..f94d3cccff 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -57,6 +57,13 @@ def validate(self, attrs): return super().validate(attrs) + def create(self, validated_data): + if validated_data["prescription"].consultation.discharge_date: + raise serializers.ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) + return super().create(validated_data) + class Meta: model = MedicineAdministration exclude = ("deleted",) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index 537faae98c..9709f21f7e 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -8,6 +8,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.serializers import ValidationError from rest_framework.viewsets import GenericViewSet, ViewSet from care.facility.api.serializers.prescription import ( @@ -76,6 +77,10 @@ def get_queryset(self): @extend_schema(tags=["prescription_administration"]) @action(methods=["POST"], detail=True) def archive(self, request, *args, **kwargs): + if self.get_consultation_obj().discharge_date: + raise ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) instance = self.get_object() if instance.archived_on: return Response( diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_administrations_api.py index 13fd8b928b..4295555074 100644 --- a/care/facility/tests/test_medicine_administrations_api.py +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -2,7 +2,12 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import MedibaseMedicine, Prescription, PrescriptionDosageType +from care.facility.models import ( + MedibaseMedicine, + MedicineAdministration, + Prescription, + PrescriptionDosageType, +) from care.utils.tests.test_utils import TestUtils @@ -34,11 +39,20 @@ def setUpTestData(cls) -> None: def setUp(self) -> None: super().setUp() self.normal_prescription = self.create_prescription() + self.discharged_prescription = self.create_prescription( + consultation=self.discharged_consultation + ) + self.discharged_administration = self.create_medicine_administration( + prescription=self.discharged_prescription + ) def create_prescription(self, **kwargs): patient = kwargs.pop("patient", self.patient) + consultation = kwargs.pop( + "consultation", self.create_consultation(patient, self.facility) + ) data = { - "consultation": self.create_consultation(patient, self.facility), + "consultation": consultation, "medicine": MedibaseMedicine.objects.first(), "prescription_type": "REGULAR", "base_dosage": "1 mg", @@ -51,6 +65,24 @@ def create_prescription(self, **kwargs): **{**data, **kwargs, "prescribed_by": self.user} ) + def create_medicine_administration(self, prescription, **kwargs): + return MedicineAdministration.objects.create( + prescription=prescription, administered_by=self.user, **kwargs + ) + + def test_administer_for_discharged_consultations(self): + prescription = self.discharged_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_archive_for_discharged_consultations(self): + res = self.client.post( + f"/api/v1/consultation/{self.discharged_prescription.consultation.external_id}/prescription_administration/{self.discharged_administration.external_id}/archive/" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + def test_administer_and_archive(self): # test administer prescription = self.normal_prescription From 536c1594ce3a5e0f9f996251328b1fb60e09108d Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 10 May 2024 20:03:48 +0530 Subject: [PATCH 4/8] adds test for administering and archiving by non home facility user --- .../tests/test_medicine_administrations_api.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_administrations_api.py index 4295555074..bbac0a5274 100644 --- a/care/facility/tests/test_medicine_administrations_api.py +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -83,6 +83,21 @@ def test_archive_for_discharged_consultations(self): ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + def test_administer_non_home_facility(self): + self.client.force_authenticate(self.remote_user) + prescription = self.discharged_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + ) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + + def test_archive_non_home_facility(self): + self.client.force_authenticate(self.remote_user) + res = self.client.post( + f"/api/v1/consultation/{self.discharged_prescription.consultation.external_id}/prescription_administration/{self.discharged_administration.external_id}/archive/" + ) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + def test_administer_and_archive(self): # test administer prescription = self.normal_prescription From a30f33b2830da5fc08fc5d6279090bf8a4089cbb Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 6 Jun 2024 10:41:40 +0530 Subject: [PATCH 5/8] revert to using original impl. of ConsultationRelatedPermissionMixin --- care/facility/models/prescription.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index 5f3eca9e09..3c9bdedba2 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -171,10 +171,7 @@ def last_administration(self): return self.administrations.order_by("-administered_date").first() def has_object_write_permission(self, request): - return ( - ConsultationRelatedPermissionMixin.has_write_permission(request) - and self.consultation.facility == request.user.home_facility - ) + return ConsultationRelatedPermissionMixin.has_write_permission(request) def __str__(self): return self.medicine + " - " + self.consultation.patient.name @@ -216,10 +213,7 @@ def get_related_consultation(self): return self.prescription.consultation def has_object_write_permission(self, request): - return ( - ConsultationRelatedPermissionMixin.has_write_permission(request) - and self.prescription.consultation.facility == request.user.home_facility - ) + return ConsultationRelatedPermissionMixin.has_write_permission(request) def validate(self) -> None: if self.prescription.discontinued: From 61935060f210c113f6987c59b59063ae98b7d3d2 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Mon, 19 Aug 2024 19:27:36 +0530 Subject: [PATCH 6/8] disallow discontinue and administer for dicharged consultations --- care/facility/api/viewsets/prescription.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index 2b285f37f2..2b97d9ab17 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -142,12 +142,16 @@ def perform_create(self, serializer): detail=True, ) def discontinue(self, request, *args, **kwargs): + consultation_obj = self.get_consultation_obj() + if consultation_obj.discharge_date: + raise ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) prescription_obj = self.get_object() prescription_obj.discontinued = True prescription_obj.discontinued_reason = request.data.get( "discontinued_reason", None ) - consultation_obj = self.get_consultation_obj() NotificationGenerator( event=Notification.Event.PATIENT_PRESCRIPTION_UPDATED, caused_by=self.request.user, @@ -165,6 +169,11 @@ def discontinue(self, request, *args, **kwargs): serializer_class=MedicineAdministrationSerializer, ) def administer(self, request, *args, **kwargs): + consultation_obj = self.get_consultation_obj() + if consultation_obj.discharge_date: + raise ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) prescription_obj = self.get_object() if prescription_obj.discontinued: return Response( From 2c6571dbb3387357feabdae1fc303b77fcff31a7 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Tue, 20 Aug 2024 15:40:18 +0530 Subject: [PATCH 7/8] remove redundant check --- care/facility/api/viewsets/prescription.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index 2b97d9ab17..a551dd639f 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -169,11 +169,6 @@ def discontinue(self, request, *args, **kwargs): serializer_class=MedicineAdministrationSerializer, ) def administer(self, request, *args, **kwargs): - consultation_obj = self.get_consultation_obj() - if consultation_obj.discharge_date: - raise ValidationError( - {"consultation": "Not allowed for discharged consultations"} - ) prescription_obj = self.get_object() if prescription_obj.discontinued: return Response( From 09a42b194c53ca31b67fd354f161cef85e539d7a Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 22 Aug 2024 18:35:52 +0530 Subject: [PATCH 8/8] Add test case for prescription discontinue for discharged --- care/facility/tests/test_prescriptions_api.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/care/facility/tests/test_prescriptions_api.py b/care/facility/tests/test_prescriptions_api.py index d481e7d392..3168aeb151 100644 --- a/care/facility/tests/test_prescriptions_api.py +++ b/care/facility/tests/test_prescriptions_api.py @@ -1,7 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import MedibaseMedicine +from care.facility.models import MedibaseMedicine, Prescription from care.utils.tests.test_utils import TestUtils @@ -44,6 +44,10 @@ def setUp(self) -> None: "frequency": "OD", "dosage_type": "REGULAR", } + self.discharged_consultation_prescription = Prescription.objects.create( + consultation=self.discharged_consultation, + medicine=self.medicine, + ) def prescription_data(self, **kwargs): data = self.normal_prescription_data @@ -72,6 +76,15 @@ def test_create_prescription_on_discharged_consultation(self): ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_discontinue_prescription_on_discharged_consultation(self): + res = self.client.post( + f"/api/v1/consultation/{self.discharged_consultation.external_id}/prescriptions/{self.discharged_consultation_prescription.external_id}/discontinue/", + { + "discontinued_reason": "Test Reason", + }, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + def test_invalid_dosage(self): data = self.prescription_data(base_dosage="abc") res = self.client.post(