From 83d212ac07d541dce09fd489cafefa319b9bebd1 Mon Sep 17 00:00:00 2001 From: Suyash Singh Date: Mon, 21 Aug 2023 23:01:36 +0530 Subject: [PATCH] Fix: add list and detail serializer --- .../api/serializers/patient_consultation.py | 391 ++++++++++++++++++ .../api/viewsets/patient_consultation.py | 9 +- .../tests/test_patient_consultation_api.py | 230 +++++++++++ care/utils/tests/test_base.py | 26 +- 4 files changed, 651 insertions(+), 5 deletions(-) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 4178ab75cd..46c27a57ce 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -39,6 +39,7 @@ class PatientConsultationSerializer(serializers.ModelSerializer): + # TODO: Remove when #5492 is completed id = serializers.CharField(source="external_id", read_only=True) facility_name = serializers.CharField(source="facility.name", read_only=True) suggestion_text = ChoiceField( @@ -548,3 +549,393 @@ def validate(self, attrs): class Meta: model = PatientConsultation fields = ("email",) + + +class PatientConsultationListSerializer(serializers.ModelSerializer): + id = serializers.CharField(source="external_id", read_only=True) + facility_name = serializers.CharField(source="facility.name", read_only=True) + facility = serializers.CharField(source="facility.external_id", read_only=True) + patient = serializers.CharField(source="patient.external_id", read_only=True) + last_edited_by = UserBaseMinimumSerializer(read_only=True) + suggestion_text = ChoiceField( + choices=PatientConsultation.SUGGESTION_CHOICES, + read_only=True, + source="suggestion", + ) + + class Meta: + model = PatientConsultation + fields = [ + "id", + "is_kasp", + "facility_name", + "is_telemedicine", + "suggestion_text", + "kasp_enabled_date", + "admitted", + "admission_date", + "admitted", + "discharge_date", + "created_date", + "modified_date", + "last_edited_by", + "facility", + "patient", + ] + read_only_fields = TIMESTAMP_FIELDS + ( + "discharge_date", + "last_edited_by", + "created_by", + "kasp_enabled_date", + ) + + +class PatientConsulationDetailSerializer(PatientConsultationListSerializer): + id = serializers.CharField(source="external_id", read_only=True) + facility_name = serializers.CharField(source="facility.name", read_only=True) + suggestion_text = ChoiceField( + choices=PatientConsultation.SUGGESTION_CHOICES, + read_only=True, + source="suggestion", + ) + + symptoms = serializers.MultipleChoiceField(choices=SYMPTOM_CHOICES) + deprecated_covid_category = ChoiceField( + choices=COVID_CATEGORY_CHOICES, required=False + ) + category = ChoiceField(choices=CATEGORY_CHOICES, required=True) + + referred_to_object = FacilityBasicInfoSerializer( + source="referred_to", read_only=True + ) + referred_to = ExternalIdSerializerField( + queryset=Facility.objects.all(), + required=False, + ) + referred_to_external = serializers.CharField( + required=False, allow_null=True, allow_blank=True + ) + patient = ExternalIdSerializerField(queryset=PatientRegistration.objects.all()) + facility = ExternalIdSerializerField(read_only=True) + + assigned_to_object = UserAssignedSerializer(source="assigned_to", read_only=True) + + assigned_to = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), required=False, allow_null=True + ) + + discharge_reason = serializers.ChoiceField( + choices=DISCHARGE_REASON_CHOICES, read_only=True, required=False + ) + discharge_notes = serializers.CharField(read_only=True) + + discharge_prescription = serializers.SerializerMethodField() + discharge_prn_prescription = serializers.SerializerMethodField() + + action = ChoiceField( + choices=PatientRegistration.ActionChoices, + write_only=True, + required=False, + ) + + review_interval = serializers.IntegerField(default=-1, required=False) + + last_edited_by = UserBaseMinimumSerializer(read_only=True) + created_by = UserBaseMinimumSerializer(read_only=True) + last_daily_round = DailyRoundSerializer(read_only=True) + + current_bed = ConsultationBedSerializer(read_only=True) + + bed = ExternalIdSerializerField(queryset=Bed.objects.all(), required=False) + + icd11_diagnoses_object = serializers.SerializerMethodField(read_only=True) + + icd11_provisional_diagnoses_object = serializers.SerializerMethodField( + read_only=True + ) + + def get_discharge_prescription(self, consultation): + return Prescription.objects.filter( + consultation=consultation, + prescription_type=PrescriptionType.DISCHARGE.value, + is_prn=False, + ).values() + + def get_discharge_prn_prescription(self, consultation): + return Prescription.objects.filter( + consultation=consultation, + prescription_type=PrescriptionType.DISCHARGE.value, + is_prn=True, + ).values() + + def get_icd11_diagnoses_object(self, consultation): + return get_icd11_diagnoses_objects_by_ids(consultation.icd11_diagnoses) + + def get_icd11_provisional_diagnoses_object(self, consultation): + return get_icd11_diagnoses_objects_by_ids( + consultation.icd11_provisional_diagnoses + ) + + class Meta: + model = PatientConsultation + read_only_fields = TIMESTAMP_FIELDS + ( + "last_updated_by_telemedicine", + "discharge_date", + "last_edited_by", + "created_by", + "kasp_enabled_date", + ) + exclude = ("deleted", "external_id") + + def validate_bed_number(self, bed_number): + try: + if not self.initial_data["admitted"]: + bed_number = None + except KeyError: + bed_number = None + return bed_number + + def update(self, instance, validated_data): + instance.last_edited_by = self.context["request"].user + + if instance.discharge_date: + raise ValidationError( + {"consultation": ["Discharged Consultation data cannot be updated"]} + ) + + if instance.suggestion == SuggestionChoices.OP: + instance.discharge_date = localtime(now()) + instance.save() + + if "action" in validated_data or "review_interval" in validated_data: + patient = instance.patient + + if "action" in validated_data: + action = validated_data.pop("action") + patient.action = action + + if "review_interval" in validated_data: + review_interval = validated_data.pop("review_interval") + instance.review_interval = review_interval + instance.save() + if review_interval >= 0: + patient.review_time = localtime(now()) + timedelta( + minutes=review_interval + ) + else: + patient.review_time = None + patient.save() + + validated_data["last_updated_by_telemedicine"] = ( + self.context["request"].user == instance.assigned_to + ) + + if "is_kasp" in validated_data: + if validated_data["is_kasp"] and (not instance.is_kasp): + validated_data["kasp_enabled_date"] = localtime(now()) + + _temp = instance.assigned_to + + consultation = super().update(instance, validated_data) + + if "assigned_to" in validated_data: + if validated_data["assigned_to"] != _temp and validated_data["assigned_to"]: + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, + caused_by=self.context["request"].user, + caused_object=instance, + facility=instance.patient.facility, + notification_mediums=[ + Notification.Medium.SYSTEM, + Notification.Medium.WHATSAPP, + ], + ).generate() + + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_UPDATED, + caused_by=self.context["request"].user, + caused_object=consultation, + facility=consultation.patient.facility, + ).generate() + + return consultation + + def create(self, validated_data): + action = -1 + review_interval = -1 + if "action" in validated_data: + action = validated_data.pop("action") + if "review_interval" in validated_data: + review_interval = validated_data.pop("review_interval") + + # Authorisation Check + + allowed_facilities = get_home_facility_queryset(self.context["request"].user) + if not allowed_facilities.filter( + id=self.validated_data["patient"].facility.id + ).exists(): + raise ValidationError( + {"facility": "Consultation creates are only allowed in home facility"} + ) + + # End Authorisation Checks + + if validated_data["patient"].last_consultation: + if ( + self.context["request"].user + == validated_data["patient"].last_consultation.assigned_to + ): + raise ValidationError( + { + "Permission Denied": "Only Facility Staff can create consultation for a Patient" + }, + ) + + if validated_data["patient"].last_consultation: + if not validated_data["patient"].last_consultation.discharge_date: + raise ValidationError( + {"consultation": "Exists please Edit Existing Consultation"} + ) + + if "is_kasp" in validated_data: + if validated_data["is_kasp"]: + validated_data["kasp_enabled_date"] = localtime(now()) + + bed = validated_data.pop("bed", None) + + validated_data["facility_id"] = validated_data[ + "patient" + ].facility_id # Coercing facility as the patient's facility + consultation = super().create(validated_data) + consultation.created_by = self.context["request"].user + consultation.last_edited_by = self.context["request"].user + consultation.save() + + if bed and consultation.suggestion == SuggestionChoices.A: + consultation_bed = ConsultationBed( + bed=bed, + consultation=consultation, + start_date=consultation.created_date, + ) + consultation_bed.save() + consultation.current_bed = consultation_bed + consultation.save(update_fields=["current_bed"]) + + patient = consultation.patient + if consultation.suggestion == SuggestionChoices.OP: + consultation.discharge_date = localtime(now()) + consultation.save() + patient.is_active = False + patient.allow_transfer = True + else: + patient.is_active = True + patient.last_consultation = consultation + + if action != -1: + patient.action = action + consultation.review_interval = review_interval + if review_interval > 0: + patient.review_time = localtime(now()) + timedelta(minutes=review_interval) + else: + patient.review_time = None + + patient.save() + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_CREATED, + caused_by=self.context["request"].user, + caused_object=consultation, + facility=patient.facility, + ).generate() + + if consultation.assigned_to: + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, + caused_by=self.context["request"].user, + caused_object=consultation, + facility=consultation.patient.facility, + notification_mediums=[ + Notification.Medium.SYSTEM, + Notification.Medium.WHATSAPP, + ], + ).generate() + + return consultation + + def validate(self, attrs): + validated = super().validate(attrs) + # TODO Add Bed Authorisation Validation + + if "suggestion" in validated: + if validated["suggestion"] is SuggestionChoices.R: + if not validated.get("referred_to") and not validated.get( + "referred_to_external" + ): + raise ValidationError( + { + "referred_to": [ + f"This field is required as the suggestion is {SuggestionChoices.R}." + ] + } + ) + if validated.get("referred_to_external"): + validated["referred_to"] = None + elif validated.get("referred_to"): + validated["referred_to_external"] = None + 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: + if "review_interval" not in validated: + raise ValidationError( + { + "review_interval": [ + "This field is required as the patient has been requested Review." + ] + } + ) + if validated["review_interval"] <= 0: + raise ValidationError( + { + "review_interval": [ + "This field value is must be greater than 0." + ] + } + ) + from care.facility.static_data.icd11 import ICDDiseases + + if "icd11_diagnoses" in validated: + for diagnosis in validated["icd11_diagnoses"]: + try: + ICDDiseases.by.id[diagnosis] + except BaseException: + raise ValidationError( + { + "icd11_diagnoses": [ + f"{diagnosis} is not a valid ICD 11 Diagnosis ID" + ] + } + ) + + if "icd11_provisional_diagnoses" in validated: + for diagnosis in validated["icd11_provisional_diagnoses"]: + try: + ICDDiseases.by.id[diagnosis] + except BaseException: + raise ValidationError( + { + "icd11_provisional_diagnoses": [ + f"{diagnosis} is not a valid ICD 11 Diagnosis ID" + ] + } + ) + return validated diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index b87856d594..3730e64b1a 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -13,9 +13,10 @@ from care.facility.api.serializers.file_upload import FileUploadRetrieveSerializer from care.facility.api.serializers.patient_consultation import ( EmailDischargeSummarySerializer, + PatientConsulationDetailSerializer, PatientConsultationDischargeSerializer, PatientConsultationIDSerializer, - PatientConsultationSerializer, + PatientConsultationListSerializer, ) from care.facility.api.viewsets.mixins.access import AssetUserAccessMixin from care.facility.models.file_upload import FileUpload @@ -44,7 +45,7 @@ class PatientConsultationViewSet( GenericViewSet, ): lookup_field = "external_id" - serializer_class = PatientConsultationSerializer + serializer_class = PatientConsulationDetailSerializer permission_classes = ( IsAuthenticated, DRYPermissions, @@ -56,6 +57,8 @@ class PatientConsultationViewSet( filterset_class = PatientConsultationFilter def get_serializer_class(self): + if self.action == "list": + return PatientConsultationListSerializer if self.action == "patient_from_asset": return PatientConsultationIDSerializer elif self.action == "discharge_patient": @@ -71,7 +74,7 @@ def get_permissions(self): return super().get_permissions() def get_queryset(self): - if self.serializer_class == PatientConsultationSerializer: + if self.serializer_class == PatientConsulationDetailSerializer: self.queryset = self.queryset.prefetch_related( "assigned_to", Prefetch( diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index acf2e5d50e..9a0cf62b36 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -1,9 +1,12 @@ import datetime +import random +from enum import Enum from django.test import TestCase from django.utils.timezone import make_aware from rest_framework import status from rest_framework.test import APIRequestFactory, APITestCase +from rest_framework_simplejwt.tokens import RefreshToken from care.facility.api.viewsets.facility_users import FacilityUserViewSet from care.facility.api.viewsets.patient_consultation import PatientConsultationViewSet @@ -17,6 +20,155 @@ from care.utils.tests.test_base import TestBase +class ExpectedPatientConsultationListKeys(Enum): + id = "id" + is_kasp = "is_kasp" + facility_name = "facility_name" + is_telemedicine = "is_telemedicine" + suggestion_text = "suggestion_text" + kasp_enabled_date = "kasp_enabled_date" + admitted = "admitted" + admission_date = "admission_date" + discharge_date = "discharge_date" + created_date = "created_date" + modified_date = "modified_date" + last_edited_by = "last_edited_by" + facility = "facility" + patient = "patient" + + +class ExpectedLastEditedByKeys(Enum): + ID = "id" + FIRST_NAME = "first_name" + USERNAME = "username" + EMAIL = "email" + LAST_NAME = "last_name" + USER_TYPE = "user_type" + LAST_LOGIN = "last_login" + + +class ExpectedPatientConsultationRetrieveKeys(Enum): + id = "id" + facility_name = "facility_name" + facility = "facility" + patient = "patient" + last_edited_by = "last_edited_by" + suggestion_text = "suggestion_text" + symptoms = "symptoms" + deprecated_covid_category = "deprecated_covid_category" + category = "category" + referred_to_object = "referred_to_object" + referred_to = "referred_to" + referred_to_external = "referred_to_external" + assigned_to_object = "assigned_to_object" + assigned_to = "assigned_to" + discharge_reason = "discharge_reason" + discharge_notes = "discharge_notes" + discharge_prescription = "discharge_prescription" + discharge_prn_prescription = "discharge_prn_prescription" + review_interval = "review_interval" + created_by = "created_by" + last_daily_round = "last_daily_round" + current_bed = "current_bed" + icd11_diagnoses_object = "icd11_diagnoses_object" + icd11_provisional_diagnoses_object = "icd11_provisional_diagnoses_object" + created_date = "created_date" + modified_date = "modified_date" + ip_no = "ip_no" + op_no = "op_no" + diagnosis = "diagnosis" + icd11_provisional_diagnoses = "icd11_provisional_diagnoses" + icd11_diagnoses = "icd11_diagnoses" + other_symptoms = "other_symptoms" + symptoms_onset_date = "symptoms_onset_date" + examination_details = "examination_details" + history_of_present_illness = "history_of_present_illness" + prescribed_medication = "prescribed_medication" + consultation_notes = "consultation_notes" + course_in_facility = "course_in_facility" + investigation = "investigation" + prescriptions = "prescriptions" + procedure = "procedure" + suggestion = "suggestion" + consultation_status = "consultation_status" + admitted = "admitted" + admission_date = "admission_date" + discharge_date = "discharge_date" + death_datetime = "death_datetime" + death_confirmed_doctor = "death_confirmed_doctor" + bed_number = "bed_number" + is_kasp = "is_kasp" + kasp_enabled_date = "kasp_enabled_date" + is_telemedicine = "is_telemedicine" + last_updated_by_telemedicine = "last_updated_by_telemedicine" + verified_by = "verified_by" + height = "height" + weight = "weight" + operation = "operation" + special_instruction = "special_instruction" + intubation_history = "intubation_history" + prn_prescription = "prn_prescription" + discharge_advice = "discharge_advice" + + +class ExpectedCreatedByKeys(Enum): + ID = "id" + FIRST_NAME = "first_name" + USERNAME = "username" + EMAIL = "email" + LAST_NAME = "last_name" + USER_TYPE = "user_type" + LAST_LOGIN = "last_login" + + +class LocalBodyKeys(Enum): + ID = "id" + NAME = "name" + BODY_TYPE = "body_type" + LOCALBODY_CODE = "localbody_code" + DISTRICT = "district" + + +class DistrictKeys(Enum): + ID = "id" + NAME = "name" + STATE = "state" + + +class StateKeys(Enum): + ID = "id" + NAME = "name" + + +class ExpectedReferredToKeys(Enum): + ID = "id" + NAME = "name" + LOCAL_BODY = "local_body" + DISTRICT = "district" + STATE = "state" + WARD_OBJECT = "ward_object" + LOCAL_BODY_OBJECT = "local_body_object" + DISTRICT_OBJECT = "district_object" + STATE_OBJECT = "state_object" + FACILITY_TYPE = "facility_type" + READ_COVER_IMAGE_URL = "read_cover_image_url" + FEATURES = "features" + PATIENT_COUNT = "patient_count" + BED_COUNT = "bed_count" + + +class WardKeys(Enum): + ID = "id" + NAME = "name" + NUMBER = "number" + LOCAL_BODY = "local_body" + + +class FacilityTypeKeys(Enum): + ID = "id" + NAME = "name" + + class FacilityUserTest(TestClassMixin, TestCase): def setUp(self): super().setUp() @@ -72,6 +224,11 @@ def setUp(self): self.consultation = self.create_consultation( suggestion="A", admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + referred_to=random.choices(Facility.objects.all())[0], + ) + refresh_token = RefreshToken.for_user(self.user) + self.client.credentials( + HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" ) def create_admission_consultation(self, patient=None, **kwargs): @@ -292,3 +449,76 @@ def test_referred_to_external_valid_value(self): referred_to_external=referred_to_external, ) self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_list_patient_consultation(self): + response = self.client.get("/api/v1/consultation/") + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsInstance(response.json()["results"], list) + + # Ensure only necessary data is being sent and no extra data + expected_keys = [key.value for key in ExpectedPatientConsultationListKeys] + + data = response.json()["results"][0] + + self.assertCountEqual(data.keys(), expected_keys) + + last_edited_by_keys = [key.value for key in ExpectedLastEditedByKeys] + + if data["last_edited_by"]: + self.assertCountEqual(data["last_edited_by"].keys(), last_edited_by_keys) + + def test_retrieve_patient_consultation(self): + response = self.client.get( + f"/api/v1/consultation/{self.consultation.external_id}/" + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + + # Ensure only necessary data is being sent and no extra data + expected_keys = [key.value for key in ExpectedPatientConsultationRetrieveKeys] + + data = response.json() + + self.assertCountEqual(data.keys(), expected_keys) + + last_edited_by_keys = [key.value for key in ExpectedLastEditedByKeys] + + if data["last_edited_by"]: + self.assertCountEqual(data["last_edited_by"].keys(), last_edited_by_keys) + + reffered_to_object_keys = [key.value for key in ExpectedReferredToKeys] + self.assertCountEqual( + data["referred_to_object"].keys(), + reffered_to_object_keys, + ) + + ward_object_keys = [key.value for key in WardKeys] + self.assertCountEqual( + data["referred_to_object"]["ward_object"].keys(), + ward_object_keys, + ) + + local_body_object_keys = [key.value for key in LocalBodyKeys] + self.assertCountEqual( + data["referred_to_object"]["local_body_object"].keys(), + local_body_object_keys, + ) + + district_object_keys = [key.value for key in DistrictKeys] + self.assertCountEqual( + data["referred_to_object"]["district_object"].keys(), + district_object_keys, + ) + + state_object_keys = [key.value for key in StateKeys] + self.assertCountEqual( + data["referred_to_object"]["state_object"].keys(), + state_object_keys, + ) + + facility_object_keys = [key.value for key in FacilityTypeKeys] + self.assertCountEqual( + data["referred_to_object"]["facility_type"].keys(), + facility_object_keys, + ) diff --git a/care/utils/tests/test_base.py b/care/utils/tests/test_base.py index 3ed212c1b2..f4b20d8b9e 100644 --- a/care/utils/tests/test_base.py +++ b/care/utils/tests/test_base.py @@ -23,7 +23,7 @@ PatientRegistration, User, ) -from care.users.models import District, State +from care.users.models import District, State, Ward from config.tests.helper import EverythingEquals, mock_equal @@ -83,7 +83,7 @@ def create_state(cls) -> State: @classmethod def create_facility( - cls, district: District, user: User = None, **kwargs + cls, district: District, user: User = None, ward=None, local_body=None, **kwargs ) -> Facility: user = user or cls.user data = { @@ -95,6 +95,8 @@ def create_facility( "oxygen_capacity": 10, "phone_number": "9998887776", "created_by": user, + "ward": ward or cls.ward, + "local_body": local_body or cls.local_body, } data.update(kwargs) f = Facility(**data) @@ -231,6 +233,8 @@ def setUpClass(cls) -> None: cls.user_type = User.TYPE_VALUE_MAP["Staff"] cls.user = cls.create_user(cls.district) cls.super_user = cls.create_super_user(district=cls.district) + cls.local_body = cls.create_local_body(cls.district) + cls.ward = cls.create_ward() cls.facility = cls.create_facility(cls.district) cls.patient = cls.create_patient() cls.state_admin = cls.create_user( @@ -448,3 +452,21 @@ def create_patient_note( } data.update(kwargs) return PatientNotes.objects.create(**data) + + @classmethod + def create_ward(cls, local_body=None, name=None, number=10, **kwargs): + data = { + "local_body": local_body or cls.local_body, + "name": "Test Ward", + "number": number, + } + return Ward.objects.create(**data) + + @classmethod + def create_local_body(cls, district=None, name=None, body_type=1, **kwargs): + data = { + "district": district or cls.district, + "name": name or "Test Local Body", + "body_type": body_type, + } + return LocalBody.objects.create(**data)