diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index 4af84080ea..7bc714ffba 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -4,6 +4,7 @@ from care.facility.models import MedibaseMedicine, MedicineAdministration, Prescription from care.users.api.serializers.user import UserBaseMinimumSerializer +from care.users.models import User class MedibaseMedicineSerializer(serializers.ModelSerializer): @@ -19,7 +20,8 @@ class Meta: ) -class PrescriptionSerializer(serializers.ModelSerializer): +class PrescriptionDetailSerializer(serializers.ModelSerializer): + # TODO: Remove when #5492 is merged id = serializers.UUIDField(source="external_id", read_only=True) prescribed_by = UserBaseMinimumSerializer(read_only=True) last_administered_on = serializers.SerializerMethodField() @@ -89,11 +91,12 @@ def validate(self, attrs): # TODO: Ensure that this medicine is not already prescribed to the same patient and is currently active. -class MedicineAdministrationSerializer(serializers.ModelSerializer): +class MedicineAdministrationDetailSerializer(serializers.ModelSerializer): + # TODO: Remove when #5492 is merged id = serializers.UUIDField(source="external_id", read_only=True) administered_by = UserBaseMinimumSerializer(read_only=True) - prescription = PrescriptionSerializer(read_only=True) + prescription = PrescriptionDetailSerializer(read_only=True) def validate_administered_date(self, value): if value > timezone.now(): @@ -116,3 +119,95 @@ class Meta: "modified_date", "prescription", ) + + +class MedibaseMedicineBareMinimumSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + + class Meta: + model = MedibaseMedicine + fields = ["id", "name", "created_date", "modified_date"] + read_only_fields = ( + "created_date", + "modified_date", + ) + + +class PrescriptionBareMinimumSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + medicine_object = MedibaseMedicineBareMinimumSerializer( + read_only=True, source="medicine" + ) + + class Meta: + model = Prescription + fields = ( + "id", + "medicine_object", + "medicine_old", + "created_date", + ) + + +class MedicineAdministrationUserBareMinimumSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "first_name", "last_name"] + + +class MedicineAdministrationListSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + + administered_by = MedicineAdministrationUserBareMinimumSerializer(read_only=True) + prescription = PrescriptionBareMinimumSerializer(read_only=True) + + class Meta: + model = MedicineAdministration + fields = [ + "id", + "prescription", + "created_date", + "administered_by", + "notes", + "modified_date", + ] + read_only_fields = ( + "id", + "administered_by", + "created_date", + "prescription", + "notes", + "modified_date", + ) + + +class PrescriptionListSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + last_administered_on = serializers.SerializerMethodField() + medicine_object = MedibaseMedicineBareMinimumSerializer( + read_only=True, source="medicine" + ) + + class Meta: + model = Prescription + exclude = [ + "created_date", + "external_id", + "deleted", + "meta", + "discontinued_date", + "is_migrated", + "consultation", + "medicine", + "prescribed_by", + ] + + def get_last_administered_on(self, obj): + last_administration = ( + MedicineAdministration.objects.filter(prescription=obj) + .order_by("-created_date") + .first() + ) + if last_administration: + return last_administration.created_date + return None diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index c18d353bd5..4d30ad97c6 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -8,8 +8,10 @@ from rest_framework.viewsets import GenericViewSet, ViewSet from care.facility.api.serializers.prescription import ( - MedicineAdministrationSerializer, - PrescriptionSerializer, + MedicineAdministrationDetailSerializer, + MedicineAdministrationListSerializer, + PrescriptionDetailSerializer, + PrescriptionListSerializer, ) from care.facility.models import ( MedicineAdministration, @@ -39,7 +41,7 @@ class MedicineAdminstrationFilter(filters.FilterSet): class MedicineAdministrationViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet ): - serializer_class = MedicineAdministrationSerializer + serializer_class = MedicineAdministrationDetailSerializer permission_classes = (IsAuthenticated,) queryset = MedicineAdministration.objects.all().order_by("-created_date") lookup_field = "external_id" @@ -57,6 +59,11 @@ def get_queryset(self): consultation_obj = self.get_consultation_obj() return self.queryset.filter(prescription__consultation_id=consultation_obj.id) + def get_serializer_class(self): + if self.action == "list": + return MedicineAdministrationListSerializer + return self.serializer_class + class ConsultationPrescriptionFilter(filters.FilterSet): is_prn = filters.BooleanFilter() @@ -69,13 +76,18 @@ class ConsultationPrescriptionViewSet( mixins.RetrieveModelMixin, GenericViewSet, ): - serializer_class = PrescriptionSerializer + serializer_class = PrescriptionDetailSerializer permission_classes = (IsAuthenticated,) queryset = Prescription.objects.all().order_by("-created_date") lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) filterset_class = ConsultationPrescriptionFilter + def get_serializer_class(self): + if self.action == "list": + return PrescriptionListSerializer + return self.serializer_class + def get_consultation_obj(self): return get_object_or_404( get_consultation_queryset(self.request.user).filter( @@ -109,7 +121,7 @@ def discontinue(self, request, *args, **kwargs): @action( methods=["POST"], detail=True, - serializer_class=MedicineAdministrationSerializer, + serializer_class=MedicineAdministrationDetailSerializer, ) def administer(self, request, *args, **kwargs): prescription_obj = self.get_object() diff --git a/care/facility/tests/test_facility_prescription_api.py b/care/facility/tests/test_facility_prescription_api.py new file mode 100644 index 0000000000..e606c17860 --- /dev/null +++ b/care/facility/tests/test_facility_prescription_api.py @@ -0,0 +1,305 @@ +import random +from enum import Enum + +from rest_framework import status +from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework_simplejwt.tokens import RefreshToken + +from care.facility.models import MedibaseMedicine +from care.facility.tests.mixins import TestClassMixin +from care.utils.tests.test_base import TestBase + + +class ExpectedPrescriptionKeys(Enum): + ID = "id" + MEDICINE_OBJECT = "medicine_object" + MEDICINE_OLD = "medicine_old" + CREATED_DATE = "created_date" + + +class ExpectedMedicineObjectListKeys(Enum): + ID = "id" + NAME = "name" + CREATED_DATE = "created_date" + MODIFIED_DATE = "modified_date" + + +class ExpectedAdministeredByKeys(Enum): + ID = "id" + FIRST_NAME = "first_name" + LAST_NAME = "last_name" + + +class ExpectedMedicineAdministrationListKeys(Enum): + ID = "id" + PRESCRIPTION = "prescription" + CREATED_DATE = "created_date" + ADMINISTERED_BY = "administered_by" + NOTES = "notes" + MODIFIED_DATE = "modified_date" + + +class ExpectedMedicineListKeys(Enum): + ID = "id" + NAME = "name" + CREATED_DATE = "created_date" + MODIFIED_DATE = "modified_date" + + +class ExpectedPrescriptionListKeys(Enum): + ID = "id" + LAST_ADMINISTERED_ON = "last_administered_on" + MEDICINE_OBJECT = "medicine_object" + MODIFIED_DATE = "modified_date" + PRESCRIPTION_TYPE = "prescription_type" + MEDICINE_OLD = "medicine_old" + ROUTE = "route" + DOSAGE = "dosage" + IS_PRN = "is_prn" + FREQUENCY = "frequency" + DAYS = "days" + INDICATOR = "indicator" + MAX_DOSAGE = "max_dosage" + MIN_HOURS_BETWEEN_DOSES = "min_hours_between_doses" + NOTES = "notes" + DISCONTINUED = "discontinued" + DISCONTINUED_REASON = "discontinued_reason" + + +class ExpectedPrescribedByRetrieveKeys(Enum): + ID = "id" + FIRST_NAME = "first_name" + USERNAME = "username" + EMAIL = "email" + LAST_NAME = "last_name" + USER_TYPE = "user_type" + LAST_LOGIN = "last_login" + + +# class ExpectedMedicineRetrieveKeys(Enum): +# ID = "id" +# EXTERNAL_ID = "external_id" +# CREATED_DATE = "created_date" +# MODIFIED_DATE = "modified_date" +# NAME = "name" +# TYPE = "type" +# GENERIC = "generic" +# COMPANY = "company" +# CONTENTS = "contents" +# CIMS_CLASS = "cims_class" +# ATC_CLASSIFICATION = "atc_classification" + +# class ExpectedPrescriptionRetrieveKeys(Enum): +# ID = "id" +# PRESCRIBED_BY = "prescribed_by" +# LAST_ADMINISTERED_ON = "last_administered_on" +# MEDICINE_OBJECT = "medicine_object" +# EXTERNAL_ID = "external_id" +# CREATED_DATE = "created_date" +# MODIFIED_DATE = "modified_date" +# PRESCRIPTION_TYPE = "prescription_type" +# MEDICINE_OLD = "medicine_old" +# ROUTE = "route" +# DOSAGE = "dosage" +# IS_PRN = "is_prn" +# FREQUENCY = "frequency" +# DAYS = "days" +# INDICATOR = "indicator" +# MAX_DOSAGE = "max_dosage" +# MIN_HOURS_BETWEEN_DOSES = "min_hours_between_doses" +# NOTES = "notes" +# META = "meta" +# DISCONTINUED = "discontinued" +# DISCONTINUED_REASON = "discontinued_reason" +# DISCONTINUED_DATE = "discontinued_date" +# IS_MIGRATED = "is_migrated" + + +class ExpectedAdministeredByRetrieveKeys(Enum): + ID = "id" + FIRST_NAME = "first_name" + USERNAME = "username" + EMAIL = "email" + LAST_NAME = "last_name" + USER_TYPE = "user_type" + LAST_LOGIN = "last_login" + + +class ExpectedMedicineRetrieveKeys(Enum): + ID = "id" + EXTERNAL_ID = "external_id" + CREATED_DATE = "created_date" + MODIFIED_DATE = "modified_date" + NAME = "name" + TYPE = "type" + GENERIC = "generic" + COMPANY = "company" + CONTENTS = "contents" + CIMS_CLASS = "cims_class" + ATC_CLASSIFICATION = "atc_classification" + + +class ExpectedPrescriptionRetrieveKeys(Enum): + ID = "id" + PRESCRIBED_BY = "prescribed_by" + LAST_ADMINISTERED_ON = "last_administered_on" + MEDICINE_OBJECT = "medicine_object" + EXTERNAL_ID = "external_id" + CREATED_DATE = "created_date" + MODIFIED_DATE = "modified_date" + PRESCRIPTION_TYPE = "prescription_type" + MEDICINE_OLD = "medicine_old" + ROUTE = "route" + DOSAGE = "dosage" + IS_PRN = "is_prn" + FREQUENCY = "frequency" + DAYS = "days" + INDICATOR = "indicator" + MAX_DOSAGE = "max_dosage" + MIN_HOURS_BETWEEN_DOSES = "min_hours_between_doses" + NOTES = "notes" + META = "meta" + DISCONTINUED = "discontinued" + DISCONTINUED_REASON = "discontinued_reason" + DISCONTINUED_DATE = "discontinued_date" + IS_MIGRATED = "is_migrated" + + +class ExpectedAdministeredPrescriptionRetrieveKeys(Enum): + ID = "id" + ADMINISTERED_BY = "administered_by" + PRESCRIPTION = "prescription" + EXTERNAL_ID = "external_id" + CREATED_DATE = "created_date" + MODIFIED_DATE = "modified_date" + NOTES = "notes" + ADMINISTERED_DATE = "administered_date" + + +class ConsultationPrescriptionViewSetTestCase(TestBase, TestClassMixin, APITestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.factory = APIRequestFactory() + + def setUp(self) -> None: + refresh_token = RefreshToken.for_user(self.user) + self.client.credentials( + HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" + ) + + def test_list_prescription(self): + response = self.client.get( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/" + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsInstance(response.json()["results"], list) + + expected_keys = [key.value for key in ExpectedPrescriptionListKeys] + data = response.json()["results"][0] + self.assertCountEqual(data.keys(), expected_keys) + + expected_medicine_objects_keys = [ + key.value for key in ExpectedMedicineObjectListKeys + ] + data = response.json()["results"][0]["medicine_object"] + self.assertCountEqual(data.keys(), expected_medicine_objects_keys) + + def test_retrieve_prescription(self): + response = self.client.get( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/{self.prescription.external_id}/" + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + expected_keys = [key.value for key in ExpectedPrescriptionRetrieveKeys] + data = response.json() + self.assertCountEqual(data.keys(), expected_keys) + + expected_prescribed_by_keys = [ + key.value for key in ExpectedPrescribedByRetrieveKeys + ] + data = response.json()["prescribed_by"] + self.assertCountEqual(data.keys(), expected_prescribed_by_keys) + + def test_create_prescription(self): + medicine = random.choice(MedibaseMedicine.objects.all()) + data = { + "medicine": medicine.external_id, + "prescription_type": "REGULAR", + "medicine_old": "Paracetamol", + "route": "ORAL", + "dosage": "1-0-1", + "is_prn": False, + "frequency": "OD", + "days": 5, + "indicator": "Take only when fever is above 100", + "max_dosage": "2-0-2", + "min_hours_between_doses": 4, + "notes": "Take with water", + "meta": {}, + "prescribed_by": self.user.external_id, + "discontinued": False, + "discontinued_reason": "", + "discontinued_date": None, + "is_migrated": False, + } + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + data=data, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + +class MedicineAdministrationViewSetTestCase(TestBase, TestClassMixin, APITestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.factory = APIRequestFactory() + + def setUp(self) -> None: + refresh_token = RefreshToken.for_user(self.user) + self.client.credentials( + HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" + ) + + def test_list_medicine_administration(self): + url = f"/api/v1/consultation/{self.consultation.external_id}/prescription_administration/" + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsInstance(response.json()["results"], list) + + expected_keys = [key.value for key in ExpectedMedicineAdministrationListKeys] + data = response.json()["results"][0] + self.assertCountEqual(data.keys(), expected_keys) + + expected_administered_by_keys = [ + key.value for key in ExpectedAdministeredByKeys + ] + data = response.json()["results"][0]["administered_by"] + self.assertCountEqual(data.keys(), expected_administered_by_keys) + + def test_retrieve_medicine_administration_keys(self): + url = f"/api/v1/consultation/{self.consultation.external_id}/prescription_administration/{self.medicine_administration.external_id}/" + response = self.client.get(url) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + expected_keys = [ + key.value for key in ExpectedAdministeredPrescriptionRetrieveKeys + ] + data = response.json() + self.assertCountEqual(data.keys(), expected_keys) + + expected_administered_by_keys = [ + key.value for key in ExpectedAdministeredByRetrieveKeys + ] + data = response.json()["administered_by"] + self.assertCountEqual(data.keys(), expected_administered_by_keys) + + expected_prescription_keys = [ + key.value for key in ExpectedPrescriptionRetrieveKeys + ] + data = response.json()["prescription"] + self.assertCountEqual(data.keys(), expected_prescription_keys) diff --git a/care/utils/tests/test_base.py b/care/utils/tests/test_base.py index 25427225e3..b05ac4b25f 100644 --- a/care/utils/tests/test_base.py +++ b/care/utils/tests/test_base.py @@ -1,5 +1,6 @@ import abc import datetime +import random from collections import OrderedDict from uuid import uuid4 @@ -19,8 +20,11 @@ DiseaseStatusEnum, Facility, LocalBody, + MedibaseMedicine, + MedicineAdministration, PatientConsultation, PatientRegistration, + Prescription, User, ) from care.users.models import District, State @@ -239,10 +243,12 @@ def setUpClass(cls) -> None: user_type=User.TYPE_VALUE_MAP["StateAdmin"], home_facility=cls.facility, ) - + cls.consultation = cls.create_consultation() cls.user_data = cls.get_user_data(cls.district, cls.user_type) cls.facility_data = cls.get_facility_data(cls.district) cls.patient_data = cls.get_patient_data(cls.district) + cls.prescription = cls.create_prescription() + cls.medicine_administration = cls.create_medicine_administration() def setUp(self) -> None: self.client.force_login(self.user) @@ -446,7 +452,6 @@ def create_patient_note( "note": note, } data.update(kwargs) - patientId = patient.external_id refresh_token = RefreshToken.for_user(created_by) @@ -455,3 +460,42 @@ def create_patient_note( ) self.client.post(f"/api/v1/patient/{patientId}/notes/", data=data) + + @classmethod + def create_prescription(cls, consultation=None, user=None, **kwargs): + medicine = random.choice(MedibaseMedicine.objects.all()) + data = { + "consultation": consultation or cls.consultation, + "prescription_type": "REGULAR", + "medicine": medicine, + "medicine_old": "Paracetamol", + "route": "ORAL", + "dosage": "1-0-1", + "is_prn": False, + "frequency": "OD", + "days": 5, + "indicator": "Take only when fever is above 100", + "max_dosage": "2-0-2", + "min_hours_between_doses": 4, + "notes": "Take with water", + "meta": {}, + "prescribed_by": user or cls.user, + "discontinued": False, + "discontinued_reason": "", + "discontinued_date": None, + "is_migrated": False, + } + data.update(**kwargs) + return Prescription.objects.create(**data) + + @classmethod + def create_medicine_administration(cls, **kwargs): + data = { + "prescription": cls.create_prescription(), + "notes": "Take with water", + "administered_by": cls.user, + "administered_date": make_aware(datetime.datetime(2020, 4, 7, 15, 30)), + } + + data.update(kwargs) + return MedicineAdministration.objects.create(**data)