From 79d6050bec3d82ee1a44a0c6a3cfe34ea8ce7a82 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 31 Aug 2023 16:32:18 +0530 Subject: [PATCH 1/8] Change Consultation `verified_by` to User FK --- care/abdm/utils/fhir.py | 5 +- .../api/serializers/patient_consultation.py | 20 +++++- care/facility/api/viewsets/facility_users.py | 7 ++- .../0383_patientconsultation_verified_by.py | 30 +++++++++ care/facility/models/patient_consultation.py | 5 +- .../patient_discharge_summary_pdf.html | 6 +- data/dummy/facility.json | 62 ++++++++++++++----- 7 files changed, 116 insertions(+), 19 deletions(-) create mode 100644 care/facility/migrations/0383_patientconsultation_verified_by.py diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c14b723ab1..088885e2eb 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -88,7 +88,10 @@ 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 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..6f9fc41320 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=True, allow_null=False + ) + 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") @@ -143,6 +148,19 @@ def validate_bed_number(self, bed_number): bed_number = None return bed_number + def validate_verified_by(self, verified_by): + if not verified_by.user_type == User.TYPE_VALUE_MAP["Doctor"]: + raise ValidationError("Only Doctors can verify a Consultation") + + if ( + str(verified_by.home_facility.external_id) + != self.context["request"].data["facility"] + ): + raise ValidationError( + "Home Facility of the Doctor must be the same as the Consultation Facility" + ) + return verified_by + def update(self, instance, validated_data): instance.last_edited_by = self.context["request"].user diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py index a03319ecf8..a9eb964ae0 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/migrations/0383_patientconsultation_verified_by.py b/care/facility/migrations/0383_patientconsultation_verified_by.py new file mode 100644 index 0000000000..c2477047d7 --- /dev/null +++ b/care/facility/migrations/0383_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", "0382_assetservice_remove_asset_last_serviced_on_and_more"), + ] + + 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/models/patient_consultation.py b/care/facility/models/patient_consultation.py index ed5f875ab7..637b3880c5 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -139,7 +139,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=False + ) created_by = models.ForeignKey( User, on_delete=models.SET_NULL, null=True, related_name="created_user" diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html index fca8b51c1a..0a6d080cbb 100644 --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ b/care/templates/reports/patient_discharge_summary_pdf.html @@ -912,7 +912,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/data/dummy/facility.json b/data/dummy/facility.json index 6f7cb4146c..29b2d54904 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, @@ -2470,8 +2469,14 @@ "default_unit": 1, "description": "", "min_quantity": 150.0, - "allowed_units": [1, 2], - "tags": [1, 2] + "allowed_units": [ + 1, + 2 + ], + "tags": [ + 1, + 2 + ] } }, { @@ -2482,8 +2487,13 @@ "default_unit": 1, "description": "", "min_quantity": 2.0, - "allowed_units": [1, 2], - "tags": [2] + "allowed_units": [ + 1, + 2 + ], + "tags": [ + 2 + ] } }, { @@ -2494,8 +2504,12 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [7], - "tags": [2] + "allowed_units": [ + 7 + ], + "tags": [ + 2 + ] } }, { @@ -2506,7 +2520,9 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [4], + "allowed_units": [ + 4 + ], "tags": [] } }, @@ -2518,7 +2534,9 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [4], + "allowed_units": [ + 4 + ], "tags": [] } }, @@ -2530,7 +2548,9 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [4], + "allowed_units": [ + 4 + ], "tags": [] } }, @@ -2542,8 +2562,12 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [7], - "tags": [2] + "allowed_units": [ + 7 + ], + "tags": [ + 2 + ] } }, { @@ -2562,7 +2586,14 @@ "address": "127.0.0.1", "pincode": 670000, "district": 7, - "features": [1, 2, 3, 4, 5, 6], + "features": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], "latitude": null, "inventory": {}, "longitude": null, @@ -2634,7 +2665,10 @@ "address": "89.66.33.55", "pincode": 670112, "district": 7, - "features": [1, 6], + "features": [ + 1, + 6 + ], "latitude": null, "inventory": {}, "longitude": null, From 07ed32cd827d53bdfd1683f221be74af5b6a14be Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 31 Aug 2023 16:45:31 +0530 Subject: [PATCH 2/8] revert unformatted json code --- data/dummy/facility.json | 61 +++++++++------------------------------- 1 file changed, 13 insertions(+), 48 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 29b2d54904..c4a05f5ee4 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -2469,14 +2469,8 @@ "default_unit": 1, "description": "", "min_quantity": 150.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 1, - 2 - ] + "allowed_units": [1, 2], + "tags": [1, 2] } }, { @@ -2487,13 +2481,8 @@ "default_unit": 1, "description": "", "min_quantity": 2.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 2 - ] + "allowed_units": [1, 2], + "tags": [2] } }, { @@ -2504,12 +2493,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -2520,9 +2505,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -2534,9 +2517,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -2548,9 +2529,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -2562,12 +2541,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -2586,14 +2561,7 @@ "address": "127.0.0.1", "pincode": 670000, "district": 7, - "features": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], + "features": [1, 2, 3, 4, 5, 6], "latitude": null, "inventory": {}, "longitude": null, @@ -2665,10 +2633,7 @@ "address": "89.66.33.55", "pincode": 670112, "district": 7, - "features": [ - 1, - 6 - ], + "features": [1, 6], "latitude": null, "inventory": {}, "longitude": null, From 03e431dd375781e1c65c789966e326a2fd16e500 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 31 Aug 2023 18:10:12 +0530 Subject: [PATCH 3/8] add `home_facility` search --- care/facility/api/serializers/patient_consultation.py | 3 ++- care/users/api/viewsets/users.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 6f9fc41320..21d9c2ae37 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -153,7 +153,8 @@ def validate_verified_by(self, verified_by): raise ValidationError("Only Doctors can verify a Consultation") if ( - str(verified_by.home_facility.external_id) + verified_by.home_facility + and str(verified_by.home_facility.external_id) != self.context["request"].data["facility"] ): raise ValidationError( 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, From 776de50168a3947e4c761903ee2605617081d108 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 1 Sep 2023 18:10:03 +0530 Subject: [PATCH 4/8] add fallback to fhir verified by --- care/abdm/utils/fhir.py | 1 + 1 file changed, 1 insertion(+) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 088885e2eb..214257bc02 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -92,6 +92,7 @@ def _practioner(self): 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( From 5bb41fc36bd74f3ce054349c7417e90c829f0d01 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 1 Sep 2023 18:11:00 +0530 Subject: [PATCH 5/8] update validation --- .../api/serializers/patient_consultation.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 21d9c2ae37..9f04ed8237 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -148,20 +148,6 @@ def validate_bed_number(self, bed_number): bed_number = None return bed_number - def validate_verified_by(self, verified_by): - if not verified_by.user_type == User.TYPE_VALUE_MAP["Doctor"]: - raise ValidationError("Only Doctors can verify a Consultation") - - if ( - verified_by.home_facility - and str(verified_by.home_facility.external_id) - != self.context["request"].data["facility"] - ): - raise ValidationError( - "Home Facility of the Doctor must be the same as the Consultation Facility" - ) - return verified_by - def update(self, instance, validated_data): instance.last_edited_by = self.context["request"].user @@ -332,6 +318,20 @@ def validate(self, attrs): validated = super().validate(attrs) # TODO Add Bed Authorisation Validation + 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( From 6a67600c3084d03c22362f20dddce90b9ae0d7ad Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 1 Sep 2023 18:35:06 +0530 Subject: [PATCH 6/8] add tests --- .../tests/test_patient_consultation_api.py | 43 ++++++++++++++----- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 48de90f695..c98ed783ac 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 + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + def test_discharge_as_recovered_preadmission(self): consultation = self.create_admission_consultation( suggestion="A", From 0074fd327943f6ebd37156a0c4747e023e8319b5 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 6 Sep 2023 16:40:29 +0530 Subject: [PATCH 7/8] merge migrations --- care/facility/migrations/0384_merge_20230906_1640.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 care/facility/migrations/0384_merge_20230906_1640.py diff --git a/care/facility/migrations/0384_merge_20230906_1640.py b/care/facility/migrations/0384_merge_20230906_1640.py new file mode 100644 index 0000000000..a36bd034d7 --- /dev/null +++ b/care/facility/migrations/0384_merge_20230906_1640.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.2 on 2023-09-06 11:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0383_patientconsultation_icd11_principal_diagnosis"), + ("facility", "0383_patientconsultation_verified_by"), + ] + + operations = [] From f12b408de31feccfb6f7397c8278857f9fa31a75 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Wed, 6 Sep 2023 18:55:09 +0530 Subject: [PATCH 8/8] rebase migrations --- care/facility/migrations/0384_merge_20230906_1640.py | 12 ------------ ...by.py => 0384_patientconsultation_verified_by.py} | 2 +- 2 files changed, 1 insertion(+), 13 deletions(-) delete mode 100644 care/facility/migrations/0384_merge_20230906_1640.py rename care/facility/migrations/{0383_patientconsultation_verified_by.py => 0384_patientconsultation_verified_by.py} (90%) diff --git a/care/facility/migrations/0384_merge_20230906_1640.py b/care/facility/migrations/0384_merge_20230906_1640.py deleted file mode 100644 index a36bd034d7..0000000000 --- a/care/facility/migrations/0384_merge_20230906_1640.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-09-06 11:10 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0383_patientconsultation_icd11_principal_diagnosis"), - ("facility", "0383_patientconsultation_verified_by"), - ] - - operations = [] diff --git a/care/facility/migrations/0383_patientconsultation_verified_by.py b/care/facility/migrations/0384_patientconsultation_verified_by.py similarity index 90% rename from care/facility/migrations/0383_patientconsultation_verified_by.py rename to care/facility/migrations/0384_patientconsultation_verified_by.py index c2477047d7..dbb85f5795 100644 --- a/care/facility/migrations/0383_patientconsultation_verified_by.py +++ b/care/facility/migrations/0384_patientconsultation_verified_by.py @@ -8,7 +8,7 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0382_assetservice_remove_asset_last_serviced_on_and_more"), + ("facility", "0383_patientconsultation_icd11_principal_diagnosis"), ] operations = [