diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000000..c75bc31fe7 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "pip" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "Weekly" diff --git a/aws/celery.json b/aws/celery.json index 52cbb8b8f7..988fb05e84 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -91,6 +91,26 @@ { "name": "USE_S3", "value": "True" + }, + { + "name": "ENABLE_ABDM", + "value": "True" + }, + { + "name": "ABDM_URL", + "value": "https://dev.abdm.gov.in" + }, + { + "name": "HEALTH_SERVICE_API_URL", + "value": "https://healthidsbx.abdm.gov.in/api" + }, + { + "name": "X_CM_ID", + "value": "sbx" + }, + { + "name": "FIDELIUS_URL", + "value": "https://fidelius.ohc.network" } ], "repositoryCredentials": { @@ -215,6 +235,14 @@ { "valueFrom": "/care/backend/HCX_IG_URL", "name": "HCX_IG_URL" + }, + { + "valueFrom": "/care/backend/ABDM_CLIENT_ID", + "name": "ABDM_CLIENT_ID" + }, + { + "valueFrom": "/care/backend/ABDM_CLIENT_SECRET", + "name": "ABDM_CLIENT_SECRET" } ], "name": "care-celery-beat" @@ -310,6 +338,26 @@ { "name": "USE_S3", "value": "True" + }, + { + "name": "ENABLE_ABDM", + "value": "True" + }, + { + "name": "ABDM_URL", + "value": "https://dev.abdm.gov.in" + }, + { + "name": "HEALTH_SERVICE_API_URL", + "value": "https://healthidsbx.abdm.gov.in/api" + }, + { + "name": "X_CM_ID", + "value": "sbx" + }, + { + "name": "FIDELIUS_URL", + "value": "https://fidelius.ohc.network" } ], "repositoryCredentials": { @@ -432,6 +480,14 @@ { "valueFrom": "/care/backend/HCX_IG_URL", "name": "HCX_IG_URL" + }, + { + "valueFrom": "/care/backend/ABDM_CLIENT_ID", + "name": "ABDM_CLIENT_ID" + }, + { + "valueFrom": "/care/backend/ABDM_CLIENT_SECRET", + "name": "ABDM_CLIENT_SECRET" } ], "name": "care-celery-worker" diff --git a/care/abdm/api/serializers/health_facility.py b/care/abdm/api/serializers/health_facility.py index b624047266..336f348584 100644 --- a/care/abdm/api/serializers/health_facility.py +++ b/care/abdm/api/serializers/health_facility.py @@ -5,6 +5,7 @@ class HealthFacilitySerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) + registered = serializers.BooleanField(read_only=True) class Meta: model = HealthFacility diff --git a/care/abdm/api/viewsets/health_facility.py b/care/abdm/api/viewsets/health_facility.py index 66acc4e741..5fb41b5b4d 100644 --- a/care/abdm/api/viewsets/health_facility.py +++ b/care/abdm/api/viewsets/health_facility.py @@ -1,4 +1,6 @@ -from django.shortcuts import get_object_or_404 +from celery import shared_task +from dry_rest_permissions.generics import DRYPermissions +from rest_framework.decorators import action from rest_framework.mixins import ( CreateModelMixin, ListModelMixin, @@ -15,6 +17,36 @@ from care.utils.queryset.facility import get_facility_queryset +@shared_task +def register_health_facility_as_service(facility_external_id): + health_facility = HealthFacility.objects.filter( + facility__external_id=facility_external_id + ).first() + + if not health_facility: + return False + + if health_facility.registered: + return True + + response = Bridge().add_update_service( + { + "id": health_facility.hf_id, + "name": health_facility.facility.name, + "type": "HIP", + "active": True, + "alias": ["CARE_HIP"], + } + ) + + if response.status_code == 200: + health_facility.registered = True + health_facility.save() + return True + + return False + + class HealthFacilityViewSet( GenericViewSet, CreateModelMixin, @@ -25,7 +57,7 @@ class HealthFacilityViewSet( serializer_class = HealthFacilitySerializer model = HealthFacility queryset = HealthFacility.objects.all() - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, DRYPermissions) lookup_field = "facility__external_id" def get_queryset(self): @@ -33,40 +65,17 @@ def get_queryset(self): facilities = get_facility_queryset(self.request.user) return queryset.filter(facility__in=facilities) - def get_facility(self, facility_external_id): - facilities = get_facility_queryset(self.request.user) - return get_object_or_404(facilities.filter(external_id=facility_external_id)) - - def link_health_facility(self, hf_id, facility_id): - facility = self.get_facility(facility_id) - return Bridge().add_update_service( - { - "id": hf_id, - "name": facility.name, - "type": "HIP", - "active": True, - "alias": ["CARE_HIP"], - } - ) - - def create(self, request, *args, **kwargs): - if ( - self.link_health_facility( - request.data["hf_id"], request.data["facility"] - ).status_code - == 200 - ): - return super().create(request, *args, **kwargs) + @action(detail=True, methods=["POST"]) + def register_service(self, request, facility__external_id): + registered = register_health_facility_as_service(facility__external_id) - return Response({"message": "Error linking health facility"}, status=400) + return Response({"registered": registered}) - def update(self, request, *args, **kwargs): - if ( - self.link_health_facility( - request.data["hf_id"], kwargs["facility__external_id"] - ).status_code - == 200 - ): - return super().update(request, *args, **kwargs) + def perform_create(self, serializer): + instance = serializer.save() + register_health_facility_as_service.delay(instance.facility.external_id) - return Response({"message": "Error linking health facility"}, status=400) + def perform_update(self, serializer): + serializer.validated_data["registered"] = False + instance = serializer.save() + register_health_facility_as_service.delay(instance.facility.external_id) diff --git a/care/abdm/migrations/0010_healthfacility_registered.py b/care/abdm/migrations/0010_healthfacility_registered.py new file mode 100644 index 0000000000..5a5d753925 --- /dev/null +++ b/care/abdm/migrations/0010_healthfacility_registered.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2023-09-05 06:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("abdm", "0009_healthfacility"), + ] + + operations = [ + migrations.AddField( + model_name="healthfacility", + name="registered", + field=models.BooleanField(default=False), + ), + ] diff --git a/care/abdm/models.py b/care/abdm/models.py index eafd5af1f7..e02bfad1b6 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -4,6 +4,7 @@ from django.db import models +from care.abdm.permissions import HealthFacilityPermissions from care.utils.models.base import BaseModel @@ -37,8 +38,9 @@ def __str__(self): return self.abha_number -class HealthFacility(BaseModel): +class HealthFacility(BaseModel, HealthFacilityPermissions): hf_id = models.CharField(max_length=50, unique=True) + registered = models.BooleanField(default=False) facility = models.OneToOneField( "facility.Facility", on_delete=models.PROTECT, to_field="external_id" ) diff --git a/care/abdm/permissions.py b/care/abdm/permissions.py new file mode 100644 index 0000000000..f1ccd0045e --- /dev/null +++ b/care/abdm/permissions.py @@ -0,0 +1,29 @@ +from care.facility.models.mixins.permissions.base import BasePermissionMixin +from care.users.models import User + + +class HealthFacilityPermissions(BasePermissionMixin): + """ + Permissions for HealthFacilityViewSet + """ + + def has_object_read_permission(self, request): + return self.facility.has_object_read_permission(request) + + def has_object_write_permission(self, request): + allowed_user_types = [ + User.TYPE_VALUE_MAP["WardAdmin"], + User.TYPE_VALUE_MAP["LocalBodyAdmin"], + User.TYPE_VALUE_MAP["DistrictAdmin"], + User.TYPE_VALUE_MAP["StateAdmin"], + ] + return request.user.is_superuser or ( + request.user.user_type in allowed_user_types + and self.facility.has_object_write_permission(request) + ) + + def has_object_update_permission(self, request): + return self.has_object_write_permission(request) + + def has_object_destroy_permission(self, request): + return self.has_object_write_permission(request) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c14b723ab1..214257bc02 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -88,7 +88,11 @@ def _practioner(self): id = str(uuid()) name = ( - self.consultation.verified_by + ( + self.consultation.verified_by + and f"{self.consultation.verified_by.first_name} {self.consultation.verified_by.last_name}" + ) + or self.consultation.deprecated_verified_by or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}" ) self._practitioner_profile = Practitioner( diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 4178ab75cd..ae2771b35c 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -67,11 +67,15 @@ class PatientConsultationSerializer(serializers.ModelSerializer): 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 ) + verified_by_object = UserBaseMinimumSerializer(source="verified_by", read_only=True) + verified_by = serializers.PrimaryKeyRelatedField( + queryset=User.objects.all(), required=False, allow_null=True + ) + discharge_reason = serializers.ChoiceField( choices=DISCHARGE_REASON_CHOICES, read_only=True, required=False ) @@ -132,6 +136,7 @@ class Meta: "last_edited_by", "created_by", "kasp_enabled_date", + "deprecated_verified_by", ) exclude = ("deleted", "external_id") @@ -313,6 +318,34 @@ def validate(self, attrs): validated = super().validate(attrs) # TODO Add Bed Authorisation Validation + if ( + "suggestion" in validated + and validated["suggestion"] != SuggestionChoices.DD + ): + if "verified_by" not in validated: + raise ValidationError( + { + "verified_by": [ + "This field is required as the suggestion is not 'Declared Death'" + ] + } + ) + if not validated["verified_by"].user_type == User.TYPE_VALUE_MAP["Doctor"]: + raise ValidationError("Only Doctors can verify a Consultation") + + facility = ( + self.instance + and self.instance.facility + or validated["patient"].facility + ) + if ( + validated["verified_by"].home_facility + and validated["verified_by"].home_facility != facility + ): + raise ValidationError( + "Home Facility of the Doctor must be the same as the Consultation Facility" + ) + if "suggestion" in validated: if validated["suggestion"] is SuggestionChoices.R: if not validated.get("referred_to") and not validated.get( @@ -361,10 +394,14 @@ def validate(self, attrs): ) from care.facility.static_data.icd11 import ICDDiseases + final_diagnosis = [] + provisional_diagnosis = [] + if "icd11_diagnoses" in validated: for diagnosis in validated["icd11_diagnoses"]: try: ICDDiseases.by.id[diagnosis] + final_diagnosis.append(diagnosis) except BaseException: raise ValidationError( { @@ -378,6 +415,7 @@ def validate(self, attrs): for diagnosis in validated["icd11_provisional_diagnoses"]: try: ICDDiseases.by.id[diagnosis] + provisional_diagnosis.append(diagnosis) except BaseException: raise ValidationError( { @@ -386,6 +424,41 @@ def validate(self, attrs): ] } ) + + if ( + "icd11_principal_diagnosis" in validated + and validated.get("suggestion") != SuggestionChoices.DD + ): + if len(final_diagnosis): + if validated["icd11_principal_diagnosis"] not in final_diagnosis: + raise ValidationError( + { + "icd11_principal_diagnosis": [ + "Principal Diagnosis must be one of the Final Diagnosis" + ] + } + ) + elif len(provisional_diagnosis): + if validated["icd11_principal_diagnosis"] not in provisional_diagnosis: + raise ValidationError( + { + "icd11_principal_diagnosis": [ + "Principal Diagnosis must be one of the Provisional Diagnosis" + ] + } + ) + else: + raise ValidationError( + { + "icd11_diagnoses": [ + "Atleast one diagnosis is required for final diagnosis" + ], + "icd11_provisional_diagnoses": [ + "Atleast one diagnosis is required for provisional diagnosis" + ], + } + ) + return validated @@ -525,10 +598,11 @@ def create(self, validated_data): class PatientConsultationIDSerializer(serializers.ModelSerializer): consultation_id = serializers.UUIDField(source="external_id", read_only=True) patient_id = serializers.UUIDField(source="patient.external_id", read_only=True) + bed_id = serializers.UUIDField(source="current_bed.bed.external_id", read_only=True) class Meta: model = PatientConsultation - fields = ("consultation_id", "patient_id") + fields = ("consultation_id", "patient_id", "bed_id") class EmailDischargeSummarySerializer(serializers.Serializer): diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index 89f5f61d9e..4af84080ea 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -1,4 +1,5 @@ from django.shortcuts import get_object_or_404 +from django.utils import timezone from rest_framework import serializers from care.facility.models import MedibaseMedicine, MedicineAdministration, Prescription @@ -57,6 +58,23 @@ def validate(self, attrs): MedibaseMedicine, external_id=attrs["medicine"] ) + if not self.instance: + if Prescription.objects.filter( + consultation__external_id=self.context["request"].parser_context[ + "kwargs" + ]["consultation_external_id"], + medicine=attrs["medicine"], + discontinued=False, + ).exists(): + raise serializers.ValidationError( + { + "medicine": ( + "This medicine is already prescribed to this patient. " + "Please discontinue the existing prescription to prescribe again." + ) + } + ) + if attrs.get("is_prn"): if not attrs.get("indicator"): raise serializers.ValidationError( @@ -77,6 +95,17 @@ class MedicineAdministrationSerializer(serializers.ModelSerializer): administered_by = UserBaseMinimumSerializer(read_only=True) prescription = PrescriptionSerializer(read_only=True) + def validate_administered_date(self, value): + if value > timezone.now(): + raise serializers.ValidationError( + "Administered Date cannot be in the future." + ) + if self.context["prescription"].created_date > value: + raise serializers.ValidationError( + "Administered Date cannot be before Prescription Date." + ) + return value + class Meta: model = MedicineAdministration exclude = ("deleted",) diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py index 2d6ea85329..578f326849 100644 --- a/care/facility/api/viewsets/facility_users.py +++ b/care/facility/api/viewsets/facility_users.py @@ -1,6 +1,7 @@ from django.db.models import Prefetch from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema, extend_schema_view +from rest_framework import filters as drf_filters from rest_framework import mixins from rest_framework.exceptions import ValidationError from rest_framework.permissions import IsAuthenticated @@ -28,7 +29,11 @@ class FacilityUserViewSet(GenericViewSet, mixins.ListModelMixin): filterset_class = UserFilter queryset = User.objects.all() permission_classes = [IsAuthenticated] - filter_backends = [filters.DjangoFilterBackend] + filter_backends = [ + filters.DjangoFilterBackend, + drf_filters.SearchFilter, + ] + search_fields = ["first_name", "last_name", "username"] def get_queryset(self): try: diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index 3231a9bc0d..c18d353bd5 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -103,7 +103,7 @@ def discontinue(self, request, *args, **kwargs): "discontinued_reason", None ) prescription_obj.save() - return Response({}, status=status.HTTP_201_CREATED) + return Response({}, status=status.HTTP_200_OK) @extend_schema(tags=["prescriptions"]) @action( @@ -113,7 +113,14 @@ def discontinue(self, request, *args, **kwargs): ) def administer(self, request, *args, **kwargs): prescription_obj = self.get_object() - serializer = MedicineAdministrationSerializer(data=request.data) + if prescription_obj.discontinued: + return Response( + {"error": "Administering discontinued prescriptions is not allowed"}, + status=status.HTTP_400_BAD_REQUEST, + ) + serializer = MedicineAdministrationSerializer( + data=request.data, context={"prescription": prescription_obj} + ) serializer.is_valid(raise_exception=True) serializer.save(prescription=prescription_obj, administered_by=request.user) return Response(serializer.data, status=status.HTTP_201_CREATED) diff --git a/care/facility/migrations/0383_patientconsultation_icd11_principal_diagnosis.py b/care/facility/migrations/0383_patientconsultation_icd11_principal_diagnosis.py new file mode 100644 index 0000000000..75b53735d9 --- /dev/null +++ b/care/facility/migrations/0383_patientconsultation_icd11_principal_diagnosis.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.2 on 2023-09-04 09:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0382_assetservice_remove_asset_last_serviced_on_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="patientconsultation", + name="icd11_principal_diagnosis", + field=models.CharField(blank=True, default="", max_length=100, null=True), + ), + ] diff --git a/care/facility/migrations/0384_patientconsultation_verified_by.py b/care/facility/migrations/0384_patientconsultation_verified_by.py new file mode 100644 index 0000000000..dbb85f5795 --- /dev/null +++ b/care/facility/migrations/0384_patientconsultation_verified_by.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.2 on 2023-08-31 04:31 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0383_patientconsultation_icd11_principal_diagnosis"), + ] + + operations = [ + migrations.RenameField( + model_name="patientconsultation", + old_name="verified_by", + new_name="deprecated_verified_by", + ), + migrations.AddField( + model_name="patientconsultation", + name="verified_by", + field=models.ForeignKey( + null=True, + blank=False, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/care/facility/migrations/0385_alter_patientconsultation_verified_by.py b/care/facility/migrations/0385_alter_patientconsultation_verified_by.py new file mode 100644 index 0000000000..f6fc284274 --- /dev/null +++ b/care/facility/migrations/0385_alter_patientconsultation_verified_by.py @@ -0,0 +1,25 @@ +# Generated by Django 4.2.2 on 2023-09-08 15:23 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0384_patientconsultation_verified_by"), + ] + + operations = [ + migrations.AlterField( + model_name="patientconsultation", + name="verified_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index ed5f875ab7..382499996a 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -64,6 +64,9 @@ class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin): icd11_diagnoses = ArrayField( models.CharField(max_length=100), default=list, blank=True, null=True ) + icd11_principal_diagnosis = models.CharField( + max_length=100, default="", blank=True, null=True + ) symptoms = MultiSelectField( choices=SYMPTOM_CHOICES, default=1, @@ -139,7 +142,10 @@ class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin): related_name="patient_assigned_to", ) - verified_by = models.TextField(default="", null=True, blank=True) + deprecated_verified_by = models.TextField(default="", null=True, blank=True) + verified_by = models.ForeignKey( + User, on_delete=models.SET_NULL, null=True, blank=True + ) created_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name="created_user" 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..e3bcff02cc --- /dev/null +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -0,0 +1,58 @@ +from django.utils import timezone +from rest_framework import status + +from care.facility.models import MedibaseMedicine, Prescription +from care.utils.tests.test_base import TestBase + + +class MedicineAdministrationsApiTestCase(TestBase): + def setUp(self) -> None: + super().setUp() + self.normal_prescription = self.create_prescription() + + def create_prescription(self, **kwargs): + data = { + "consultation": self.create_consultation(), + "medicine": MedibaseMedicine.objects.first(), + "prescription_type": "REGULAR", + "dosage": "1 mg", + "frequency": "OD", + "is_prn": False, + } + return Prescription.objects.create( + **{**data, **kwargs, "prescribed_by": self.user} + ) + + def test_administer(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"}, + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + 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) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 48de90f695..5e11c5743c 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -7,7 +7,7 @@ from care.facility.api.viewsets.facility_users import FacilityUserViewSet from care.facility.api.viewsets.patient_consultation import PatientConsultationViewSet -from care.facility.models.facility import Facility +from care.facility.models import Facility, User from care.facility.models.patient_consultation import ( CATEGORY_CHOICES, PatientConsultation, @@ -58,17 +58,25 @@ def test_get_queryset_with_prefetching(self): class TestPatientConsultation(TestBase, TestClassMixin, APITestCase): - default_data = { - "symptoms": [1], - "category": CATEGORY_CHOICES[0][0], - "examination_details": "examination_details", - "history_of_present_illness": "history_of_present_illness", - "treatment_plan": "treatment_plan", - "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], - } + def get_default_data(self): + return { + "symptoms": [1], + "category": CATEGORY_CHOICES[0][0], + "examination_details": "examination_details", + "history_of_present_illness": "history_of_present_illness", + "treatment_plan": "treatment_plan", + "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], + "verified_by": self.doctor.id, + } def setUp(self): self.factory = APIRequestFactory() + self.doctor = self.create_user( + username="doctor1", + district=self.district, + user_type=User.TYPE_VALUE_MAP["Doctor"], + ) + self.consultation = self.create_consultation( suggestion="A", admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), @@ -76,7 +84,7 @@ def setUp(self): def create_admission_consultation(self, patient=None, **kwargs): patient = patient or self.create_patient(facility_id=self.facility.id) - data = self.default_data.copy() + data = self.get_default_data() kwargs.update( { "patient": patient.external_id, @@ -93,6 +101,15 @@ def create_admission_consultation(self, patient=None, **kwargs): ) return PatientConsultation.objects.get(external_id=res.data["id"]) + def update_consultation(self, consultation, **kwargs): + return self.new_request( + (self.get_url(consultation), kwargs, "json"), + {"patch": "partial_update"}, + PatientConsultationViewSet, + self.state_admin, + {"external_id": consultation.external_id}, + ) + def get_url(self, consultation=None): if consultation: return f"/api/v1/consultation/{consultation.external_id}" @@ -107,6 +124,12 @@ def discharge(self, consultation, **kwargs): {"external_id": consultation.external_id}, ) + def test_create_consultation_verified_by_invalid_user(self): + res = self.update_consultation( + self.consultation, verified_by=self.state_admin.id, suggestion="A" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + def test_discharge_as_recovered_preadmission(self): consultation = self.create_admission_consultation( suggestion="A", diff --git a/care/facility/tests/test_prescriptions_api.py b/care/facility/tests/test_prescriptions_api.py new file mode 100644 index 0000000000..869120d114 --- /dev/null +++ b/care/facility/tests/test_prescriptions_api.py @@ -0,0 +1,61 @@ +from rest_framework import status + +from care.facility.models import MedibaseMedicine +from care.utils.tests.test_base import TestBase + + +class PrescriptionsApiTestCase(TestBase): + def setUp(self) -> None: + super().setUp() + self.medicine = MedibaseMedicine.objects.first() + + self.normal_prescription_data = { + "medicine": self.medicine.external_id, + "prescription_type": "REGULAR", + "dosage": "1 mg", + "frequency": "OD", + "is_prn": False, + } + + def test_create_normal_prescription(self): + consultation = self.create_consultation() + response = self.client.post( + f"/api/v1/consultation/{consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_prescribe_duplicate_active_medicine_and_discontinue(self): + """ + 1. Creates a prescription with Medicine A + 2. Attempts to create another prescription with Medicine A (expecting failure) + 3. Discontinues the first prescription + 4. Re-attempts to create another prescription with Medicine A (expecting success) + """ + consultation = self.create_consultation() + res = self.client.post( + f"/api/v1/consultation/{consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + discontinue_prescription_id = res.data["id"] + + res = self.client.post( + f"/api/v1/consultation/{consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + res = self.client.post( + f"/api/v1/consultation/{consultation.external_id}/prescriptions/{discontinue_prescription_id}/discontinue/", + { + "discontinued_reason": "Test Reason", + }, + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + res = self.client.post( + f"/api/v1/consultation/{consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index bf5f2ed27d..072ca43266 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -56,6 +56,11 @@ def get_discharge_summary_data(consultation: PatientConsultation): provisional_diagnosis = get_icd11_diagnoses_objects_by_ids( consultation.icd11_provisional_diagnoses ) + principal_diagnosis = get_icd11_diagnoses_objects_by_ids( + [consultation.icd11_principal_diagnosis] + if consultation.icd11_principal_diagnosis + else [] + ) investigations = InvestigationValue.objects.filter( Q(consultation=consultation.id) & (Q(value__isnull=False) | Q(notes__isnull=False)) @@ -94,6 +99,7 @@ def get_discharge_summary_data(consultation: PatientConsultation): "hcx": hcx, "diagnosis": diagnosis, "provisional_diagnosis": provisional_diagnosis, + "principal_diagnosis": principal_diagnosis, "consultation": consultation, "prescriptions": prescriptions, "prn_prescriptions": prn_prescriptions, diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html index fca8b51c1a..7e3b83e09b 100644 --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ b/care/templates/reports/patient_discharge_summary_pdf.html @@ -247,6 +247,39 @@

{% endif %} + {% if principal_diagnosis %} +

+ Principal Diagnosis (as per ICD-11 recommended by WHO): +

+
+ + + + + + + + + {% for disease in principal_diagnosis %} + + + + + {% endfor %} + +
+ ID + + Name +
+ {{disease.id}} + + {{disease.label}} +
+
+ {% endif %} + {% if medical_history %}

@@ -912,7 +945,11 @@

Verified By
- {{consultation.verified_by|linebreaks}} + {% if consultation.verified_by %} + {{ consultation.verified_by.first_name }} {{ consultation.verified_by.last_name }} + {% else %} + - + {% endif %}
diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index bd0faa6ecb..9d590b546d 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -55,6 +55,9 @@ class UserFilterSet(filters.FilterSet): ) last_login = filters.DateFromToRangeFilter(field_name="last_login") district_id = filters.NumberFilter(field_name="district_id", lookup_expr="exact") + home_facility = filters.UUIDFilter( + field_name="home_facility__external_id", lookup_expr="exact" + ) def get_user_type( self, diff --git a/config/api_router.py b/config/api_router.py index 6c7415dbdb..33b612d8d1 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -223,9 +223,9 @@ # ABDM endpoints if settings.ENABLE_ABDM: router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") - router.register( - "abdm/health_facility", HealthFacilityViewSet, basename="abdm-healthfacility" - ) +router.register( + "abdm/health_facility", HealthFacilityViewSet, basename="abdm-healthfacility" +) app_name = "api" urlpatterns = [ diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 6f7cb4146c..c4a05f5ee4 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -199,7 +199,6 @@ "is_telemedicine": false, "last_updated_by_telemedicine": false, "assigned_to": null, - "verified_by": "", "created_by": 2, "last_edited_by": 2, "last_daily_round": null,