From 7e6e862a3f07c779af62d06aa1e56d9ba357f796 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Wed, 17 Jul 2024 23:05:59 +0530 Subject: [PATCH 1/6] Adds a last online filter for users --- care/users/api/viewsets/users.py | 12 +++++++ care/users/tests/test_api.py | 55 +++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index bbf587dec7..d581b170c0 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -1,6 +1,9 @@ +from datetime import timedelta + from django.core.cache import cache from django.db.models import F, Q, Subquery from django.http import Http404 +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 @@ -57,6 +60,7 @@ class UserFilterSet(filters.FilterSet): home_facility = filters.UUIDFilter( field_name="home_facility__external_id", lookup_expr="exact" ) + last_active_days = filters.CharFilter(method="last_active_after") def get_user_type( self, @@ -71,6 +75,14 @@ def get_user_type( user_type = filters.CharFilter(method="get_user_type", field_name="user_type") + def last_active_after(self, queryset, name, value): + if value == "never": + return queryset.filter(last_login__isnull=True) + # convert days to date + date = timezone.now() - timedelta(days=int(value)) + + return queryset.filter(last_login__gte=date) + class UserViewSet( mixins.RetrieveModelMixin, diff --git a/care/users/tests/test_api.py b/care/users/tests/test_api.py index 6ad1342f93..bcd7216e0d 100644 --- a/care/users/tests/test_api.py +++ b/care/users/tests/test_api.py @@ -1,5 +1,6 @@ -from datetime import date +from datetime import date, timedelta +from django.utils import timezone from rest_framework import status from rest_framework.test import APITestCase @@ -200,3 +201,55 @@ def test_user_cannot_delete_others(self): self.data_2[field], User.objects.get(username=self.data_2[field]).username, ) + + +class TestUserFilter(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.user_1 = cls.create_user("staff1", cls.district, home_facility=cls.facility) + + cls.user_2 = cls.create_user("staff2", cls.district, home_facility=cls.facility) + + cls.user_3 = cls.create_user("staff3", cls.district, home_facility=cls.facility) + + def setUp(self): + self.client.force_authenticate(self.super_user) + self.user_1.last_login = timezone.now() - timedelta(hours=1) + self.user_1.save() + self.user_2.last_login = timezone.now() - timedelta(days=5) + self.user_2.save() + self.user_3.last_login = None + self.user_3.save() + + def test_last_active_filter(self): + """Test last active filter""" + response = self.client.get("/api/v1/users/?last_active_days=1") + self.assertEqual(response.status_code, status.HTTP_200_OK) + res_data_json = response.json() + self.assertEqual(res_data_json["count"], 1) + self.assertIn( + self.user_1.username, {r["username"] for r in res_data_json["results"]} + ) + + response = self.client.get("/api/v1/users/?last_active_days=10") + self.assertEqual(response.status_code, status.HTTP_200_OK) + res_data_json = response.json() + print(res_data_json) + self.assertEqual(res_data_json["count"], 2) + self.assertIn( + self.user_2.username, {r["username"] for r in res_data_json["results"]} + ) + + response = self.client.get("/api/v1/users/?last_active_days=never") + self.assertEqual(response.status_code, status.HTTP_200_OK) + res_data_json = response.json() + self.assertEqual(res_data_json["count"], 1) + self.assertIn( + self.user_3.username, {r["username"] for r in res_data_json["results"]} + ) From d085c2a235bcc4afee79de4145f23188c02c7867 Mon Sep 17 00:00:00 2001 From: Rashmik <146672184+rash-27@users.noreply.github.com> Date: Thu, 18 Jul 2024 15:52:07 +0530 Subject: [PATCH 2/6] add notifications for file upload , prescription (#2070) Co-authored-by: Aakash Singh --- care/facility/api/serializers/file_upload.py | 21 ++++++++++ care/facility/api/viewsets/prescription.py | 17 ++++++++ .../0446_alter_notification_event.py | 41 +++++++++++++++++++ care/facility/models/notification.py | 2 + care/utils/notification_handler.py | 16 ++++++++ 5 files changed, 97 insertions(+) create mode 100644 care/facility/migrations/0446_alter_notification_event.py diff --git a/care/facility/api/serializers/file_upload.py b/care/facility/api/serializers/file_upload.py index 399d7862ce..e1f2c77752 100644 --- a/care/facility/api/serializers/file_upload.py +++ b/care/facility/api/serializers/file_upload.py @@ -6,6 +6,7 @@ from care.facility.api.serializers.shifting import has_facility_permission from care.facility.models.facility import Facility from care.facility.models.file_upload import FileUpload +from care.facility.models.notification import Notification from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import ( PatientConsent, @@ -14,6 +15,7 @@ from care.facility.models.patient_sample import PatientSample from care.users.api.serializers.user import UserBaseMinimumSerializer from care.users.models import User +from care.utils.notification_handler import NotificationGenerator from config.serializers import ChoiceField @@ -156,12 +158,31 @@ def create(self, validated_data): internal_id = check_permissions( validated_data["file_type"], validated_data["associating_id"], user ) + associating_id = validated_data["associating_id"] validated_data["associating_id"] = internal_id validated_data["uploaded_by"] = user validated_data["internal_name"] = validated_data["original_name"] del validated_data["original_name"] file_upload: FileUpload = super().create(validated_data) file_upload.signed_url = file_upload.signed_url(mime_type=mime_type) + if validated_data["file_type"] == FileUpload.FileType.CONSULTATION.value: + consultation = PatientConsultation.objects.get(external_id=associating_id) + NotificationGenerator( + event=Notification.Event.CONSULTATION_FILE_UPLOAD_CREATED, + caused_by=user, + caused_object=consultation, + facility=consultation.facility, + generate_for_facility=True, + ).generate() + if validated_data["file_type"] == FileUpload.FileType.PATIENT.value: + patient = PatientRegistration.objects.get(external_id=associating_id) + NotificationGenerator( + event=Notification.Event.PATIENT_FILE_UPLOAD_CREATED, + caused_by=user, + caused_object=patient, + facility=patient.facility, + generate_for_facility=True, + ).generate() return file_upload diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index e8322aebee..c949e350e5 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -21,9 +21,11 @@ PrescriptionType, generate_choices, ) +from care.facility.models.notification import Notification from care.facility.static_data.medibase import MedibaseMedicine from care.utils.filters.choicefilter import CareChoiceFilter from care.utils.filters.multiselect import MultiSelectFilter +from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.consultation import get_consultation_queryset from care.utils.static_data.helpers import query_builder, token_escaper @@ -120,6 +122,13 @@ def get_queryset(self): def perform_create(self, serializer): consultation_obj = self.get_consultation_obj() + NotificationGenerator( + event=Notification.Event.PATIENT_PRESCRIPTION_CREATED, + caused_by=self.request.user, + caused_object=consultation_obj, + facility=consultation_obj.facility, + generate_for_facility=True, + ).generate() serializer.save(prescribed_by=self.request.user, consultation=consultation_obj) @extend_schema(tags=["prescriptions"]) @@ -133,6 +142,14 @@ def discontinue(self, request, *args, **kwargs): prescription_obj.discontinued_reason = request.data.get( "discontinued_reason", None ) + consultation_obj = self.get_consultation_obj() + NotificationGenerator( + event=Notification.Event.PATIENT_PRESCRIPTION_UPDATED, + caused_by=self.request.user, + caused_object=consultation_obj, + facility=consultation_obj.facility, + generate_for_facility=True, + ).generate() prescription_obj.save() return Response({}, status=status.HTTP_200_OK) diff --git a/care/facility/migrations/0446_alter_notification_event.py b/care/facility/migrations/0446_alter_notification_event.py new file mode 100644 index 0000000000..cc3bd00180 --- /dev/null +++ b/care/facility/migrations/0446_alter_notification_event.py @@ -0,0 +1,41 @@ +# Generated by Django 4.2.10 on 2024-07-18 10:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("facility", "0445_merge_20240715_0301"), + ] + + operations = [ + migrations.AlterField( + model_name="notification", + name="event", + field=models.IntegerField( + choices=[ + (0, "MESSAGE"), + (20, "PATIENT_CREATED"), + (30, "PATIENT_UPDATED"), + (40, "PATIENT_DELETED"), + (50, "PATIENT_CONSULTATION_CREATED"), + (60, "PATIENT_CONSULTATION_UPDATED"), + (70, "PATIENT_CONSULTATION_DELETED"), + (80, "INVESTIGATION_SESSION_CREATED"), + (90, "INVESTIGATION_UPDATED"), + (100, "PATIENT_FILE_UPLOAD_CREATED"), + (110, "CONSULTATION_FILE_UPLOAD_CREATED"), + (120, "PATIENT_CONSULTATION_UPDATE_CREATED"), + (130, "PATIENT_CONSULTATION_UPDATE_UPDATED"), + (140, "PATIENT_CONSULTATION_ASSIGNMENT"), + (200, "SHIFTING_UPDATED"), + (210, "PATIENT_NOTE_ADDED"), + (220, "PUSH_MESSAGE"), + (230, "PATIENT_PRESCRIPTION_CREATED"), + (240, "PATIENT_PRESCRIPTION_UPDATED"), + ], + default=0, + ), + ), + ] diff --git a/care/facility/models/notification.py b/care/facility/models/notification.py index e5bbdba8f3..420699d646 100644 --- a/care/facility/models/notification.py +++ b/care/facility/models/notification.py @@ -39,6 +39,8 @@ class Event(enum.Enum): SHIFTING_UPDATED = 200 PATIENT_NOTE_ADDED = 210 PUSH_MESSAGE = 220 + PATIENT_PRESCRIPTION_CREATED = 230 + PATIENT_PRESCRIPTION_UPDATED = 240 EventChoices = [(e.value, e.name) for e in Event] diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index bbb910fb7f..ea38baf668 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -177,6 +177,10 @@ def generate_system_message(self): message = "Patient {} was deleted by {}".format( self.caused_object.name, self.caused_by.get_full_name() ) + if self.event == Notification.Event.PATIENT_FILE_UPLOAD_CREATED.value: + message = "A file for patient {} was uploaded by {}".format( + self.caused_object.name, self.caused_by.get_full_name() + ) elif isinstance(self.caused_object, PatientConsultation): if self.event == Notification.Event.PATIENT_CONSULTATION_CREATED.value: message = "Consultation for Patient {} was created by {}".format( @@ -190,6 +194,18 @@ def generate_system_message(self): message = "Consultation for Patient {} was deleted by {}".format( self.caused_object.patient.name, self.caused_by.get_full_name() ) + if self.event == Notification.Event.CONSULTATION_FILE_UPLOAD_CREATED.value: + message = "Consultation file for Patient {} was uploaded by {}".format( + self.caused_object.patient.name, self.caused_by.get_full_name() + ) + if self.event == Notification.Event.PATIENT_PRESCRIPTION_CREATED.value: + message = "Prescription for Patient {} was created by {}".format( + self.caused_object.patient.name, self.caused_by.get_full_name() + ) + if self.event == Notification.Event.PATIENT_PRESCRIPTION_UPDATED.value: + message = "Prescription for Patient {} was updated by {}".format( + self.caused_object.patient.name, self.caused_by.get_full_name() + ) elif isinstance(self.caused_object, InvestigationSession): if self.event == Notification.Event.INVESTIGATION_SESSION_CREATED.value: message = ( From d42e76963a966b42630682ca469e7719cfcfc8c1 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 23 Jul 2024 16:47:31 +0530 Subject: [PATCH 3/6] Adds support for phone number in HCX Create Patient Profile (#2319) --- care/hcx/api/viewsets/gateway.py | 62 ++++++++++++++++++++------------ care/hcx/utils/fhir.py | 37 +++++++++++-------- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/care/hcx/api/viewsets/gateway.py b/care/hcx/api/viewsets/gateway.py index a236df222d..a8a71abd0c 100644 --- a/care/hcx/api/viewsets/gateway.py +++ b/care/hcx/api/viewsets/gateway.py @@ -74,16 +74,19 @@ def check_eligibility(self, request): "7894561232", policy["patient_object"]["id"], policy["patient_object"]["name"], - "male" - if policy["patient_object"]["gender"] == 1 - else "female" - if policy["patient_object"]["gender"] == 2 - else "other", + ( + "male" + if policy["patient_object"]["gender"] == 1 + else ( + "female" if policy["patient_object"]["gender"] == 2 else "other" + ) + ), policy["subscriber_id"], policy["policy_id"], policy["id"], policy["id"], policy["id"], + policy["patient_object"]["phone_number"], REVERSE_STATUS_CHOICES[policy["status"]], REVERSE_PRIORITY_CHOICES[policy["priority"]], REVERSE_PURPOSE_CHOICES[policy["purpose"]], @@ -125,17 +128,23 @@ def make_claim(self, request): lambda procedure: { "id": str(uuid()), "name": procedure["procedure"], - "performed": procedure["time"] - if "time" in procedure - else procedure["frequency"], + "performed": ( + procedure["time"] + if "time" in procedure + else procedure["frequency"] + ), "status": ( - "completed" - if datetime.strptime(procedure["time"], "%Y-%m-%dT%H:%M") - < datetime.now() - else "preparation" - ) - if "time" in procedure - else "in-progress", + ( + "completed" + if datetime.strptime( + procedure["time"], "%Y-%m-%dT%H:%M" + ) + < datetime.now() + else "preparation" + ) + if "time" in procedure + else "in-progress" + ), }, consultation.procedure, ) @@ -208,17 +217,22 @@ def make_claim(self, request): "GICOFINDIA", claim["policy_object"]["patient_object"]["id"], claim["policy_object"]["patient_object"]["name"], - "male" - if claim["policy_object"]["patient_object"]["gender"] == 1 - else "female" - if claim["policy_object"]["patient_object"]["gender"] == 2 - else "other", + ( + "male" + if claim["policy_object"]["patient_object"]["gender"] == 1 + else ( + "female" + if claim["policy_object"]["patient_object"]["gender"] == 2 + else "other" + ) + ), claim["policy_object"]["subscriber_id"], claim["policy_object"]["policy_id"], claim["policy_object"]["id"], claim["id"], claim["id"], claim["items"], + claim["policy_object"]["patient_object"]["phone_number"], REVERSE_USE_CHOICES[claim["use"]], REVERSE_STATUS_CHOICES[claim["status"]], REVERSE_CLAIM_TYPE_CHOICES[claim["type"]], @@ -237,9 +251,11 @@ def make_claim(self, request): response = Hcx().generateOutgoingHcxCall( fhirPayload=json.loads(claim_fhir_bundle.json()), - operation=HcxOperations.CLAIM_SUBMIT - if REVERSE_USE_CHOICES[claim["use"]] == "claim" - else HcxOperations.PRE_AUTH_SUBMIT, + operation=( + HcxOperations.CLAIM_SUBMIT + if REVERSE_USE_CHOICES[claim["use"]] == "claim" + else HcxOperations.PRE_AUTH_SUBMIT + ), recipientCode=claim["policy_object"]["insurer_id"], ) diff --git a/care/hcx/utils/fhir.py b/care/hcx/utils/fhir.py index 1ecb041025..3f3b974a57 100644 --- a/care/hcx/utils/fhir.py +++ b/care/hcx/utils/fhir.py @@ -258,7 +258,7 @@ def get_reference_url(self, resource: domainresource.DomainResource): return f"{resource.resource_type}/{resource.id}" def create_patient_profile( - self, id: str, name: str, gender: str, identifier_value: str + self, id: str, name: str, gender: str, phone: str, identifier_value: str ): return patient.Patient( id=id, @@ -280,6 +280,7 @@ def create_patient_profile( ], name=[{"text": name}], gender=gender, + telecom=[{"system": "phone", "value": phone}], ) def create_provider_profile(self, id: str, name: str, identifier_value: str): @@ -558,9 +559,11 @@ def create_claim_profile( category=codeableconcept.CodeableConcept( coding=[ coding.Coding( - system=SYSTEM.claim_item_category_pmjy - if item["category"] == "HBP" - else SYSTEM.claim_item_category, + system=( + SYSTEM.claim_item_category_pmjy + if item["category"] == "HBP" + else SYSTEM.claim_item_category + ), code=item["category"], ) ] @@ -695,6 +698,7 @@ def create_coverage_eligibility_request_bundle( coverage_id: str, eligibility_request_id: str, eligibility_request_identifier_value: str, + patient_phone: str, status="active", priority="normal", purpose="validation", @@ -709,7 +713,7 @@ def create_coverage_eligibility_request_bundle( insurer_id, insurer_name, insurer_identifier_value ) patient = self.create_patient_profile( - patient_id, pateint_name, patient_gender, subscriber_id + patient_id, pateint_name, patient_gender, patient_phone, subscriber_id ) enterer = self.create_practitioner_role_profile( enterer_id, enterer_identifier_value, enterer_speciality, enterer_phone @@ -792,6 +796,7 @@ def create_claim_bundle( claim_id: str, claim_identifier_value: str, items: list[IClaimItem], + patient_phone: str, use="claim", status="active", type="institutional", @@ -810,7 +815,7 @@ def create_claim_bundle( insurer_id, insurer_name, insurer_identifier_value ) patient = self.create_patient_profile( - patient_id, pateint_name, patient_gender, subscriber_id + patient_id, pateint_name, patient_gender, patient_phone, subscriber_id ) coverage = self.create_coverage_profile( coverage_id, @@ -947,15 +952,17 @@ def create_communication_profile( map( lambda content: ( communication.CommunicationPayload( - contentString=content["data"] - if content["type"] == "text" - else None, - contentAttachment=attachment.Attachment( - url=content["data"], - title=content["name"] if content["name"] else None, - ) - if content["type"] == "url" - else None, + contentString=( + content["data"] if content["type"] == "text" else None + ), + contentAttachment=( + attachment.Attachment( + url=content["data"], + title=content["name"] if content["name"] else None, + ) + if content["type"] == "url" + else None + ), ) ), payload, From 14f1fe3ce6570c7af90fa2d79f1b8e6f65a75e0c Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 23 Jul 2024 22:03:52 +0530 Subject: [PATCH 4/6] HCX: Adds "use" in patient profile's telecom (#2321) --- care/hcx/utils/fhir.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/hcx/utils/fhir.py b/care/hcx/utils/fhir.py index 3f3b974a57..bf6ca83558 100644 --- a/care/hcx/utils/fhir.py +++ b/care/hcx/utils/fhir.py @@ -280,7 +280,7 @@ def create_patient_profile( ], name=[{"text": name}], gender=gender, - telecom=[{"system": "phone", "value": phone}], + telecom=[{"system": "phone", "use": "mobile", "value": phone}], ) def create_provider_profile(self, id: str, name: str, identifier_value: str): From cc1185c990c243b539a442d357f4f24752c1a1fc Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Fri, 26 Jul 2024 13:29:42 +0530 Subject: [PATCH 5/6] Fix Scribe on iOS (#2323) --- config/settings/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index c9430e24f8..551f9d1319 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -555,7 +555,7 @@ "audio/midi", "audio/x-midi", "audio/webm", - "audio/mp4" + "audio/mp4", # Documents "text/plain", "text/csv", From eab3290e447737dbce056e4979215d41a9f620b5 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 26 Jul 2024 13:34:46 +0530 Subject: [PATCH 6/6] HCX: Fix payor search filtering and claim supporting info category URL (#2325) --- care/hcx/api/viewsets/gateway.py | 14 ++++++++++++-- care/hcx/utils/fhir.py | 4 +++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/care/hcx/api/viewsets/gateway.py b/care/hcx/api/viewsets/gateway.py index a8a71abd0c..be621f51bf 100644 --- a/care/hcx/api/viewsets/gateway.py +++ b/care/hcx/api/viewsets/gateway.py @@ -322,7 +322,17 @@ def send_communication(self, request): def payors(self, request): payors = Hcx().searchRegistry("roles", "payor")["participants"] - active_payors = list(filter(lambda payor: payor["status"] == "Active", payors)) + result = filter(lambda payor: payor["status"] == "Active", payors) + + if query := request.query_params.get("query"): + query = query.lower() + result = filter( + lambda payor: ( + query in payor["participant_name"].lower() + or query in payor["participant_code"].lower() + ), + result, + ) response = list( map( @@ -330,7 +340,7 @@ def payors(self, request): "name": payor["participant_name"], "code": payor["participant_code"], }, - active_payors, + result, ) ) diff --git a/care/hcx/utils/fhir.py b/care/hcx/utils/fhir.py index bf6ca83558..6b13693fce 100644 --- a/care/hcx/utils/fhir.py +++ b/care/hcx/utils/fhir.py @@ -71,7 +71,9 @@ class SYSTEM: claim_bundle_identifier = "https://www.tmh.in/bundle" coverage_eligibility_request_bundle_identifier = "https://www.tmh.in/bundle" practitioner_speciality = "http://snomed.info/sct" - claim_supporting_info_category = "https://ig.hcxprotocol.io/v0.7.1/ValueSet-claim-supporting-info-categories.html" + claim_supporting_info_category = ( + "http://hcxprotocol.io/codes/claim-supporting-info-categories" + ) related_claim_relationship = ( "http://terminology.hl7.org/CodeSystem/ex-relatedclaimrelationship" )