From 931e61c0835b794b469cdb613d8164a2fdf73e4d Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Sun, 9 Jun 2024 05:00:02 +0530 Subject: [PATCH 1/9] Return files with consent lists and consent filters --- care/facility/api/serializers/patient.py | 17 +- .../api/serializers/patient_consultation.py | 20 ++ care/facility/api/viewsets/patient.py | 53 +++++- care/facility/tests/test_patient_api.py | 172 +++++++++++++++++- 4 files changed, 256 insertions(+), 6 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 048b3ba715..34af0b3d67 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -29,6 +29,7 @@ PatientRegistration, ) from care.facility.models.bed import ConsultationBed +from care.facility.models.file_upload import FileUpload from care.facility.models.notification import Notification from care.facility.models.patient import PatientNotesEdit from care.facility.models.patient_base import ( @@ -37,7 +38,10 @@ DiseaseStatusEnum, NewDischargeReasonEnum, ) -from care.facility.models.patient_consultation import PatientConsultation +from care.facility.models.patient_consultation import ( + PatientConsent, + PatientConsultation, +) from care.facility.models.patient_external_test import PatientExternalTest from care.hcx.models.claim import Claim from care.hcx.models.policy import Policy @@ -75,6 +79,7 @@ class PatientListSerializer(serializers.ModelSerializer): state_object = StateSerializer(source="state", read_only=True) last_consultation = PatientConsultationSerializer(read_only=True) + has_consents = serializers.SerializerMethodField(read_only=True) blood_group = ChoiceField(choices=BLOOD_GROUP_CHOICES, required=True) disease_status = ChoiceField( @@ -101,6 +106,16 @@ def get_has_eligible_policy(self, patient): "get_approved_claim_amount", read_only=True ) + def get_has_consents(self, patient: PatientRegistration): + consents = PatientConsent.objects.filter( + consultation=patient.last_consultation, archived=False + ).values_list("external_id", flat=True) + return FileUpload.objects.filter( + associating_id__in=list(consents), + file_type=FileUpload.FileType.CONSENT_RECORD, + is_archived=False, + ).exists() + def get_approved_claim_amount(self, patient): if patient.last_consultation is not None: claim = ( diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 6c0796328a..c4c34bf596 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -861,6 +861,7 @@ class PatientConsentSerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) created_by = UserBaseMinimumSerializer(read_only=True) archived_by = UserBaseMinimumSerializer(read_only=True) + files = serializers.SerializerMethodField() class Meta: model = PatientConsent @@ -869,6 +870,7 @@ class Meta: "id", "type", "patient_code_status", + "files", "archived", "archived_by", "archived_date", @@ -878,6 +880,7 @@ class Meta: read_only_fields = ( "id", + "files", "created_by", "created_date", "archived", @@ -885,6 +888,23 @@ class Meta: "archived_date", ) + def get_files(self, obj): + from care.facility.api.serializers.file_upload import ( + FileUploadListSerializer, + check_permissions, + ) + + user = self.context["request"].user + file_type = FileUpload.FileType.CONSENT_RECORD + if check_permissions(file_type, obj.external_id, user, "read"): + return FileUploadListSerializer( + FileUpload.objects.filter( + associating_id=obj.external_id, file_type=file_type + ), + many=True, + ).data + return None + def validate_patient_code_status(self, value): if value == PatientCodeStatusType.NOT_SPECIFIED: raise ValidationError( diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 9e8f6d3cf3..14fd57600e 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -68,6 +68,7 @@ ) from care.facility.models.base import covert_choice_dict from care.facility.models.bed import AssetBed +from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( INACTIVE_CONDITION_VERIFICATION_STATUSES, ConditionVerificationStatus, @@ -78,7 +79,10 @@ DISEASE_STATUS_DICT, NewDischargeReasonEnum, ) -from care.facility.models.patient_consultation import PatientConsultation +from care.facility.models.patient_consultation import ( + PatientConsent, + PatientConsultation, +) from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter @@ -279,6 +283,53 @@ def filter_by_diagnoses(self, queryset, name, value): ) return queryset.filter(filter_q) + last_consultation__consent_types = MultiSelectFilter( + method="filter_by_has_consents" + ) + + def filter_by_has_consents(self, queryset, name, value: str): + + if not value: + return queryset + + values = value.split(",") + + filter_q = Q() + + consultation_consent_files = ( + FileUpload.objects.filter( + file_type=FileUpload.FileType.CONSENT_RECORD, + is_archived=False, + ) + .order_by("associating_id") + .distinct("associating_id") + .values_list("associating_id", flat=True) + ) + + consultations = PatientConsent.objects.filter( + external_id__in=[x for x in list(consultation_consent_files)], + archived=False, + ) + + if "None" in values: + filter_q |= ~Q( + last_consultation__id__in=list( + consultations.values_list("consultation_id", flat=True) + ) + ) + values.remove("None") + + if values: + filter_q |= Q( + last_consultation__id__in=list( + consultations.filter(type__in=values).values_list( + "consultation_id", flat=True + ) + ) + ) + + return queryset.filter(filter_q) + class PatientDRYFilter(DRYPermissionFiltersBase): def filter_queryset(self, request, queryset, view): diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index f52f7deb19..ffe9745788 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -5,6 +5,7 @@ from rest_framework.test import APITestCase from care.facility.models import PatientNoteThreadChoices +from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, ICD11Diagnosis, @@ -282,6 +283,90 @@ def test_patient_note_edit(self): self.assertEqual(data[1]["note"], note_content) +class PatientTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + 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.user = cls.create_user( + "doctor1", cls.district, home_facility=cls.facility, user_type=15 + ) + cls.patient = cls.create_patient(cls.district, cls.facility) + cls.consultation = cls.create_consultation( + patient_no="IP5678", + patient=cls.patient, + facility=cls.facility, + created_by=cls.user, + suggestion="A", + encounter_date=now(), + ) + cls.patient_2 = cls.create_patient(cls.district, cls.facility) + cls.consultation_2 = cls.create_consultation( + patient_no="IP5679", + patient=cls.patient_2, + facility=cls.facility, + created_by=cls.user, + suggestion="A", + encounter_date=now(), + ) + + cls.consent = cls.create_patient_consent(cls.consultation, created_by=cls.user) + FileUpload.objects.create( + internal_name="test.pdf", + file_type=FileUpload.FileType.CONSENT_RECORD, + name="Test File", + associating_id=cls.consent.external_id, + file_category=FileUpload.FileCategory.UNSPECIFIED, + ) + + def get_base_url(self) -> str: + return "/api/v1/patient/" + + def test_has_consent(self): + self.client.force_authenticate(user=self.user) + response = self.client.get(self.get_base_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + patient_1_response = response.data["results"][0] + patient_2_response = response.data["results"][1] + self.assertEqual(patient_1_response["has_consents"], True) + self.assertEqual(patient_2_response["has_consents"], False) + + def test_has_consents_archived(self): + self.client.force_authenticate(user=self.user) + self.create_patient_consent( + self.consultation_2, created_by=self.user, is_archived=True + ) + file = FileUpload.objects.create( + internal_name="test.pdf", + file_type=FileUpload.FileType.CONSENT_RECORD, + name="Test File", + associating_id=self.consultation_2.external_id, + file_category=FileUpload.FileCategory.UNSPECIFIED, + ) + response = self.client.get(self.get_base_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + patient_1_response = response.data["results"][0] + patient_2_response = response.data["results"][1] + self.assertEqual(patient_1_response["has_consents"], True) + self.assertEqual(patient_2_response["has_consents"], True) + + file.is_archived = True + file.save() + + response = self.client.get(self.get_base_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["count"], 2) + patient_1_response = response.data["results"][0] + patient_2_response = response.data["results"][1] + self.assertEqual(patient_1_response["has_consents"], True) + self.assertEqual(patient_2_response["has_consents"], False) + + class PatientFilterTestCase(TestUtils, APITestCase): @classmethod def setUpTestData(cls): @@ -331,6 +416,49 @@ def setUpTestData(cls): verification_status=ConditionVerificationStatus.UNCONFIRMED, ) + cls.consent = cls.create_patient_consent( + cls.consultation, created_by=cls.user, type=1, patient_code_status=None + ) + + cls.patient_2 = cls.create_patient(cls.district, cls.facility) + cls.consultation_2 = cls.create_consultation( + patient_no="IP5679", + patient=cls.patient_2, + facility=cls.facility, + created_by=cls.user, + suggestion="A", + encounter_date=now(), + ) + cls.consent2 = cls.create_patient_consent( + cls.consultation_2, created_by=cls.user + ) + + cls.patient_3 = cls.create_patient(cls.district, cls.facility) + cls.consultation_3 = cls.create_consultation( + patient_no="IP5680", + patient=cls.patient_3, + facility=cls.facility, + created_by=cls.user, + suggestion="A", + encounter_date=now(), + ) + + FileUpload.objects.create( + internal_name="test.pdf", + file_type=FileUpload.FileType.CONSENT_RECORD, + name="Test File", + associating_id=cls.consent.external_id, + file_category=FileUpload.FileCategory.UNSPECIFIED, + ) + + FileUpload.objects.create( + internal_name="test.pdf", + file_type=FileUpload.FileType.CONSENT_RECORD, + name="Test File", + associating_id=cls.consent2.external_id, + file_category=FileUpload.FileCategory.UNSPECIFIED, + ) + def get_base_url(self) -> str: return "/api/v1/patient/" @@ -353,10 +481,8 @@ def test_filter_by_location(self): }, ) self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.data["count"], 1) - self.assertEqual( - response.data["results"][0]["id"], str(self.patient.external_id) - ) + self.assertEqual(response.data["count"], 3) + self.assertContains(response, str(self.patient.external_id)) def test_filter_by_diagnoses(self): self.client.force_authenticate(user=self.user) @@ -431,6 +557,44 @@ def test_filter_by_review_missed(self): else: self.assertIsNone(patient["review_time"]) + def test_filter_by_has_consents(self): + + choices = ["1", "2", "3", "4", "5", "None"] + + self.client.force_authenticate(user=self.user) + res = self.client.get( + self.get_base_url(), {"last_consultation__consent_types": choices[5]} + ) + + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.json()["count"], 1) + self.assertContains(res, self.patient_3.external_id) + + res = self.client.get( + self.get_base_url(), + {"last_consultation__consent_types": ",".join(choices[:4])}, + ) + + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.json()["count"], 2) + self.assertContains(res, self.patient.external_id) + self.assertContains(res, self.patient_2.external_id) + + res = self.client.get( + self.get_base_url(), {"last_consultation__consent_types": choices[0]} + ) + + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.json()["count"], 1) + self.assertContains(res, self.patient.external_id) + + res = self.client.get( + self.get_base_url(), {"last_consultation__consent_types": ",".join(choices)} + ) + + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.json()["count"], 3) + class PatientTransferTestCase(TestUtils, APITestCase): @classmethod From c3015bc9ec12e7719d7c4310d73d0f8337efcd7c Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Sun, 9 Jun 2024 05:22:40 +0530 Subject: [PATCH 2/9] fix tests --- care/facility/tests/test_patient_api.py | 42 +++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index ffe9745788..89e08b9f86 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -330,28 +330,42 @@ def test_has_consent(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = response.data["results"][0] - patient_2_response = response.data["results"][1] + patient_1_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient.external_id) + ][0] + patient_2_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient_2.external_id) + ][0] self.assertEqual(patient_1_response["has_consents"], True) self.assertEqual(patient_2_response["has_consents"], False) def test_has_consents_archived(self): self.client.force_authenticate(user=self.user) - self.create_patient_consent( - self.consultation_2, created_by=self.user, is_archived=True - ) + consent = self.create_patient_consent(self.consultation_2, created_by=self.user) file = FileUpload.objects.create( internal_name="test.pdf", file_type=FileUpload.FileType.CONSENT_RECORD, name="Test File", - associating_id=self.consultation_2.external_id, + associating_id=consent.external_id, file_category=FileUpload.FileCategory.UNSPECIFIED, ) response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = response.data["results"][0] - patient_2_response = response.data["results"][1] + patient_1_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient.external_id) + ][0] + patient_2_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient_2.external_id) + ][0] self.assertEqual(patient_1_response["has_consents"], True) self.assertEqual(patient_2_response["has_consents"], True) @@ -361,8 +375,16 @@ def test_has_consents_archived(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = response.data["results"][0] - patient_2_response = response.data["results"][1] + patient_1_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient.external_id) + ][0] + patient_2_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient_2.external_id) + ][0] self.assertEqual(patient_1_response["has_consents"], True) self.assertEqual(patient_2_response["has_consents"], False) From 80f1d1372e4a3120eee39d2cfd20b4e19d311440 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 24 Jun 2024 02:20:56 +0530 Subject: [PATCH 3/9] used boolean field --- care/facility/api/serializers/patient.py | 17 +------ ...tientregistration_has_consents_and_more.py | 47 +++++++++++++++++++ care/facility/models/file_upload.py | 19 ++++++++ care/facility/models/patient.py | 4 ++ 4 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 34af0b3d67..048b3ba715 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -29,7 +29,6 @@ PatientRegistration, ) from care.facility.models.bed import ConsultationBed -from care.facility.models.file_upload import FileUpload from care.facility.models.notification import Notification from care.facility.models.patient import PatientNotesEdit from care.facility.models.patient_base import ( @@ -38,10 +37,7 @@ DiseaseStatusEnum, NewDischargeReasonEnum, ) -from care.facility.models.patient_consultation import ( - PatientConsent, - PatientConsultation, -) +from care.facility.models.patient_consultation import PatientConsultation from care.facility.models.patient_external_test import PatientExternalTest from care.hcx.models.claim import Claim from care.hcx.models.policy import Policy @@ -79,7 +75,6 @@ class PatientListSerializer(serializers.ModelSerializer): state_object = StateSerializer(source="state", read_only=True) last_consultation = PatientConsultationSerializer(read_only=True) - has_consents = serializers.SerializerMethodField(read_only=True) blood_group = ChoiceField(choices=BLOOD_GROUP_CHOICES, required=True) disease_status = ChoiceField( @@ -106,16 +101,6 @@ def get_has_eligible_policy(self, patient): "get_approved_claim_amount", read_only=True ) - def get_has_consents(self, patient: PatientRegistration): - consents = PatientConsent.objects.filter( - consultation=patient.last_consultation, archived=False - ).values_list("external_id", flat=True) - return FileUpload.objects.filter( - associating_id__in=list(consents), - file_type=FileUpload.FileType.CONSENT_RECORD, - is_archived=False, - ).exists() - def get_approved_claim_amount(self, patient): if patient.last_consultation is not None: claim = ( diff --git a/care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py b/care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py new file mode 100644 index 0000000000..7e1a3e8787 --- /dev/null +++ b/care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.10 on 2024-06-23 19:13 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + def migrate_has_consents(apps, schema_editor): + PatientRegistration = apps.get_model("facility", "PatientRegistration") + FileUpload = apps.get_model("facility", "FileUpload") + PatientConsent = apps.get_model("facility", "PatientConsent") + for patient_registration in PatientRegistration.objects.all(): + consents = PatientConsent.objects.filter( + consultation__patient=patient_registration, archived=False + ).values_list("external_id", flat=True) + uuid_array = [str(uuid) for uuid in consents] + has_consents = FileUpload.objects.filter( + associating_id__in=uuid_array, + file_type=7, + is_archived=False, + ).exists() + patient_registration.has_consents = has_consents + patient_registration.save() + + dependencies = [ + ("facility", "0443_remove_patientconsultation_consent_records_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="historicalpatientregistration", + name="has_consents", + field=models.BooleanField( + default=False, verbose_name="Patient has consent files" + ), + ), + migrations.AddField( + model_name="patientregistration", + name="has_consents", + field=models.BooleanField( + default=False, verbose_name="Patient has consent files" + ), + ), + migrations.RunPython( + migrate_has_consents, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 5ac205f82c..a41f163232 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -163,5 +163,24 @@ class FileType(models.IntegerChoices): FileTypeChoices = [(x.value, x.name) for x in FileType] FileCategoryChoices = [(x.value, x.name) for x in BaseFileUpload.FileCategory] + def save(self, *args, **kwargs): + from care.facility.models import PatientConsent # here to avoid circular import + + if self.file_type == self.FileType.CONSENT_RECORD: + other_files = FileUpload.objects.filter( + associating_id=self.associating_id, + file_type=self.FileType.CONSENT_RECORD, + is_archived=False, + ) + if self.pk: + other_files = other_files.exclude(pk=self.pk) + consent = PatientConsent.objects.get(external_id=self.associating_id) + patient_registration = consent.consultation.patient + patient_registration.has_consents = ( + not self.is_archived or other_files.exists() + ) + patient_registration.save() + return super().save(*args, **kwargs) + def __str__(self): return f"{self.FileTypeChoices[self.file_type][1]} - {self.name}{' (Archived)' if self.is_archived else ''}" diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 6e8d020f95..322b599ba6 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -432,6 +432,10 @@ class TestTypeEnum(enum.Enum): AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True ) + has_consents = models.BooleanField( + default=False, verbose_name="Patient has consent files" + ) + history = HistoricalRecords(excluded_fields=["meta_info"]) objects = BaseManager() From bafa39c27cb8b848324907b11b17b7b26c1574f9 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 24 Jun 2024 23:15:06 +0530 Subject: [PATCH 4/9] made changes --- care/facility/api/viewsets/patient.py | 3 +- ...tientregistration_has_consents_and_more.py | 47 ---------------- ...tientconsultation_has_consents_and_more.py | 55 +++++++++++++++++++ care/facility/models/file_upload.py | 8 +-- care/facility/models/patient.py | 4 -- care/facility/models/patient_consultation.py | 8 ++- 6 files changed, 66 insertions(+), 59 deletions(-) delete mode 100644 care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py create mode 100644 care/facility/migrations/0444_patientconsultation_has_consents_and_more.py diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 14fd57600e..2096aed894 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -302,12 +302,11 @@ def filter_by_has_consents(self, queryset, name, value: str): is_archived=False, ) .order_by("associating_id") - .distinct("associating_id") .values_list("associating_id", flat=True) ) consultations = PatientConsent.objects.filter( - external_id__in=[x for x in list(consultation_consent_files)], + external_id__in=[x for x in consultation_consent_files], archived=False, ) diff --git a/care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py b/care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py deleted file mode 100644 index 7e1a3e8787..0000000000 --- a/care/facility/migrations/0444_historicalpatientregistration_has_consents_and_more.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 4.2.10 on 2024-06-23 19:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - def migrate_has_consents(apps, schema_editor): - PatientRegistration = apps.get_model("facility", "PatientRegistration") - FileUpload = apps.get_model("facility", "FileUpload") - PatientConsent = apps.get_model("facility", "PatientConsent") - for patient_registration in PatientRegistration.objects.all(): - consents = PatientConsent.objects.filter( - consultation__patient=patient_registration, archived=False - ).values_list("external_id", flat=True) - uuid_array = [str(uuid) for uuid in consents] - has_consents = FileUpload.objects.filter( - associating_id__in=uuid_array, - file_type=7, - is_archived=False, - ).exists() - patient_registration.has_consents = has_consents - patient_registration.save() - - dependencies = [ - ("facility", "0443_remove_patientconsultation_consent_records_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="historicalpatientregistration", - name="has_consents", - field=models.BooleanField( - default=False, verbose_name="Patient has consent files" - ), - ), - migrations.AddField( - model_name="patientregistration", - name="has_consents", - field=models.BooleanField( - default=False, verbose_name="Patient has consent files" - ), - ), - migrations.RunPython( - migrate_has_consents, reverse_code=migrations.RunPython.noop - ), - ] diff --git a/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py b/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py new file mode 100644 index 0000000000..56ff042f4e --- /dev/null +++ b/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py @@ -0,0 +1,55 @@ +# Generated by Django 4.2.10 on 2024-06-24 16:38 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + def migrate_has_consents(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + FileUpload = apps.get_model("facility", "FileUpload") + PatientConsent = apps.get_model("facility", "PatientConsent") + + files = FileUpload.objects.filter( + file_type=7, + is_archived=False, + ).values_list("associating_id", flat=True) + + consents = PatientConsent.objects.filter( + external_id__in=[uuid.UUID(file) for file in files], archived=False + ) + + consultations = PatientConsultation.objects.filter( + consents__in=consents + ).distinct() + + consultations.update(has_consents=True) + + dependencies = [ + ("facility", "0443_remove_patientconsultation_consent_records_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="patientconsultation", + name="has_consents", + field=models.BooleanField( + default=False, verbose_name="Patient has consent files" + ), + ), + migrations.AlterField( + model_name="patientconsent", + name="consultation", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="consents", + to="facility.patientconsultation", + ), + ), + migrations.RunPython( + migrate_has_consents, reverse_code=migrations.RunPython.noop + ), + ] diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index a41f163232..5e2ff1ef67 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -175,11 +175,9 @@ def save(self, *args, **kwargs): if self.pk: other_files = other_files.exclude(pk=self.pk) consent = PatientConsent.objects.get(external_id=self.associating_id) - patient_registration = consent.consultation.patient - patient_registration.has_consents = ( - not self.is_archived or other_files.exists() - ) - patient_registration.save() + consultation = consent.consultation + consultation.has_consents = not self.is_archived or other_files.exists() + consultation.save() return super().save(*args, **kwargs) def __str__(self): diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 322b599ba6..6e8d020f95 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -432,10 +432,6 @@ class TestTypeEnum(enum.Enum): AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True ) - has_consents = models.BooleanField( - default=False, verbose_name="Patient has consent files" - ) - history = HistoricalRecords(excluded_fields=["meta_info"]) objects = BaseManager() diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 889f2a4067..3365d580c4 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -248,6 +248,10 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): prn_prescription = JSONField(default=dict) discharge_advice = JSONField(default=dict) + has_consents = models.BooleanField( + default=False, verbose_name="Patient has consent files" + ) + def get_related_consultation(self): return self @@ -387,7 +391,9 @@ class ConsultationClinician(models.Model): class PatientConsent(BaseModel, ConsultationRelatedPermissionMixin): - consultation = models.ForeignKey(PatientConsultation, on_delete=models.CASCADE) + consultation = models.ForeignKey( + PatientConsultation, on_delete=models.CASCADE, related_name="consents" + ) type = models.IntegerField(choices=ConsentType.choices) patient_code_status = models.IntegerField( choices=PatientCodeStatusType.choices, null=True, blank=True From f15a6fbff31722811e9d87c971d08754fb3af38f Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 24 Jun 2024 23:26:15 +0530 Subject: [PATCH 5/9] fixed tests --- care/facility/tests/test_patient_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 89e08b9f86..8f25ddc0e8 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -340,8 +340,8 @@ def test_has_consent(self): for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) ][0] - self.assertEqual(patient_1_response["has_consents"], True) - self.assertEqual(patient_2_response["has_consents"], False) + self.assertEqual(patient_1_response["last_consultation"]["has_consents"], True) + self.assertEqual(patient_2_response["last_consultation"]["has_consents"], False) def test_has_consents_archived(self): self.client.force_authenticate(user=self.user) @@ -366,8 +366,8 @@ def test_has_consents_archived(self): for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) ][0] - self.assertEqual(patient_1_response["has_consents"], True) - self.assertEqual(patient_2_response["has_consents"], True) + self.assertEqual(patient_1_response["last_consultation"]["has_consents"], True) + self.assertEqual(patient_2_response["last_consultation"]["has_consents"], True) file.is_archived = True file.save() @@ -385,8 +385,8 @@ def test_has_consents_archived(self): for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) ][0] - self.assertEqual(patient_1_response["has_consents"], True) - self.assertEqual(patient_2_response["has_consents"], False) + self.assertEqual(patient_1_response["last_consultation"]["has_consents"], True) + self.assertEqual(patient_2_response["last_consultation"]["has_consents"], False) class PatientFilterTestCase(TestUtils, APITestCase): From fada1368a2c3eb735d04be7e726f9c09f4e0e292 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 2 Jul 2024 17:18:06 +0530 Subject: [PATCH 6/9] minor fixes --- care/facility/api/viewsets/patient.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 2096aed894..fcdc343a52 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -306,13 +306,13 @@ def filter_by_has_consents(self, queryset, name, value: str): ) consultations = PatientConsent.objects.filter( - external_id__in=[x for x in consultation_consent_files], + external_id__in=consultation_consent_files, archived=False, ) if "None" in values: filter_q |= ~Q( - last_consultation__id__in=list( + last_consultation__id__in=( consultations.values_list("consultation_id", flat=True) ) ) @@ -320,7 +320,7 @@ def filter_by_has_consents(self, queryset, name, value: str): if values: filter_q |= Q( - last_consultation__id__in=list( + last_consultation__id__in=( consultations.filter(type__in=values).values_list( "consultation_id", flat=True ) From e45b4306e07a2d5786d5468557e6db4c14bf92b6 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 2 Jul 2024 18:29:57 +0530 Subject: [PATCH 7/9] revert change --- care/facility/api/viewsets/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index fcdc343a52..bbeb774344 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -306,7 +306,7 @@ def filter_by_has_consents(self, queryset, name, value: str): ) consultations = PatientConsent.objects.filter( - external_id__in=consultation_consent_files, + external_id__in=[x for x in consultation_consent_files], archived=False, ) From 02a2170d8c9cf7ae36f46264374a81667d865025 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Fri, 5 Jul 2024 05:28:59 +0530 Subject: [PATCH 8/9] switched to array for inexpensive queries --- care/facility/api/viewsets/patient.py | 30 +------- ...tientconsultation_has_consents_and_more.py | 58 ++++++++++----- care/facility/models/file_upload.py | 44 +++++++++--- care/facility/models/patient_consultation.py | 21 +++--- care/facility/tests/test_patient_api.py | 71 +++++++++++++++---- 5 files changed, 146 insertions(+), 78 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index bbeb774344..be47a8f29d 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -68,7 +68,6 @@ ) from care.facility.models.base import covert_choice_dict from care.facility.models.bed import AssetBed -from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( INACTIVE_CONDITION_VERIFICATION_STATUSES, ConditionVerificationStatus, @@ -79,10 +78,7 @@ DISEASE_STATUS_DICT, NewDischargeReasonEnum, ) -from care.facility.models.patient_consultation import ( - PatientConsent, - PatientConsultation, -) +from care.facility.models.patient_consultation import PatientConsultation from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter @@ -296,35 +292,15 @@ def filter_by_has_consents(self, queryset, name, value: str): filter_q = Q() - consultation_consent_files = ( - FileUpload.objects.filter( - file_type=FileUpload.FileType.CONSENT_RECORD, - is_archived=False, - ) - .order_by("associating_id") - .values_list("associating_id", flat=True) - ) - - consultations = PatientConsent.objects.filter( - external_id__in=[x for x in consultation_consent_files], - archived=False, - ) - if "None" in values: filter_q |= ~Q( - last_consultation__id__in=( - consultations.values_list("consultation_id", flat=True) - ) + last_consultation__has_consents__len__gt=0, ) values.remove("None") if values: filter_q |= Q( - last_consultation__id__in=( - consultations.filter(type__in=values).values_list( - "consultation_id", flat=True - ) - ) + last_consultation__has_consents__overlap=values, ) return queryset.filter(filter_q) diff --git a/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py b/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py index 56ff042f4e..9126f9b48f 100644 --- a/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py +++ b/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py @@ -1,32 +1,44 @@ -# Generated by Django 4.2.10 on 2024-06-24 16:38 +# Generated by Django 4.2.10 on 2024-07-04 16:20 import uuid +import django.contrib.postgres.fields import django.db.models.deletion from django.db import migrations, models +from django.db.models import Subquery class Migration(migrations.Migration): def migrate_has_consents(apps, schema_editor): - PatientConsultation = apps.get_model("facility", "PatientConsultation") FileUpload = apps.get_model("facility", "FileUpload") PatientConsent = apps.get_model("facility", "PatientConsent") - files = FileUpload.objects.filter( - file_type=7, - is_archived=False, - ).values_list("associating_id", flat=True) - - consents = PatientConsent.objects.filter( - external_id__in=[uuid.UUID(file) for file in files], archived=False - ) - - consultations = PatientConsultation.objects.filter( - consents__in=consents - ).distinct() - - consultations.update(has_consents=True) + consents = PatientConsent.objects.filter(archived=False) + for consent in consents: + consultation = consent.consultation + consent_types = ( + PatientConsent.objects.filter(consultation=consultation, archived=False) + .annotate( + str_external_id=models.functions.Cast( + "external_id", models.CharField() + ) + ) + .annotate( + has_files=models.Exists( + FileUpload.objects.filter( + associating_id=models.OuterRef("str_external_id"), + file_type=7, + is_archived=False, + ) + ) + ) + .filter(has_files=True) + .distinct("type") + .values_list("type", flat=True) + ) + consultation.has_consents = list(consent_types) + consultation.save() dependencies = [ ("facility", "0443_remove_patientconsultation_consent_records_and_more"), @@ -36,8 +48,18 @@ def migrate_has_consents(apps, schema_editor): migrations.AddField( model_name="patientconsultation", name="has_consents", - field=models.BooleanField( - default=False, verbose_name="Patient has consent files" + field=django.contrib.postgres.fields.ArrayField( + base_field=models.IntegerField( + choices=[ + (1, "Consent for Admission"), + (2, "Patient Code Status"), + (3, "Consent for Procedure"), + (4, "High Risk Consent"), + (5, "Others"), + ] + ), + default=list, + size=None, ), ), migrations.AlterField( diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 5e2ff1ef67..2b71c65d46 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -1,4 +1,5 @@ import time +import uuid from uuid import uuid4 import boto3 @@ -164,20 +165,43 @@ class FileType(models.IntegerChoices): FileCategoryChoices = [(x.value, x.name) for x in BaseFileUpload.FileCategory] def save(self, *args, **kwargs): - from care.facility.models import PatientConsent # here to avoid circular import + from care.facility.models import PatientConsent if self.file_type == self.FileType.CONSENT_RECORD: - other_files = FileUpload.objects.filter( - associating_id=self.associating_id, - file_type=self.FileType.CONSENT_RECORD, - is_archived=False, - ) - if self.pk: - other_files = other_files.exclude(pk=self.pk) - consent = PatientConsent.objects.get(external_id=self.associating_id) + new_consent = False + if not self.pk and not self.is_archived: + new_consent = True + consent = PatientConsent.objects.filter( + external_id=uuid.UUID(self.associating_id), archived=False + ).first() consultation = consent.consultation - consultation.has_consents = not self.is_archived or other_files.exists() + consent_types = ( + PatientConsent.objects.filter(consultation=consultation, archived=False) + .annotate( + str_external_id=models.functions.Cast( + "external_id", models.CharField() + ) + ) + .annotate( + has_files=( + models.Exists( + FileUpload.objects.filter( + associating_id=models.OuterRef("str_external_id"), + file_type=self.FileType.CONSENT_RECORD, + is_archived=False, + ).exclude(pk=self.pk if self.is_archived else None) + ) + if not new_consent + else models.Value(True) + ) + ) + .filter(has_files=True) + .distinct("type") + .values_list("type", flat=True) + ) + consultation.has_consents = list(consent_types) consultation.save() + return super().save(*args, **kwargs) def __str__(self): diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 3365d580c4..fa092905ea 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -29,6 +29,14 @@ from care.utils.models.base import BaseModel +class ConsentType(models.IntegerChoices): + CONSENT_FOR_ADMISSION = 1, "Consent for Admission" + PATIENT_CODE_STATUS = 2, "Patient Code Status" + CONSENT_FOR_PROCEDURE = 3, "Consent for Procedure" + HIGH_RISK_CONSENT = 4, "High Risk Consent" + OTHERS = 5, "Others" + + class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): SUGGESTION_CHOICES = [ (SuggestionChoices.HI, "HOME ISOLATION"), @@ -248,8 +256,9 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): prn_prescription = JSONField(default=dict) discharge_advice = JSONField(default=dict) - has_consents = models.BooleanField( - default=False, verbose_name="Patient has consent files" + has_consents = ArrayField( + models.IntegerField(choices=ConsentType.choices), + default=list, ) def get_related_consultation(self): @@ -363,14 +372,6 @@ def has_object_generate_discharge_summary_permission(self, request): return self.has_object_read_permission(request) -class ConsentType(models.IntegerChoices): - CONSENT_FOR_ADMISSION = 1, "Consent for Admission" - PATIENT_CODE_STATUS = 2, "Patient Code Status" - CONSENT_FOR_PROCEDURE = 3, "Consent for Procedure" - HIGH_RISK_CONSENT = 4, "High Risk Consent" - OTHERS = 5, "Others" - - class PatientCodeStatusType(models.IntegerChoices): NOT_SPECIFIED = 0, "Not Specified" DNH = 1, "Do Not Hospitalize" diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 8f25ddc0e8..919fdb2ee3 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -11,6 +11,7 @@ ICD11Diagnosis, ) from care.facility.models.patient_base import NewDischargeReasonEnum +from care.facility.models.patient_consultation import ConsentType, PatientCodeStatusType from care.utils.tests.test_utils import TestUtils @@ -313,12 +314,17 @@ def setUpTestData(cls): encounter_date=now(), ) - cls.consent = cls.create_patient_consent(cls.consultation, created_by=cls.user) - FileUpload.objects.create( + cls.consent = cls.create_patient_consent( + cls.consultation, + created_by=cls.user, + type=ConsentType.CONSENT_FOR_ADMISSION, + patient_code_status=None, + ) + cls.file = FileUpload.objects.create( internal_name="test.pdf", file_type=FileUpload.FileType.CONSENT_RECORD, name="Test File", - associating_id=cls.consent.external_id, + associating_id=str(cls.consent.external_id), file_category=FileUpload.FileCategory.UNSPECIFIED, ) @@ -340,17 +346,41 @@ def test_has_consent(self): for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) ][0] - self.assertEqual(patient_1_response["last_consultation"]["has_consents"], True) - self.assertEqual(patient_2_response["last_consultation"]["has_consents"], False) + self.assertEqual( + patient_1_response["last_consultation"]["has_consents"], + [ConsentType.CONSENT_FOR_ADMISSION], + ) + self.assertEqual(patient_2_response["last_consultation"]["has_consents"], []) + + def test_consent_edit(self): + self.file.name = "Test File 1 Edited" + self.file.save() + self.client.force_authenticate(user=self.user) + response = self.client.get(self.get_base_url()) + self.assertEqual(response.status_code, status.HTTP_200_OK) + patient_1_response = [ + x + for x in response.data["results"] + if x["id"] == str(self.patient.external_id) + ][0] + self.assertEqual( + patient_1_response["last_consultation"]["has_consents"], + [ConsentType.CONSENT_FOR_ADMISSION], + ) def test_has_consents_archived(self): self.client.force_authenticate(user=self.user) - consent = self.create_patient_consent(self.consultation_2, created_by=self.user) + consent = self.create_patient_consent( + self.consultation_2, + created_by=self.user, + type=ConsentType.HIGH_RISK_CONSENT, + patient_code_status=None, + ) file = FileUpload.objects.create( internal_name="test.pdf", file_type=FileUpload.FileType.CONSENT_RECORD, name="Test File", - associating_id=consent.external_id, + associating_id=str(consent.external_id), file_category=FileUpload.FileCategory.UNSPECIFIED, ) response = self.client.get(self.get_base_url()) @@ -366,8 +396,14 @@ def test_has_consents_archived(self): for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) ][0] - self.assertEqual(patient_1_response["last_consultation"]["has_consents"], True) - self.assertEqual(patient_2_response["last_consultation"]["has_consents"], True) + self.assertEqual( + patient_1_response["last_consultation"]["has_consents"], + [ConsentType.CONSENT_FOR_ADMISSION], + ) + self.assertEqual( + patient_2_response["last_consultation"]["has_consents"], + [ConsentType.HIGH_RISK_CONSENT], + ) file.is_archived = True file.save() @@ -385,8 +421,11 @@ def test_has_consents_archived(self): for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) ][0] - self.assertEqual(patient_1_response["last_consultation"]["has_consents"], True) - self.assertEqual(patient_2_response["last_consultation"]["has_consents"], False) + self.assertEqual( + patient_1_response["last_consultation"]["has_consents"], + [ConsentType.CONSENT_FOR_ADMISSION], + ) + self.assertEqual(patient_2_response["last_consultation"]["has_consents"], []) class PatientFilterTestCase(TestUtils, APITestCase): @@ -439,7 +478,10 @@ def setUpTestData(cls): ) cls.consent = cls.create_patient_consent( - cls.consultation, created_by=cls.user, type=1, patient_code_status=None + cls.consultation, + created_by=cls.user, + type=1, + patient_code_status=None, ) cls.patient_2 = cls.create_patient(cls.district, cls.facility) @@ -452,7 +494,10 @@ def setUpTestData(cls): encounter_date=now(), ) cls.consent2 = cls.create_patient_consent( - cls.consultation_2, created_by=cls.user + cls.consultation_2, + created_by=cls.user, + type=ConsentType.PATIENT_CODE_STATUS, + patient_code_status=PatientCodeStatusType.ACTIVE_TREATMENT, ) cls.patient_3 = cls.create_patient(cls.district, cls.facility) From 5b743016077c21fb0ced672a7196d0767cf41b72 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Fri, 5 Jul 2024 05:43:11 +0530 Subject: [PATCH 9/9] fix --- care/facility/tests/test_patient_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 919fdb2ee3..f703bc640b 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -514,7 +514,7 @@ def setUpTestData(cls): internal_name="test.pdf", file_type=FileUpload.FileType.CONSENT_RECORD, name="Test File", - associating_id=cls.consent.external_id, + associating_id=str(cls.consent.external_id), file_category=FileUpload.FileCategory.UNSPECIFIED, ) @@ -522,7 +522,7 @@ def setUpTestData(cls): internal_name="test.pdf", file_type=FileUpload.FileType.CONSENT_RECORD, name="Test File", - associating_id=cls.consent2.external_id, + associating_id=str(cls.consent2.external_id), file_category=FileUpload.FileCategory.UNSPECIFIED, )