From bf7ff291e8a6cd1b2b44d53a9e5a8e7f283b8387 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 4 Mar 2024 18:46:23 +0530 Subject: [PATCH 01/11] Adds authz for Prescription & Medicine Administrations (#1704) * Adds authz for Prescription & MAR fixes #1695 * adds missing `DRYPermissions` in viewsets * fix missing object write permission --------- Co-authored-by: Vignesh Hari Co-authored-by: Aakash Singh --- care/facility/api/viewsets/prescription.py | 5 +++-- care/facility/models/prescription.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index e31359ac85..779287ef7d 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -2,6 +2,7 @@ from django.utils import timezone from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema +from dry_rest_permissions.generics import DRYPermissions from redis_om import FindQuery from rest_framework import mixins, status from rest_framework.decorators import action @@ -50,7 +51,7 @@ class MedicineAdministrationViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, GenericViewSet ): serializer_class = MedicineAdministrationSerializer - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, DRYPermissions) queryset = MedicineAdministration.objects.all().order_by("-created_date") lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) @@ -94,7 +95,7 @@ class ConsultationPrescriptionViewSet( GenericViewSet, ): serializer_class = PrescriptionSerializer - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, DRYPermissions) queryset = Prescription.objects.all().order_by("-created_date") lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index d7f70521a1..5c34ab0e18 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -5,6 +5,9 @@ from django.db.models import JSONField from django.utils import timezone +from care.facility.models.mixins.permissions.patient import ( + ConsultationRelatedPermissionMixin, +) from care.facility.models.patient_consultation import PatientConsultation from care.utils.models.base import BaseModel from care.utils.models.validators import dosage_validator @@ -73,7 +76,7 @@ def __str__(self): return " - ".join(filter(None, [self.name, self.generic, self.company])) -class Prescription(BaseModel): +class Prescription(BaseModel, ConsultationRelatedPermissionMixin): consultation = models.ForeignKey( PatientConsultation, on_delete=models.PROTECT, @@ -148,11 +151,14 @@ def save(self, *args, **kwargs) -> None: def medicine_name(self): return str(self.medicine) if self.medicine else self.medicine_old + def has_object_write_permission(self, request): + return ConsultationRelatedPermissionMixin.has_write_permission(request) + def __str__(self): return self.medicine + " - " + self.consultation.patient.name -class MedicineAdministration(BaseModel): +class MedicineAdministration(BaseModel, ConsultationRelatedPermissionMixin): prescription = models.ForeignKey( Prescription, on_delete=models.PROTECT, @@ -181,6 +187,12 @@ def __str__(self): + self.prescription.consultation.patient.name ) + def get_related_consultation(self): + return self.prescription.consultation + + def has_object_write_permission(self, request): + return ConsultationRelatedPermissionMixin.has_write_permission(request) + def validate(self) -> None: if self.prescription.discontinued: raise ValidationError( From 48f1d8900c402009a2fc5edf93cc1a912899b294 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 4 Mar 2024 18:47:37 +0530 Subject: [PATCH 02/11] Adds view for listing discharged patients of a facility (#1866) * Adds view for listing discharged patients of a facility * return distinct patient records * Update patient registration permissions * Update permissions and add test cases * fix patient facility not being updated on readmission and other bugs * sort imports * update patient filter queryset to be distinct on `id` --------- Co-authored-by: Vignesh Hari --- care/facility/api/viewsets/patient.py | 42 ++++- .../api/viewsets/patient_consultation.py | 9 +- .../models/mixins/permissions/patient.py | 1 + .../test_patient_and_consultation_access.py | 159 ++++++++++++++++++ .../tests/test_upload_external_result.py | 3 +- care/utils/tests/test_utils.py | 3 +- config/api_router.py | 4 + 7 files changed, 213 insertions(+), 8 deletions(-) create mode 100644 care/facility/tests/test_patient_and_consultation_access.py diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 517cb3dc51..e6fc0e5c0e 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -6,6 +6,7 @@ from django.contrib.postgres.search import TrigramSimilarity from django.db import models from django.db.models import Case, OuterRef, Q, Subquery, When +from django.db.models.query import QuerySet from django_filters import rest_framework as filters from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view @@ -272,10 +273,11 @@ def filter_queryset(self, request, queryset, view): elif view.action != "transfer": allowed_facilities = get_accessible_facilities(request.user) q_filters = Q(facility__id__in=allowed_facilities) + if view.action == "retrieve": + q_filters |= Q(consultations__facility__id__in=allowed_facilities) q_filters |= Q(last_consultation__assigned_to=request.user) q_filters |= Q(assigned_to=request.user) - queryset = queryset.filter(q_filters) - + queryset = queryset.filter(q_filters).distinct("id") return queryset def filter_list_queryset(self, request, queryset, view): @@ -509,6 +511,42 @@ def transfer(self, request, *args, **kwargs): return Response(data=response_serializer.data, status=status.HTTP_200_OK) +class FacilityDischargedPatientFilterSet(filters.FilterSet): + name = filters.CharFilter(field_name="name", lookup_expr="icontains") + + +@extend_schema_view(tags=["patient"]) +class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): + permission_classes = (IsAuthenticated, DRYPermissions) + lookup_field = "external_id" + serializer_class = PatientListSerializer + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = FacilityDischargedPatientFilterSet + queryset = PatientRegistration.objects.select_related( + "local_body", + "district", + "state", + "ward", + "assigned_to", + "facility", + "facility__ward", + "facility__local_body", + "facility__district", + "facility__state", + "last_consultation", + "last_consultation__assigned_to", + "last_edited", + "created_by", + ) + + def get_queryset(self) -> QuerySet: + qs = super().get_queryset() + return qs.filter( + Q(consultations__facility__external_id=self.kwargs["facility_external_id"]) + & Q(consultations__discharge_date__isnull=False) + ).distinct() + + class FacilityPatientStatsHistoryFilterSet(filters.FilterSet): entry_date = filters.DateFromToRangeFilter(field_name="entry_date") diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index d147ea611d..df5d207c3f 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -95,9 +95,12 @@ def get_queryset(self): patient__facility__district=self.request.user.district ) allowed_facilities = get_accessible_facilities(self.request.user) - applied_filters = Q(patient__facility__id__in=allowed_facilities) - applied_filters |= Q(assigned_to=self.request.user) - applied_filters |= Q(patient__assigned_to=self.request.user) + # A user should be able to see all the consultations of a patient if the patient is active in an accessible facility + applied_filters = Q( + Q(patient__is_active=True) & Q(patient__facility__id__in=allowed_facilities) + ) + # A user should be able to see all consultations part of their home facility + applied_filters |= Q(facility=self.request.user.home_facility) return self.queryset.filter(applied_filters) @extend_schema(tags=["consultation"]) diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index 9cbd335df2..b814cde15a 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -28,6 +28,7 @@ def has_object_read_permission(self, request): or ( self.facility and request.user in self.facility.users.all() + or self.consultations.filter(facility__users=request.user).exists() or doctor_allowed ) or ( diff --git a/care/facility/tests/test_patient_and_consultation_access.py b/care/facility/tests/test_patient_and_consultation_access.py new file mode 100644 index 0000000000..e28eca466d --- /dev/null +++ b/care/facility/tests/test_patient_and_consultation_access.py @@ -0,0 +1,159 @@ +import datetime + +from django.utils.timezone import make_aware +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models.patient_base import NewDischargeReasonEnum +from care.utils.tests.test_utils import TestUtils + + +class TestPatientConsultationAccess(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) + # Variables named in the perspective of reader being a member of the home facility + cls.home_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body, name="Home Facility" + ) + cls.remote_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body, name="Remote Facility" + ) + cls.facility = cls.remote_facility + cls.unrelated_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body, name="Unreleated Facility" + ) + cls.home_doctor = cls.create_user( + "home-doctor", + cls.district, + home_facility=cls.home_facility, + user_type=15, + ) + cls.user = cls.home_doctor + cls.remote_doctor = cls.create_user( + "remote-doctor", + cls.district, + home_facility=cls.remote_facility, + user_type=15, + ) + # remote doctor has access to the my home facility + cls.link_user_with_facility( + cls.remote_doctor, cls.home_facility, created_by=cls.super_user + ) + # this doctor has no idea about whats going to happen. + cls.unrelated_doctor = cls.create_user( + "unrelated-doctor", + cls.district, + home_facility=cls.unrelated_facility, + user_type=15, + ) + cls.patient = cls.create_patient(cls.district, cls.remote_facility) + + def retrieve_patient(self, patient): + return self.client.get(f"/api/v1/patient/{patient.external_id}/") + + def retieve_patient_consultations(self, patient): + return self.client.get( + "/api/v1/consultation/", data={"patient": patient.external_id} + ) + + def retrieve_discharged_patients_of_facility(self, facility): + return self.client.get( + f"/api/v1/facility/{facility.external_id}/discharged_patients/" + ) + + def discharge(self, consultation, **kwargs): + return self.client.post( + f"/api/v1/consultation/{consultation.external_id}/discharge_patient/", + data={ + "new_discharge_reason": NewDischargeReasonEnum.RECOVERED, + **kwargs, + }, + format="json", + ) + + def test_patient_consultation_access(self): + # In this test, a patient is admitted to a remote facility and then later admitted to a home facility. + + # Admit patient to the remote facility + remote_consultation = self.create_consultation( + self.patient, + self.remote_facility, + suggestion="A", + encounter_date=make_aware(datetime.datetime(2024, 1, 1)), + ) + + # Permission Check: Remote doctor must have access to the patient details and the current consultation + self.client.force_authenticate(user=self.remote_doctor) + res = self.retrieve_discharged_patients_of_facility(self.remote_facility) + self.assertNotContains(res, self.patient.external_id) + res = self.retrieve_patient(self.patient) + self.assertEqual(res.status_code, status.HTTP_200_OK) + res = self.retieve_patient_consultations(self.patient) + self.assertContains(res, remote_consultation.external_id) + + # Home doctor must NOT have access to this facility. + # Also should not have access to the patient or consultation at present. + self.client.force_authenticate(user=self.home_doctor) + res = self.retrieve_patient(self.patient) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + res = self.retieve_patient_consultations(self.patient) + self.assertNotContains(res, remote_consultation.external_id) + + # Unrelated doctor must NOT have access + self.client.force_authenticate(user=self.unrelated_doctor) + res = self.retrieve_patient(self.patient) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + res = self.retieve_patient_consultations(self.patient) + self.assertNotContains(res, remote_consultation.external_id) + + # Discharge the patient from remote facility. + self.client.force_authenticate(user=self.remote_doctor) + res = self.discharge(remote_consultation, discharge_date="2024-01-02T00:00:00Z") + self.assertEqual(res.status_code, status.HTTP_200_OK) + + # Admit to home facility + self.client.force_authenticate(user=self.home_doctor) + home_consultation = self.create_consultation( + self.patient, + self.home_facility, + suggestion="A", + encounter_date=make_aware(datetime.datetime(2024, 1, 3)), + ) + + # Remote doctor must have access to patient and remote_consultation and home_consultation + self.client.force_authenticate(user=self.remote_doctor) + res = self.retrieve_discharged_patients_of_facility(self.remote_facility) + self.assertContains(res, self.patient.external_id) + res = self.retrieve_patient(self.patient) + self.assertEqual(res.status_code, status.HTTP_200_OK) + res = self.retieve_patient_consultations(self.patient) + self.assertContains(res, remote_consultation.external_id) + self.assertContains(res, home_consultation.external_id) + + # Home doctor should have access to the patient and all past and current consultation + self.client.force_authenticate(user=self.home_doctor) + res = self.retrieve_patient(self.patient) + self.assertEqual(res.status_code, status.HTTP_200_OK) + res = self.retieve_patient_consultations(self.patient) + self.assertContains(res, home_consultation.external_id) + self.assertContains(res, remote_consultation.external_id) + + # Discharge the patient from home facility. + res = self.discharge(home_consultation, discharge_date="2024-01-04T00:00:00Z") + self.assertEqual(res.status_code, status.HTTP_200_OK) + res = self.retrieve_discharged_patients_of_facility(self.home_facility) + self.assertContains(res, self.patient.external_id) + self.assertContains(res, home_consultation.external_id) + self.assertNotContains(res, remote_consultation.external_id) + + # Now remote doctor should have access to the patient and the remote consultation, but not home consultation + self.client.force_authenticate(user=self.remote_doctor) + res = self.retrieve_discharged_patients_of_facility(self.remote_facility) + self.assertContains(res, self.patient.external_id) + res = self.retieve_patient_consultations(self.patient) + self.assertContains(res, remote_consultation.external_id) + self.assertNotContains(res, home_consultation.external_id) diff --git a/care/facility/tests/test_upload_external_result.py b/care/facility/tests/test_upload_external_result.py index 0ea4e3ed36..fe4f0dc918 100644 --- a/care/facility/tests/test_upload_external_result.py +++ b/care/facility/tests/test_upload_external_result.py @@ -67,7 +67,7 @@ def test_same_district_upload(self): "gender": "m", "mobile_number": 8888888888, "address": "Upload test address", - "ward": self.ward.id, + "ward": self.ward.number, "local_body": str(self.local_body.name), "local_body_type": "municipality", "source": "Secondary contact aparna", @@ -87,4 +87,5 @@ def test_same_district_upload(self): response = self.client.post( "/api/v1/external_result/bulk_upsert/", sample_data, format="json" ) + print(response.data) self.assertEqual(response.status_code, status.HTTP_202_ACCEPTED) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index c71622b2cf..faaf046b37 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -11,7 +11,6 @@ from care.facility.models import ( CATEGORY_CHOICES, - COVID_CATEGORY_CHOICES, DISEASE_CHOICES_MAP, SYMPTOM_CHOICES, Disease, @@ -307,7 +306,6 @@ def get_consultation_data(cls): "symptoms": [SYMPTOM_CHOICES[0][0], SYMPTOM_CHOICES[1][0]], "other_symptoms": "No other symptoms", "symptoms_onset_date": make_aware(datetime(2020, 4, 7, 15, 30)), - "deprecated_covid_category": COVID_CATEGORY_CHOICES[0][0], "category": CATEGORY_CHOICES[0][0], "examination_details": "examination_details", "history_of_present_illness": "history_of_present_illness", @@ -341,6 +339,7 @@ def create_consultation( data.update(kwargs) consultation = PatientConsultation.objects.create(**data) patient.last_consultation = consultation + patient.facility = consultation.facility patient.save() return consultation diff --git a/config/api_router.py b/config/api_router.py index 0433b3c791..4e17320300 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -47,6 +47,7 @@ ) from care.facility.api.viewsets.notification import NotificationViewSet from care.facility.api.viewsets.patient import ( + FacilityDischargedPatientViewSet, FacilityPatientStatsHistoryViewSet, PatientNotesEditViewSet, PatientNotesViewSet, @@ -194,6 +195,9 @@ facility_location_nested_router.register(r"availability", AvailabilityViewSet) facility_nested_router.register(r"patient_asset_beds", PatientAssetBedViewSet) +facility_nested_router.register( + r"discharged_patients", FacilityDischargedPatientViewSet +) # facility_nested_router.register("burn_rate", FacilityInventoryBurnRateViewSet) router.register("asset", AssetViewSet) From 8ca6e551c2c5fea3234da47017bcebed8b7a5583 Mon Sep 17 00:00:00 2001 From: Dhruv Goyal <91484101+dhruv-goyal-10@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:55:17 +0530 Subject: [PATCH 03/11] Fixed N+1 queries issue in /api/v1/bed/ (#1908) * Fixed N+1 queries issue in /api/v1/bed/ * Feat: PR review changes for issue #1338 Removed is_occupied property from Bed model as it is not used anywhere Annotated is_occupied field in the queryset instead of _is_occupied * Feat: PR review changes Changed is_occupied from SerializerMethodField to Boolean Field * Feat: PR Review Changes Annotated is_occupied field irrespective of action * Added the test case for listing beds API * add comment explaining number of queries --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/bed.py | 13 ++++++++-- care/facility/models/bed.py | 4 --- care/facility/tests/test_bed_api.py | 38 +++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 6 deletions(-) create mode 100644 care/facility/tests/test_bed_api.py diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 8bce2cb17b..cdda8af3c1 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -1,6 +1,6 @@ from django.core.exceptions import ValidationError as DjangoValidationError from django.db import IntegrityError, transaction -from django.db.models import OuterRef, Subquery +from django.db.models import Exists, OuterRef, Subquery 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 @@ -50,7 +50,7 @@ class BedViewSet( ): queryset = ( Bed.objects.all() - .select_related("facility", "location") + .select_related("facility", "location", "location__facility") .order_by("-created_date") ) serializer_class = BedSerializer @@ -63,6 +63,15 @@ class BedViewSet( def get_queryset(self): user = self.request.user queryset = self.queryset + + queryset = queryset.annotate( + is_occupied=Exists( + ConsultationBed.objects.filter( + bed__id=OuterRef("id"), end_date__isnull=True + ) + ) + ) + if user.is_superuser: pass elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index d701dba41a..1f6ae30777 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -40,10 +40,6 @@ class Meta: ) ] - @property - def is_occupied(self) -> bool: - return ConsultationBed.objects.filter(bed=self, end_date__isnull=True).exists() - def __str__(self): return self.name diff --git a/care/facility/tests/test_bed_api.py b/care/facility/tests/test_bed_api.py new file mode 100644 index 0000000000..973db41a96 --- /dev/null +++ b/care/facility/tests/test_bed_api.py @@ -0,0 +1,38 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models import Bed +from care.utils.tests.test_utils import TestUtils + + +class BedViewSetTestCase(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.asset_location = cls.create_asset_location(cls.facility) + cls.user = cls.create_user("staff", 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.bed1 = Bed.objects.create( + name="bed1", location=self.asset_location, facility=self.facility + ) + self.bed2 = Bed.objects.create( + name="bed2", location=self.asset_location, facility=self.facility + ) + self.bed3 = Bed.objects.create( + name="bed3", location=self.asset_location, facility=self.facility + ) + + def test_list_beds(self): + # includes 3 queries for auth and 1 for pagination count + with self.assertNumQueries(5): + response = self.client.get("/api/v1/bed/") + self.assertEqual(response.status_code, status.HTTP_200_OK) From 90fae78a3c08923f0b4a924ff33ba74813134ca5 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 4 Mar 2024 18:56:38 +0530 Subject: [PATCH 04/11] Fix failing test cases and shuffle case before run (#1924) fix failing test cases and shuffle case before run --- Makefile | 4 ++-- care/facility/tests/test_upload_external_result.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index bbd32d5c3d..37bae1c950 100644 --- a/Makefile +++ b/Makefile @@ -41,9 +41,9 @@ makemigrations: docker compose exec backend bash -c "python manage.py makemigrations" test: - docker compose exec backend bash -c "python manage.py test --keepdb --parallel" + docker compose exec backend bash -c "python manage.py test --keepdb --parallel --shuffle" test-coverage: - docker compose exec backend bash -c "coverage run manage.py test --settings=config.settings.test --keepdb --parallel" + docker compose exec backend bash -c "coverage run manage.py test --settings=config.settings.test --keepdb --parallel --shuffle" docker compose exec backend bash -c "coverage combine || true; coverage xml" docker compose cp backend:/app/coverage.xml coverage.xml diff --git a/care/facility/tests/test_upload_external_result.py b/care/facility/tests/test_upload_external_result.py index fe4f0dc918..7e0cee89a6 100644 --- a/care/facility/tests/test_upload_external_result.py +++ b/care/facility/tests/test_upload_external_result.py @@ -32,7 +32,7 @@ def test_different_district_upload(self): "gender": "m", "mobile_number": 8888888888, "address": "Upload test address", - "ward": self.ward.id, + "ward": self.ward.number, "local_body": str(self.local_body.name), "local_body_type": "municipality", "source": "Secondary contact aparna", From de399f4e607de14babcb5370a57a9c1debeaae4c Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Mon, 4 Mar 2024 18:57:17 +0530 Subject: [PATCH 05/11] Added dummy data for shifting and Resource (#1910) --- data/dummy/facility.json | 736 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 736 insertions(+) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index f2a1113659..22908ab01e 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -7377,5 +7377,741 @@ "created_by": 2, "last_edited_by": 2 } + }, + { + "model": "facility.shiftingrequest", + "pk": 2, + "fields": { + "external_id": "b1e6df81-35a2-4dc2-89cf-1b78a968e456", + "created_date": "2023-05-12T09:18:40.123Z", + "modified_date": "2023-11-20T14:55:30.987Z", + "deleted": false, + "origin_facility": 3, + "shifting_approving_facility": 4, + "assigned_facility_type": 1, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 2, + "emergency": false, + "is_up_shift": false, + "reason": "Scheduled transfer for further treatment", + "vehicle_preference": "Car", + "preferred_vehicle_choice": 40, + "comments": "No special instructions", + "refering_facility_contact_name": "Alice Johnson", + "refering_facility_contact_number": "+1122334455", + "is_kasp": true, + "status": 20, + "breathlessness_level": 15, + "is_assigned_to_user": true, + "assigned_to": 3, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 4, + "last_edited_by": 3 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 3, + "fields": { + "external_id": "c2d8ab92-15e3-4f67-8d36-9a5175f83467", + "created_date": "2023-07-18T15:40:20.789Z", + "modified_date": "2023-10-30T11:25:55.321Z", + "deleted": false, + "origin_facility": 2, + "shifting_approving_facility": 1, + "assigned_facility_type": 3, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 3, + "emergency": true, + "is_up_shift": true, + "reason": "Patient needs immediate ICU admission", + "vehicle_preference": "All double chambered Ambulance with EMT", + "preferred_vehicle_choice": 20, + "comments": "", + "refering_facility_contact_name": "Emily Clark", + "refering_facility_contact_number": "+9988776655", + "is_kasp": false, + "status": 40, + "breathlessness_level": 40, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "Michael Johnson", + "ambulance_phone_number": "+3344556677", + "ambulance_number": "", + "created_by": 1, + "last_edited_by": 1 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 4, + "fields": { + "external_id": "d3e9bcf3-6a7f-4c7e-ba5f-5c1f98535d82", + "created_date": "2023-08-24T13:55:10.422Z", + "modified_date": "2023-11-15T16:30:45.123Z", + "deleted": false, + "origin_facility": 4, + "shifting_approving_facility": 3, + "assigned_facility_type": null, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 4, + "emergency": false, + "is_up_shift": true, + "reason": "Patient needs to be shifted to a specialized facility", + "vehicle_preference": "Auto-rickshaw", + "preferred_vehicle_choice": 50, + "comments": "", + "refering_facility_contact_name": "David Smith", + "refering_facility_contact_number": "+7788990011", + "is_kasp": true, + "status": 80, + "breathlessness_level": 20, + "is_assigned_to_user": true, + "assigned_to": 2, + "ambulance_driver_name": "Sarah Brown", + "ambulance_phone_number": "+6655443322", + "ambulance_number": "", + "created_by": 3, + "last_edited_by": 4 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 5, + "fields": { + "external_id": "e4f5cd6e-8b0d-4b68-86d3-2f3f51c4af90", + "created_date": "2023-10-01T09:30:45.654Z", + "modified_date": "2023-12-05T14:20:30.987Z", + "deleted": false, + "origin_facility": 1, + "shifting_approving_facility": null, + "assigned_facility_type": 2, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 5, + "emergency": true, + "is_up_shift": false, + "reason": "Patient requires transfer for specialized surgery", + "vehicle_preference": "", + "preferred_vehicle_choice": null, + "comments": "Urgent transfer needed", + "refering_facility_contact_name": "Jennifer Lee", + "refering_facility_contact_number": "+1122334455", + "is_kasp": false, + "status": 15, + "breathlessness_level": null, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 1, + "last_edited_by": 2 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 6, + "fields": { + "external_id": "550e8400-e29b-41d4-a716-446655440000", + "created_date": "2023-11-15T10:20:30.123Z", + "modified_date": "2023-12-06T08:00:15.789Z", + "deleted": false, + "origin_facility": 3, + "shifting_approving_facility": 1, + "assigned_facility_type": 1, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 6, + "emergency": false, + "is_up_shift": false, + "reason": "Scheduled transfer for follow-up treatment", + "vehicle_preference": "D Level Ambulance", + "preferred_vehicle_choice": 10, + "comments": "", + "refering_facility_contact_name": "Mark Taylor", + "refering_facility_contact_number": "+3344556677", + "is_kasp": true, + "status": 70, + "breathlessness_level": 30, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "Emma White", + "ambulance_phone_number": "+9988776655", + "ambulance_number": "", + "created_by": 4, + "last_edited_by": 3 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 7, + "fields": { + "external_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7", + "created_date": "2023-12-06T08:30:45.987Z", + "modified_date": "2023-12-06T08:30:45.987Z", + "deleted": false, + "origin_facility": 2, + "shifting_approving_facility": null, + "assigned_facility_type": null, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 7, + "emergency": true, + "is_up_shift": true, + "reason": "Urgent transfer for advanced treatment", + "vehicle_preference": "Car", + "preferred_vehicle_choice": 40, + "comments": "", + "refering_facility_contact_name": "Sophia Garcia", + "refering_facility_contact_number": "+5566778899", + "is_kasp": false, + "status": 60, + "breathlessness_level": 15, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 2, + "last_edited_by": 1 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 8, + "fields": { + "external_id": "123e4567-e89b-12d3-a456-426614174000", + "created_date": "2023-12-06T08:47:55.705Z", + "modified_date": "2023-12-06T08:47:55.705Z", + "deleted": false, + "origin_facility": 4, + "shifting_approving_facility": 3, + "assigned_facility_type": 3, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 8, + "emergency": false, + "is_up_shift": true, + "reason": "Patient needs specialized care", + "vehicle_preference": "", + "preferred_vehicle_choice": null, + "comments": "Patient requires specific equipment", + "refering_facility_contact_name": "Oliver Brown", + "refering_facility_contact_number": "+9988776655", + "is_kasp": true, + "status": 100, + "breathlessness_level": 40, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 3, + "last_edited_by": 4 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 9, + "fields": { + "external_id": "09876543-210e-4567-a890-1234567890ab", + "created_date": "2023-12-06T09:00:00.000Z", + "modified_date": "2023-12-06T09:00:00.000Z", + "deleted": false, + "origin_facility": 1, + "shifting_approving_facility": 2, + "assigned_facility_type": 2, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 9, + "emergency": true, + "is_up_shift": false, + "reason": "Patient needs immediate attention", + "vehicle_preference": "Ambulance without EMT", + "preferred_vehicle_choice": 30, + "comments": "Critical condition, prioritize", + "refering_facility_contact_name": "Liam Wilson", + "refering_facility_contact_number": "+9988776655", + "is_kasp": false, + "status": 20, + "breathlessness_level": 20, + "is_assigned_to_user": true, + "assigned_to": 1, + "ambulance_driver_name": "Ethan Davis", + "ambulance_phone_number": "+1122334455", + "ambulance_number": "", + "created_by": 2, + "last_edited_by": 1 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 11, + "fields": { + "external_id": "abcdef01-2345-6789-abcd-ef0123456789", + "created_date": "2023-08-19T16:40:55.743Z", + "modified_date": "2023-11-30T10:20:05.214Z", + "deleted": false, + "origin_facility": 1, + "shifting_approving_facility": 2, + "assigned_facility_type": 2, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 11, + "emergency": true, + "is_up_shift": false, + "reason": "Patient needs immediate care", + "vehicle_preference": "Ambulance without EMT", + "preferred_vehicle_choice": 30, + "comments": "Contact family members", + "refering_facility_contact_name": "John Doe", + "refering_facility_contact_number": "+9988776655", + "is_kasp": true, + "status": 20, + "breathlessness_level": 20, + "is_assigned_to_user": true, + "assigned_to": 1, + "ambulance_driver_name": "Ethan Davis", + "ambulance_phone_number": "+1122334455", + "ambulance_number": "AMB1122", + "created_by": 2, + "last_edited_by": 1 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 12, + "fields": { + "external_id": "a0b1c2d3-e4f5-6789-abcd-0123456789ab", + "created_date": "2023-04-15T14:30:20.990Z", + "modified_date": "2023-10-25T08:55:15.789Z", + "deleted": false, + "origin_facility": 2, + "shifting_approving_facility": null, + "assigned_facility_type": null, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 12, + "emergency": false, + "is_up_shift": true, + "reason": "Patient needs specialized care", + "vehicle_preference": "Car", + "preferred_vehicle_choice": 40, + "comments": "Coordinate with receiving facility", + "refering_facility_contact_name": "Jane Smith", + "refering_facility_contact_number": "+5566778899", + "is_kasp": false, + "status": 60, + "breathlessness_level": 15, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 1, + "last_edited_by": 2 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 13, + "fields": { + "external_id": "e3a2dc83-82e1-422a-9c4f-86b4c1f0c2d3", + "created_date": "2023-11-10T11:05:45.372Z", + "modified_date": "2023-12-03T16:25:35.948Z", + "deleted": false, + "origin_facility": 3, + "shifting_approving_facility": 1, + "assigned_facility_type": 1, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 13, + "emergency": true, + "is_up_shift": false, + "reason": "Patient requires transfer for specialized surgery", + "vehicle_preference": "Auto-rickshaw", + "preferred_vehicle_choice": 50, + "comments": "Ensure medical records are transferred", + "refering_facility_contact_name": "Jennifer Lee", + "refering_facility_contact_number": "+1122334455", + "is_kasp": true, + "status": 15, + "breathlessness_level": null, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 2, + "last_edited_by": 1 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 15, + "fields": { + "external_id": "98765432-10e2-40f1-a0b9-876543210abc", + "created_date": "2023-09-05T22:50:10.221Z", + "modified_date": "2023-12-04T14:30:45.501Z", + "deleted": false, + "origin_facility": 1, + "shifting_approving_facility": 2, + "assigned_facility_type": 2, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 15, + "emergency": true, + "is_up_shift": true, + "reason": "Test", + "vehicle_preference": "", + "preferred_vehicle_choice": 10, + "comments": "", + "refering_facility_contact_name": "Someone at Facility", + "refering_facility_contact_number": "+914455666777", + "is_kasp": false, + "status": 100, + "breathlessness_level": 30, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 2, + "last_edited_by": 2 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 14, + "fields": { + "external_id": "f0e1d2c3-b4a5-6789-cdef-0123456789ab", + "created_date": "2023-06-20T18:15:30.125Z", + "modified_date": "2023-12-02T09:40:55.801Z", + "deleted": false, + "origin_facility": 4, + "shifting_approving_facility": 3, + "assigned_facility_type": 3, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 14, + "emergency": false, + "is_up_shift": true, + "reason": "Scheduled transfer for follow-up treatment", + "vehicle_preference": "D Level Ambulance", + "preferred_vehicle_choice": 10, + "comments": "Provide discharge summary", + "refering_facility_contact_name": "Mark Taylor", + "refering_facility_contact_number": "+3344556677", + "is_kasp": true, + "status": 70, + "breathlessness_level": 30, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "Emma White", + "ambulance_phone_number": "+9988776655", + "ambulance_number": "", + "created_by": 4, + "last_edited_by": 3 + } + }, + { + "model": "facility.shiftingrequest", + "pk": 15, + "fields": { + "external_id": "98765432-10e2-40f1-a0b9-876543210abc", + "created_date": "2023-09-05T22:50:10.221Z", + "modified_date": "2023-12-04T14:30:45.501Z", + "deleted": false, + "origin_facility": 1, + "shifting_approving_facility": 2, + "assigned_facility_type": 2, + "assigned_facility": null, + "assigned_facility_external": null, + "patient": 15, + "emergency": true, + "is_up_shift": true, + "reason": "Test", + "vehicle_preference": "", + "preferred_vehicle_choice": 10, + "comments": "", + "refering_facility_contact_name": "Someone at Facility", + "refering_facility_contact_number": "+914455666777", + "is_kasp": false, + "status": 100, + "breathlessness_level": 30, + "is_assigned_to_user": false, + "assigned_to": null, + "ambulance_driver_name": "", + "ambulance_phone_number": "", + "ambulance_number": "", + "created_by": 2, + "last_edited_by": 2 + } + }, + { + "model": "facility.resourcerequest", + "pk": 1, + "fields": { + "origin_facility": 1, + "approving_facility": 2, + "assigned_facility": 3, + "emergency": false, + "title": "Oxygen Cylinder Request", + "reason": "Insufficient oxygen supply", + "refering_facility_contact_name": "Dr. Smith", + "refering_facility_contact_number": "+1234567890", + "status": 10, + "category": 100, + "sub_category": 120, + "priority": null, + "requested_quantity": 5, + "assigned_quantity": 0, + "is_assigned_to_user": false, + "assigned_to": null, + "created_by": 1, + "last_edited_by": 1, + "created_date": "2023-09-05T22:50:10.221Z", + "modified_date": "2023-09-05T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 2, + "fields": { + "origin_facility": 1, + "approving_facility": 5, + "assigned_facility": 8, + "emergency": true, + "title": "Oxygen Cylinder Request", + "reason": "Urgent need for oxygen supply", + "refering_facility_contact_name": "Dr. John", + "refering_facility_contact_number": "+987654321", + "status": 15, + "category": 100, + "sub_category": 130, + "priority": 1, + "requested_quantity": 10, + "assigned_quantity": 2, + "is_assigned_to_user": true, + "assigned_to": 3, + "created_by": 2, + "last_edited_by": 2, + "created_date": "2023-09-06T22:50:10.221Z", + "modified_date": "2023-09-06T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 3, + "fields": { + "origin_facility": 7, + "approving_facility": 2, + "assigned_facility": 3, + "emergency": false, + "title": "Medical Supplies Request", + "reason": "Need for surgical supplies", + "refering_facility_contact_name": "Dr. Emily", + "refering_facility_contact_number": "+1122334455", + "status": 20, + "category": 200, + "sub_category": 1000, + "priority": null, + "requested_quantity": 20, + "assigned_quantity": 10, + "is_assigned_to_user": false, + "assigned_to": null, + "created_by": 3, + "last_edited_by": 3, + "created_date": "2023-09-07T22:50:10.221Z", + "modified_date": "2023-09-07T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 4, + "fields": { + "origin_facility": 9, + "approving_facility": 1, + "assigned_facility": 4, + "emergency": true, + "title": "Oxygen Cylinder Request", + "reason": "Critical shortage of oxygen", + "refering_facility_contact_name": "Dr. Patel", + "refering_facility_contact_number": "+9988776655", + "status": 30, + "category": 100, + "sub_category": 110, + "priority": 2, + "requested_quantity": 15, + "assigned_quantity": 0, + "is_assigned_to_user": false, + "assigned_to": null, + "created_by": 4, + "last_edited_by": 4, + "created_date": "2023-09-08T22:50:10.221Z", + "modified_date": "2023-09-08T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 5, + "fields": { + "origin_facility": 5, + "approving_facility": 6, + "assigned_facility": 7, + "emergency": false, + "title": "Medical Supplies Request", + "reason": "Need for bandages and gauze", + "refering_facility_contact_name": "Dr. Lee", + "refering_facility_contact_number": "+3344556677", + "status": 55, + "category": 200, + "sub_category": 1000, + "priority": null, + "requested_quantity": 30, + "assigned_quantity": 20, + "is_assigned_to_user": false, + "assigned_to": null, + "created_by": 5, + "last_edited_by": 5, + "created_date": "2023-09-09T22:50:10.221Z", + "modified_date": "2023-09-09T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 6, + "fields": { + "origin_facility": 12, + "approving_facility": 9, + "assigned_facility": 1, + "emergency": true, + "title": "Oxygen Cylinder Request", + "reason": "Urgent need for oxygen supply", + "refering_facility_contact_name": "Dr. Chang", + "refering_facility_contact_number": "+5566778899", + "status": 80, + "category": 100, + "sub_category": 120, + "priority": 1, + "requested_quantity": 8, + "assigned_quantity": 8, + "is_assigned_to_user": true, + "assigned_to": 6, + "created_by": 6, + "last_edited_by": 6, + "created_date": "2023-09-10T22:50:10.221Z", + "modified_date": "2023-09-10T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 7, + "fields": { + "origin_facility": 10, + "approving_facility": 11, + "assigned_facility": 9, + "emergency": false, + "title": "Medical Supplies Request", + "reason": "Need for disposable gloves", + "refering_facility_contact_name": "Dr. Johnson", + "refering_facility_contact_number": "+1122334455", + "status": 70, + "category": 200, + "sub_category": 1000, + "priority": null, + "requested_quantity": 50, + "assigned_quantity": 30, + "is_assigned_to_user": false, + "assigned_to": null, + "created_by": 7, + "last_edited_by": 7, + "created_date": "2023-09-11T22:50:10.221Z", + "modified_date": "2023-09-11T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 8, + "fields": { + "origin_facility": 15, + "approving_facility": 7, + "assigned_facility": 6, + "emergency": true, + "title": "Oxygen Cylinder Request", + "reason": "Critical need for oxygen cylinders", + "refering_facility_contact_name": "Dr. Rodriguez", + "refering_facility_contact_number": "+3344556677", + "status": 80, + "category": 100, + "sub_category": 120, + "priority": 2, + "requested_quantity": 12, + "assigned_quantity": 10, + "is_assigned_to_user": true, + "assigned_to": 8, + "created_by": 8, + "last_edited_by": 8, + "created_date": "2023-09-12T22:50:10.221Z", + "modified_date": "2023-09-12T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 9, + "fields": { + "origin_facility": 3, + "approving_facility": 9, + "assigned_facility": 1, + "emergency": false, + "title": "Medical Supplies Request", + "reason": "Need for IV fluids", + "refering_facility_contact_name": "Dr. Wang", + "refering_facility_contact_number": "+5566778899", + "status": 70, + "category": 200, + "sub_category": 1000, + "priority": null, + "requested_quantity": 40, + "assigned_quantity": 25, + "is_assigned_to_user": false, + "assigned_to": null, + "created_by": 9, + "last_edited_by": 9, + "created_date": "2023-09-13T22:50:10.221Z", + "modified_date": "2023-09-13T22:50:10.221Z" + } + }, + { + "model": "facility.resourcerequest", + "pk": 10, + "fields": { + "origin_facility": 10, + "approving_facility": 1, + "assigned_facility": 5, + "emergency": true, + "title": "Oxygen Cylinder Request", + "reason": "Urgent need for oxygen supply", + "refering_facility_contact_name": "Dr. Park", + "refering_facility_contact_number": "+1122334455", + "status": 10, + "category": 100, + "sub_category": 140, + "priority": 1, + "requested_quantity": 20, + "assigned_quantity": 15, + "is_assigned_to_user": true, + "assigned_to": 10, + "created_by": 10, + "last_edited_by": 10, + "created_date": "2023-09-14T22:50:10.221Z", + "modified_date": "2023-09-14T22:50:10.221Z" + } } ] From 5b317ca8cd0dd7740374e423e584543519d437e5 Mon Sep 17 00:00:00 2001 From: konavivekramakrishna Date: Mon, 4 Mar 2024 19:04:36 +0530 Subject: [PATCH 06/11] Track Occupation of Patient (#1901) track occupation --- care/facility/api/serializers/patient.py | 15 +++++---- ...ientmetainfo_head_of_household_and_more.py | 33 +++++++++++++++++++ care/facility/models/patient.py | 15 ++++----- 3 files changed, 49 insertions(+), 14 deletions(-) create mode 100644 care/facility/migrations/0415_alter_patientmetainfo_head_of_household_and_more.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index c4059c2577..721df68fbe 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -56,7 +56,7 @@ class PatientMetaInfoSerializer(serializers.ModelSerializer): - occupation = ChoiceField(choices=PatientMetaInfo.OccupationChoices) + occupation = ChoiceField(choices=PatientMetaInfo.OccupationChoices, allow_null=True) class Meta: model = PatientMetaInfo @@ -312,9 +312,8 @@ def create(self, validated_data): Disease.objects.bulk_create(diseases, ignore_conflicts=True) if meta_info: - meta_info_obj = PatientMetaInfo.objects.create(**meta_info) - patient.meta_info = meta_info_obj - patient.save() + patient.meta_info = PatientMetaInfo.objects.create(**meta_info) + patient.meta_info.save() if contacted_patients: contacted_patient_objs = [ @@ -361,8 +360,12 @@ def update(self, instance, validated_data): Disease.objects.bulk_create(diseases, ignore_conflicts=True) if meta_info: - for key, value in meta_info.items(): - setattr(patient.meta_info, key, value) + if patient.meta_info is None: + meta_info_obj = PatientMetaInfo.objects.create(**meta_info) + patient.meta_info = meta_info_obj + else: + for key, value in meta_info.items(): + setattr(patient.meta_info, key, value) patient.meta_info.save() if self.partial is not True: # clear the list and enter details if PUT diff --git a/care/facility/migrations/0415_alter_patientmetainfo_head_of_household_and_more.py b/care/facility/migrations/0415_alter_patientmetainfo_head_of_household_and_more.py new file mode 100644 index 0000000000..473d4c8ddb --- /dev/null +++ b/care/facility/migrations/0415_alter_patientmetainfo_head_of_household_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.10 on 2024-02-20 13:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0414_remove_bed_old_name"), + ] + + operations = [ + migrations.AlterField( + model_name="patientmetainfo", + name="head_of_household", + field=models.BooleanField(blank=True, null=True), + ), + migrations.AlterField( + model_name="patientmetainfo", + name="occupation", + field=models.IntegerField( + blank=True, + choices=[ + (1, "STUDENT"), + (2, "BUSINESSMAN"), + (3, "HEALTH_CARE_WORKER"), + (4, "HEALTH_CARE_LAB_WORKER"), + (5, "ANIMAL_HANDLER"), + (6, "OTHERS"), + ], + null=True, + ), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index b967a6e956..1c5408bebe 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -577,17 +577,16 @@ def format_diagnoses(diagnosis_ids): class PatientMetaInfo(models.Model): class OccupationEnum(enum.Enum): STUDENT = 1 - MEDICAL_WORKER = 2 - GOVT_EMPLOYEE = 3 - PRIVATE_EMPLOYEE = 4 - HOME_MAKER = 5 - WORKING_ABROAD = 6 - OTHERS = 7 + BUSINESSMAN = 2 + HEALTH_CARE_WORKER = 3 + HEALTH_CARE_LAB_WORKER = 4 + ANIMAL_HANDLER = 5 + OTHERS = 6 OccupationChoices = [(item.value, item.name) for item in OccupationEnum] - occupation = models.IntegerField(choices=OccupationChoices) - head_of_household = models.BooleanField() + occupation = models.IntegerField(choices=OccupationChoices, blank=True, null=True) + head_of_household = models.BooleanField(blank=True, null=True) class PatientContactDetails(models.Model): From 284c639a1c3bca867df14f1e8a77c9a184c097ff Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Mon, 4 Mar 2024 19:09:37 +0530 Subject: [PATCH 07/11] added contraints in consultations and custom migrations (#1917) * added contraints in consultations and custom migrations * added noop reverse * updated test_utils to handle the constraint * added a test for consultations * removed print statements * added one more test * updated tests for consultations * updated dummy data --------- Co-authored-by: Aakash Singh --- .../api/serializers/patient_consultation.py | 13 +++ .../migrations/0415_auto_20240227_0130.py | 38 +++++++++ ...ation_unique_patient_no_within_facility.py | 19 +++++ care/facility/models/patient_consultation.py | 4 + .../tests/test_patient_consultation_api.py | 85 +++++++++++++++++++ care/utils/tests/test_utils.py | 1 + data/dummy/facility.json | 32 +++---- 7 files changed, 176 insertions(+), 16 deletions(-) create mode 100644 care/facility/migrations/0415_auto_20240227_0130.py create mode 100644 care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index b3759ee369..b9f1c98ac3 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -512,6 +512,19 @@ def validate_encounter_date(self, value): def validate(self, attrs): validated = super().validate(attrs) # TODO Add Bed Authorisation Validation + if "patient_no" in validated: + facility = ( + self.instance.facility + if self.instance + else validated.get("patient").facility + ) + patient_no = validated["patient_no"] + if PatientConsultation.objects.filter( + patient_no=patient_no, facility=facility + ).exists(): + raise ValidationError( + "Patient number must be unique within the facility." + ) if ( "suggestion" in validated diff --git a/care/facility/migrations/0415_auto_20240227_0130.py b/care/facility/migrations/0415_auto_20240227_0130.py new file mode 100644 index 0000000000..809b2bd42e --- /dev/null +++ b/care/facility/migrations/0415_auto_20240227_0130.py @@ -0,0 +1,38 @@ +# Generated by Django 4.2.10 on 2024-02-26 20:00 + +from django.db import migrations +from django.db.models import Count, F, Value, Window +from django.db.models.functions import RowNumber + + +def fix_duplicate_patient_numbers(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + + window = Window( + expression=RowNumber(), + partition_by=[F("patient_no"), F("facility")], + order_by=F("id").asc(), + ) + + consultations = PatientConsultation.objects.annotate(row_number=window).filter( + row_number__gt=1 + ) + + for consultation in consultations: + consultation.patient_no = ( + f"{consultation.patient_no}_{consultation.row_number - 1}" + ) + + PatientConsultation.objects.bulk_update( + consultations, ["patient_no"], batch_size=2000 + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0414_remove_bed_old_name"), + ] + + operations = [ + migrations.RunPython(fix_duplicate_patient_numbers, migrations.RunPython.noop), + ] diff --git a/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py b/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py new file mode 100644 index 0000000000..d488b7aaae --- /dev/null +++ b/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.10 on 2024-02-26 20:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0415_auto_20240227_0130"), + ] + + operations = [ + migrations.AddConstraint( + model_name="patientconsultation", + constraint=models.UniqueConstraint( + fields=("patient_no", "facility"), + name="unique_patient_no_within_facility", + ), + ), + ] diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index a16c08e2ae..0559046551 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -287,6 +287,10 @@ class Meta: | models.Q(referred_to__isnull=False) | models.Q(referred_to_external__isnull=False), ), + models.UniqueConstraint( + fields=["patient_no", "facility"], + name="unique_patient_no_within_facility", + ), ] @staticmethod diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 9b74338d1a..856b932012 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -30,6 +30,7 @@ def setUpTestData(cls) -> None: cls.doctor = cls.create_user( "doctor", cls.district, home_facility=cls.facility, user_type=15 ) + cls.patient1 = cls.create_patient(cls.district, cls.facility) def get_default_data(self): return { @@ -529,3 +530,87 @@ def test_add_principal_edit_as_inactive_add_principal(self): verification_status=ConditionVerificationStatus.PROVISIONAL, ) self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_create_consultations_with_duplicate_patient_no_within_facility(self): + patient2 = self.create_patient(self.district, self.facility) + data = self.get_default_data().copy() + data.update( + { + "patient_no": "IP1234", + "patient": patient2.external_id, + "facility": self.facility.external_id, + "created_by": self.user.external_id, + "suggestion": "A", + } + ) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + data.update( + { + "patient_no": "IP1234", + "patient": self.patient1.external_id, + "facility": self.facility.external_id, + "created_by": self.user.external_id, + } + ) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_consultations_with_same_patient_no_in_different_facilities(self): + facility2 = self.create_facility( + self.super_user, self.district, self.local_body, name="bar" + ) + patient2 = self.create_patient(self.district, facility2) + doctor2 = self.create_user( + "doctor2", self.district, home_facility=facility2, user_type=15 + ) + data = self.get_default_data().copy() + data.update( + { + "patient_no": "IP1234", + "patient": self.patient1.external_id, + "created_by": self.user.external_id, + "suggestion": "A", + } + ) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + data.update( + { + "patient_no": "IP1234", + "patient": patient2.external_id, + "created_by": self.user.external_id, + "suggestion": "A", + "treating_physician": doctor2.id, + } + ) + self.client.force_authenticate(user=doctor2) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_patient_was_discharged_and_then_added_with_a_different_patient_number( + self, + ): + consultation = self.create_admission_consultation( + suggestion="A", + encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + patient=self.patient1, + ) + res = self.discharge( + consultation, + new_discharge_reason=NewDischargeReasonEnum.RECOVERED, + discharge_date="2020-04-02T15:30:00Z", + discharge_notes="Discharge as recovered after admission before future", + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + data = self.get_default_data().copy() + data.update( + { + "patient_no": "IP1234", + "patient": self.patient1.external_id, + "created_by": self.user.external_id, + "suggestion": "A", + } + ) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index faaf046b37..c76271bedf 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -318,6 +318,7 @@ def get_consultation_data(cls): "course_in_facility": "", "created_date": mock_equal, "modified_date": mock_equal, + "patient_no": int(datetime.now().timestamp() * 1000), } @classmethod diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 22908ab01e..39b690712b 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -2070,7 +2070,7 @@ "modified_date": "2023-12-06T08:39:48.823Z", "deleted": false, "patient": 3, - "patient_no": "IP007", + "patient_no": "IP008", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2141,7 +2141,7 @@ "modified_date": "2023-12-06T08:42:30.134Z", "deleted": false, "patient": 4, - "patient_no": "IP007", + "patient_no": "IP009", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2212,7 +2212,7 @@ "modified_date": "2023-12-06T08:42:52.652Z", "deleted": false, "patient": 5, - "patient_no": "IP007", + "patient_no": "IP017", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2283,7 +2283,7 @@ "modified_date": "2023-12-06T08:43:14.972Z", "deleted": false, "patient": 6, - "patient_no": "IP007", + "patient_no": "IP087", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2354,7 +2354,7 @@ "modified_date": "2023-12-06T08:43:38.025Z", "deleted": false, "patient": 7, - "patient_no": "IP007", + "patient_no": "IP00527", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2425,7 +2425,7 @@ "modified_date": "2023-12-06T08:44:01.909Z", "deleted": false, "patient": 8, - "patient_no": "IP007", + "patient_no": "IP0KI07", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2496,7 +2496,7 @@ "modified_date": "2023-12-06T08:44:25.107Z", "deleted": false, "patient": 9, - "patient_no": "IP007", + "patient_no": "IP00767", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2567,7 +2567,7 @@ "modified_date": "2023-12-06T08:44:47.683Z", "deleted": false, "patient": 10, - "patient_no": "IP007", + "patient_no": "IP001237", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2638,7 +2638,7 @@ "modified_date": "2023-12-06T08:45:10.167Z", "deleted": false, "patient": 11, - "patient_no": "IP007", + "patient_no": "IP007963", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2709,7 +2709,7 @@ "modified_date": "2023-12-06T08:45:34.527Z", "deleted": false, "patient": 12, - "patient_no": "IP007", + "patient_no": "IP0001257", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2780,7 +2780,7 @@ "modified_date": "2023-12-06T08:45:57.145Z", "deleted": false, "patient": 13, - "patient_no": "IP007", + "patient_no": "IP075389007", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2851,7 +2851,7 @@ "modified_date": "2023-12-06T08:46:19.952Z", "deleted": false, "patient": 14, - "patient_no": "IP007", + "patient_no": "IP099907", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2922,7 +2922,7 @@ "modified_date": "2023-12-06T08:46:42.597Z", "deleted": false, "patient": 15, - "patient_no": "IP007", + "patient_no": "IP00700", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -2993,7 +2993,7 @@ "modified_date": "2023-12-06T08:47:07.447Z", "deleted": false, "patient": 16, - "patient_no": "IP007", + "patient_no": "IP00744", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -3064,7 +3064,7 @@ "modified_date": "2023-12-06T08:47:30.863Z", "deleted": false, "patient": 17, - "patient_no": "IP007", + "patient_no": "IP00117", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -3135,7 +3135,7 @@ "modified_date": "2023-12-06T08:47:53.751Z", "deleted": false, "patient": 18, - "patient_no": "IP007", + "patient_no": "IP02507", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", From 93c86049890dbff5eb5611b274c714d58ec8ef20 Mon Sep 17 00:00:00 2001 From: Yuvraj Singh <98007961+SinghYuvraj0506@users.noreply.github.com> Date: Mon, 4 Mar 2024 19:10:27 +0530 Subject: [PATCH 08/11] Fix:Migration of new facility types (#1936) * changed some facility types and also made custom migration for old data * update migrations --------- Co-authored-by: Aakash Singh --- .../migrations/0416_update_facility_types.py | 30 +++++++++++++++++++ care/facility/models/facility.py | 17 ++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 care/facility/migrations/0416_update_facility_types.py diff --git a/care/facility/migrations/0416_update_facility_types.py b/care/facility/migrations/0416_update_facility_types.py new file mode 100644 index 0000000000..0462816a81 --- /dev/null +++ b/care/facility/migrations/0416_update_facility_types.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-03-02 05:39 + +from django.db import migrations + + +def update_facility_types(apps, schema_editor): + Facility = apps.get_model("facility", "Facility") + facilities_to_update = { + 801: 800, # 24x7 Public Health Centres to Primary Health Centres + 820: 800, # Urban Primary Health Center to Primary Health Centres + 831: 830, # Taluk Headquarters Hospitals to Taluk Hospitals + 850: 860, # General hospitals to District Hospitals + 900: 910, # Co-operative hospitals to Autonomous healthcare facility + 950: 870, # Corona Testing Labs to Govt. Labs + 1000: 3, # Corona Care Centre to Other + 8: 870, # Govt Hospital to Govt Medical College Hospitals + } + + for old_id, new_id in facilities_to_update.items(): + Facility.objects.filter(facility_type=old_id).update(facility_type=new_id) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0415_alter_patientmetainfo_head_of_household_and_more"), + ] + + operations = [ + migrations.RunPython(update_facility_types, migrations.RunPython.noop), + ] diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index fb3eaff08c..f417092853 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -59,26 +59,27 @@ (5, "Hotel"), (6, "Lodge"), (7, "TeleMedicine"), - (8, "Govt Hospital"), - (9, "Labs"), + # (8, "Govt Hospital"), # Change from "Govt Hospital" to "Govt Medical College Hospitals" + (9, "Govt Labs"), + (10, "Private Labs"), # Use 8xx for Govt owned hospitals and health centres (800, "Primary Health Centres"), - (801, "24x7 Public Health Centres"), + # (801, "24x7 Public Health Centres"), # Change from "24x7 Public Health Centres" to "Primary Health Centres" (802, "Family Health Centres"), (803, "Community Health Centres"), - (820, "Urban Primary Health Center"), + # (820, "Urban Primary Health Center"), # Change from "Urban Primary Health Center" to "Primary Health Centres" (830, "Taluk Hospitals"), - (831, "Taluk Headquarters Hospitals"), + # (831, "Taluk Headquarters Hospitals"), # Change from "Taluk Headquarters Hospitals" to "Taluk Hospitals" (840, "Women and Child Health Centres"), - (850, "General hospitals"), # TODO: same as 8, need to merge + # (850, "General hospitals"), # Change from "General hospitals" to "District Hospitals" (860, "District Hospitals"), (870, "Govt Medical College Hospitals"), (900, "Co-operative hospitals"), (910, "Autonomous healthcare facility"), # Use 9xx for Labs - (950, "Corona Testing Labs"), + # (950, "Corona Testing Labs"), # Change from "Corona Testing Labs" to "Govt Labs" # Use 10xx for Corona Care Center - (1000, "Corona Care Centre"), + # (1000, "Corona Care Centre"), # Change from "Corona Care Centre" to "Other" (1010, "COVID-19 Domiciliary Care Center"), # Use 11xx for First Line Treatment Centre (1100, "First Line Treatment Centre"), From 7c854645da979bcb35af3e3fabafd5b995aac20c Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 6 Mar 2024 22:56:31 +0530 Subject: [PATCH 09/11] Merge migrations (#1946) * Merge migrations * update migrations --- .../migrations/0417_merge_20240305_1805.py | 12 +++ ...8_alter_facility_facility_type_and_more.py | 83 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 care/facility/migrations/0417_merge_20240305_1805.py create mode 100644 care/facility/migrations/0418_alter_facility_facility_type_and_more.py diff --git a/care/facility/migrations/0417_merge_20240305_1805.py b/care/facility/migrations/0417_merge_20240305_1805.py new file mode 100644 index 0000000000..c87e5b6916 --- /dev/null +++ b/care/facility/migrations/0417_merge_20240305_1805.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.10 on 2024-03-05 12:35 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0416_patientconsultation_unique_patient_no_within_facility"), + ("facility", "0416_update_facility_types"), + ] + + operations = [] diff --git a/care/facility/migrations/0418_alter_facility_facility_type_and_more.py b/care/facility/migrations/0418_alter_facility_facility_type_and_more.py new file mode 100644 index 0000000000..c777601822 --- /dev/null +++ b/care/facility/migrations/0418_alter_facility_facility_type_and_more.py @@ -0,0 +1,83 @@ +# Generated by Django 4.2.10 on 2024-03-05 12:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0417_merge_20240305_1805"), + ] + + operations = [ + migrations.AlterField( + model_name="facility", + name="facility_type", + field=models.IntegerField( + choices=[ + (1, "Educational Inst"), + (2, "Private Hospital"), + (3, "Other"), + (4, "Hostel"), + (5, "Hotel"), + (6, "Lodge"), + (7, "TeleMedicine"), + (9, "Govt Labs"), + (10, "Private Labs"), + (800, "Primary Health Centres"), + (802, "Family Health Centres"), + (803, "Community Health Centres"), + (830, "Taluk Hospitals"), + (840, "Women and Child Health Centres"), + (860, "District Hospitals"), + (870, "Govt Medical College Hospitals"), + (900, "Co-operative hospitals"), + (910, "Autonomous healthcare facility"), + (1010, "COVID-19 Domiciliary Care Center"), + (1100, "First Line Treatment Centre"), + (1200, "Second Line Treatment Center"), + (1300, "Shifting Centre"), + (1400, "Covid Management Center"), + (1500, "Request Approving Center"), + (1510, "Request Fulfilment Center"), + (1600, "District War Room"), + ] + ), + ), + migrations.AlterField( + model_name="shiftingrequest", + name="assigned_facility_type", + field=models.IntegerField( + blank=True, + choices=[ + (1, "Educational Inst"), + (2, "Private Hospital"), + (3, "Other"), + (4, "Hostel"), + (5, "Hotel"), + (6, "Lodge"), + (7, "TeleMedicine"), + (9, "Govt Labs"), + (10, "Private Labs"), + (800, "Primary Health Centres"), + (802, "Family Health Centres"), + (803, "Community Health Centres"), + (830, "Taluk Hospitals"), + (840, "Women and Child Health Centres"), + (860, "District Hospitals"), + (870, "Govt Medical College Hospitals"), + (900, "Co-operative hospitals"), + (910, "Autonomous healthcare facility"), + (1010, "COVID-19 Domiciliary Care Center"), + (1100, "First Line Treatment Centre"), + (1200, "Second Line Treatment Center"), + (1300, "Shifting Centre"), + (1400, "Covid Management Center"), + (1500, "Request Approving Center"), + (1510, "Request Fulfilment Center"), + (1600, "District War Room"), + ], + default=None, + null=True, + ), + ), + ] From 56f2357b943c41c18ceecf70b4be302fc96614cf Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 7 Mar 2024 19:58:59 +0530 Subject: [PATCH 10/11] Allow empty op number (#1954) * allow empty op number * update migrations --- .../api/serializers/patient_consultation.py | 30 +++++--- .../migrations/0415_auto_20240227_0130.py | 6 +- ...ation_unique_patient_no_within_facility.py | 1 + ...19_alter_patientconsultation_patient_no.py | 23 +++++++ care/facility/models/patient_consultation.py | 3 +- .../tests/test_patient_consultation_api.py | 68 +++++++++++-------- 6 files changed, 94 insertions(+), 37 deletions(-) create mode 100644 care/facility/migrations/0419_alter_patientconsultation_patient_no.py diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index b9f1c98ac3..f3df403581 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -509,18 +509,32 @@ def validate_encounter_date(self, value): ) return value + def validate_patient_no(self, value): + if value is None: + return None + return value.strip() + def validate(self, attrs): validated = super().validate(attrs) # TODO Add Bed Authorisation Validation - if "patient_no" in validated: - facility = ( - self.instance.facility - if self.instance - else validated.get("patient").facility - ) - patient_no = validated["patient_no"] + + if ( + not self.instance + and "suggestion" in validated + and validated["suggestion"] == SuggestionChoices.A + ): + patient_no = validated.get("patient_no") + if not patient_no: + raise ValidationError( + {"ip_no": ["This field is required for admission."]} + ) if PatientConsultation.objects.filter( - patient_no=patient_no, facility=facility + patient_no=patient_no, + facility=( + self.instance.facility + if self.instance + else validated.get("patient").facility + ), ).exists(): raise ValidationError( "Patient number must be unique within the facility." diff --git a/care/facility/migrations/0415_auto_20240227_0130.py b/care/facility/migrations/0415_auto_20240227_0130.py index 809b2bd42e..805ec79680 100644 --- a/care/facility/migrations/0415_auto_20240227_0130.py +++ b/care/facility/migrations/0415_auto_20240227_0130.py @@ -8,6 +8,10 @@ def fix_duplicate_patient_numbers(apps, schema_editor): PatientConsultation = apps.get_model("facility", "PatientConsultation") + PatientConsultation.objects.filter(patient_no__regex=r"^\s*$").update( + patient_no=None + ) + window = Window( expression=RowNumber(), partition_by=[F("patient_no"), F("facility")], @@ -15,7 +19,7 @@ def fix_duplicate_patient_numbers(apps, schema_editor): ) consultations = PatientConsultation.objects.annotate(row_number=window).filter( - row_number__gt=1 + row_number__gt=1, patient_no__isnull=False ) for consultation in consultations: diff --git a/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py b/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py index d488b7aaae..3c7450b24b 100644 --- a/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py +++ b/care/facility/migrations/0416_patientconsultation_unique_patient_no_within_facility.py @@ -12,6 +12,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="patientconsultation", constraint=models.UniqueConstraint( + condition=models.Q(("patient_no__isnull", False)), fields=("patient_no", "facility"), name="unique_patient_no_within_facility", ), diff --git a/care/facility/migrations/0419_alter_patientconsultation_patient_no.py b/care/facility/migrations/0419_alter_patientconsultation_patient_no.py new file mode 100644 index 0000000000..0b3206461a --- /dev/null +++ b/care/facility/migrations/0419_alter_patientconsultation_patient_no.py @@ -0,0 +1,23 @@ +# Generated by Django 4.2.10 on 2024-03-07 12:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0418_alter_facility_facility_type_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="patientconsultation", + name="patient_no", + field=models.CharField( + blank=True, + default=None, + help_text="Patient's unique number in the facility. IP number for inpatients and OP number for outpatients.", + max_length=100, + null=True, + ), + ), + ] diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 0559046551..5c9d8802f3 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -46,7 +46,7 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): patient_no = models.CharField( max_length=100, - default="", + default=None, null=True, blank=True, help_text=( @@ -290,6 +290,7 @@ class Meta: models.UniqueConstraint( fields=["patient_no", "facility"], name="unique_patient_no_within_facility", + condition=models.Q(patient_no__isnull=False), ), ] diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 856b932012..1fa6f056af 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -9,7 +9,7 @@ ConditionVerificationStatus, ICD11Diagnosis, ) -from care.facility.models.patient_base import NewDischargeReasonEnum +from care.facility.models.patient_base import NewDischargeReasonEnum, SuggestionChoices from care.facility.models.patient_consultation import ( CATEGORY_CHOICES, PatientConsultation, @@ -40,7 +40,7 @@ def get_default_data(self): "examination_details": "examination_details", "history_of_present_illness": "history_of_present_illness", "treatment_plan": "treatment_plan", - "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], + "suggestion": SuggestionChoices.HI, "treating_physician": self.doctor.id, "create_diagnoses": [ { @@ -49,6 +49,7 @@ def get_default_data(self): "verification_status": ConditionVerificationStatus.CONFIRMED, } ], + "patient_no": datetime.datetime.now().timestamp(), } def get_url(self, consultation=None): @@ -125,7 +126,7 @@ def test_create_consultation_treating_physician_invalid_user(self): def test_discharge_as_recovered_preadmission(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -138,7 +139,7 @@ def test_discharge_as_recovered_preadmission(self): def test_discharge_as_recovered_future(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -151,7 +152,7 @@ def test_discharge_as_recovered_future(self): def test_discharge_as_recovered_after_admission(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -164,7 +165,7 @@ def test_discharge_as_recovered_after_admission(self): def test_discharge_as_expired_pre_admission(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -178,7 +179,7 @@ def test_discharge_as_expired_pre_admission(self): def test_discharge_as_expired_future(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -192,7 +193,7 @@ def test_discharge_as_expired_future(self): def test_discharge_as_expired_after_admission(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -207,7 +208,7 @@ def test_discharge_as_expired_after_admission(self): def test_discharge_as_recovered_with_expired_fields(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2023, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -225,7 +226,7 @@ def test_discharge_as_recovered_with_expired_fields(self): def test_referred_to_external_null(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -239,7 +240,7 @@ def test_referred_to_external_null(self): def test_referred_to_external_empty_string(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -253,7 +254,7 @@ def test_referred_to_external_empty_string(self): def test_referred_to_empty_facility(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -267,7 +268,7 @@ def test_referred_to_empty_facility(self): def test_referred_to_and_external_together(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -282,7 +283,7 @@ def test_referred_to_and_external_together(self): def test_referred_to_valid_value(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) referred_to_external = "Test Hospital" @@ -296,7 +297,7 @@ def test_referred_to_valid_value(self): def test_referred_to_external_valid_value(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) referred_to_external = "Test Hospital" @@ -379,7 +380,7 @@ def test_medico_legal_case(self): def test_update_consultation_after_discharge(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.discharge( @@ -397,7 +398,7 @@ def test_update_consultation_after_discharge(self): def test_add_diagnoses_and_duplicate_diagnoses(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) diagnosis = ICD11Diagnosis.objects.all()[0].id @@ -418,7 +419,7 @@ def test_add_diagnoses_and_duplicate_diagnoses(self): def test_add_diagnosis_inactive(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) diagnosis = ICD11Diagnosis.objects.first().id @@ -439,7 +440,7 @@ def test_add_diagnosis_inactive(self): def mark_inactive_diagnosis_as_principal(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) diagnosis = ICD11Diagnosis.objects.first().id @@ -465,7 +466,7 @@ def mark_inactive_diagnosis_as_principal(self): def test_change_diagnosis(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.add_diagnosis( @@ -486,7 +487,7 @@ def test_change_diagnosis(self): def test_add_multiple_principal_diagnosis(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.add_diagnosis( @@ -506,7 +507,7 @@ def test_add_multiple_principal_diagnosis(self): def test_add_principal_edit_as_inactive_add_principal(self): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), ) res = self.add_diagnosis( @@ -540,7 +541,7 @@ def test_create_consultations_with_duplicate_patient_no_within_facility(self): "patient": patient2.external_id, "facility": self.facility.external_id, "created_by": self.user.external_id, - "suggestion": "A", + "suggestion": SuggestionChoices.A, } ) res = self.client.post(self.get_url(), data, format="json") @@ -570,7 +571,7 @@ def test_create_consultations_with_same_patient_no_in_different_facilities(self) "patient_no": "IP1234", "patient": self.patient1.external_id, "created_by": self.user.external_id, - "suggestion": "A", + "suggestion": SuggestionChoices.A, } ) res = self.client.post(self.get_url(), data, format="json") @@ -580,7 +581,7 @@ def test_create_consultations_with_same_patient_no_in_different_facilities(self) "patient_no": "IP1234", "patient": patient2.external_id, "created_by": self.user.external_id, - "suggestion": "A", + "suggestion": SuggestionChoices.A, "treating_physician": doctor2.id, } ) @@ -592,7 +593,7 @@ def test_patient_was_discharged_and_then_added_with_a_different_patient_number( self, ): consultation = self.create_admission_consultation( - suggestion="A", + suggestion=SuggestionChoices.A, encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), patient=self.patient1, ) @@ -609,7 +610,20 @@ def test_patient_was_discharged_and_then_added_with_a_different_patient_number( "patient_no": "IP1234", "patient": self.patient1.external_id, "created_by": self.user.external_id, - "suggestion": "A", + "suggestion": SuggestionChoices.A, + } + ) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_allow_empty_op_no(self): + data = self.get_default_data().copy() + data.update( + { + "patient_no": "", + "patient": self.patient1.external_id, + "created_by": self.user.external_id, + "suggestion": SuggestionChoices.OP, } ) res = self.client.post(self.get_url(), data, format="json") From bd161efc5d86f2d65a0d0b7ca7f670216fe7a055 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 7 Mar 2024 20:00:06 +0530 Subject: [PATCH 11/11] Fix patient viewset distinct issue (#1953) * Distinct on the ordering field if ordering present for Patient Viewset Queryset * Add tests * fix typo --- care/facility/api/viewsets/patient.py | 4 ++-- .../test_patient_and_consultation_access.py | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index e6fc0e5c0e..631928d5f4 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -277,7 +277,7 @@ def filter_queryset(self, request, queryset, view): q_filters |= Q(consultations__facility__id__in=allowed_facilities) q_filters |= Q(last_consultation__assigned_to=request.user) q_filters |= Q(assigned_to=request.user) - queryset = queryset.filter(q_filters).distinct("id") + queryset = queryset.filter(q_filters) return queryset def filter_list_queryset(self, request, queryset, view): @@ -314,7 +314,7 @@ def filter_queryset(self, request, queryset, view): ) ).order_by(ordering) - return queryset + return queryset.distinct(ordering.lstrip("-") if ordering else "id") @extend_schema_view(history=extend_schema(tags=["patient"])) diff --git a/care/facility/tests/test_patient_and_consultation_access.py b/care/facility/tests/test_patient_and_consultation_access.py index e28eca466d..7235602695 100644 --- a/care/facility/tests/test_patient_and_consultation_access.py +++ b/care/facility/tests/test_patient_and_consultation_access.py @@ -52,6 +52,9 @@ def setUpTestData(cls) -> None: ) cls.patient = cls.create_patient(cls.district, cls.remote_facility) + def list_patients(self, **kwargs): + return self.client.get("/api/v1/patient/", data=kwargs) + def retrieve_patient(self, patient): return self.client.get(f"/api/v1/patient/{patient.external_id}/") @@ -90,6 +93,10 @@ def test_patient_consultation_access(self): self.client.force_authenticate(user=self.remote_doctor) res = self.retrieve_discharged_patients_of_facility(self.remote_facility) self.assertNotContains(res, self.patient.external_id) + res = self.list_patients( + is_active="true", ordering="-last_consultation__current_bed__bed__name" + ) + self.assertContains(res, self.patient.external_id) res = self.retrieve_patient(self.patient) self.assertEqual(res.status_code, status.HTTP_200_OK) res = self.retieve_patient_consultations(self.patient) @@ -114,6 +121,12 @@ def test_patient_consultation_access(self): self.client.force_authenticate(user=self.remote_doctor) res = self.discharge(remote_consultation, discharge_date="2024-01-02T00:00:00Z") self.assertEqual(res.status_code, status.HTTP_200_OK) + res = self.list_patients(is_active="true", ordering="created_date") + self.assertNotContains(res, self.patient.external_id) + res = self.list_patients( + is_active="false", ordering="-last_consultation__current_bed__bed__name" + ) + self.assertContains(res, self.patient.external_id) # Admit to home facility self.client.force_authenticate(user=self.home_doctor) @@ -129,6 +142,8 @@ def test_patient_consultation_access(self): res = self.retrieve_discharged_patients_of_facility(self.remote_facility) self.assertContains(res, self.patient.external_id) res = self.retrieve_patient(self.patient) + res = self.list_patients(is_active="true", ordering="-category_severity") + self.assertContains(res, self.patient.external_id) self.assertEqual(res.status_code, status.HTTP_200_OK) res = self.retieve_patient_consultations(self.patient) self.assertContains(res, remote_consultation.external_id) @@ -145,6 +160,8 @@ def test_patient_consultation_access(self): # Discharge the patient from home facility. res = self.discharge(home_consultation, discharge_date="2024-01-04T00:00:00Z") self.assertEqual(res.status_code, status.HTTP_200_OK) + res = self.list_patients(is_active="false", ordering="name") + self.assertContains(res, self.patient.external_id) res = self.retrieve_discharged_patients_of_facility(self.home_facility) self.assertContains(res, self.patient.external_id) self.assertContains(res, home_consultation.external_id)