From db7b2caf0c4936919b352d8afc53afad61f65fb0 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 12 Dec 2022 21:29:02 +0530 Subject: [PATCH 001/180] Basic Setup; Aadhaar Registration --- care/abdm/__init__.py | 0 care/abdm/admin.py | 1 + care/abdm/api/__init__.py | 0 care/abdm/api/serializers/auth.py | 13 ++ care/abdm/api/serializers/healthid.py | 124 ++++++++++++++++ care/abdm/api/viewsets/healthid.py | 126 ++++++++++++++++ care/abdm/apps.py | 13 ++ care/abdm/migrations/__init__.py | 0 care/abdm/models.py | 3 + care/abdm/tests.py | 3 + care/abdm/utils/api_call.py | 137 ++++++++++++++++++ care/abdm/views.py | 1 + .../migrations/0329_auto_20221212_1112.py | 35 +++++ care/facility/models/patient.py | 9 ++ care/utils/assetintegration/onvif.py | 11 +- config/api_router.py | 4 + config/settings/base.py | 4 + requirements/local.txt | 2 +- 18 files changed, 483 insertions(+), 3 deletions(-) create mode 100644 care/abdm/__init__.py create mode 100644 care/abdm/admin.py create mode 100644 care/abdm/api/__init__.py create mode 100644 care/abdm/api/serializers/auth.py create mode 100644 care/abdm/api/serializers/healthid.py create mode 100644 care/abdm/api/viewsets/healthid.py create mode 100644 care/abdm/apps.py create mode 100644 care/abdm/migrations/__init__.py create mode 100644 care/abdm/models.py create mode 100644 care/abdm/tests.py create mode 100644 care/abdm/utils/api_call.py create mode 100644 care/abdm/views.py create mode 100644 care/facility/migrations/0329_auto_20221212_1112.py diff --git a/care/abdm/__init__.py b/care/abdm/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/abdm/admin.py b/care/abdm/admin.py new file mode 100644 index 0000000000..846f6b4061 --- /dev/null +++ b/care/abdm/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/care/abdm/api/__init__.py b/care/abdm/api/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py new file mode 100644 index 0000000000..6c4180aa8d --- /dev/null +++ b/care/abdm/api/serializers/auth.py @@ -0,0 +1,13 @@ +from rest_framework.serializers import CharField, IntegerField, Serializer + + +class AbdmAuthResponseSerializer(Serializer): + """ + Serializer for the response of the authentication API + """ + + accessToken = CharField() + refreshToken = CharField() + expiresIn = IntegerField() + refreshExpiresIn = IntegerField() + tokenType = CharField() diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py new file mode 100644 index 0000000000..c66264e1bc --- /dev/null +++ b/care/abdm/api/serializers/healthid.py @@ -0,0 +1,124 @@ +from rest_framework.serializers import CharField, Serializer + + +class AadharOtpGenerateRequestPayloadSerializer(Serializer): + aadhaar = CharField( + max_length=16, + min_length=12, + required=True, + help_text="Aadhaar Number", + validators=[], + ) + + +class AadharOtpResendRequestPayloadSerializer(Serializer): + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + +class GenerateMobileOtpRequestPayloadSerializer(Serializer): + mobile = CharField( + max_length=10, + min_length=10, + required=True, + help_text="Mobile Number", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + +class VerifyOtpRequestPayloadSerializer(Serializer): + otp = CharField( + max_length=6, + min_length=6, + required=True, + help_text="OTP", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + +# { +# "email": "Example@Demo.com", +# "firstName": "manoj", +# "healthId": "deepak.pant", +# "lastName": "singh", +# "middleName": "kishan", +# "password": "India@143", +# "profilePhoto": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkJCQkJCQoLCwoODw0PDhQSERESFB4WFxYXFh4uHSEdHSEdLikxKCUoMSlJOTMzOUlUR0NHVGZbW2aBeoGoqOIBCQkJCQkJCgsLCg4PDQ8OFBIRERIUHhYXFhcWHi4dIR0dIR0uKTEoJSgxKUk5MzM5SVRHQ0dUZltbZoF6gaio4v/CABEIBLAHgAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAACAwABBAUGB//aAAgBAQAAAADwawLpMspcK7qrlE5F0Vtul2bVywMUNeBHUkW/bmxvYELGuNjh2VDvixxo5ViljKjDRMoahCULjs2JCShjhjh2OGxo0Y2MoXHOLszsKLhw7tD99mpZQxj8xceofmLEKFwXLTIyHwY1Ls+iEotjHY0M0pjRYxtGj4VFKLPohQlFQyy4Qipc0XG9pS+CP/2Q==", +# "txnId": "a825f76b-0696-40f3-864c-5a3a5b389a83" +# } +class CreateHealthIdSerializer(Serializer): + email = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Email", + validators=[], + ) + firstName = CharField( + max_length=64, + min_length=1, + required=False, + help_text="First Name", + validators=[], + ) + healthId = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Health ID", + validators=[], + ) + lastName = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Last Name", + validators=[], + ) + middleName = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Middle Name", + validators=[], + ) + password = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Password", + validators=[], + ) + profilePhoto = CharField( + max_length=64, + min_length=1, + required=False, + help_text="Profile Photo", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="PreVerified Transaction ID", + validators=[], + ) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py new file mode 100644 index 0000000000..7ee04793f7 --- /dev/null +++ b/care/abdm/api/viewsets/healthid.py @@ -0,0 +1,126 @@ +# ABDM HealthID APIs + +from drf_yasg.utils import swagger_auto_schema +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.api.serializers.healthid import ( + AadharOtpGenerateRequestPayloadSerializer, + AadharOtpResendRequestPayloadSerializer, + CreateHealthIdSerializer, + GenerateMobileOtpRequestPayloadSerializer, + VerifyOtpRequestPayloadSerializer, +) +from care.abdm.utils.api_call import HealthIdGateway + + +# API for Generating OTP for HealthID +class ABDMHealthIDViewSet(GenericViewSet): + base_name = "healthid" + + @swagger_auto_schema( + operation_id="generate_aadhaar_otp", + request_body=AadharOtpGenerateRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def generate_aadhaar_otp(self, request): + data = request.data + serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().generate_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/resendAadhaarOtp + operation_id="resend_aadhaar_otp", + request_body=AadharOtpResendRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def resend_aadhaar_otp(self, request): + data = request.data + serializer = AadharOtpResendRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().resend_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/verifyAadhaarOtp + operation_id="verify_aadhaar_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def verify_aadhaar_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().verify_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/generateMobileOTP + operation_id="generate_mobile_otp", + request_body=GenerateMobileOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def generate_mobile_otp(self, request): + data = request.data + serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().generate_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/verifyMobileOTP + operation_id="verify_mobile_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def verify_mobile_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().verify_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) + + @swagger_auto_schema( + # /v1/registration/aadhaar/createHealthId + operation_id="create_health_id", + request_body=CreateHealthIdSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def create_health_id(self, request): + data = request.data + serializer = CreateHealthIdSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().create_health_id(data) + return Response(response, status=status.HTTP_200_OK) + + # HealthID V2 APIs + @swagger_auto_schema( + # /v1/registration/aadhaar/checkAndGenerateMobileOTP + operation_id="check_and_generate_mobile_otp", + request_body=GenerateMobileOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID V2"], + ) + @action(detail=False, methods=["post"]) + def check_and_generate_mobile_otp(self, request): + data = request.data + serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().check_and_generate_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) diff --git a/care/abdm/apps.py b/care/abdm/apps.py new file mode 100644 index 0000000000..f00da32cb9 --- /dev/null +++ b/care/abdm/apps.py @@ -0,0 +1,13 @@ +from django.apps import AppConfig +from django.utils.translation import gettext_lazy as _ + + +class AbdmConfig(AppConfig): + name = "abdm" + verbose_name = _("ABDM Integration") + + def ready(self): + try: + import care.abdm.signals # noqa F401 + except ImportError: + pass diff --git a/care/abdm/migrations/__init__.py b/care/abdm/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/abdm/models.py b/care/abdm/models.py new file mode 100644 index 0000000000..0b4331b362 --- /dev/null +++ b/care/abdm/models.py @@ -0,0 +1,3 @@ +# from django.db import models + +# Create your models here. diff --git a/care/abdm/tests.py b/care/abdm/tests.py new file mode 100644 index 0000000000..a79ca8be56 --- /dev/null +++ b/care/abdm/tests.py @@ -0,0 +1,3 @@ +# from django.test import TestCase + +# Create your tests here. diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py new file mode 100644 index 0000000000..e23612f212 --- /dev/null +++ b/care/abdm/utils/api_call.py @@ -0,0 +1,137 @@ +import json + +import requests +from django.conf import settings +from django.core.cache import cache + +GATEWAY_API_URL = "https://dev.abdm.gov.in/" +HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" +ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" +ABDM_TOKEN_CACHE_KEY = "abdm_token" + + +class APIGateway: + def __init__(self, gateway, token): + if gateway == "health": + self.url = HEALTH_SERVICE_API_URL + else: + self.url = GATEWAY_API_URL + self.token = token + + def encrypt(self, data): + cert = cache.get("abdm_cert") + if not cert: + cert = requests.get(settings.ABDM_CERT_URL).text + cache.set("abdm_cert", cert, 3600) + + def add_auth_header(self, headers): + token = cache.get(ABDM_TOKEN_CACHE_KEY) + print("Cached Token: {}".format(token)) + if not token: + data = { + "clientId": settings.ABDM_CLIENT_ID, + "clientSecret": settings.ABDM_CLIENT_SECRET, + } + auth_headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + resp = requests.post( + ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers + ) + print("Token Response Status: {}".format(resp.status_code)) + if resp.status_code < 300: + # Checking if Content-Type is application/json + if resp.headers["Content-Type"] != "application/json": + print( + "Unsupported Content-Type: {}".format( + resp.headers["Content-Type"] + ) + ) + print("Response: {}".format(resp.text)) + return None + else: + data = resp.json() + token = data["accessToken"] + expires_in = data["expiresIn"] + print("New Token: {}".format(token)) + print("Expires in: {}".format(expires_in)) + cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) + else: + print("Bad Response: {}".format(resp.text)) + return None + print("Returning Authorization Header: Bearer {}".format(token)) + auth_header = {"Authorization": "Bearer {}".format(token)} + return {**headers, **auth_header} + + def get(self, path, params=None): + url = self.url + path + headers = {} + headers = self.add_auth_header(headers) + response = requests.get(url, headers=headers, params=params) + return response + + def post(self, path, data=None): + url = self.url + path + headers = { + "Content-Type": "application/json", + "accept": "*/*", + "Accept-Language": "en-US", + } + headers = self.add_auth_header(headers) + headers_string = " ".join( + ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] + ) + data_json = json.dumps(data) + print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + response = requests.post(url, headers=headers, data=data_json) + return response + + +class HealthIdGateway: + def __init__(self): + self.api = APIGateway("health", None) + + def generate_aadhaar_otp(self, data): + path = "/v1/registration/aadhaar/generateOtp" + response = self.api.post(path, data) + print("{} Response: {}".format(response.status_code, response.text)) + return response.json() + + def resend_aadhaar_otp(self, data): + path = "/v1/registration/aadhaar/resendAadhaarOtp" + response = self.api.post(path, data) + return response.json() + + def verify_aadhaar_otp(self, data): + path = "/v1/registration/aadhaar/verifyOTP" + response = self.api.post(path, data) + return response.json() + + def generate_mobile_otp(self, data): + path = "/v1/registration/aadhaar/generateMobileOTP" + response = self.api.post(path, data) + return response.json() + + # /v1/registration/aadhaar/verifyMobileOTP + def verify_mobile_otp(self, data): + path = "/v1/registration/aadhaar/verifyMobileOTP" + response = self.api.post(path, data) + return response.json() + + # /v1/registration/aadhaar/createHealthIdWithPreVerified + def create_health_id(self, data): + path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" + response = self.api.post(path, data) + return response.json() + + +class HealthIdGatewayV2: + def __init__(self): + self.api = APIGateway("health", None) + + # V2 APIs + def check_and_generate_mobile_otp(self, data): + path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" + response = self.api.post(path, data) + return response.json() diff --git a/care/abdm/views.py b/care/abdm/views.py new file mode 100644 index 0000000000..60f00ef0ef --- /dev/null +++ b/care/abdm/views.py @@ -0,0 +1 @@ +# Create your views here. diff --git a/care/facility/migrations/0329_auto_20221212_1112.py b/care/facility/migrations/0329_auto_20221212_1112.py new file mode 100644 index 0000000000..fd68457684 --- /dev/null +++ b/care/facility/migrations/0329_auto_20221212_1112.py @@ -0,0 +1,35 @@ +# Generated by Django 2.2.11 on 2022-12-12 05:42 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0328_merge_20221208_1110'), + ] + + operations = [ + migrations.AddField( + model_name='historicalpatientregistration', + name='abha_number', + field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), + ), + migrations.AddField( + model_name='patientregistration', + name='abha_number', + field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), + ), + migrations.AlterField( + model_name='fileupload', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='fileupload', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 618cc5929e..a628e6abec 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -391,6 +391,15 @@ class TestTypeEnum(enum.Enum): related_name="root_patient_assigned_to", ) + # ABDM Health ID + abha_number = models.CharField( + max_length=255, + default=None, + verbose_name="ABHA Number", + null=True, + blank=True, + ) + history = HistoricalRecords(excluded_fields=["patient_search_id", "meta_info"]) objects = BaseManager() diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index f029e6c4dc..ecdae6d179 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -14,6 +14,8 @@ class OnvifActions(enum.Enum): GOTO_PRESET = "goto_preset" ABSOLUTE_MOVE = "absolute_move" RELATIVE_MOVE = "relative_move" + # STEP 1 | Part 1 + # GET_STREAMING_TOKEN = "getStreamingToken" def __init__(self, meta): try: @@ -23,7 +25,8 @@ def __init__(self, meta): self.access_key = self.meta["camera_access_key"].split(":")[2] except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args)) + dict((key, f"{key} not found in asset metadata") for key in e.args) + ) def handle_action(self, action): action_type = action["type"] @@ -35,7 +38,7 @@ def handle_action(self, action): "username": self.username, "password": self.password, "accessKey": self.access_key, - **action_data + **action_data, } if action_type == self.OnvifActions.GET_CAMERA_STATUS.value: @@ -53,4 +56,8 @@ def handle_action(self, action): if action_type == self.OnvifActions.RELATIVE_MOVE.value: return self.api_post(self.get_url("relativeMove"), request_body) + # STEP 1 | Part 3 + # if action_type == self.OnvifActions.GET_STREAMING_TOKEN.value: + # return self.api_post(self.get_url("getStreamingToken"), request_body) + raise ValidationError({"action": "invalid action type"}) diff --git a/config/api_router.py b/config/api_router.py index dd949a3a1d..8fbc3bd85c 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,6 +3,7 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_nested.routers import NestedSimpleRouter +from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, AmbulanceViewSet, @@ -193,6 +194,9 @@ # Public endpoints router.register("public/asset", AssetPublicViewSet) +# ABDM endpoints +router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") + app_name = "api" urlpatterns = [ url(r"^", include(router.urls)), diff --git a/config/settings/base.py b/config/settings/base.py index 31d0d5ccc6..6e0cbcc6e0 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -103,6 +103,7 @@ "care.users.apps.UsersConfig", "care.facility", "care.audit_log.apps.AuditLogConfig", + "care.abdm.apps.AbdmConfig", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -508,3 +509,6 @@ def GETKEY(group, request): JWKS = JsonWebKey.import_key_set( json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks()))) ) + +ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") +ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") diff --git a/requirements/local.txt b/requirements/local.txt index 84dcf6d7a7..cc56584e89 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -2,7 +2,7 @@ -r ./docs.txt Werkzeug==1.0.0 # https://github.com/pallets/werkzeug ipdb==0.13.2 # https://github.com/gotcha/ipdb -psycopg2-binary==2.8.4 # https://github.com/psycopg/psycopg2 +psycopg2-binary==2.8.6 # https://github.com/psycopg/psycopg2 # Code quality # ------------------------------------------------------------------------------ isort==5.10.1 # https://github.com/PyCQA/isort From 517adf46df04985a66e2dbe9979bc8ae3e59b9aa Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 19 Dec 2022 17:43:27 +0400 Subject: [PATCH 002/180] Add models --- care/abdm/api/viewsets/healthid.py | 8 +++++++- care/abdm/models.py | 22 ++++++++++++++++++++++ care/abdm/utils/api_call.py | 10 ++++++---- 3 files changed, 35 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 7ee04793f7..2b01d587b4 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -3,6 +3,7 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.decorators import action +from rest_framework.mixins import CreateModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -13,12 +14,16 @@ GenerateMobileOtpRequestPayloadSerializer, VerifyOtpRequestPayloadSerializer, ) +from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import HealthIdGateway # API for Generating OTP for HealthID -class ABDMHealthIDViewSet(GenericViewSet): +class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" + model = AbhaNumber + # Override Create method + # def create(self, request, *args, **kwargs): @swagger_auto_schema( operation_id="generate_aadhaar_otp", @@ -107,6 +112,7 @@ def create_health_id(self, request): serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().create_health_id(data) + print(response.status_code) return Response(response, status=status.HTTP_200_OK) # HealthID V2 APIs diff --git a/care/abdm/models.py b/care/abdm/models.py index 0b4331b362..6d5d3f07dd 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -1,3 +1,25 @@ # from django.db import models # Create your models here. + +from django.db import models + +from care.utils.models.base import BaseModel + + +class AbhaNumber(BaseModel): + abha_number = models.CharField(max_length=50) + email = models.CharField(max_length=50) + first_name = models.CharField(max_length=50) + health_id = models.CharField(max_length=50) + last_name = models.CharField(max_length=50) + middle_name = models.CharField(max_length=50) + password = models.CharField(max_length=50) + profile_photo = models.CharField(max_length=50) + txn_id = models.CharField(max_length=50) + + access_token = models.CharField(max_length=50) + refresh_token = models.CharField(max_length=50) + + def __str__(self): + return self.abha_number diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index e23612f212..efc5f72f44 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -79,12 +79,14 @@ def post(self, path, data=None): "Accept-Language": "en-US", } headers = self.add_auth_header(headers) - headers_string = " ".join( - ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] - ) + # headers_string = " ".join( + # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] + # ) data_json = json.dumps(data) - print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + print("Posting Request to: {}".format(url)) response = requests.post(url, headers=headers, data=data_json) + print("{} Response: {}".format(response.status_code, response.text)) return response From ba2076330aeeea8afd46d6ce98120662993f994e Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 19 Dec 2022 19:55:56 +0530 Subject: [PATCH 003/180] Fix models issue --- .../migrations/0329_auto_20221212_1112.py | 35 ------------------- config/settings/base.py | 4 +-- 2 files changed, 2 insertions(+), 37 deletions(-) delete mode 100644 care/facility/migrations/0329_auto_20221212_1112.py diff --git a/care/facility/migrations/0329_auto_20221212_1112.py b/care/facility/migrations/0329_auto_20221212_1112.py deleted file mode 100644 index fd68457684..0000000000 --- a/care/facility/migrations/0329_auto_20221212_1112.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-12 05:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('facility', '0328_merge_20221208_1110'), - ] - - operations = [ - migrations.AddField( - model_name='historicalpatientregistration', - name='abha_number', - field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), - ), - migrations.AddField( - model_name='patientregistration', - name='abha_number', - field=models.CharField(blank=True, default=None, max_length=255, null=True, verbose_name='ABHA Number'), - ), - migrations.AlterField( - model_name='fileupload', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='fileupload', - name='uploaded_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/config/settings/base.py b/config/settings/base.py index 6e0cbcc6e0..705ab9d694 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -103,7 +103,7 @@ "care.users.apps.UsersConfig", "care.facility", "care.audit_log.apps.AuditLogConfig", - "care.abdm.apps.AbdmConfig", + "care.abdm", ] # https://docs.djangoproject.com/en/dev/ref/settings/#installed-apps INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS @@ -269,7 +269,7 @@ "formatters": { "verbose": { "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" + "%(process)d %(thread)d %(message)s" } }, "handlers": { From c2e0a61e8ed928ba6a13c4cb2fd006aeb0e7fbf4 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Tue, 20 Dec 2022 23:37:33 +0530 Subject: [PATCH 004/180] Fixed patient to abha integration --- care/abdm/api/serializers/healthid.py | 44 +------------ care/abdm/api/viewsets/healthid.py | 32 +++++++++- care/abdm/migrations/0001_initial.py | 39 ++++++++++++ .../migrations/0002_auto_20221220_2312.py | 33 ++++++++++ .../migrations/0003_auto_20221220_2321.py | 22 +++++++ .../migrations/0004_auto_20221220_2325.py | 53 ++++++++++++++++ .../migrations/0005_auto_20221220_2327.py | 63 +++++++++++++++++++ care/abdm/models.py | 21 +++---- care/abdm/utils/api_call.py | 2 + .../migrations/0329_auto_20221219_1936.py | 25 ++++++++ .../migrations/0330_auto_20221220_2312.py | 25 ++++++++ care/facility/models/patient.py | 10 +-- 12 files changed, 306 insertions(+), 63 deletions(-) create mode 100644 care/abdm/migrations/0001_initial.py create mode 100644 care/abdm/migrations/0002_auto_20221220_2312.py create mode 100644 care/abdm/migrations/0003_auto_20221220_2321.py create mode 100644 care/abdm/migrations/0004_auto_20221220_2325.py create mode 100644 care/abdm/migrations/0005_auto_20221220_2327.py create mode 100644 care/facility/migrations/0329_auto_20221219_1936.py create mode 100644 care/facility/migrations/0330_auto_20221220_2312.py diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index c66264e1bc..0a164a5b1f 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -66,20 +66,6 @@ class VerifyOtpRequestPayloadSerializer(Serializer): # "txnId": "a825f76b-0696-40f3-864c-5a3a5b389a83" # } class CreateHealthIdSerializer(Serializer): - email = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Email", - validators=[], - ) - firstName = CharField( - max_length=64, - min_length=1, - required=False, - help_text="First Name", - validators=[], - ) healthId = CharField( max_length=64, min_length=1, @@ -87,34 +73,6 @@ class CreateHealthIdSerializer(Serializer): help_text="Health ID", validators=[], ) - lastName = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Last Name", - validators=[], - ) - middleName = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Middle Name", - validators=[], - ) - password = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Password", - validators=[], - ) - profilePhoto = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Profile Photo", - validators=[], - ) txnId = CharField( max_length=64, min_length=1, @@ -122,3 +80,5 @@ class CreateHealthIdSerializer(Serializer): help_text="PreVerified Transaction ID", validators=[], ) + patientId = CharField(required=True, help_text="Patient ID to be linked", + validators=[]) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 2b01d587b4..e6fe40b916 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -3,6 +3,7 @@ from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError from rest_framework.mixins import CreateModelMixin from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -16,15 +17,15 @@ ) from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import HealthIdGateway +from care.utils.queryset.patient import get_patient_queryset # API for Generating OTP for HealthID class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" model = AbhaNumber - # Override Create method - # def create(self, request, *args, **kwargs): + # TODO: Ratelimiting for all endpoints that generate OTP's / Critical API's @swagger_auto_schema( operation_id="generate_aadhaar_otp", request_body=AadharOtpGenerateRequestPayloadSerializer, @@ -111,8 +112,33 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) + patient_id = data.pop("patientId") + allowed_patients = get_patient_queryset(request.user) + patient_obj = allowed_patients.filter(external_id=patient_id).first() + if not patient_obj: + raise ValidationError({"patient": "Not Found"}) response = HealthIdGateway().create_health_id(data) - print(response.status_code) + abha_object = AbhaNumber.objects.filter(abha_number=response["healthIdNumber"]).first() + if abha_object: + # Flow when abha number exists in db somehow! + pass + else: + # Create abha number flow + abha_object = AbhaNumber() + abha_object.abha_number = response["healthIdNumber"] + abha_object.email = response["email"] + abha_object.first_name = response["firstName"] + abha_object.health_id = response["healthId"] + abha_object.last_name = response["lastName"] + abha_object.middle_name = response["middleName"] + abha_object.profile_photo = response["profilePhoto"] + abha_object.txn_id = response["healthIdNumber"] + abha_object.access_token = response["token"] + abha_object.refresh_token = data["txnId"] + abha_object.save() + + patient_obj.abha_number = abha_object + patient_obj.save() return Response(response, status=status.HTTP_200_OK) # HealthID V2 APIs diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py new file mode 100644 index 0000000000..20784886b7 --- /dev/null +++ b/care/abdm/migrations/0001_initial.py @@ -0,0 +1,39 @@ +# Generated by Django 2.2.11 on 2022-12-19 14:06 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='AbhaNumber', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), + ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), + ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), + ('deleted', models.BooleanField(db_index=True, default=False)), + ('abha_number', models.CharField(max_length=50)), + ('email', models.CharField(max_length=50)), + ('first_name', models.CharField(max_length=50)), + ('health_id', models.CharField(max_length=50)), + ('last_name', models.CharField(max_length=50)), + ('middle_name', models.CharField(max_length=50)), + ('password', models.CharField(max_length=50)), + ('profile_photo', models.CharField(max_length=50)), + ('txn_id', models.CharField(max_length=50)), + ('access_token', models.CharField(max_length=50)), + ('refresh_token', models.CharField(max_length=50)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/abdm/migrations/0002_auto_20221220_2312.py b/care/abdm/migrations/0002_auto_20221220_2312.py new file mode 100644 index 0000000000..fff590d3d5 --- /dev/null +++ b/care/abdm/migrations/0002_auto_20221220_2312.py @@ -0,0 +1,33 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:42 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='abhanumber', + name='email', + field=models.EmailField(max_length=254), + ), + migrations.AlterField( + model_name='abhanumber', + name='first_name', + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name='abhanumber', + name='last_name', + field=models.CharField(max_length=512), + ), + migrations.AlterField( + model_name='abhanumber', + name='middle_name', + field=models.CharField(max_length=512), + ), + ] diff --git a/care/abdm/migrations/0003_auto_20221220_2321.py b/care/abdm/migrations/0003_auto_20221220_2321.py new file mode 100644 index 0000000000..7e2fd3121b --- /dev/null +++ b/care/abdm/migrations/0003_auto_20221220_2321.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:51 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0002_auto_20221220_2312'), + ] + + operations = [ + migrations.RemoveField( + model_name='abhanumber', + name='password', + ), + migrations.AlterField( + model_name='abhanumber', + name='profile_photo', + field=models.TextField(), + ), + ] diff --git a/care/abdm/migrations/0004_auto_20221220_2325.py b/care/abdm/migrations/0004_auto_20221220_2325.py new file mode 100644 index 0000000000..7e90626f80 --- /dev/null +++ b/care/abdm/migrations/0004_auto_20221220_2325.py @@ -0,0 +1,53 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0003_auto_20221220_2321'), + ] + + operations = [ + migrations.AlterField( + model_name='abhanumber', + name='abha_number', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='access_token', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='first_name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='health_id', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='last_name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='middle_name', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='refresh_token', + field=models.TextField(), + ), + migrations.AlterField( + model_name='abhanumber', + name='txn_id', + field=models.TextField(), + ), + ] diff --git a/care/abdm/migrations/0005_auto_20221220_2327.py b/care/abdm/migrations/0005_auto_20221220_2327.py new file mode 100644 index 0000000000..f9781a1dbf --- /dev/null +++ b/care/abdm/migrations/0005_auto_20221220_2327.py @@ -0,0 +1,63 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:57 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0004_auto_20221220_2325'), + ] + + operations = [ + migrations.AlterField( + model_name='abhanumber', + name='abha_number', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='access_token', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='first_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='health_id', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='last_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='middle_name', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='profile_photo', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='refresh_token', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='abhanumber', + name='txn_id', + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/care/abdm/models.py b/care/abdm/models.py index 6d5d3f07dd..d931cc98c6 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -8,18 +8,17 @@ class AbhaNumber(BaseModel): - abha_number = models.CharField(max_length=50) - email = models.CharField(max_length=50) - first_name = models.CharField(max_length=50) - health_id = models.CharField(max_length=50) - last_name = models.CharField(max_length=50) - middle_name = models.CharField(max_length=50) - password = models.CharField(max_length=50) - profile_photo = models.CharField(max_length=50) - txn_id = models.CharField(max_length=50) + abha_number = models.TextField(null=True, blank=True) + email = models.EmailField(null=True, blank=True) + first_name = models.TextField(null=True, blank=True) + health_id = models.TextField(null=True, blank=True) + last_name = models.TextField(null=True, blank=True) + middle_name = models.TextField(null=True, blank=True) + profile_photo = models.TextField(null=True, blank=True) # What is profile photo? how is it stored as? + txn_id = models.TextField(null=True, blank=True) # 50? - access_token = models.CharField(max_length=50) - refresh_token = models.CharField(max_length=50) + access_token = models.TextField(null=True, blank=True) # 50 seems a bit too low for access tokens + refresh_token = models.TextField(null=True, blank=True) def __str__(self): return self.abha_number diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index efc5f72f44..a793650cfb 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -10,6 +10,8 @@ ABDM_TOKEN_CACHE_KEY = "abdm_token" +# TODO: Exception handling for all api calls, need to gracefully handle known exceptions + class APIGateway: def __init__(self, gateway, token): if gateway == "health": diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py new file mode 100644 index 0000000000..7468cd13ec --- /dev/null +++ b/care/facility/migrations/0329_auto_20221219_1936.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.11 on 2022-12-19 14:06 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0328_merge_20221208_1110'), + ] + + operations = [ + migrations.AlterField( + model_name='fileupload', + name='archived_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), + ), + migrations.AlterField( + model_name='fileupload', + name='uploaded_by', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0330_auto_20221220_2312.py new file mode 100644 index 0000000000..0d972fe276 --- /dev/null +++ b/care/facility/migrations/0330_auto_20221220_2312.py @@ -0,0 +1,25 @@ +# Generated by Django 2.2.11 on 2022-12-20 17:42 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('abdm', '0002_auto_20221220_2312'), + ('facility', '0329_auto_20221219_1936'), + ] + + operations = [ + migrations.AddField( + model_name='historicalpatientregistration', + name='abha_number', + field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='abdm.AbhaNumber'), + ), + migrations.AddField( + model_name='patientregistration', + name='abha_number', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='abdm.AbhaNumber'), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index a628e6abec..6997027460 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -7,6 +7,7 @@ from partial_index import PQ, PartialIndex from simple_history.models import HistoricalRecords +from care.abdm.models import AbhaNumber from care.facility.models import ( DISEASE_CHOICES, DiseaseStatusEnum, @@ -392,13 +393,8 @@ class TestTypeEnum(enum.Enum): ) # ABDM Health ID - abha_number = models.CharField( - max_length=255, - default=None, - verbose_name="ABHA Number", - null=True, - blank=True, - ) + + abha_number = models.OneToOneField(AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True) history = HistoricalRecords(excluded_fields=["patient_search_id", "meta_info"]) From d7cd31b221d3126c67e8e832b53a5cf7425170d0 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Tue, 20 Dec 2022 23:37:44 +0530 Subject: [PATCH 005/180] Fixed patient to abha integration --- care/abdm/migrations/0001_initial.py | 4 ++-- care/facility/migrations/0329_auto_20221219_1936.py | 9 +++++---- care/facility/migrations/0330_auto_20221220_2312.py | 10 ++++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py index 20784886b7..cb8481db18 100644 --- a/care/abdm/migrations/0001_initial.py +++ b/care/abdm/migrations/0001_initial.py @@ -1,11 +1,11 @@ # Generated by Django 2.2.11 on 2022-12-19 14:06 -from django.db import migrations, models import uuid +from django.db import migrations, models + class Migration(migrations.Migration): - initial = True dependencies = [ diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py index 7468cd13ec..c0b161de69 100644 --- a/care/facility/migrations/0329_auto_20221219_1936.py +++ b/care/facility/migrations/0329_auto_20221219_1936.py @@ -1,12 +1,11 @@ # Generated by Django 2.2.11 on 2022-12-19 14:06 +import django.db.models.deletion from django.conf import settings from django.db import migrations, models -import django.db.models.deletion class Migration(migrations.Migration): - dependencies = [ ('facility', '0328_merge_20221208_1110'), ] @@ -15,11 +14,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='fileupload', name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='archived_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='archived_by', to=settings.AUTH_USER_MODEL), ), migrations.AlterField( model_name='fileupload', name='uploaded_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, + related_name='uploaded_by', to=settings.AUTH_USER_MODEL), ), ] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0330_auto_20221220_2312.py index 0d972fe276..53e3038ed7 100644 --- a/care/facility/migrations/0330_auto_20221220_2312.py +++ b/care/facility/migrations/0330_auto_20221220_2312.py @@ -1,11 +1,10 @@ # Generated by Django 2.2.11 on 2022-12-20 17:42 -from django.db import migrations, models import django.db.models.deletion +from django.db import migrations, models class Migration(migrations.Migration): - dependencies = [ ('abdm', '0002_auto_20221220_2312'), ('facility', '0329_auto_20221219_1936'), @@ -15,11 +14,14 @@ class Migration(migrations.Migration): migrations.AddField( model_name='historicalpatientregistration', name='abha_number', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='abdm.AbhaNumber'), + field=models.ForeignKey(blank=True, db_constraint=False, null=True, + on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', + to='abdm.AbhaNumber'), ), migrations.AddField( model_name='patientregistration', name='abha_number', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='abdm.AbhaNumber'), + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, + to='abdm.AbhaNumber'), ), ] From 51916373ef7f7b176103500b8a41bf56f8ba29f7 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Sun, 1 Jan 2023 20:44:24 +0530 Subject: [PATCH 006/180] Add V2 API's --- care/abdm/api/viewsets/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e6fe40b916..63577d83bb 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -16,7 +16,7 @@ VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway +from care.abdm.utils.api_call import HealthIdGateway, HealthIdGatewayV2 from care.utils.queryset.patient import get_patient_queryset @@ -37,7 +37,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_aadhaar_otp(data) + response = HealthIdGatewayV2().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( From 354d58ce823a4e65c0d6cef84b4960228b26a8d2 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Sun, 1 Jan 2023 20:44:34 +0530 Subject: [PATCH 007/180] Add V2 API's --- care/abdm/utils/api_call.py | 43 +++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index a793650cfb..b5cccd5a39 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -9,9 +9,21 @@ ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" ABDM_TOKEN_CACHE_KEY = "abdm_token" - # TODO: Exception handling for all api calls, need to gracefully handle known exceptions +from Crypto.Cipher import PKCS1_OAEP +from Crypto.PublicKey import RSA +from base64 import b64encode + + +def encrypt_with_public_key(a_message): + rsa_public_key = RSA.importKey(requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert", verify=False).text.strip()) + rsa_public_key = PKCS1_OAEP.new(rsa_public_key) + encrypted_text = rsa_public_key.encrypt(a_message.encode()) + + return b64encode(encrypted_text).decode() + + class APIGateway: def __init__(self, gateway, token): if gateway == "health": @@ -20,11 +32,11 @@ def __init__(self, gateway, token): self.url = GATEWAY_API_URL self.token = token - def encrypt(self, data): - cert = cache.get("abdm_cert") - if not cert: - cert = requests.get(settings.ABDM_CERT_URL).text - cache.set("abdm_cert", cert, 3600) + # def encrypt(self, data): + # cert = cache.get("abdm_cert") + # if not cert: + # cert = requests.get(settings.ABDM_CERT_URL).text + # cache.set("abdm_cert", cert, 3600) def add_auth_header(self, headers): token = cache.get(ABDM_TOKEN_CACHE_KEY) @@ -39,7 +51,7 @@ def add_auth_header(self, headers): "Accept": "application/json", } resp = requests.post( - ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers + ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers, verify=False ) print("Token Response Status: {}".format(resp.status_code)) if resp.status_code < 300: @@ -70,7 +82,7 @@ def get(self, path, params=None): url = self.url + path headers = {} headers = self.add_auth_header(headers) - response = requests.get(url, headers=headers, params=params) + response = requests.get(url, headers=headers, params=params, verify=False) return response def post(self, path, data=None): @@ -87,7 +99,7 @@ def post(self, path, data=None): data_json = json.dumps(data) # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) print("Posting Request to: {}".format(url)) - response = requests.post(url, headers=headers, data=data_json) + response = requests.post(url, headers=headers, data=data_json, verify=False) print("{} Response: {}".format(response.status_code, response.text)) return response @@ -135,7 +147,16 @@ def __init__(self): self.api = APIGateway("health", None) # V2 APIs - def check_and_generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" + def generate_aadhaar_otp(self, data): + path = "/v2/registration/aadhaar/generateOtp" + data["aadhaar"] = encrypt_with_public_key(data["aadhaar"]) + data.pop("cancelToken", {}) + response = self.api.post(path, data) + return response.json() + + def generate_mobile_otp(self, data): + path = "/v2/registration/aadhaar/generateOtp" + data["aadhaar"] = encrypt_with_public_key(data["aadhaar"]) + data.pop("cancelToken", {}) response = self.api.post(path, data) return response.json() From 1307f0049bd16f8e9a69d2278829e00b1a625d67 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 2 Jan 2023 11:33:11 +0300 Subject: [PATCH 008/180] Skip SSL Verification --- care/abdm/utils/api_call.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index a793650cfb..56c2b77e45 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -12,6 +12,7 @@ # TODO: Exception handling for all api calls, need to gracefully handle known exceptions + class APIGateway: def __init__(self, gateway, token): if gateway == "health": @@ -39,7 +40,10 @@ def add_auth_header(self, headers): "Accept": "application/json", } resp = requests.post( - ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers + ABDM_TOKEN_URL, + data=json.dumps(data), + headers=auth_headers, + verify=False, ) print("Token Response Status: {}".format(resp.status_code)) if resp.status_code < 300: @@ -70,7 +74,7 @@ def get(self, path, params=None): url = self.url + path headers = {} headers = self.add_auth_header(headers) - response = requests.get(url, headers=headers, params=params) + response = requests.get(url, headers=headers, params=params, verify=False) return response def post(self, path, data=None): @@ -87,7 +91,7 @@ def post(self, path, data=None): data_json = json.dumps(data) # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) print("Posting Request to: {}".format(url)) - response = requests.post(url, headers=headers, data=data_json) + response = requests.post(url, headers=headers, data=data_json, verify=False) print("{} Response: {}".format(response.status_code, response.text)) return response From 430ef5c19051cdab139fe9c8314f37e405c6111a Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 2 Jan 2023 18:46:18 +0530 Subject: [PATCH 009/180] Fix V2 API's --- care/abdm/api/viewsets/healthid.py | 4 ++-- care/abdm/utils/api_call.py | 18 ++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 63577d83bb..9ece094ae2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -37,7 +37,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().generate_aadhaar_otp(data) + response = HealthIdGatewayV2().generate_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -67,7 +67,7 @@ def verify_aadhaar_otp(self, request): data = request.data serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_aadhaar_otp(data) + response = HealthIdGatewayV2().verify_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b5cccd5a39..40e1c07415 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -11,16 +11,15 @@ # TODO: Exception handling for all api calls, need to gracefully handle known exceptions -from Crypto.Cipher import PKCS1_OAEP +from Crypto.Cipher import PKCS1_v1_5 from Crypto.PublicKey import RSA from base64 import b64encode def encrypt_with_public_key(a_message): rsa_public_key = RSA.importKey(requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert", verify=False).text.strip()) - rsa_public_key = PKCS1_OAEP.new(rsa_public_key) + rsa_public_key = PKCS1_v1_5.new(rsa_public_key) encrypted_text = rsa_public_key.encrypt(a_message.encode()) - return b64encode(encrypted_text).decode() @@ -154,9 +153,16 @@ def generate_aadhaar_otp(self, data): response = self.api.post(path, data) return response.json() - def generate_mobile_otp(self, data): - path = "/v2/registration/aadhaar/generateOtp" - data["aadhaar"] = encrypt_with_public_key(data["aadhaar"]) + def generate_document_mobile_otp(self, data): + path = "/v2/document/generate/mobile/otp" + data["mobile"] = "ENTER MOBILE NUMBER HERE" # Hard Coding for test + data.pop("cancelToken", {}) + response = self.api.post(path, data) + return response.json() + + def verify_document_mobile_otp(self, data): + path = "/v2/document/verify/mobile/otp" + data["otp"] = encrypt_with_public_key(data["otp"]) data.pop("cancelToken", {}) response = self.api.post(path, data) return response.json() From 6014b8bcc5f8f47af67db7197ed46a0ed0c1f8b1 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 2 Jan 2023 19:50:20 +0530 Subject: [PATCH 010/180] Fix V2 API's --- care/abdm/api/serializers/abha.py | 0 care/abdm/api/viewsets/abha.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 care/abdm/api/serializers/abha.py create mode 100644 care/abdm/api/viewsets/abha.py diff --git a/care/abdm/api/serializers/abha.py b/care/abdm/api/serializers/abha.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py new file mode 100644 index 0000000000..e69de29bb2 From 99ac633b6481e695e5ad0b3eff897f01c9d0e949 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Mon, 2 Jan 2023 19:50:49 +0530 Subject: [PATCH 011/180] Added More Views --- care/abdm/api/serializers/abha.py | 9 +++++++++ care/abdm/api/viewsets/abha.py | 26 ++++++++++++++++++++++++++ config/api_router.py | 2 ++ 3 files changed, 37 insertions(+) diff --git a/care/abdm/api/serializers/abha.py b/care/abdm/api/serializers/abha.py index e69de29bb2..c4d88dbc0f 100644 --- a/care/abdm/api/serializers/abha.py +++ b/care/abdm/api/serializers/abha.py @@ -0,0 +1,9 @@ +from rest_framework.serializers import ModelSerializer + +from care.abdm.models import AbhaNumber + + +class AbhaSerializer(ModelSerializer): + class Meta: + exclude = ("deleted",) + model = AbhaNumber diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index e69de29bb2..9b2e112354 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -0,0 +1,26 @@ +from rest_framework.decorators import action +from rest_framework.generics import get_object_or_404 +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.api.serializers.abha import AbhaSerializer +from care.abdm.models import AbhaNumber +from care.utils.queryset.patient import get_patient_queryset + + +class AbhaViewSet(GenericViewSet): + serializer_class = AbhaSerializer + model = AbhaNumber + queryset = AbhaNumber.objects.all() + + def get_abha_object(self): + queryset = get_patient_queryset(self.request.user) + patient_obj = get_object_or_404(queryset.filter.filter( + external_id=self.kwargs.get("patient_external_id") + )) + return patient_obj.abha_number + + @action(detail=False, methods=["POST"]) + def something_here(self, request, *args, **kwargs): + obj = self.get_abha_object() + return Response({}) diff --git a/config/api_router.py b/config/api_router.py index 8fbc3bd85c..a6abe88f5c 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,6 +3,7 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_nested.routers import NestedSimpleRouter +from care.abdm.api.viewsets.abha import AbhaViewSet from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, @@ -184,6 +185,7 @@ patient_nested_router.register(r"test_sample", PatientSampleViewSet) patient_nested_router.register(r"investigation", PatientInvestigationSummaryViewSet) patient_nested_router.register(r"notes", PatientNotesViewSet) +patient_nested_router.register(r"abha", AbhaViewSet) consultation_nested_router = NestedSimpleRouter( router, r"consultation", lookup="consultation" From 586ba6506bc99f14bfaf4cef5be9430d84cdcc6d Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 2 Jan 2023 18:44:04 +0300 Subject: [PATCH 012/180] Add ABHA_obj;Ignore HealthID --- care/abdm/api/serializers/abhanumber.py | 10 ++++++++++ care/abdm/utils/api_call.py | 2 ++ care/facility/api/serializers/patient.py | 17 +++++++++++------ 3 files changed, 23 insertions(+), 6 deletions(-) create mode 100644 care/abdm/api/serializers/abhanumber.py diff --git a/care/abdm/api/serializers/abhanumber.py b/care/abdm/api/serializers/abhanumber.py new file mode 100644 index 0000000000..1dfdf95d4e --- /dev/null +++ b/care/abdm/api/serializers/abhanumber.py @@ -0,0 +1,10 @@ +# ModelSerializer +from rest_framework import serializers + +from care.abdm.models import AbhaNumber + + +class AbhaNumberSerializer(serializers.ModelSerializer): + class Meta: + model = AbhaNumber + exclude = ("access_token", "refresh_token", "txn_id") diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 56c2b77e45..e5b0b9f754 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -130,6 +130,8 @@ def verify_mobile_otp(self, data): # /v1/registration/aadhaar/createHealthIdWithPreVerified def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" + print("Creating Health ID with data: {}".format(data)) + data.pop("healthId", None) response = self.api.post(path, data) return response.json() diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 5a0bdbd7c5..91b3d5233e 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -4,6 +4,7 @@ from django.utils.timezone import localtime, make_aware, now from rest_framework import serializers +from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.api.serializers.facility import ( FacilityBasicInfoSerializer, @@ -132,7 +133,9 @@ class Meta: phone_number = PhoneNumberIsPossibleField() - facility = ExternalIdSerializerField(queryset=Facility.objects.all(), required=False) + facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=False + ) medical_history = serializers.ListSerializer( child=MedicalHistorySerializer(), required=False ) @@ -177,6 +180,8 @@ class Meta: queryset=User.objects.all(), required=False, allow_null=True ) + abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True) + class Meta: model = PatientRegistration exclude = ( @@ -386,11 +391,11 @@ class PatientSearchSerializer(serializers.ModelSerializer): class Meta: model = PatientSearch exclude = ( - "date_of_birth", - "year_of_birth", - "external_id", - "id", - ) + TIMESTAMP_FIELDS + "date_of_birth", + "year_of_birth", + "external_id", + "id", + ) + TIMESTAMP_FIELDS class PatientTransferSerializer(serializers.ModelSerializer): From a4c9d2a4cf6f31309bd1dfc731c4808484c9df97 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 5 Jan 2023 10:23:19 +0300 Subject: [PATCH 013/180] Add APIs --- care/abdm/api/serializers/healthid.py | 31 ++++++++- care/abdm/api/viewsets/abha.py | 19 ++++-- care/abdm/api/viewsets/healthid.py | 23 ++++++- care/abdm/utils/api_call.py | 94 +++++++++++++++++++++++++-- 4 files changed, 154 insertions(+), 13 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 0a164a5b1f..daa00189fa 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -21,6 +21,32 @@ class AadharOtpResendRequestPayloadSerializer(Serializer): ) +class HealthIdSerializer(Serializer): + health_id = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Health ID", + ) + + +# "gender": "M", +# "mobile": "9545812125", +# "name": "suraj singh karki", +# "yearOfBirth": "1994" + + +class ABHASearchRequestSerializer: + name = CharField(max_length=64, min_length=1, required=False, help_text="Name") + mobile = CharField( + max_length=10, min_length=10, required=False, help_text="Mobile Number" + ) + gender = CharField(max_length=1, min_length=1, required=False, help_text="Gender") + yearOfBirth = CharField( + max_length=4, min_length=4, required=False, help_text="Year of Birth" + ) + + class GenerateMobileOtpRequestPayloadSerializer(Serializer): mobile = CharField( max_length=10, @@ -80,5 +106,6 @@ class CreateHealthIdSerializer(Serializer): help_text="PreVerified Transaction ID", validators=[], ) - patientId = CharField(required=True, help_text="Patient ID to be linked", - validators=[]) # TODO: Add UUID Validation + patientId = CharField( + required=True, help_text="Patient ID to be linked", validators=[] + ) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index 9b2e112354..649de4c01d 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -5,6 +5,7 @@ from care.abdm.api.serializers.abha import AbhaSerializer from care.abdm.models import AbhaNumber +from care.abdm.utils.api_call import HealthIdGateway from care.utils.queryset.patient import get_patient_queryset @@ -15,12 +16,18 @@ class AbhaViewSet(GenericViewSet): def get_abha_object(self): queryset = get_patient_queryset(self.request.user) - patient_obj = get_object_or_404(queryset.filter.filter( - external_id=self.kwargs.get("patient_external_id") - )) + print( + "Finding patient with external_id: ", self.kwargs.get("patient_external_id") + ) + patient_obj = get_object_or_404( + queryset.filter(external_id=self.kwargs.get("patient_external_id")) + ) return patient_obj.abha_number - @action(detail=False, methods=["POST"]) - def something_here(self, request, *args, **kwargs): + @action(detail=False, methods=["GET"]) + def get_qr_code(self, request, *args, **kwargs): obj = self.get_abha_object() - return Response({}) + gateway = HealthIdGateway() + # Empty Dict as data, obj.access_token as auth + response = gateway.get_qr_code(obj) + return Response(response) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 9ece094ae2..43c30e05b9 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -13,6 +13,7 @@ AadharOtpResendRequestPayloadSerializer, CreateHealthIdSerializer, GenerateMobileOtpRequestPayloadSerializer, + HealthIdSerializer, VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber @@ -118,7 +119,9 @@ def create_health_id(self, request): if not patient_obj: raise ValidationError({"patient": "Not Found"}) response = HealthIdGateway().create_health_id(data) - abha_object = AbhaNumber.objects.filter(abha_number=response["healthIdNumber"]).first() + abha_object = AbhaNumber.objects.filter( + abha_number=response["healthIdNumber"] + ).first() if abha_object: # Flow when abha number exists in db somehow! pass @@ -141,6 +144,24 @@ def create_health_id(self, request): patient_obj.save() return Response(response, status=status.HTTP_200_OK) + # APIs to Find & Link Existing HealthID + # searchByHealthId + @swagger_auto_schema( + # /v1/registration/aadhaar/searchByHealthId + operation_id="search_by_health_id", + request_body=HealthIdSerializer, + responses={"200": "{'status': 'boolean'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def search_by_health_id(self, request): + data = request.data + serializer = HealthIdSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().search_by_health_id(data) + return Response(response, status=status.HTTP_200_OK) + + ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( # /v1/registration/aadhaar/checkAndGenerateMobileOTP diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index eeb0745c23..edde98ec81 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -30,6 +30,8 @@ class APIGateway: def __init__(self, gateway, token): if gateway == "health": self.url = HEALTH_SERVICE_API_URL + elif gateway == "abdm": + self.url = GATEWAY_API_URL else: self.url = GATEWAY_API_URL self.token = token @@ -40,10 +42,19 @@ def __init__(self, gateway, token): # cert = requests.get(settings.ABDM_CERT_URL).text # cache.set("abdm_cert", cert, 3600) + def add_user_header(self, headers, user_token): + headers.update( + { + "X-Token": user_token, + } + ) + return headers + def add_auth_header(self, headers): token = cache.get(ABDM_TOKEN_CACHE_KEY) - print("Cached Token: {}".format(token)) + print("Using Cached Token") if not token: + print("No Token in Cache") data = { "clientId": settings.ABDM_CLIENT_ID, "clientSecret": settings.ABDM_CLIENT_SECRET, @@ -79,18 +90,23 @@ def add_auth_header(self, headers): else: print("Bad Response: {}".format(resp.text)) return None - print("Returning Authorization Header: Bearer {}".format(token)) + # print("Returning Authorization Header: Bearer {}".format(token)) + print("Adding Authorization Header") auth_header = {"Authorization": "Bearer {}".format(token)} return {**headers, **auth_header} - def get(self, path, params=None): + def get(self, path, params=None, auth=None): url = self.url + path headers = {} headers = self.add_auth_header(headers) + if auth: + headers = self.add_user_header(headers, auth) + print("Making GET Request to: {}".format(url)) response = requests.get(url, headers=headers, params=params, verify=False) + print("{} Response: {}".format(response.status_code, response.text)) return response - def post(self, path, data=None): + def post(self, path, data=None, auth=None): url = self.url + path headers = { "Content-Type": "application/json", @@ -98,6 +114,8 @@ def post(self, path, data=None): "Accept-Language": "en-US", } headers = self.add_auth_header(headers) + if auth: + headers = self.add_user_header(headers, auth) # headers_string = " ".join( # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] # ) @@ -148,6 +166,63 @@ def create_health_id(self, data): response = self.api.post(path, data) return response.json() + # /v1/search/existsByHealthId + # API checks if ABHA Address/ABHA Number is reserved/used which includes permanently deleted ABHA Addresses + # Return { status: true } + def exists_by_health_id(self, data): + path = "/v1/search/existsByHealthId" + response = self.api.post(path, data) + return response.json() + + # /v1/search/searchByHealthId + # API returns only Active or Deactive ABHA Number/ Address (Never returns Permanently Deleted ABHA Number/Address) + # Returns { + # "authMethods": [ + # "AADHAAR_OTP" + # ], + # "healthId": "deepakndhm", + # "healthIdNumber": "43-4221-5105-6749", + # "name": "kishan kumar singh", + # "status": "ACTIVE" + # } + def search_by_health_id(self, data): + path = "/v1/search/searchByHealthId" + response = self.api.post(path, data) + return response.json() + + # /v1/search/searchByMobile + def search_by_mobile(self, data): + path = "/v1/search/searchByMobile" + response = self.api.post(path, data) + return response.json() + + # Auth APIs + + # /v1/auth/generate/access-token + def generate_access_token(self, data): + path = "/v1/auth/generate/access-token" + print("Generating Access Token for: {}".format(data["abha_number"])) + response = self.api.post(path, {"refreshToken": data["refresh_token"]}) + return response.json() + + # Account APIs + + # /v1/account/profile + def get_profile(self, data): + path = "/v1/account/profile" + access_token = self.generate_access_token(data) + response = self.api.get(path, {}, access_token) + return response.json() + + # /v1/account/qrCode + def get_qr_code(self, data, auth): + path = "/v1/account/qrCode" + access_token = self.generate_access_token(data) + print("Getting QR Code for: {}".format(data)) + response = self.api.get(path, {}, access_token) + print("QR Code Response: {}".format(response.text)) + return response.json() + class HealthIdGatewayV2: def __init__(self): @@ -174,3 +249,14 @@ def verify_document_mobile_otp(self, data): data.pop("cancelToken", {}) response = self.api.post(path, data) return response.json() + + +class AbdmGateway: + def __init__(self): + self.api = APIGateway("abdm", None) + + # /v0.5/users/auth/fetch-modes + def fetch_modes(self, data): + path = "/v0.5/users/auth/fetch-modes" + response = self.api.post(path, data) + return response.json() From b636084071eaf7ba6b7e0e9d915ca168374516d1 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 5 Jan 2023 11:24:29 +0300 Subject: [PATCH 014/180] Add Init & Verify APIs --- care/abdm/api/serializers/auth.py | 11 +++++++ care/abdm/api/serializers/healthid.py | 19 +++++++++++ care/abdm/api/viewsets/healthid.py | 47 +++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 18 ++++++++++ 4 files changed, 95 insertions(+) diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py index 6c4180aa8d..9b533d0c9b 100644 --- a/care/abdm/api/serializers/auth.py +++ b/care/abdm/api/serializers/auth.py @@ -11,3 +11,14 @@ class AbdmAuthResponseSerializer(Serializer): expiresIn = IntegerField() refreshExpiresIn = IntegerField() tokenType = CharField() + + +class AbdmAuthInitResponseSerializer(Serializer): + """ + Serializer for the response of the authentication API + """ + + token = CharField() + refreshToken = CharField() + expiresIn = IntegerField() + refreshExpiresIn = IntegerField() diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index daa00189fa..e5528ed97a 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -30,6 +30,25 @@ class HealthIdSerializer(Serializer): ) +# { +# "authMethod": "AADHAAR_OTP", +# "healthid": "43-4221-5105-6749" +# } +class HealthIdAuthSerializer(Serializer): + auth_method = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Auth Method", + ) + healthid = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Health ID", + ) + + # "gender": "M", # "mobile": "9545812125", # "name": "suraj singh karki", diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 43c30e05b9..e664c954d0 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -13,6 +13,7 @@ AadharOtpResendRequestPayloadSerializer, CreateHealthIdSerializer, GenerateMobileOtpRequestPayloadSerializer, + HealthIdAuthSerializer, HealthIdSerializer, VerifyOtpRequestPayloadSerializer, ) @@ -161,6 +162,52 @@ def search_by_health_id(self, request): response = HealthIdGateway().search_by_health_id(data) return Response(response, status=status.HTTP_200_OK) + # auth/init + @swagger_auto_schema( + # /v1/auth/init + operation_id="auth_init", + request_body=HealthIdAuthSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def auth_init(self, request): + data = request.data + serializer = HealthIdAuthSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().auth_init(data) + return Response(response, status=status.HTTP_200_OK) + + # /v1/auth/confirmWithAadhaarOtp + @swagger_auto_schema( + operation_id="confirm_with_aadhaar_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def confirm_with_aadhaar_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().confirm_with_aadhaar_otp(data) + return Response(response, status=status.HTTP_200_OK) + + # /v1/auth/confirmWithMobileOtp + @swagger_auto_schema( + operation_id="confirm_with_mobile_otp", + request_body=VerifyOtpRequestPayloadSerializer, + responses={"200": "{'txnId': 'string'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def confirm_with_mobile_otp(self, request): + data = request.data + serializer = VerifyOtpRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().confirm_with_mobile_otp(data) + return Response(response, status=status.HTTP_200_OK) + ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index edde98ec81..6961485fc1 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -198,6 +198,24 @@ def search_by_mobile(self, data): # Auth APIs + # /v1/auth/init + def auth_init(self, data): + path = "/v1/auth/init" + response = self.api.post(path, data) + return response.json() + + # /v1/auth/confirmWithAadhaarOtp + def confirm_with_aadhaar_otp(self, data): + path = "/v1/auth/confirmWithAadhaarOtp" + response = self.api.post(path, data) + return response.json() + + # /v1/auth/confirmWithMobileOTP + def confirm_with_mobile_otp(self, data): + path = "/v1/auth/confirmWithMobileOTP" + response = self.api.post(path, data) + return response.json() + # /v1/auth/generate/access-token def generate_access_token(self, data): path = "/v1/auth/generate/access-token" From 7f6464deaca09dee06ed26f0366246dfd12873be Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 5 Jan 2023 11:40:48 +0300 Subject: [PATCH 015/180] Fix SearchByHealthId Serializer --- care/abdm/api/serializers/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index e5528ed97a..4c0a477c6b 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -22,7 +22,7 @@ class AadharOtpResendRequestPayloadSerializer(Serializer): class HealthIdSerializer(Serializer): - health_id = CharField( + healthId = CharField( max_length=64, min_length=1, required=True, @@ -41,7 +41,7 @@ class HealthIdAuthSerializer(Serializer): required=True, help_text="Auth Method", ) - healthid = CharField( + healthId = CharField( max_length=64, min_length=1, required=True, From 8ec246f77fe33183719e79d9fe551668f2ed5b6e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 5 Jan 2023 19:48:18 +0530 Subject: [PATCH 016/180] renamed attributes in HealthIdAuthSerializer --- care/abdm/api/serializers/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 4c0a477c6b..736da66b94 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -35,13 +35,13 @@ class HealthIdSerializer(Serializer): # "healthid": "43-4221-5105-6749" # } class HealthIdAuthSerializer(Serializer): - auth_method = CharField( + authMethod = CharField( max_length=64, min_length=1, required=True, help_text="Auth Method", ) - healthId = CharField( + healthid = CharField( max_length=64, min_length=1, required=True, From 3abbf8d58cb154d920c8d231b2ce42d864652483 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 6 Jan 2023 01:16:47 +0530 Subject: [PATCH 017/180] fixed get_profile api in abdm --- care/abdm/api/serializers/healthid.py | 3 ++ care/abdm/api/viewsets/healthid.py | 61 ++++++++++++++++++++++++++- care/abdm/utils/api_call.py | 20 +++++++-- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 736da66b94..3c93bf8647 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -98,6 +98,9 @@ class VerifyOtpRequestPayloadSerializer(Serializer): help_text="Transaction ID", validators=[], ) + patientId = CharField( + required=True, help_text="Patient ID to be linked", validators=[] + ) # TODO: Add UUID Validation # { diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e664c954d0..15497e5d2f 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -102,6 +102,29 @@ def verify_mobile_otp(self, request): response = HealthIdGateway().verify_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) + def add_abha_details_to_patient(self, data, patient_obj): + abha_object = AbhaNumber.objects.filter( + abha_number=data["healthIdNumber"] + ).first() + if abha_object: + # Flow when abha number exists in db somehow! + return False + else: + # Create abha number flow + abha_object = AbhaNumber() + abha_object.abha_number = data["healthIdNumber"] + abha_object.email = data["email"] + abha_object.first_name = data["firstName"] + abha_object.health_id = data["healthId"] + abha_object.last_name = data["lastName"] + abha_object.middle_name = data["middleName"] + abha_object.profile_photo = data["profilePhoto"] + abha_object.save() + + patient_obj.abha_number = abha_object + patient_obj.save() + return True + @swagger_auto_schema( # /v1/registration/aadhaar/createHealthId operation_id="create_health_id", @@ -191,7 +214,24 @@ def confirm_with_aadhaar_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) + abha_object = HealthIdGateway().get_profile(response) + + patient_id = data.pop("patientId") + allowed_patients = get_patient_queryset(request.user) + patient_obj = allowed_patients.filter(external_id=patient_id).first() + if not patient_obj: + raise ValidationError({"patient": "Not Found"}) + + if self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response(abha_object, status=status.HTTP_200_OK) + else: + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) # /v1/auth/confirmWithMobileOtp @swagger_auto_schema( @@ -206,7 +246,24 @@ def confirm_with_mobile_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) + abha_object = HealthIdGateway().get_profile(response) + + patient_id = data.pop("patientId") + allowed_patients = get_patient_queryset(request.user) + patient_obj = allowed_patients.filter(external_id=patient_id).first() + if not patient_obj: + raise ValidationError({"patient": "Not Found"}) + + if self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response(abha_object, status=status.HTTP_200_OK) + else: + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) ############################################################################################################ # HealthID V2 APIs diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 6961485fc1..1635b498e6 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -45,7 +45,7 @@ def __init__(self, gateway, token): def add_user_header(self, headers, user_token): headers.update( { - "X-Token": user_token, + "X-Token": "Bearer " + user_token, } ) return headers @@ -218,10 +218,22 @@ def confirm_with_mobile_otp(self, data): # /v1/auth/generate/access-token def generate_access_token(self, data): + if "access_token" in data: + return data["access_token"] + elif "accessToken" in data: + return data["accessToken"] + elif "token" in data: + return data["token"] + + if "refreshToken" in data: + refreshToken = data["refreshToken"] + elif "refresh_token" in data: + refreshToken = data["refresh_token"] + else: + return None path = "/v1/auth/generate/access-token" - print("Generating Access Token for: {}".format(data["abha_number"])) - response = self.api.post(path, {"refreshToken": data["refresh_token"]}) - return response.json() + response = self.api.post(path, {"refreshToken": refreshToken}) + return response.json()["accessToken"] # Account APIs From 5a80d18e1ea5e1dfdd510d072cd356e3dc8062e0 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 9 Jan 2023 12:02:40 +0530 Subject: [PATCH 018/180] Add get_profile API --- care/abdm/api/viewsets/abha.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index 649de4c01d..d6eba3fe8e 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -31,3 +31,11 @@ def get_qr_code(self, request, *args, **kwargs): # Empty Dict as data, obj.access_token as auth response = gateway.get_qr_code(obj) return Response(response) + + @action(detail=False, methods=["GET"]) + def get_profile(self, request, *args, **kwargs): + obj = self.get_abha_object() + gateway = HealthIdGateway() + # Empty Dict as data, obj.access_token as auth + response = gateway.get_profile(obj) + return Response(response) From 9e65e94b6a93b22cc4c56c8c27f941084a295661 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 9 Jan 2023 18:59:47 +0530 Subject: [PATCH 019/180] fixed aadhaar otp api call --- care/abdm/api/serializers/healthid.py | 2 +- care/abdm/api/viewsets/healthid.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 3c93bf8647..44b5f1d0c5 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -99,7 +99,7 @@ class VerifyOtpRequestPayloadSerializer(Serializer): validators=[], ) patientId = CharField( - required=True, help_text="Patient ID to be linked", validators=[] + required=False, help_text="Patient ID to be linked", validators=[] ) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 15497e5d2f..37c0178e36 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -39,7 +39,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().generate_document_mobile_otp(data) + response = HealthIdGatewayV2().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( From 2a5f1e0862d7064f2e0ecc30feccd06fcffedaf0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 11 Jan 2023 14:52:49 +0530 Subject: [PATCH 020/180] added abha_number to patient serializer --- care/abdm/api/serializers/healthid.py | 2 +- care/abdm/api/viewsets/healthid.py | 20 +++++++++++--------- care/facility/api/serializers/patient.py | 4 ++++ 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 44b5f1d0c5..e8ca07d242 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -129,5 +129,5 @@ class CreateHealthIdSerializer(Serializer): validators=[], ) patientId = CharField( - required=True, help_text="Patient ID to be linked", validators=[] + required=False, help_text="Patient ID to be linked", validators=[] ) # TODO: Add UUID Validation diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 37c0178e36..324a0123fa 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -69,7 +69,9 @@ def verify_aadhaar_otp(self, request): data = request.data serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().verify_document_mobile_otp(data) + response = HealthIdGateway().verify_aadhaar_otp( + data + ) # HealthIdGatewayV2().verify_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -137,11 +139,11 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError({"patient": "Not Found"}) + # patient_id = data.pop("patientId") + # allowed_patients = get_patient_queryset(request.user) + # patient_obj = allowed_patients.filter(external_id=patient_id).first() + # if not patient_obj: + # raise ValidationError({"patient": "Not Found"}) response = HealthIdGateway().create_health_id(data) abha_object = AbhaNumber.objects.filter( abha_number=response["healthIdNumber"] @@ -164,9 +166,9 @@ def create_health_id(self, request): abha_object.refresh_token = data["txnId"] abha_object.save() - patient_obj.abha_number = abha_object - patient_obj.save() - return Response(response, status=status.HTTP_200_OK) + # patient_obj.abha_number = abha_object + # patient_obj.save() + return Response({"abha": abha_object.external_id}, status=status.HTTP_200_OK) # APIs to Find & Link Existing HealthID # searchByHealthId diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 91b3d5233e..2852b0cbe7 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -5,6 +5,7 @@ from rest_framework import serializers from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer +from care.abdm.models import AbhaNumber from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.api.serializers.facility import ( FacilityBasicInfoSerializer, @@ -180,6 +181,9 @@ class Meta: queryset=User.objects.all(), required=False, allow_null=True ) + abha_number = ExternalIdSerializerField( + queryset=AbhaNumber.objects.all(), required=False + ) abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True) class Meta: From 9478eca9be9c6771fc449960a2b04dc59df87120 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 29 Jan 2023 13:53:55 +0530 Subject: [PATCH 021/180] send entire response after health id creation --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 324a0123fa..d9353bd55c 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -168,7 +168,7 @@ def create_health_id(self, request): # patient_obj.abha_number = abha_object # patient_obj.save() - return Response({"abha": abha_object.external_id}, status=status.HTTP_200_OK) + return Response(response, status=status.HTTP_200_OK) # APIs to Find & Link Existing HealthID # searchByHealthId From 076df0747b1cc75c6b9c2e4e67b4f595c7fdf950 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 30 Jan 2023 17:20:28 +0530 Subject: [PATCH 022/180] feat: added share_profile endpoint --- care/abdm/api/serializers/healthid.py | 31 ++++ care/abdm/api/serializers/hip.py | 33 +++++ care/abdm/api/viewsets/healthid.py | 33 +++++ care/abdm/api/viewsets/hip.py | 132 ++++++++++++++++++ care/abdm/utils/api_call.py | 6 + .../migrations/0331_auto_20230130_1652.py | 18 +++ care/facility/models/patient.py | 6 +- config/api_router.py | 2 + 8 files changed, 259 insertions(+), 2 deletions(-) create mode 100644 care/abdm/api/serializers/hip.py create mode 100644 care/abdm/api/viewsets/hip.py create mode 100644 care/facility/migrations/0331_auto_20230130_1652.py diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index e8ca07d242..4701170711 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -103,6 +103,37 @@ class VerifyOtpRequestPayloadSerializer(Serializer): ) # TODO: Add UUID Validation +class VerifyDemographicsRequestPayloadSerializer(Serializer): + gender = CharField( + max_length=10, + min_length=1, + required=True, + help_text="Gender", + validators=[], + ) + name = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Name", + validators=[], + ) + yearOfBirth = CharField( + max_length=4, + min_length=4, + required=True, + help_text="Year Of Birth", + validators=[], + ) + txnId = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Transaction ID", + validators=[], + ) + + # { # "email": "Example@Demo.com", # "firstName": "manoj", diff --git a/care/abdm/api/serializers/hip.py b/care/abdm/api/serializers/hip.py new file mode 100644 index 0000000000..4e3bb0f9ab --- /dev/null +++ b/care/abdm/api/serializers/hip.py @@ -0,0 +1,33 @@ +from rest_framework.serializers import CharField, IntegerField, Serializer + + +class AddressSerializer(Serializer): + line = CharField() + district = CharField() + state = CharField() + pincode = CharField() + + +class PatientSerializer(Serializer): + healthId = CharField(allow_null=True) + healthIdNumber = CharField() + name = CharField() + gender = CharField() + yearOfBirth = IntegerField() + dayOfBirth = IntegerField() + monthOfBirth = IntegerField() + address = AddressSerializer() + + +class ProfileSerializer(Serializer): + hipCode = CharField() + patient = PatientSerializer() + + +class HipShareProfileSerializer(Serializer): + """ + Serializer for the request of the share_profile + """ + + requestId = CharField() + profile = ProfileSerializer() diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index d9353bd55c..83ff1708a2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -15,6 +15,7 @@ GenerateMobileOtpRequestPayloadSerializer, HealthIdAuthSerializer, HealthIdSerializer, + VerifyDemographicsRequestPayloadSerializer, VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber @@ -267,6 +268,38 @@ def confirm_with_mobile_otp(self, request): status=status.HTTP_400_BAD_REQUEST, ) + @swagger_auto_schema( + operation_id="confirm_with_demographics", + request_body=VerifyDemographicsRequestPayloadSerializer, + responses={"200": "{'status': true}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def confirm_with_demographics(self, request): + data = request.data + serializer = VerifyDemographicsRequestPayloadSerializer(data=data) + serializer.is_valid(raise_exception=True) + response = HealthIdGateway().confirm_with_demographics(data) + return Response(response, status=status.HTTP_200_OK) + + # patient_id = data.pop("patientId") + # if patient_id and response.status: + # allowed_patients = get_patient_queryset(request.user) + # patient_obj = allowed_patients.filter(external_id=patient_id).first() + # if not patient_obj: + # raise ValidationError({"patient": "Not Found"}) + + # if self.add_abha_details_to_patient( + # abha_object, + # patient_obj, + # ): + # return Response(abha_object, status=status.HTTP_200_OK) + # else: + # return Response( + # {"message": "ABHA NUmber / Health ID already Exists"}, + # status=status.HTTP_400_BAD_REQUEST, + # ) + ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py new file mode 100644 index 0000000000..75500438e0 --- /dev/null +++ b/care/abdm/api/viewsets/hip.py @@ -0,0 +1,132 @@ +import datetime + +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from care.abdm.api.serializers.hip import HipShareProfileSerializer +from care.abdm.models import AbhaNumber +from care.abdm.utils.api_call import HealthIdGateway +from care.facility.models.facility import Facility +from care.facility.models.patient import PatientRegistration + + +class HipViewSet(GenericViewSet): + def add_abha_details_to_patient(self, data, patient_obj): + abha_object = AbhaNumber.objects.filter( + abha_number=data["healthIdNumber"] + ).first() + if abha_object: + # Flow when abha number exists in db somehow! + pass + else: + # Create abha number flow + abha_object = AbhaNumber() + abha_object.abha_number = data["healthIdNumber"] + # abha_object.email = data["email"] + # abha_object.first_name = data["firstName"] + abha_object.health_id = data["healthId"] + # abha_object.last_name = data["lastName"] + # abha_object.middle_name = data["middleName"] + # abha_object.profile_photo = data["profilePhoto"] + abha_object.save() + + patient_obj.abha_number = abha_object + patient_obj.save() + return True + + def demographics_verification(self, data): + auth_init_response = HealthIdGateway().auth_init( + {"authMethod": "DEMOGRAPHICS", "healthid": data["healthIdNumber"]} + ) + if "txnId" in auth_init_response: + demographics_response = HealthIdGateway().confirm_with_demographics( + { + "txnId": auth_init_response["txnId"], + "name": data["name"], + "gender": data["gender"], + "yearOfBirth": data["yearOfBirth"], + } + ) + return "status" in demographics_response and demographics_response["status"] + else: + return False + + @action(detail=False, methods=["POST"]) + def share_profile(self, request, *args, **kwargs): + data = request.data + patient_data = data["profile"]["patient"] + # hip_id = self.request.GET.get("hip_id") + counter_id = self.request.GET.get("counter_id") # facility_id + + patient_data["mobile"] = "" + for identifier in patient_data["identifiers"]: + if identifier["type"] == "MOBILE": + patient_data["mobile"] = identifier["value"] + + serializer = HipShareProfileSerializer(data=data) + serializer.is_valid(raise_exception=True) + + # create a patient or search for existing patient with this abha number + patient = PatientRegistration.objects.filter( + abha_number__abha_number=patient_data["healthIdNumber"] + ) + if not patient: + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=counter_id), + name=patient_data["name"], + gender=1 + if patient_data["gender"] == "M" + else 2 + if patient_data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=patient_data["mobile"], + emergency_phone_number=patient_data["mobile"], + date_of_birth=datetime.datetime.strptime( + f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", + "%Y-%m-%d", + ).date(), + blood_group="UNK", + nationality="India", + address=patient_data["address"]["line"], + pincode=patient_data["address"]["pincode"], + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + + # verify details using demographics method (name, gender and yearOfBirth) + if self.demographics_verification(patient_data): + self.add_abha_details_to_patient(patient_data, patient) + return Response( + { + "requestId": data["requestId"], + "timestamp": str(datetime.datetime.now()), + "acknowledgement": { + "status": "SUCCESS", + "healthId": patient_data["healthId"], + "healthIdNumber": patient_data["healthIdNumber"], + "tokenNumber": "01", # this is for out patients + }, + }, + status=status.HTTP_202_ACCEPTED, + ) + else: + return Response( + { + "requestId": data["requestId"], + "timestamp": str(datetime.datetime.now()), + "acknowledgement": { + "status": "FAILURE", + }, + "error": { + "code": 1000, + "message": "Demographics verification failed", + }, + }, + status=status.HTTP_401_UNAUTHORIZED, + ) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 1635b498e6..afb3f1e1f9 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -216,6 +216,12 @@ def confirm_with_mobile_otp(self, data): response = self.api.post(path, data) return response.json() + # /v1/auth/confirmWithDemographics + def confirm_with_demographics(self, data): + path = "/v1/auth/confirmWithDemographics" + response = self.api.post(path, data) + return response.json() + # /v1/auth/generate/access-token def generate_access_token(self, data): if "access_token" in data: diff --git a/care/facility/migrations/0331_auto_20230130_1652.py b/care/facility/migrations/0331_auto_20230130_1652.py new file mode 100644 index 0000000000..7b96181775 --- /dev/null +++ b/care/facility/migrations/0331_auto_20230130_1652.py @@ -0,0 +1,18 @@ +# Generated by Django 2.2.11 on 2023-01-30 11:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0330_auto_20221220_2312'), + ] + + operations = [ + migrations.AlterField( + model_name='patientsearch', + name='state_id', + field=models.IntegerField(null=True), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 6997027460..b4daf34522 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -394,7 +394,9 @@ class TestTypeEnum(enum.Enum): # ABDM Health ID - abha_number = models.OneToOneField(AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True) + abha_number = models.OneToOneField( + AbhaNumber, on_delete=models.SET_NULL, null=True, blank=True + ) history = HistoricalRecords(excluded_fields=["patient_search_id", "meta_info"]) @@ -594,7 +596,7 @@ class PatientSearch(PatientBaseModel): phone_number = models.CharField(max_length=14) date_of_birth = models.DateField(null=True) year_of_birth = models.IntegerField() - state_id = models.IntegerField() + state_id = models.IntegerField(null=True) facility = models.ForeignKey("Facility", on_delete=models.SET_NULL, null=True) patient_external_id = EncryptedCharField(max_length=100, default="") diff --git a/config/api_router.py b/config/api_router.py index a6abe88f5c..014973d2d7 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -5,6 +5,7 @@ from care.abdm.api.viewsets.abha import AbhaViewSet from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet +from care.abdm.api.viewsets.hip import HipViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, AmbulanceViewSet, @@ -198,6 +199,7 @@ # ABDM endpoints router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") +router.register("abdm/hip", HipViewSet, basename="hip") app_name = "api" urlpatterns = [ From 291074446c798d0ad9252b8fe8003a2aa9aa2d22 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 1 Feb 2023 07:38:23 +0530 Subject: [PATCH 023/180] changed endpoint of abdm-hip-share --- care/abdm/api/viewsets/hip.py | 111 ++++++++++++++++++++-------------- care/abdm/utils/api_call.py | 19 +++++- config/api_router.py | 15 ++++- config/urls.py | 1 + 4 files changed, 97 insertions(+), 49 deletions(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 75500438e0..93c2a23556 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -2,17 +2,21 @@ from rest_framework import status from rest_framework.decorators import action +from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet from care.abdm.api.serializers.hip import HipShareProfileSerializer from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway +from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration class HipViewSet(GenericViewSet): + permission_classes = (AllowAny,) + authentication_classes = [] + def add_abha_details_to_patient(self, data, patient_obj): abha_object = AbhaNumber.objects.filter( abha_number=data["healthIdNumber"] @@ -54,11 +58,10 @@ def demographics_verification(self, data): return False @action(detail=False, methods=["POST"]) - def share_profile(self, request, *args, **kwargs): + def share(self, request, *args, **kwargs): data = request.data patient_data = data["profile"]["patient"] - # hip_id = self.request.GET.get("hip_id") - counter_id = self.request.GET.get("counter_id") # facility_id + counter_id = data["profile"]["hipCode"] patient_data["mobile"] = "" for identifier in patient_data["identifiers"]: @@ -72,61 +75,77 @@ def share_profile(self, request, *args, **kwargs): patient = PatientRegistration.objects.filter( abha_number__abha_number=patient_data["healthIdNumber"] ) - if not patient: - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=counter_id), - name=patient_data["name"], - gender=1 - if patient_data["gender"] == "M" - else 2 - if patient_data["gender"] == "F" - else 3, - is_antenatal=False, - phone_number=patient_data["mobile"], - emergency_phone_number=patient_data["mobile"], - date_of_birth=datetime.datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ).date(), - blood_group="UNK", - nationality="India", - address=patient_data["address"]["line"], - pincode=patient_data["address"]["pincode"], - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, + if patient: + res = AbdmGateway().on_share( + { + "requestId": data["requestId"], + # "timestamp": str(datetime.datetime.now()), + "acknowledgement": { + "status": "SUCCESS", + "healthId": patient_data["healthId"], + "tokenNumber": "02", + }, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["requestId"]}, + } + ) + print(res) + + return Response( + { + "requestId": data["requestId"], + "status": "SUCCESS", + "healthId": patient_data["healthIdNumber"], + # "healthIdNumber": patient_data["healthIdNumber"], + "tokenNumber": "02", # this is for out patients + }, + status=status.HTTP_202_ACCEPTED, ) + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=counter_id), + name=patient_data["name"], + gender=1 + if patient_data["gender"] == "M" + else 2 + if patient_data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=patient_data["mobile"], + emergency_phone_number=patient_data["mobile"], + date_of_birth=datetime.datetime.strptime( + f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", + "%Y-%m-%d", + ).date(), + blood_group="UNK", + nationality="India", + address=patient_data["address"]["line"], + pincode=patient_data["address"]["pincode"], + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + # verify details using demographics method (name, gender and yearOfBirth) if self.demographics_verification(patient_data): self.add_abha_details_to_patient(patient_data, patient) return Response( { - "requestId": data["requestId"], - "timestamp": str(datetime.datetime.now()), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"], - "healthIdNumber": patient_data["healthIdNumber"], - "tokenNumber": "01", # this is for out patients - }, + "status": "SUCCESS", + "healthId": patient_data["healthId"], + # "healthIdNumber": patient_data["healthIdNumber"], + "tokenNumber": "02", # this is for out patients }, status=status.HTTP_202_ACCEPTED, ) else: return Response( { - "requestId": data["requestId"], - "timestamp": str(datetime.datetime.now()), - "acknowledgement": { - "status": "FAILURE", - }, - "error": { - "code": 1000, - "message": "Demographics verification failed", - }, + "status": "FAILURE", + "healthId": patient_data["healthId"], + "healthIdNumber": patient_data["healthIdNumber"], }, status=status.HTTP_401_UNAUTHORIZED, ) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index afb3f1e1f9..9780473773 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -10,6 +10,7 @@ GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" +ABDM_GATEWAY_URL = GATEWAY_API_URL + "gateway" ABDM_TOKEN_CACHE_KEY = "abdm_token" # TODO: Exception handling for all api calls, need to gracefully handle known exceptions @@ -32,6 +33,8 @@ def __init__(self, gateway, token): self.url = HEALTH_SERVICE_API_URL elif gateway == "abdm": self.url = GATEWAY_API_URL + elif gateway == "abdm_gateway": + self.url = ABDM_GATEWAY_URL else: self.url = GATEWAY_API_URL self.token = token @@ -95,6 +98,9 @@ def add_auth_header(self, headers): auth_header = {"Authorization": "Bearer {}".format(token)} return {**headers, **auth_header} + def add_additional_headers(self, headers, additional_headers): + return {**headers, **additional_headers} + def get(self, path, params=None, auth=None): url = self.url + path headers = {} @@ -106,7 +112,7 @@ def get(self, path, params=None, auth=None): print("{} Response: {}".format(response.status_code, response.text)) return response - def post(self, path, data=None, auth=None): + def post(self, path, data=None, auth=None, additional_headers=None): url = self.url + path headers = { "Content-Type": "application/json", @@ -116,6 +122,8 @@ def post(self, path, data=None, auth=None): headers = self.add_auth_header(headers) if auth: headers = self.add_user_header(headers, auth) + if additional_headers: + headers = self.add_additional_headers(headers, additional_headers) # headers_string = " ".join( # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] # ) @@ -289,10 +297,17 @@ def verify_document_mobile_otp(self, data): class AbdmGateway: def __init__(self): - self.api = APIGateway("abdm", None) + self.api = APIGateway("abdm_gateway", None) # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): path = "/v0.5/users/auth/fetch-modes" response = self.api.post(path, data) return response.json() + + # /v1.0/patients/profile/on-share + def on_share(self, data): + path = "/v1.0/patients/profile/on-share" + additional_headers = {"X-CM-ID": "sbx"} + response = self.api.post(path, data, None, additional_headers) + return response diff --git a/config/api_router.py b/config/api_router.py index 014973d2d7..ed6767ab22 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -82,10 +82,19 @@ from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet + +class OptionalSlashRouter(SimpleRouter): + def __init__(self): + super().__init__() + self.trailing_slash = "/?" + + if settings.DEBUG: router = DefaultRouter() + # abdm_router = DefaultRouter() else: router = SimpleRouter() +abdm_router = OptionalSlashRouter() router.register("users", UserViewSet) user_nested_rotuer = NestedSimpleRouter(router, r"users", lookup="users") @@ -199,7 +208,7 @@ # ABDM endpoints router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") -router.register("abdm/hip", HipViewSet, basename="hip") +abdm_router.register("profile", HipViewSet, basename="hip") app_name = "api" urlpatterns = [ @@ -211,3 +220,7 @@ url(r"^", include(resource_nested_router.urls)), url(r"^", include(shifting_nested_router.urls)), ] + +abdm_urlpatterns = [ + url(r"^", include(abdm_router.urls)), +] diff --git a/config/urls.py b/config/urls.py index 270d8bb96c..9fbfc17816 100644 --- a/config/urls.py +++ b/config/urls.py @@ -81,6 +81,7 @@ name="change_password_view", ), path("api/v1/", include(api_router.urlpatterns)), + path("v1.0/patients/", include(api_router.abdm_urlpatterns)), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 5d1e1fb81a7fd498880cee3e9206179cd7e635e5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Feb 2023 14:12:53 +0530 Subject: [PATCH 024/180] if patientId then link --- care/abdm/api/viewsets/healthid.py | 80 ++++++++++++++++-------------- 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 83ff1708a2..4e4677a411 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -140,11 +140,6 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) - # patient_id = data.pop("patientId") - # allowed_patients = get_patient_queryset(request.user) - # patient_obj = allowed_patients.filter(external_id=patient_id).first() - # if not patient_obj: - # raise ValidationError({"patient": "Not Found"}) response = HealthIdGateway().create_health_id(data) abha_object = AbhaNumber.objects.filter( abha_number=response["healthIdNumber"] @@ -167,8 +162,15 @@ def create_health_id(self, request): abha_object.refresh_token = data["txnId"] abha_object.save() - # patient_obj.abha_number = abha_object - # patient_obj.save() + if "patientId" in data: + patient_id = data.pop("patientId") + allowed_patients = get_patient_queryset(request.user) + patient_obj = allowed_patients.filter(external_id=patient_id).first() + if not patient_obj: + raise ValidationError({"patient": "Not Found"}) + patient_obj.abha_number = abha_object + patient_obj.save() + return Response(response, status=status.HTTP_200_OK) # APIs to Find & Link Existing HealthID @@ -219,28 +221,29 @@ def confirm_with_aadhaar_otp(self, request): response = HealthIdGateway().confirm_with_aadhaar_otp(data) abha_object = HealthIdGateway().get_profile(response) - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError({"patient": "Not Found"}) + if "patientId" in data: + patient_id = data.pop("patientId") + allowed_patients = get_patient_queryset(request.user) + patient_obj = allowed_patients.filter(external_id=patient_id).first() + if not patient_obj: + raise ValidationError({"patient": "Not Found"}) - if self.add_abha_details_to_patient( - abha_object, - patient_obj, - ): - return Response(abha_object, status=status.HTTP_200_OK) - else: - return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response(abha_object, status=status.HTTP_200_OK) # /v1/auth/confirmWithMobileOtp @swagger_auto_schema( operation_id="confirm_with_mobile_otp", request_body=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, + # responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @action(detail=False, methods=["post"]) @@ -251,22 +254,23 @@ def confirm_with_mobile_otp(self, request): response = HealthIdGateway().confirm_with_mobile_otp(data) abha_object = HealthIdGateway().get_profile(response) - patient_id = data.pop("patientId") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError({"patient": "Not Found"}) + if "patientId" in data: + patient_id = data.pop("patientId") + allowed_patients = get_patient_queryset(request.user) + patient_obj = allowed_patients.filter(external_id=patient_id).first() + if not patient_obj: + raise ValidationError({"patient": "Not Found"}) - if self.add_abha_details_to_patient( - abha_object, - patient_obj, - ): - return Response(abha_object, status=status.HTTP_200_OK) - else: - return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, - status=status.HTTP_400_BAD_REQUEST, - ) + if not self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response( + {"message": "ABHA NUmber / Health ID already Exists"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response(abha_object, status=status.HTTP_200_OK) @swagger_auto_schema( operation_id="confirm_with_demographics", From 94ec9b3d55841c0abdc1320fefe1d99ebc70ff8f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Feb 2023 18:18:36 +0530 Subject: [PATCH 025/180] added patient profile share --- care/abdm/api/viewsets/hip.py | 135 ++++++++++++++++------------------ 1 file changed, 62 insertions(+), 73 deletions(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 93c2a23556..424eb4dcbc 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -1,4 +1,5 @@ -import datetime +import uuid +from datetime import datetime, timezone from rest_framework import status from rest_framework.decorators import action @@ -60,6 +61,7 @@ def demographics_verification(self, data): @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data + patient_data = data["profile"]["patient"] counter_id = data["profile"]["hipCode"] @@ -71,81 +73,68 @@ def share(self, request, *args, **kwargs): serializer = HipShareProfileSerializer(data=data) serializer.is_valid(raise_exception=True) - # create a patient or search for existing patient with this abha number - patient = PatientRegistration.objects.filter( - abha_number__abha_number=patient_data["healthIdNumber"] - ) - if patient: - res = AbdmGateway().on_share( - { - "requestId": data["requestId"], - # "timestamp": str(datetime.datetime.now()), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"], - "tokenNumber": "02", - }, - # "error": {"code": 1000, "message": "string"}, - "resp": {"requestId": data["requestId"]}, - } + if self.demographics_verification(patient_data): + patient = PatientRegistration.objects.filter( + abha_number__abha_number=patient_data["healthIdNumber"] ) - print(res) - return Response( - { - "requestId": data["requestId"], - "status": "SUCCESS", - "healthId": patient_data["healthIdNumber"], - # "healthIdNumber": patient_data["healthIdNumber"], - "tokenNumber": "02", # this is for out patients - }, - status=status.HTTP_202_ACCEPTED, - ) + if not patient: + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=counter_id), + name=patient_data["name"], + gender=1 + if patient_data["gender"] == "M" + else 2 + if patient_data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=patient_data["mobile"], + emergency_phone_number=patient_data["mobile"], + date_of_birth=datetime.strptime( + f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", + "%Y-%m-%d", + ).date(), + blood_group="UNK", + nationality="India", + address=patient_data["address"]["line"], + pincode=patient_data["address"]["pincode"], + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + self.add_abha_details_to_patient(patient_data, patient) - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=counter_id), - name=patient_data["name"], - gender=1 - if patient_data["gender"] == "M" - else 2 - if patient_data["gender"] == "F" - else 3, - is_antenatal=False, - phone_number=patient_data["mobile"], - emergency_phone_number=patient_data["mobile"], - date_of_birth=datetime.datetime.strptime( - f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", - "%Y-%m-%d", - ).date(), - blood_group="UNK", - nationality="India", - address=patient_data["address"]["line"], - pincode=patient_data["address"]["pincode"], - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, - ) - - # verify details using demographics method (name, gender and yearOfBirth) - if self.demographics_verification(patient_data): - self.add_abha_details_to_patient(patient_data, patient) - return Response( - { + payload = { + "requestId": str(uuid.uuid4()), + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "acknowledgement": { "status": "SUCCESS", - "healthId": patient_data["healthId"], - # "healthIdNumber": patient_data["healthIdNumber"], - "tokenNumber": "02", # this is for out patients + "healthId": patient_data["healthId"] + or patient_data["healthIdNumber"], + "tokenNumber": "100", }, - status=status.HTTP_202_ACCEPTED, - ) - else: - return Response( - { - "status": "FAILURE", - "healthId": patient_data["healthId"], - "healthIdNumber": patient_data["healthIdNumber"], + "error": None, + "resp": { + "requestId": data["requestId"], }, - status=status.HTTP_401_UNAUTHORIZED, - ) + } + + on_share_response = AbdmGateway().on_share(payload) + if on_share_response.status_code == 202: + print("on_share_header", on_share_response.request.body) + return Response( + on_share_response.request.body, + status=status.HTTP_202_ACCEPTED, + ) + + return Response( + { + "status": "FAILURE", + "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], + }, + status=status.HTTP_401_UNAUTHORIZED, + ) From 8f32f83e19cf575d0bfbdd3a138462471f8dae8f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Feb 2023 23:58:46 +0530 Subject: [PATCH 026/180] changed response structure of abha_object --- care/abdm/api/viewsets/healthid.py | 123 ++++++++++++++--------------- 1 file changed, 58 insertions(+), 65 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 4e4677a411..50a496ee19 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -105,27 +105,33 @@ def verify_mobile_otp(self, request): response = HealthIdGateway().verify_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) - def add_abha_details_to_patient(self, data, patient_obj): + def create_abha(self, abha_profile): abha_object = AbhaNumber.objects.filter( - abha_number=data["healthIdNumber"] + abha_number=abha_profile["healthIdNumber"] ).first() + if abha_object: - # Flow when abha number exists in db somehow! + return abha_object + + abha_object = AbhaNumber() + abha_object.abha_number = abha_profile["healthIdNumber"] + abha_object.email = abha_profile["email"] + abha_object.first_name = abha_profile["firstName"] + abha_object.health_id = abha_profile["healthId"] + abha_object.last_name = abha_profile["lastName"] + abha_object.middle_name = abha_profile["middleName"] + abha_object.profile_photo = abha_profile["profilePhoto"] + abha_object.txn_id = abha_profile["healthIdNumber"] + abha_object.save() + + return abha_object + + def add_abha_details_to_patient(self, abha_object, patient_object): + if patient_object.abha_number: return False - else: - # Create abha number flow - abha_object = AbhaNumber() - abha_object.abha_number = data["healthIdNumber"] - abha_object.email = data["email"] - abha_object.first_name = data["firstName"] - abha_object.health_id = data["healthId"] - abha_object.last_name = data["lastName"] - abha_object.middle_name = data["middleName"] - abha_object.profile_photo = data["profilePhoto"] - abha_object.save() - - patient_obj.abha_number = abha_object - patient_obj.save() + + patient_object.abha_number = abha_object + patient_object.save() return True @swagger_auto_schema( @@ -140,27 +146,10 @@ def create_health_id(self, request): data = request.data serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGateway().create_health_id(data) - abha_object = AbhaNumber.objects.filter( - abha_number=response["healthIdNumber"] - ).first() - if abha_object: - # Flow when abha number exists in db somehow! - pass - else: - # Create abha number flow - abha_object = AbhaNumber() - abha_object.abha_number = response["healthIdNumber"] - abha_object.email = response["email"] - abha_object.first_name = response["firstName"] - abha_object.health_id = response["healthId"] - abha_object.last_name = response["lastName"] - abha_object.middle_name = response["middleName"] - abha_object.profile_photo = response["profilePhoto"] - abha_object.txn_id = response["healthIdNumber"] - abha_object.access_token = response["token"] - abha_object.refresh_token = data["txnId"] - abha_object.save() + abha_profile = HealthIdGateway().create_health_id(data) + + # have a serializer to verify data of abha_profile + abha_object = self.create_abha(abha_profile) if "patientId" in data: patient_id = data.pop("patientId") @@ -168,10 +157,20 @@ def create_health_id(self, request): patient_obj = allowed_patients.filter(external_id=patient_id).first() if not patient_obj: raise ValidationError({"patient": "Not Found"}) - patient_obj.abha_number = abha_object - patient_obj.save() - return Response(response, status=status.HTTP_200_OK) + if not self.add_abha_details_to_patient( + abha_object, + patient_obj, + ): + return Response( + {"message": "Failed to add abha Number to the patient"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + return Response( + {"id": abha_object.external_id, "abha_profile": abha_profile}, + status=status.HTTP_200_OK, + ) # APIs to Find & Link Existing HealthID # searchByHealthId @@ -219,7 +218,10 @@ def confirm_with_aadhaar_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_aadhaar_otp(data) - abha_object = HealthIdGateway().get_profile(response) + abha_profile = HealthIdGateway().get_profile(response) + + # have a serializer to verify data of abha_profile + abha_object = self.create_abha(abha_profile) if "patientId" in data: patient_id = data.pop("patientId") @@ -233,11 +235,14 @@ def confirm_with_aadhaar_otp(self, request): patient_obj, ): return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, + {"message": "Failed to add abha Number to the patient"}, status=status.HTTP_400_BAD_REQUEST, ) - return Response(abha_object, status=status.HTTP_200_OK) + return Response( + {"id": abha_object.external_id, "abha_profile": abha_profile}, + status=status.HTTP_200_OK, + ) # /v1/auth/confirmWithMobileOtp @swagger_auto_schema( @@ -252,7 +257,10 @@ def confirm_with_mobile_otp(self, request): serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_mobile_otp(data) - abha_object = HealthIdGateway().get_profile(response) + abha_profile = HealthIdGateway().get_profile(response) + + # have a serializer to verify data of abha_profile + abha_object = self.create_abha(abha_profile) if "patientId" in data: patient_id = data.pop("patientId") @@ -266,11 +274,14 @@ def confirm_with_mobile_otp(self, request): patient_obj, ): return Response( - {"message": "ABHA NUmber / Health ID already Exists"}, + {"message": "Failed to add abha Number to the patient"}, status=status.HTTP_400_BAD_REQUEST, ) - return Response(abha_object, status=status.HTTP_200_OK) + return Response( + {"id": abha_object.external_id, "abha_profile": abha_profile}, + status=status.HTTP_200_OK, + ) @swagger_auto_schema( operation_id="confirm_with_demographics", @@ -286,24 +297,6 @@ def confirm_with_demographics(self, request): response = HealthIdGateway().confirm_with_demographics(data) return Response(response, status=status.HTTP_200_OK) - # patient_id = data.pop("patientId") - # if patient_id and response.status: - # allowed_patients = get_patient_queryset(request.user) - # patient_obj = allowed_patients.filter(external_id=patient_id).first() - # if not patient_obj: - # raise ValidationError({"patient": "Not Found"}) - - # if self.add_abha_details_to_patient( - # abha_object, - # patient_obj, - # ): - # return Response(abha_object, status=status.HTTP_200_OK) - # else: - # return Response( - # {"message": "ABHA NUmber / Health ID already Exists"}, - # status=status.HTTP_400_BAD_REQUEST, - # ) - ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( From d64f4ee199a024f565d5f76274e6221ca5094d1d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Feb 2023 00:18:36 +0530 Subject: [PATCH 027/180] added extra condition while linking abha with patient --- care/abdm/api/viewsets/healthid.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 50a496ee19..d3e13c19f2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -20,6 +20,7 @@ ) from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import HealthIdGateway, HealthIdGatewayV2 +from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset @@ -127,7 +128,11 @@ def create_abha(self, abha_profile): return abha_object def add_abha_details_to_patient(self, abha_object, patient_object): - if patient_object.abha_number: + patient = PatientRegistration.objects.filter( + abha_number__abha_number=abha_object.abha_number + ).first() + + if patient or patient_object.abha_number: return False patient_object.abha_number = abha_object From 77ba47923264c2586870ba8f30f2938a81888ed1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Feb 2023 19:10:48 +0530 Subject: [PATCH 028/180] added v0.5 apis for demographic auth --- care/abdm/api/serializers/healthid.py | 34 +++++++ care/abdm/api/viewsets/auth.py | 43 +++++++++ care/abdm/api/viewsets/healthid.py | 62 ++++++++++++- care/abdm/api/viewsets/hip.py | 45 +++++---- care/abdm/utils/api_call.py | 129 +++++++++++++++++++++++++- config/urls.py | 16 ++++ 6 files changed, 301 insertions(+), 28 deletions(-) create mode 100644 care/abdm/api/viewsets/auth.py diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 4701170711..25b157208e 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -30,6 +30,40 @@ class HealthIdSerializer(Serializer): ) +class QRContentSerializer(Serializer): + hidn = CharField( + max_length=17, + min_length=17, + required=True, + help_text="Health ID Number", + ) + phr = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Health ID", + ) + name = CharField( + max_length=64, + min_length=1, + required=True, + help_text="Name", + ) + gender = CharField( + max_length=1, + min_length=1, + required=True, + help_text="Name", + ) + dob = CharField( + max_length=10, + min_length=8, + required=True, + help_text="Name", + ) + # {"statelgd":"33","distlgd":"573","address":"C/O Gopalsamy NO 33 A WESTSTREET ODANILAI KASTHURIBAI GRAMAM ARACHALUR Erode","state name":"TAMIL NADU","dist name":"Erode","mobile":"7639899448"} + + # { # "authMethod": "AADHAAR_OTP", # "healthid": "43-4221-5105-6749" diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py new file mode 100644 index 0000000000..7f81d8ae36 --- /dev/null +++ b/care/abdm/api/viewsets/auth.py @@ -0,0 +1,43 @@ +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from care.abdm.utils.api_call import AbdmGateway + + +class OnFetchView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("on-fetch-modes", data) + AbdmGateway().init(data["resp"]["requestId"]) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class OnInitView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("on-init", data) + AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class OnConfirmView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + AbdmGateway().link_patient_abha( + data["auth"]["patient"], + data["auth"]["accessToken"], + data["resp"]["requestId"], + ) + return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index d3e13c19f2..319e8b5530 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -1,5 +1,7 @@ # ABDM HealthID APIs +from datetime import datetime + from drf_yasg.utils import swagger_auto_schema from rest_framework import status from rest_framework.decorators import action @@ -15,11 +17,13 @@ GenerateMobileOtpRequestPayloadSerializer, HealthIdAuthSerializer, HealthIdSerializer, + QRContentSerializer, VerifyDemographicsRequestPayloadSerializer, VerifyOtpRequestPayloadSerializer, ) from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway, HealthIdGatewayV2 +from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway +from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset @@ -41,7 +45,7 @@ def generate_aadhaar_otp(self, request): data = request.data serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) - response = HealthIdGatewayV2().generate_aadhaar_otp(data) + response = HealthIdGateway().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) @swagger_auto_schema( @@ -194,6 +198,60 @@ def search_by_health_id(self, request): response = HealthIdGateway().search_by_health_id(data) return Response(response, status=status.HTTP_200_OK) + @swagger_auto_schema( + # /v1/registration/aadhaar/searchByHealthId + operation_id="link_via_qr", + request_body=HealthIdSerializer, + responses={"200": "{'status': 'boolean'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def link_via_qr(self, request): + data = request.data + serializer = QRContentSerializer(data=data) + serializer.is_valid(raise_exception=True) + + if "patientId" not in data: + patient = PatientRegistration.objects.create( + facility=Facility.objects.get(external_id=data["facilityId"]), + name=data["name"], + gender=1 + if data["gender"] == "M" + else 2 + if data["gender"] == "F" + else 3, + is_antenatal=False, + phone_number=data["mobile"], + emergency_phone_number=data["mobile"], + date_of_birth=datetime.strptime(data["dob"], "%d-%m-%Y").date(), + blood_group="UNK", + nationality="India", + address=data["address"], + pincode=None, + created_by=None, + state=None, + district=None, + local_body=None, + ward=None, + ) + patient.save() + + patient_id = patient.external_id + else: + patient_id = data["patientId"] + + AbdmGateway().fetch_modes( + { + "healthId": data["phr"] or data["hidn"], + "name": data["name"], + "gender": data["gender"], + "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], + "patientId": patient_id, + } + ) + + return Response({}, status=status.HTTP_200_OK) + # auth/init @swagger_auto_schema( # /v1/auth/init diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 424eb4dcbc..05e4b6c42e 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -8,7 +8,6 @@ from rest_framework.viewsets import GenericViewSet from care.abdm.api.serializers.hip import HipShareProfileSerializer -from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration @@ -18,27 +17,8 @@ class HipViewSet(GenericViewSet): permission_classes = (AllowAny,) authentication_classes = [] - def add_abha_details_to_patient(self, data, patient_obj): - abha_object = AbhaNumber.objects.filter( - abha_number=data["healthIdNumber"] - ).first() - if abha_object: - # Flow when abha number exists in db somehow! - pass - else: - # Create abha number flow - abha_object = AbhaNumber() - abha_object.abha_number = data["healthIdNumber"] - # abha_object.email = data["email"] - # abha_object.first_name = data["firstName"] - abha_object.health_id = data["healthId"] - # abha_object.last_name = data["lastName"] - # abha_object.middle_name = data["middleName"] - # abha_object.profile_photo = data["profilePhoto"] - abha_object.save() - - patient_obj.abha_number = abha_object - patient_obj.save() + def add_abha_details_to_patient(self, data): + AbdmGateway().fetch_modes(data) return True def demographics_verification(self, data): @@ -63,7 +43,9 @@ def share(self, request, *args, **kwargs): data = request.data patient_data = data["profile"]["patient"] - counter_id = data["profile"]["hipCode"] + counter_id = ( + "8be5ab36-1b66-44ca-ae77-c719e084160d" or data["profile"]["hipCode"] + ) patient_data["mobile"] = "" for identifier in patient_data["identifiers"]: @@ -104,7 +86,22 @@ def share(self, request, *args, **kwargs): local_body=None, ward=None, ) - self.add_abha_details_to_patient(patient_data, patient) + patient.save() + self.add_abha_details_to_patient( + { + "healthId": patient_data["healthId"] + or patient_data["healthIdNumber"], + "name": patient_data["name"], + "gender": patient_data["gender"], + "dateOfBirth": str( + datetime.strptime( + f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", + "%Y-%m-%d", + ) + )[0:10], + "patientId": patient.external_id, + } + ) payload = { "requestId": str(uuid.uuid4()), diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 9780473773..bcaf936742 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -1,5 +1,7 @@ import json +import uuid from base64 import b64encode +from datetime import datetime, timezone import requests from Crypto.Cipher import PKCS1_v1_5 @@ -7,6 +9,9 @@ from django.conf import settings from django.core.cache import cache +from care.abdm.models import AbhaNumber +from care.facility.models import PatientRegistration + GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" @@ -296,14 +301,134 @@ def verify_document_mobile_otp(self, data): class AbdmGateway: + # TODO: replace this with in-memory db (redis) + temp_memory = {} + hip_id = "IN3210000017" + def __init__(self): self.api = APIGateway("abdm_gateway", None) + def link_patient_abha(self, patient, access_token, request_id): + data = self.temp_memory[request_id] + + abha_object = AbhaNumber() + abha_object.health_id = patient["id"] or data["healthId"] + # abha_object.email = data["email"] + # abha_object.first_name = data["firstName"] + # abha_object.health_id = data["healthId"] + # abha_object.last_name = data["lastName"] + # abha_object.middle_name = data["middleName"] + # abha_object.profile_photo = data["profilePhoto"] + abha_object.access_token = access_token + abha_object.save() + + patient = PatientRegistration.objects.filter( + external_id=data["patientId"] + ).first() + patient.abha_number = abha_object + patient.save() + # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): path = "/v0.5/users/auth/fetch-modes" - response = self.api.post(path, data) - return response.json() + additional_headers = {"X-CM-ID": "sbx"} + request_id = str(uuid.uuid4()) + + """ + data = { + healthId, + name, + gender, + dateOfBirth, + patientId + } + """ + self.temp_memory[request_id] = data + + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "query": { + "id": data["healthId"], + "purpose": "KYC_AND_LINK", + "requester": {"type": "HIP", "id": self.hip_id}, + }, + } + response = self.api.post(path, payload, None, additional_headers) + return response + + # "/v0.5/users/auth/init" + def init(self, prev_request_id): + path = "/v0.5/users/auth/init" + additional_headers = {"X-CM-ID": "sbx"} + + request_id = str(uuid.uuid4()) + + data = self.temp_memory[prev_request_id] + self.temp_memory[request_id] = data + + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "query": { + "id": data["healthId"], + "purpose": "KYC_AND_LINK", + "authMode": "DEMOGRAPHICS", + "requester": {"type": "HIP", "id": self.hip_id}, + }, + } + response = self.api.post(path, payload, None, additional_headers) + return response + + """ + { + "requestId": guidv4, + "timestamp": isotime, + "transactionId": "xxxxxxxxxxxxxxx from on-init", + "credential": { + "demographic": { + "name": "Khavin", + "gender": "M", + "dateOfBirth": "1999-01-18" + }, + "authCode": "" + } + } + """ + # "/v0.5/users/auth/confirm" + def confirm(self, transaction_id, prev_request_id): + path = "/v0.5/users/auth/confirm" + additional_headers = {"X-CM-ID": "sbx"} + + request_id = str(uuid.uuid4()) + + data = self.temp_memory[prev_request_id] + self.temp_memory[request_id] = data + + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "transactionId": transaction_id, + "credential": { + "demographic": { + "name": data["name"], + "gender": data["gender"], + "dateOfBirth": data["dateOfBirth"], + }, + "authCode": "", + }, + } + + print(payload) + + response = self.api.post(path, payload, None, additional_headers) + return response # /v1.0/patients/profile/on-share def on_share(self, data): diff --git a/config/urls.py b/config/urls.py index 9fbfc17816..b33c324130 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,6 +9,7 @@ from rest_framework import permissions from rest_framework_simplejwt.views import TokenVerifyView +from care.abdm.api.viewsets.auth import OnConfirmView, OnFetchView, OnInitView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.users.api.viewsets.change_password import ChangePasswordView from care.users.reset_password_views import ( @@ -82,6 +83,21 @@ ), path("api/v1/", include(api_router.urlpatterns)), path("v1.0/patients/", include(api_router.abdm_urlpatterns)), + path( + "v0.5/users/auth/on-fetch-modes", + OnFetchView.as_view(), + name="abdm_on_fetch_modes_view", + ), + path( + "v0.5/users/auth/on-init", + OnInitView.as_view(), + name="abdm_on_init_view", + ), + path( + "v0.5/users/auth/on-confirm", + OnConfirmView.as_view(), + name="abdm_on_confirm_view", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 86fcdf86b4ec3cc3536bcfd47929472ae5cc6d96 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 09:19:27 +0530 Subject: [PATCH 029/180] update flow for share profile --- care/abdm/api/viewsets/hip.py | 52 +++++++++++-------- .../migrations/0006_auto_20230208_0915.py | 48 +++++++++++++++++ care/abdm/models.py | 25 ++++++--- care/abdm/utils/api_call.py | 46 ++++++++++------ 4 files changed, 126 insertions(+), 45 deletions(-) create mode 100644 care/abdm/migrations/0006_auto_20230208_0915.py diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 05e4b6c42e..e0cdd5cdd9 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -8,6 +8,7 @@ from rest_framework.viewsets import GenericViewSet from care.abdm.api.serializers.hip import HipShareProfileSerializer +from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration @@ -17,27 +18,10 @@ class HipViewSet(GenericViewSet): permission_classes = (AllowAny,) authentication_classes = [] - def add_abha_details_to_patient(self, data): + def get_linking_token(self, data): AbdmGateway().fetch_modes(data) return True - def demographics_verification(self, data): - auth_init_response = HealthIdGateway().auth_init( - {"authMethod": "DEMOGRAPHICS", "healthid": data["healthIdNumber"]} - ) - if "txnId" in auth_init_response: - demographics_response = HealthIdGateway().confirm_with_demographics( - { - "txnId": auth_init_response["txnId"], - "name": data["name"], - "gender": data["gender"], - "yearOfBirth": data["yearOfBirth"], - } - ) - return "status" in demographics_response and demographics_response["status"] - else: - return False - @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data @@ -55,7 +39,12 @@ def share(self, request, *args, **kwargs): serializer = HipShareProfileSerializer(data=data) serializer.is_valid(raise_exception=True) - if self.demographics_verification(patient_data): + if HealthIdGateway().verify_demographics( + patient_data["healthIdNumber"], + patient_data["name"], + patient_data["gender"], + patient_data["yearOfBirth"], + ): patient = PatientRegistration.objects.filter( abha_number__abha_number=patient_data["healthIdNumber"] ) @@ -86,8 +75,29 @@ def share(self, request, *args, **kwargs): local_body=None, ward=None, ) + + abha_number = AbhaNumber.objects.create( + abha_number=patient_data["healthIdNumber"], + health_id=patient_data["healthId"], + name=patient_data["name"], + gender=patient_data["gender"], + date_of_birth=str( + datetime.strptime( + f"{patient_data['yearOfBirth']}-{patient_data['monthOfBirth']}-{patient_data['dayOfBirth']}", + "%Y-%m-%d", + ) + )[0:10], + address=patient_data["address"]["line"], + district=patient_data["address"]["district"], + state=patient_data["address"]["state"], + pincode=patient_data["address"]["pincode"], + ) + + abha_number.save() + patient.abha_number = abha_number patient.save() - self.add_abha_details_to_patient( + + self.get_linking_token( { "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], @@ -99,7 +109,6 @@ def share(self, request, *args, **kwargs): "%Y-%m-%d", ) )[0:10], - "patientId": patient.external_id, } ) @@ -122,7 +131,6 @@ def share(self, request, *args, **kwargs): on_share_response = AbdmGateway().on_share(payload) if on_share_response.status_code == 202: - print("on_share_header", on_share_response.request.body) return Response( on_share_response.request.body, status=status.HTTP_202_ACCEPTED, diff --git a/care/abdm/migrations/0006_auto_20230208_0915.py b/care/abdm/migrations/0006_auto_20230208_0915.py new file mode 100644 index 0000000000..ed00f037cd --- /dev/null +++ b/care/abdm/migrations/0006_auto_20230208_0915.py @@ -0,0 +1,48 @@ +# Generated by Django 2.2.11 on 2023-02-08 03:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("abdm", "0005_auto_20221220_2327"), + ] + + operations = [ + migrations.AddField( + model_name="abhanumber", + name="address", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="date_of_birth", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="district", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="gender", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="name", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="pincode", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="abhanumber", + name="state", + field=models.TextField(blank=True, null=True), + ), + ] diff --git a/care/abdm/models.py b/care/abdm/models.py index d931cc98c6..18e4c0add7 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -9,15 +9,28 @@ class AbhaNumber(BaseModel): abha_number = models.TextField(null=True, blank=True) - email = models.EmailField(null=True, blank=True) - first_name = models.TextField(null=True, blank=True) health_id = models.TextField(null=True, blank=True) - last_name = models.TextField(null=True, blank=True) + + name = models.TextField(null=True, blank=True) + first_name = models.TextField(null=True, blank=True) middle_name = models.TextField(null=True, blank=True) - profile_photo = models.TextField(null=True, blank=True) # What is profile photo? how is it stored as? - txn_id = models.TextField(null=True, blank=True) # 50? + last_name = models.TextField(null=True, blank=True) + + gender = models.TextField(null=True, blank=True) + date_of_birth = models.TextField(null=True, blank=True) + + address = models.TextField(null=True, blank=True) + district = models.TextField(null=True, blank=True) + state = models.TextField(null=True, blank=True) + pincode = models.TextField(null=True, blank=True) + + email = models.EmailField(null=True, blank=True) + profile_photo = models.TextField( + null=True, blank=True + ) # What is profile photo? how is it stored as? - access_token = models.TextField(null=True, blank=True) # 50 seems a bit too low for access tokens + txn_id = models.TextField(null=True, blank=True) + access_token = models.TextField(null=True, blank=True) refresh_token = models.TextField(null=True, blank=True) def __str__(self): diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index bcaf936742..0875d7cd60 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -8,9 +8,9 @@ from Crypto.PublicKey import RSA from django.conf import settings from django.core.cache import cache +from django.db.models import Q from care.abdm.models import AbhaNumber -from care.facility.models import PatientRegistration GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" @@ -235,6 +235,23 @@ def confirm_with_demographics(self, data): response = self.api.post(path, data) return response.json() + def verify_demographics(self, health_id, name, gender, year_of_birth): + auth_init_response = HealthIdGateway().auth_init( + {"authMethod": "DEMOGRAPHICS", "healthid": health_id} + ) + if "txnId" in auth_init_response: + demographics_response = HealthIdGateway().confirm_with_demographics( + { + "txnId": auth_init_response["txnId"], + "name": name, + "gender": gender, + "yearOfBirth": year_of_birth, + } + ) + return "status" in demographics_response and demographics_response["status"] + + return False + # /v1/auth/generate/access-token def generate_access_token(self, data): if "access_token" in data: @@ -308,25 +325,20 @@ class AbdmGateway: def __init__(self): self.api = APIGateway("abdm_gateway", None) - def link_patient_abha(self, patient, access_token, request_id): + def save_linking_token(self, patient, access_token, request_id): data = self.temp_memory[request_id] + health_id = patient["id"] or data["healthId"] - abha_object = AbhaNumber() - abha_object.health_id = patient["id"] or data["healthId"] - # abha_object.email = data["email"] - # abha_object.first_name = data["firstName"] - # abha_object.health_id = data["healthId"] - # abha_object.last_name = data["lastName"] - # abha_object.middle_name = data["middleName"] - # abha_object.profile_photo = data["profilePhoto"] - abha_object.access_token = access_token - abha_object.save() - - patient = PatientRegistration.objects.filter( - external_id=data["patientId"] + abha_object = AbhaNumber.objects.filter( + Q(abha_number=health_id) | Q(health_id=health_id) ).first() - patient.abha_number = abha_object - patient.save() + + if abha_object: + abha_object.access_token = access_token + abha_object.save() + return True + + return False # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): From ecdef7e045018978640b40b5b290f6b850c266d6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 09:39:01 +0530 Subject: [PATCH 030/180] updated the flow of link_via_qr --- care/abdm/api/viewsets/healthid.py | 49 +++++++++++++++++++++++++++--- care/abdm/api/viewsets/hip.py | 2 +- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 319e8b5530..cd20139377 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -212,6 +212,26 @@ def link_via_qr(self, request): serializer.is_valid(raise_exception=True) if "patientId" not in data: + patient = PatientRegistration.objects.filter( + abha_number__abha_number=data["hidn"] + ).first() + if patient: + return Response( + { + "message": "A patient is already associated with the provided Abha Number" + }, + status=status.HTTP_400_BAD_REQUEST, + ) + + if ( + "facilityId" not in data + or not Facility.objects.filter(external_id=data["facilityId"]).first() + ): + return Response( + {"message": "Enter a valid facilityId"}, + status=status.HTTP_400_BAD_REQUEST, + ) + patient = PatientRegistration.objects.create( facility=Facility.objects.get(external_id=data["facilityId"]), name=data["name"], @@ -234,11 +254,31 @@ def link_via_qr(self, request): local_body=None, ward=None, ) - patient.save() - - patient_id = patient.external_id else: - patient_id = data["patientId"] + patient = PatientRegistration.objects.filter( + external_id=data["patientId"] + ).first() + + if not patient: + return Response( + {"message": "Enter a valid patientId"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + abha_number = AbhaNumber.objects.create( + abha_number=data["hidn"], + health_id=data["phr"], + name=data["name"], + gender=data["gender"], + date_of_birth=str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], + address=data["address"], + district=data["dist name"], + state=data["state name"], + ) + + abha_number.save() + patient.abha_number = abha_number + patient.save() AbdmGateway().fetch_modes( { @@ -246,7 +286,6 @@ def link_via_qr(self, request): "name": data["name"], "gender": data["gender"], "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], - "patientId": patient_id, } ) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index e0cdd5cdd9..12b2c5d922 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -47,7 +47,7 @@ def share(self, request, *args, **kwargs): ): patient = PatientRegistration.objects.filter( abha_number__abha_number=patient_data["healthIdNumber"] - ) + ).first() if not patient: patient = PatientRegistration.objects.create( From b6306f7518f925014b860b0d8de1267e25cf3aec Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 09:57:23 +0530 Subject: [PATCH 031/180] verify demographics before patient creation --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/api/viewsets/healthid.py | 18 +++++++++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7f81d8ae36..b7c2596f35 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -35,7 +35,7 @@ class OnConfirmView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data print(data) - AbdmGateway().link_patient_abha( + AbdmGateway().save_linking_token( data["auth"]["patient"], data["auth"]["accessToken"], data["resp"]["requestId"], diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index cd20139377..57ea1673f9 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -232,6 +232,18 @@ def link_via_qr(self, request): status=status.HTTP_400_BAD_REQUEST, ) + dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() + if not HealthIdGateway().verify_demographics( + data["phr"] or data["hdin"], + data["name"], + data["gender"], + str(dob.year), + ): + return Response( + {"message": "Please enter valid data"}, + status=status.HTTP_403_FORBIDDEN, + ) + patient = PatientRegistration.objects.create( facility=Facility.objects.get(external_id=data["facilityId"]), name=data["name"], @@ -243,7 +255,7 @@ def link_via_qr(self, request): is_antenatal=False, phone_number=data["mobile"], emergency_phone_number=data["mobile"], - date_of_birth=datetime.strptime(data["dob"], "%d-%m-%Y").date(), + date_of_birth=dob, blood_group="UNK", nationality="India", address=data["address"], @@ -270,7 +282,7 @@ def link_via_qr(self, request): health_id=data["phr"], name=data["name"], gender=data["gender"], - date_of_birth=str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], + date_of_birth=str(dob)[0:10], address=data["address"], district=data["dist name"], state=data["state name"], @@ -289,7 +301,7 @@ def link_via_qr(self, request): } ) - return Response({}, status=status.HTTP_200_OK) + return Response({"message": "success"}, status=status.HTTP_200_OK) # auth/init @swagger_auto_schema( From 526fe218408de879dae6af6b2a5a30089126871d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 10:51:15 +0530 Subject: [PATCH 032/180] linking token while confirming with otps --- care/abdm/api/viewsets/healthid.py | 62 +++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 57ea1673f9..6550de47af 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -110,7 +110,7 @@ def verify_mobile_otp(self, request): response = HealthIdGateway().verify_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) - def create_abha(self, abha_profile): + def create_abha(self, abha_profile, token): abha_object = AbhaNumber.objects.filter( abha_number=abha_profile["healthIdNumber"] ).first() @@ -118,15 +118,30 @@ def create_abha(self, abha_profile): if abha_object: return abha_object - abha_object = AbhaNumber() - abha_object.abha_number = abha_profile["healthIdNumber"] - abha_object.email = abha_profile["email"] - abha_object.first_name = abha_profile["firstName"] - abha_object.health_id = abha_profile["healthId"] - abha_object.last_name = abha_profile["lastName"] - abha_object.middle_name = abha_profile["middleName"] - abha_object.profile_photo = abha_profile["profilePhoto"] - abha_object.txn_id = abha_profile["healthIdNumber"] + abha_object = AbhaNumber().objects.create( + abha_number=abha_profile["healthIdNumber"], + health_id=abha_profile["healthId"], + name=abha_profile["name"], + first_name=abha_profile["firstName"], + middle_name=abha_profile["middleName"], + last_name=abha_profile["lastName"], + gender=abha_profile["gender"], + date_of_birth=str( + datetime.strptime( + f"{abha_profile['yearOfBirth']}-{abha_profile['monthOfBirth']}-{abha_profile['dayOfBirth']}", + "%Y-%m-%d", + ) + )[0:10], + address=abha_profile["address"], + district=abha_profile["districtName"], + state=abha_profile["stateName"], + pincode=abha_profile["pincode"], + email=abha_profile["email"], + profile_photo=abha_profile["profilePhoto"], + txn_id=token["txn_id"], + access_token=token["access_token"], + refresh_token=token["refresh_token"], + ) abha_object.save() return abha_object @@ -158,7 +173,14 @@ def create_health_id(self, request): abha_profile = HealthIdGateway().create_health_id(data) # have a serializer to verify data of abha_profile - abha_object = self.create_abha(abha_profile) + abha_object = self.create_abha( + abha_profile, + { + "txn_id": data["txnId"], + "access_token": abha_profile["token"], + "refresh_token": None, + }, + ) if "patientId" in data: patient_id = data.pop("patientId") @@ -335,7 +357,14 @@ def confirm_with_aadhaar_otp(self, request): abha_profile = HealthIdGateway().get_profile(response) # have a serializer to verify data of abha_profile - abha_object = self.create_abha(abha_profile) + abha_object = self.create_abha( + abha_profile, + { + "access_token": response["token"], + "refresh_token": response["refreshToken"], + "txn_id": data["txnId"], + }, + ) if "patientId" in data: patient_id = data.pop("patientId") @@ -374,7 +403,14 @@ def confirm_with_mobile_otp(self, request): abha_profile = HealthIdGateway().get_profile(response) # have a serializer to verify data of abha_profile - abha_object = self.create_abha(abha_profile) + abha_object = self.create_abha( + abha_profile, + { + "access_token": response["token"], + "refresh_token": response["refreshToken"], + "txn_id": data["txnId"], + }, + ) if "patientId" in data: patient_id = data.pop("patientId") From ded150ed4e0b24adfb5529b6411f9a78ba8d0d1b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 11:06:26 +0530 Subject: [PATCH 033/180] fixed abha creation error in healthid --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 6550de47af..a978eb2f74 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -118,7 +118,7 @@ def create_abha(self, abha_profile, token): if abha_object: return abha_object - abha_object = AbhaNumber().objects.create( + abha_object = AbhaNumber.objects.create( abha_number=abha_profile["healthIdNumber"], health_id=abha_profile["healthId"], name=abha_profile["name"], From 226d5c1a550a246f0f9b675cf5f4eed60a1f1add Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 11:57:36 +0530 Subject: [PATCH 034/180] fixed address key missing issue --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index a978eb2f74..3341729371 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -132,7 +132,7 @@ def create_abha(self, abha_profile, token): "%Y-%m-%d", ) )[0:10], - address=abha_profile["address"], + address=abha_profile["address"] if "address" in abha_profile else "", district=abha_profile["districtName"], state=abha_profile["stateName"], pincode=abha_profile["pincode"], From 8e0f8498d72feb0a9fa7c00acfeb06f09861973d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 12:18:30 +0530 Subject: [PATCH 035/180] removed facility id hardcoding in profile share --- care/abdm/api/viewsets/hip.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 12b2c5d922..09945866f9 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -28,7 +28,9 @@ def share(self, request, *args, **kwargs): patient_data = data["profile"]["patient"] counter_id = ( - "8be5ab36-1b66-44ca-ae77-c719e084160d" or data["profile"]["hipCode"] + data["profile"]["hipCode"] + if len(data["profile"]["hipCode"]) == 36 + else Facility.objects.first().external_id ) patient_data["mobile"] = "" From 5143a862c0aa2b2e0ac0b505e7771ca6c3b25db9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 8 Feb 2023 12:48:31 +0530 Subject: [PATCH 036/180] added pycryptodome --- requirements/base.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements/base.txt b/requirements/base.txt index da425f6ba1..057c1fe19c 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -62,3 +62,5 @@ typed-ast==1.5.0 # In Memory Database # ------------------------------------------------------------------------------ littletable==2.0.7 +pycryptodome==3.16.0 +pycryptodomex==3.16.0 From 323cbc4df3f252c32d5a5feaf1081621e6dab1f1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 13:05:01 +0530 Subject: [PATCH 037/180] minor changes --- care/abdm/utils/api_call.py | 25 +++++-------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 0875d7cd60..434d3d93d7 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -219,7 +219,7 @@ def auth_init(self, data): # /v1/auth/confirmWithAadhaarOtp def confirm_with_aadhaar_otp(self, data): - path = "/v1/auth/confirmWithAadhaarOtp" + path = "/v1/auth/confirmWithAadhaarOTP" response = self.api.post(path, data) return response.json() @@ -343,7 +343,7 @@ def save_linking_token(self, patient, access_token, request_id): # /v0.5/users/auth/fetch-modes def fetch_modes(self, data): path = "/v0.5/users/auth/fetch-modes" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} request_id = str(uuid.uuid4()) """ @@ -374,7 +374,7 @@ def fetch_modes(self, data): # "/v0.5/users/auth/init" def init(self, prev_request_id): path = "/v0.5/users/auth/init" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} request_id = str(uuid.uuid4()) @@ -396,25 +396,10 @@ def init(self, prev_request_id): response = self.api.post(path, payload, None, additional_headers) return response - """ - { - "requestId": guidv4, - "timestamp": isotime, - "transactionId": "xxxxxxxxxxxxxxx from on-init", - "credential": { - "demographic": { - "name": "Khavin", - "gender": "M", - "dateOfBirth": "1999-01-18" - }, - "authCode": "" - } - } - """ # "/v0.5/users/auth/confirm" def confirm(self, transaction_id, prev_request_id): path = "/v0.5/users/auth/confirm" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} request_id = str(uuid.uuid4()) @@ -445,6 +430,6 @@ def confirm(self, transaction_id, prev_request_id): # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" - additional_headers = {"X-CM-ID": "sbx"} + additional_headers = {"X-CM-ID": settings.X_CM_ID} response = self.api.post(path, data, None, additional_headers) return response From d73681193f55c74dbfe498f90a17d6a1cd2929f0 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 27 Feb 2023 15:29:58 +0530 Subject: [PATCH 038/180] Create deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 61 ++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/deployment-branch.yaml diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml new file mode 100644 index 0000000000..eff2d34813 --- /dev/null +++ b/.github/workflows/deployment-branch.yaml @@ -0,0 +1,61 @@ +name: Branch based deploy + +on: + workflow_dispatch: + +env: + IMAGE_NAME: care-${{ github.ref_name}} +jobs: + + build-image: + name: Build & Push Staging to container registries + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest-${{ github.run_number }} + type=raw,value=latest + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + flavor: | + latest=true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Cache Docker layers + uses: actions/cache@v2 + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ hashFiles('r*/base.txt', 'r*/production.txt', 'Dockerfile') }} + restore-keys: | + ${{ runner.os }}-buildx- + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build image + uses: docker/build-push-action@v3 + with: + context: . + file: Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new + + - name: Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache From 2a877a0d6cf0fa525c71129ba427732a90a8dea8 Mon Sep 17 00:00:00 2001 From: Mathew Alex Date: Mon, 27 Feb 2023 16:21:30 +0530 Subject: [PATCH 039/180] udpated image tag --- .github/workflows/deployment-branch.yaml | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index eff2d34813..3b1368df66 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -3,8 +3,6 @@ name: Branch based deploy on: workflow_dispatch: -env: - IMAGE_NAME: care-${{ github.ref_name}} jobs: build-image: @@ -20,12 +18,8 @@ jobs: images: | ghcr.io/${{ github.repository }} tags: | - type=raw,value=latest-${{ github.run_number }} - type=raw,value=latest - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=true + type=raw,value=${{ github.ref_name}}-${{ github.run_number }} + type=raw,value=${{ github.ref_name}} - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 From 131063202f95882419f0a311eaacfcbb740dc68a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 16:23:54 +0530 Subject: [PATCH 040/180] fixed dob used before declaration error in link_via_qr --- care/abdm/api/viewsets/healthid.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 3341729371..326be9cb51 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -230,9 +230,12 @@ def search_by_health_id(self, request): @action(detail=False, methods=["post"]) def link_via_qr(self, request): data = request.data + serializer = QRContentSerializer(data=data) serializer.is_valid(raise_exception=True) + dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() + if "patientId" not in data: patient = PatientRegistration.objects.filter( abha_number__abha_number=data["hidn"] @@ -254,7 +257,6 @@ def link_via_qr(self, request): status=status.HTTP_400_BAD_REQUEST, ) - dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() if not HealthIdGateway().verify_demographics( data["phr"] or data["hdin"], data["name"], From 87b89f105d40ebae9d7b618fdc6ff952b8685f87 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 17:32:21 +0530 Subject: [PATCH 041/180] added cm_id env --- config/settings/base.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/settings/base.py b/config/settings/base.py index 705ab9d694..9f3db9e747 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -269,7 +269,7 @@ "formatters": { "verbose": { "format": "%(levelname)s %(asctime)s %(module)s " - "%(process)d %(thread)d %(message)s" + "%(process)d %(thread)d %(message)s" } }, "handlers": { @@ -512,3 +512,4 @@ def GETKEY(group, request): ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") +X_CM_ID = env("X_CM_ID", default="sbx") From ce03be075dfc9cc7811a43284e0509795177d8a0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 27 Feb 2023 17:51:09 +0530 Subject: [PATCH 042/180] return patient as response for link_via_qr --- care/abdm/api/viewsets/healthid.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 326be9cb51..b7470b531e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -23,6 +23,7 @@ ) from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway +from care.facility.api.serializers.patient import PatientDetailSerializer from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset @@ -325,7 +326,8 @@ def link_via_qr(self, request): } ) - return Response({"message": "success"}, status=status.HTTP_200_OK) + patient_serialized = PatientDetailSerializer(patient).data + return Response(patient_serialized, status=status.HTTP_200_OK) # auth/init @swagger_auto_schema( From 3130220eaa12d0f49a1a5ae43d1feaa2856a1b39 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Mar 2023 12:39:52 +0530 Subject: [PATCH 043/180] added authentication to abdm apis --- care/abdm/api/viewsets/abha.py | 2 ++ care/abdm/api/viewsets/healthid.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index d6eba3fe8e..ac1957a82d 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -1,5 +1,6 @@ from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -13,6 +14,7 @@ class AbhaViewSet(GenericViewSet): serializer_class = AbhaSerializer model = AbhaNumber queryset = AbhaNumber.objects.all() + permission_classes = (IsAuthenticated,) def get_abha_object(self): queryset = get_patient_queryset(self.request.user) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b7470b531e..0ce25fa822 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -7,6 +7,7 @@ from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.mixins import CreateModelMixin +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -33,6 +34,7 @@ class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" model = AbhaNumber + permission_classes = (IsAuthenticated,) # TODO: Ratelimiting for all endpoints that generate OTP's / Critical API's @swagger_auto_schema( From d0bdf95354db73353e28020bb929843edda945bf Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Fri, 3 Mar 2023 12:51:52 +0530 Subject: [PATCH 044/180] Add Basic Auth Ratelimiting on all Endpoints --- config/authentication.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/config/authentication.py b/config/authentication.py index 0d104efc78..a2dd9d69ef 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -2,9 +2,11 @@ import jwt import requests +from django.contrib.auth import authenticate from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ -from rest_framework import HTTP_HEADER_ENCODING +from rest_framework import HTTP_HEADER_ENCODING, exceptions +from rest_framework import status from rest_framework.authentication import BasicAuthentication from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken @@ -12,6 +14,7 @@ from care.facility.models import Facility from care.facility.models.asset import Asset from care.users.models import User +from config.ratelimit import ratelimit class CustomJWTAuthentication(JWTAuthentication): @@ -31,6 +34,32 @@ def get_validated_token(self, raw_token): class CustomBasicAuthentication(BasicAuthentication): + + def authenticate_credentials(self, userid, password, request=None): + """ + Authenticate the userid and password against username and password + with optional request for context. + """ + from config.auth_views import CaptchaRequiredException + credentials = { + User.USERNAME_FIELD: userid, + 'password': password + } + if ratelimit(request, "login", [userid]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + user = authenticate(request=request, **credentials) + + if user is None: + raise exceptions.AuthenticationFailed(_('Invalid username/password.')) + + if not user.is_active: + raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + + return (user, None) + def authenticate_header(self, request): return "" From 1155a8c3b7e7a31943343baa72e9af3e5951a4c2 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Fri, 3 Mar 2023 13:08:57 +0530 Subject: [PATCH 045/180] Fix Ratelimting bug --- config/authentication.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/authentication.py b/config/authentication.py index a2dd9d69ef..7953c4526e 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -45,7 +45,7 @@ def authenticate_credentials(self, userid, password, request=None): User.USERNAME_FIELD: userid, 'password': password } - if ratelimit(request, "login", [userid]): + if ratelimit(request, "login", [userid], increment=False): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, @@ -53,6 +53,7 @@ def authenticate_credentials(self, userid, password, request=None): user = authenticate(request=request, **credentials) if user is None: + ratelimit(request, "login", [userid]) raise exceptions.AuthenticationFailed(_('Invalid username/password.')) if not user.is_active: From 007688babdff7131f7af71c76660804101531233 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 3 Mar 2023 15:22:48 +0530 Subject: [PATCH 046/180] added rate limit for abdm apis --- care/abdm/api/viewsets/healthid.py | 86 ++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 0ce25fa822..8ad029865b 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -28,6 +28,8 @@ from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration from care.utils.queryset.patient import get_patient_queryset +from config.auth_views import CaptchaRequiredException +from config.ratelimit import ratelimit # API for Generating OTP for HealthID @@ -46,6 +48,13 @@ class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): @action(detail=False, methods=["post"]) def generate_aadhaar_otp(self, request): data = request.data + + if ratelimit(request, "generate_aadhaar_otp", [data["aadhaar"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().generate_aadhaar_otp(data) @@ -61,6 +70,13 @@ def generate_aadhaar_otp(self, request): @action(detail=False, methods=["post"]) def resend_aadhaar_otp(self, request): data = request.data + + if ratelimit(request, "resend_aadhaar_otp", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = AadharOtpResendRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().resend_aadhaar_otp(data) @@ -76,6 +92,13 @@ def resend_aadhaar_otp(self, request): @action(detail=False, methods=["post"]) def verify_aadhaar_otp(self, request): data = request.data + + if ratelimit(request, "verify_aadhaar_otp", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().verify_aadhaar_otp( @@ -93,6 +116,13 @@ def verify_aadhaar_otp(self, request): @action(detail=False, methods=["post"]) def generate_mobile_otp(self, request): data = request.data + + if ratelimit(request, "generate_mobile_otp", [data["mobile"], data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().generate_mobile_otp(data) @@ -108,6 +138,13 @@ def generate_mobile_otp(self, request): @action(detail=False, methods=["post"]) def verify_mobile_otp(self, request): data = request.data + + if ratelimit(request, "verify_mobile_otp", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().verify_mobile_otp(data) @@ -171,6 +208,13 @@ def add_abha_details_to_patient(self, abha_object, patient_object): @action(detail=False, methods=["post"]) def create_health_id(self, request): data = request.data + + if ratelimit(request, "create_health_id", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = CreateHealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) abha_profile = HealthIdGateway().create_health_id(data) @@ -218,6 +262,13 @@ def create_health_id(self, request): @action(detail=False, methods=["post"]) def search_by_health_id(self, request): data = request.data + + if ratelimit(request, "search_by_health_id", [data["healthId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = HealthIdSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().search_by_health_id(data) @@ -342,6 +393,13 @@ def link_via_qr(self, request): @action(detail=False, methods=["post"]) def auth_init(self, request): data = request.data + + if ratelimit(request, "auth_init", [data["healthid"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = HealthIdAuthSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().auth_init(data) @@ -357,6 +415,13 @@ def auth_init(self, request): @action(detail=False, methods=["post"]) def confirm_with_aadhaar_otp(self, request): data = request.data + + if ratelimit(request, "confirm_with_aadhaar_otp", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_aadhaar_otp(data) @@ -403,6 +468,13 @@ def confirm_with_aadhaar_otp(self, request): @action(detail=False, methods=["post"]) def confirm_with_mobile_otp(self, request): data = request.data + + if ratelimit(request, "confirm_with_mobile_otp", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = VerifyOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_mobile_otp(data) @@ -448,6 +520,13 @@ def confirm_with_mobile_otp(self, request): @action(detail=False, methods=["post"]) def confirm_with_demographics(self, request): data = request.data + + if ratelimit(request, "confirm_with_demographics", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = VerifyDemographicsRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().confirm_with_demographics(data) @@ -465,6 +544,13 @@ def confirm_with_demographics(self, request): @action(detail=False, methods=["post"]) def check_and_generate_mobile_otp(self, request): data = request.data + + if ratelimit(request, "check_and_generate_mobile_otp", [data["txnId"]]): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) serializer.is_valid(raise_exception=True) response = HealthIdGateway().check_and_generate_mobile_otp(data) From 31f17d44d75df4752bf117e463dc4af607a56455 Mon Sep 17 00:00:00 2001 From: Mathew Date: Sat, 4 Mar 2023 00:14:45 +0530 Subject: [PATCH 047/180] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index a08bc74922..9508259583 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # Care Backend +

From 9c76263e3201bbf79857944e34425bac71c94926 Mon Sep 17 00:00:00 2001 From: Mathew Date: Sat, 4 Mar 2023 00:15:37 +0530 Subject: [PATCH 048/180] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 3b1368df66..6ede8a37f7 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -2,7 +2,12 @@ name: Branch based deploy on: workflow_dispatch: - + + push: + branches: + - abdm-m1-vignesh + paths-ignore: + - "docs/**" jobs: build-image: From c8e7a126c2d942f2fd6d5dfaef58ca315d7290f6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 6 Mar 2023 17:06:40 +0530 Subject: [PATCH 049/180] fixed ratelimiting for generate_mobile_otp --- care/abdm/api/viewsets/healthid.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 8ad029865b..3d66336ba8 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -117,7 +117,7 @@ def verify_aadhaar_otp(self, request): def generate_mobile_otp(self, request): data = request.data - if ratelimit(request, "generate_mobile_otp", [data["mobile"], data["txnId"]]): + if ratelimit(request, "generate_mobile_otp", [data["txnId"]]): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, @@ -263,7 +263,9 @@ def create_health_id(self, request): def search_by_health_id(self, request): data = request.data - if ratelimit(request, "search_by_health_id", [data["healthId"]]): + if ratelimit( + request, "search_by_health_id", [data["healthId"]], increment=False + ): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, From 2649f9c2193b6b625179840c3ec46676df3842ac Mon Sep 17 00:00:00 2001 From: Gigin George Date: Mon, 6 Mar 2023 18:54:28 +0530 Subject: [PATCH 050/180] Add Ratelimiting Docs comment --- config/ratelimit.py | 1 + config/settings/base.py | 1 + 2 files changed, 2 insertions(+) diff --git a/config/ratelimit.py b/config/ratelimit.py index 4d59c8dcf3..435076c726 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -21,6 +21,7 @@ def validatecaptcha(request): return False +# refer https://django-ratelimit.readthedocs.io/en/stable/rates.html for rate def ratelimit( request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True ): diff --git a/config/settings/base.py b/config/settings/base.py index 9f3db9e747..e5e1976226 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -324,6 +324,7 @@ def GETKEY(group, request): return "ratelimit" +# https://django-ratelimit.readthedocs.io/en/stable/rates.html DJANGO_RATE_LIMIT = env("RATE_LIMIT", default="5/10m") GOOGLE_RECAPTCHA_SECRET_KEY = env("GOOGLE_RECAPTCHA_SECRET_KEY", default="") From 50332f993f4eac263c7f66f06161d1422cb0920b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Mar 2023 17:22:31 +0530 Subject: [PATCH 051/180] allow null for abha_number while registering patient --- care/facility/api/serializers/patient.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 2852b0cbe7..16373a0a9e 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -182,7 +182,7 @@ class Meta: ) abha_number = ExternalIdSerializerField( - queryset=AbhaNumber.objects.all(), required=False + queryset=AbhaNumber.objects.all(), required=False, allow_null=True ) abha_number_object = AbhaNumberSerializer(source="abha_number", read_only=True) From 4605dc3f9a55f5236ada639735ddd48439dd8b86 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Mar 2023 17:42:05 +0530 Subject: [PATCH 052/180] fixed confirm_with_aadhaar_otp path --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 434d3d93d7..5359637ef1 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -219,7 +219,7 @@ def auth_init(self, data): # /v1/auth/confirmWithAadhaarOtp def confirm_with_aadhaar_otp(self, data): - path = "/v1/auth/confirmWithAadhaarOTP" + path = "/v1/auth/confirmWithAadhaarOtp" response = self.api.post(path, data) return response.json() From 3ba753ae587c146a8399f5db246a1b0a4633cec9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 7 Mar 2023 17:58:24 +0530 Subject: [PATCH 053/180] removed existing abha id validation --- care/abdm/api/viewsets/healthid.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 3d66336ba8..a7d77fd1ad 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -187,12 +187,12 @@ def create_abha(self, abha_profile, token): return abha_object def add_abha_details_to_patient(self, abha_object, patient_object): - patient = PatientRegistration.objects.filter( - abha_number__abha_number=abha_object.abha_number - ).first() + # patient = PatientRegistration.objects.filter( + # abha_number__abha_number=abha_object.abha_number + # ).first() - if patient or patient_object.abha_number: - return False + # if patient or patient_object.abha_number: + # return False patient_object.abha_number = abha_object patient_object.save() From c63b2bedbcb422e2d8a11341c7bfb124d2559bbf Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 30 Mar 2023 10:03:44 +0530 Subject: [PATCH 054/180] added care-context linking flow --- care/abdm/api/viewsets/auth.py | 130 +++++++++++++++++++++++++++++ care/abdm/api/viewsets/healthid.py | 24 ++++++ care/abdm/api/viewsets/hip.py | 26 +++++- care/abdm/utils/api_call.py | 125 ++++++++++++++++++++++++++- config/urls.py | 30 ++++++- 5 files changed, 332 insertions(+), 3 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index b7c2596f35..3a7a0fb957 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -4,6 +4,8 @@ from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway +from care.facility.models.patient import PatientRegistration +from care.facility.models.patient_consultation import PatientConsultation class OnFetchView(GenericAPIView): @@ -41,3 +43,131 @@ def post(self, request, *args, **kwargs): data["resp"]["requestId"], ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +class OnAddContextsView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class DiscoverView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + + patients = PatientRegistration.objects.all() + verified_identifiers = data["patient"]["verifiedIdentifiers"] + matched_by = [] + if len(verified_identifiers) == 0: + return Response( + "No matching records found, need more data", + status=status.HTTP_404_NOT_FOUND, + ) + else: + for identifier in verified_identifiers: + if identifier["type"] == "MOBILE": + matched_by.append(identifier["value"]) + patients = patients.filter(phone_number=identifier["value"]) + + if identifier["type"] == "NDHM_HEALTH_NUMBER": + matched_by.append(identifier["value"]) + patients = patients.filter( + abha_number__abha_number=identifier["value"] + ) + + if identifier["type"] == "HEALTH_ID": + matched_by.append(identifier["value"]) + patients = patients.filter( + abha_number__health_id=identifier["value"] + ) + + patients.filter( + abha_number__name=data["patient"]["name"], + abha_number__gender=data["patient"]["gender"], + # TODO: check date also + ) + + if len(patients) != 1: + return Response( + "No matching records found, need more data", + status=status.HTTP_404_NOT_FOUND, + ) + + AbdmGateway().on_discover( + { + "request_id": data["requestId"], + "transaction_id": data["transactionId"], + "patient_id": str(patients[0].external_id), + "patient_name": patients[0].name, + "care_contexts": list( + map( + lambda consultation: { + "id": str(consultation.external_id), + "name": f"Encounter: {str(consultation.created_date.date())}", + }, + PatientConsultation.objects.filter(patient=patients[0]), + ) + ), + "matched_by": matched_by, + } + ) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class LinkInitView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + + # TODO: send otp to patient + + AbdmGateway().on_link_init( + { + "request_id": data["requestId"], + "transaction_id": data["transactionId"], + "patient_id": data["patient"]["referenceNumber"], + "phone": "7639899448", + } + ) + return Response({}, status=status.HTTP_202_ACCEPTED) + + +class LinkConfirmView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + + # TODO: verify otp + + patient = PatientRegistration.objects.get( + external_id=data["confirmation"]["linkRefNumber"] + ) + AbdmGateway().on_link_confirm( + { + "request_id": data["requestId"], + "patient_id": str(patient.external_id), + "patient_name": patient.name, + "care_contexts": list( + map( + lambda consultation: { + "id": str(consultation.external_id), + "name": f"Encounter: {str(consultation.created_date.date())}", + }, + PatientConsultation.objects.filter(patient=patient), + ) + ), + } + ) + + return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index a7d77fd1ad..553de6278e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -384,6 +384,30 @@ def link_via_qr(self, request): patient_serialized = PatientDetailSerializer(patient).data return Response(patient_serialized, status=status.HTTP_200_OK) + @swagger_auto_schema( + operation_id="get_new_linking_token", + responses={"200": "{'status': 'boolean'}"}, + tags=["ABDM HealthID"], + ) + @action(detail=False, methods=["post"]) + def get_new_linking_token(self, request): + data = request.data + + patient = PatientDetailSerializer( + PatientRegistration.objects.get(external_id=data["patient"]) + ).data + + AbdmGateway().fetch_modes( + { + "healthId": patient["abha_number_object"]["abha_number"], + "name": patient["abha_number_object"]["name"], + "gender": patient["abha_number_object"]["gender"], + "dateOfBirth": str(patient["abha_number_object"]["date_of_birth"]), + } + ) + + return Response({}, status=status.HTTP_200_OK) + # auth/init @swagger_auto_schema( # /v1/auth/init diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 09945866f9..c218d657f0 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -11,7 +11,7 @@ from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility -from care.facility.models.patient import PatientRegistration +from care.facility.models.patient import PatientConsultation, PatientRegistration class HipViewSet(GenericViewSet): @@ -145,3 +145,27 @@ def share(self, request, *args, **kwargs): }, status=status.HTTP_401_UNAUTHORIZED, ) + + # TODO: move it somewhere appropriate + @action(detail=False, methods=["POST"]) + def add_care_context(self, request, *args, **kwargs): + consultation_id = request.data["consultation"] + + consultation = PatientConsultation.objects.get(external_id=consultation_id) + + if not consultation: + return Response( + {"consultation": "No matching records found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + response = AbdmGateway().add_contexts( + { + "access_token": consultation.patient.abha_number.access_token, + "patient_id": str(consultation.patient.external_id), + "patient_name": consultation.patient.name, + "context_id": str(consultation.external_id), + "context_name": f"Encounter: {str(consultation.created_date.date())}", + } + ) + return Response(response) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 5359637ef1..6fa0af658e 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -1,7 +1,7 @@ import json import uuid from base64 import b64encode -from datetime import datetime, timezone +from datetime import datetime, timedelta, timezone import requests from Crypto.Cipher import PKCS1_v1_5 @@ -427,6 +427,129 @@ def confirm(self, transaction_id, prev_request_id): response = self.api.post(path, payload, None, additional_headers) return response + # TODO: make it dynamic and call it at discharge (call it from on_confirm) + def add_contexts(self, data): + path = "/v0.5/links/link/add-contexts" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "link": { + "accessToken": data["access_token"], + "patient": { + "referenceNumber": data["patient_id"], + "display": data["patient_name"], + "careContexts": [ + { + "referenceNumber": data["context_id"], + "display": data["context_name"], + } + ], + }, + }, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + + def on_discover(self, data): + path = "/v0.5/care-contexts/on-discover" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "transactionId": data["transaction_id"], + "patient": { + "referenceNumber": data["patient_id"], + "display": data["patient_name"], + "careContexts": list( + map( + lambda context: { + "referenceNumber": context["id"], + "display": context["name"], + }, + data["care_contexts"], + ) + ), + "matchedBy": data["matched_by"], + }, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + + def on_link_init(self, data): + path = "/v0.5/links/link/on-init" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "transactionId": data["transaction_id"], + "link": { + "referenceNumber": data["patient_id"], + "authenticationType": "DIRECT", + "meta": { + "communicationMedium": "MOBILE", + "communicationHint": data["phone"], + "communicationExpiry": str( + (datetime.now() + timedelta(minutes=15)).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ) + ), + }, + }, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + + def on_link_confirm(self, data): + path = "/v0.5/links/link/on-confirm" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "patient": { + "referenceNumber": data["patient_id"], + "display": data["patient_name"], + "careContexts": list( + map( + lambda context: { + "referenceNumber": context["id"], + "display": context["name"], + }, + data["care_contexts"], + ) + ), + }, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index b33c324130..efadb8bbd7 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,7 +9,15 @@ from rest_framework import permissions from rest_framework_simplejwt.views import TokenVerifyView -from care.abdm.api.viewsets.auth import OnConfirmView, OnFetchView, OnInitView +from care.abdm.api.viewsets.auth import ( + DiscoverView, + LinkConfirmView, + LinkInitView, + OnAddContextsView, + OnConfirmView, + OnFetchView, + OnInitView, +) from care.facility.api.viewsets.open_id import OpenIdConfigView from care.users.api.viewsets.change_password import ChangePasswordView from care.users.reset_password_views import ( @@ -98,6 +106,26 @@ OnConfirmView.as_view(), name="abdm_on_confirm_view", ), + path( + "v0.5/links/link/on-add-contexts", + OnAddContextsView.as_view(), + name="abdm_on_add_context_view", + ), + path( + "v0.5/care-contexts/discover", + DiscoverView.as_view(), + name="abdm_discover_view", + ), + path( + "v0.5/links/link/init", + LinkInitView.as_view(), + name="abdm_link_init_view", + ), + path( + "v0.5/links/link/confirm", + LinkConfirmView.as_view(), + name="abdm_link_confirm_view", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 70e437b20c6680d1e2dfeb0b3d0653dca152666a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 30 Mar 2023 11:40:24 +0530 Subject: [PATCH 055/180] added consent flow --- care/abdm/api/viewsets/auth.py | 59 ++++++++++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 18 +++++++++++ config/urls.py | 6 ++++ 3 files changed, 83 insertions(+) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 3a7a0fb957..10162adbe4 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,3 +1,6 @@ +import json + +from django.core.cache import cache from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny @@ -171,3 +174,59 @@ def post(self, request, *args, **kwargs): ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +class NotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + + # TODO: create a seperate cache and also add a expiration time + cache.set(data["notification"]["consentId"], json.dumps(data)) + + # data = { + # "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", + # "timestamp": "2023-03-30T05:00:31.288Z", + # "notification": { + # "status": "GRANTED", + # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + # "consentDetail": { + # "schemaVersion": "string", + # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + # "createdAt": "2023-03-30T05:00:31.288Z", + # "patient": {"id": "hinapatel79@ndhm"}, + # "careContexts": [ + # { + # "patientReference": "hinapatel79@hospital", + # "careContextReference": "Episode1", + # } + # ], + # "purpose": {"text": "string", "code": "string", "refUri": "string"}, + # "hip": {"id": "string", "name": "TESI-HIP"}, + # "consentManager": {"id": "string"}, + # "hiTypes": ["OPConsultation"], + # "permission": { + # "accessMode": "VIEW", + # "dateRange": { + # "from": "2023-03-30T05:00:31.288Z", + # "to": "2023-03-30T05:00:31.288Z", + # }, + # "dataEraseAt": "2023-03-30T05:00:31.288Z", + # "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, + # }, + # }, + # "signature": "Signature of CM as defined in W3C standards; Base64 encoded", + # "grantAcknowledgement": False, + # }, + # } + + AbdmGateway().on_notify( + { + "request_id": data["requestId"], + "consent_id": data["notification"]["consentId"], + } + ) + return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 6fa0af658e..e6f7ef4937 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -550,6 +550,24 @@ def on_link_confirm(self, data): response = self.api.post(path, payload, None, additional_headers) return response + def on_notify(self, data): + path = "/v0.5/consents/hip/on-notify" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "acknowledgement": {"status": "OK", "consentId": data["consent_id"]}, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index efadb8bbd7..64c58c67e4 100644 --- a/config/urls.py +++ b/config/urls.py @@ -13,6 +13,7 @@ DiscoverView, LinkConfirmView, LinkInitView, + NotifyView, OnAddContextsView, OnConfirmView, OnFetchView, @@ -126,6 +127,11 @@ LinkConfirmView.as_view(), name="abdm_link_confirm_view", ), + path( + "v0.5/consents/hip/notify", + NotifyView.as_view(), + name="abdm_notify_view", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), From 859891f778a5806c780ad094ca83176bd46adc0e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 31 Mar 2023 10:59:26 +0530 Subject: [PATCH 056/180] added data transfer flow --- care/abdm/api/viewsets/auth.py | 156 +++++++++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 81 +++++++++++++++++ care/abdm/utils/fhir.py | 23 +++++ config/urls.py | 6 ++ requirements/base.txt | 1 + 5 files changed, 267 insertions(+) create mode 100644 care/abdm/utils/fhir.py diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 10162adbe4..5b3031da59 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,12 +1,16 @@ import json +from datetime import datetime, timezone +import nacl.utils from django.core.cache import cache +from nacl.public import Box, PrivateKey from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway +from care.abdm.utils.fhir import create_consultation_bundle from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation @@ -230,3 +234,155 @@ def post(self, request, *args, **kwargs): } ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +class RequestDataView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + + consent_id = data["hiRequest"]["consent"]["id"] + consent = cache[consent_id] if consent_id not in cache else None + if not consent or not consent["notification"]["status"] == "GRANTED": + return Response({}, status=status.HTTP_401_UNAUTHORIZED) + + consent_from = datetime.fromisoformat( + consent["notification"]["permission"]["dateRange"]["from"][:-1] + ) + consent_to = datetime.fromisoformat( + consent["notification"]["permission"]["dateRange"]["to"][:-1] + ) + now = datetime.now() + if not consent_from < now and now > consent_to: + return Response({}, status=status.HTTP_403_FORBIDDEN) + + AbdmGateway.on_data_request( + {"request_id": data["requestId"], "transaction_id": data["transactionId"]} + ) + + secret_key = PrivateKey.generate() + public_key = secret_key.public_key.encode().hex() + hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"].replace("-", "") + nonce = nacl.utils.random(32).hex() + xor_nonce = hex(int(hiu_nonce, base=16) ^ int(nonce, base=16))[2:] + + shared_key = Box( + secret_key, data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] + ) + + AbdmGateway.data_transfer( + { + "transaction_id": data["transactionId"], + "data_push_url": data["hiRequest"]["dataPushUrl"], + "care_contexts": list( + map( + lambda context: { + "patient_id": context["patientReference"], + "consultation_id": context["careContextReference"], + "data": shared_key.encrypt( + create_consultation_bundle( + PatientConsultation.objects.get( + external_id=context["consultation_id"] + ) + ), + xor_nonce, + ), + }, + consent["notification"]["consentDetail"]["careContexts"], + ) + ), + "key_material": { + "cryptoAlg": "ECDH", + "curve": "Curve25519", + "dhPublicKey": { + "expiry": str( + datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" + ) + ), # not sure what to put here + "parameters": f"Curve25519/{public_key}", # not sure what to put here + "keyValue": public_key, + }, + "nonce": nonce, + }, + } + ) + + AbdmGateway().data_notify( + { + "consent_id": data["hiRequest"]["consent"]["id"], + "transaction_id": data["transactionId"], + "care_contexts": list( + map( + lambda context: {"id": context["careContextReference"]}, + consent["notification"]["consentDetail"]["careContexts"], + ) + ), + } + ) + + return Response({}, status=status.HTTP_202_ACCEPTED) + + +# consent = { +# "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", +# "timestamp": "2023-03-30T05:00:31.288Z", +# "notification": { +# "status": "GRANTED", +# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# "consentDetail": { +# "schemaVersion": "string", +# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# "createdAt": "2023-03-30T05:00:31.288Z", +# "patient": {"id": "hinapatel79@ndhm"}, +# "careContexts": [ +# { +# "patientReference": "hinapatel79@hospital", +# "careContextReference": "Episode1", +# } +# ], +# "purpose": {"text": "string", "code": "string", "refUri": "string"}, +# "hip": {"id": "string", "name": "TESI-HIP"}, +# "consentManager": {"id": "string"}, +# "hiTypes": ["OPConsultation"], +# "permission": { +# "accessMode": "VIEW", +# "dateRange": { +# "from": "2023-03-30T05:00:31.288Z", +# "to": "2023-03-30T05:00:31.288Z", +# }, +# "dataEraseAt": "2023-03-30T05:00:31.288Z", +# "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, +# }, +# }, +# "signature": "Signature of CM as defined in W3C standards; Base64 encoded", +# "grantAcknowledgement": False, +# }, +# } + +# data = { +# "requestId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", +# "timestamp": "2023-03-30T06:37:05.476Z", +# "transactionId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", +# "hiRequest": { +# "consent": {"id": "string"}, +# "dateRange": { +# "from": "2023-03-30T06:37:05.476Z", +# "to": "2023-03-30T06:37:05.476Z", +# }, +# "dataPushUrl": "string", +# "keyMaterial": { +# "cryptoAlg": "ECDH", +# "curve": "Curve25519", +# "dhPublicKey": { +# "expiry": "2023-03-30T06:37:05.476Z", +# "parameters": "Curve25519/32byte random key", +# "keyValue": "string", +# }, +# "nonce": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# }, +# }, +# } diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index e6f7ef4937..b4ab714ff7 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -568,6 +568,87 @@ def on_notify(self, data): response = self.api.post(path, payload, None, additional_headers) return response + def on_data_request(self, data): + path = "/v0.5/health-information/hip/on-request" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "hiRequest": { + "transactionId": data["transaction_id"], + "sessionStatus": "ACKNOWLEDGED", + }, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + + def data_transfer(self, data): + headers = {"Authorization": f"Bearer {cache.get(ABDM_TOKEN_CACHE_KEY)}"} + + payload = { + "pageNumber": 0, + "pageCount": 0, + "transactionId": data["transaction_id"], + "entries": list( + map( + lambda context: { + "content": context["data"], + "media": "application/fhir+json", + "checksum": "string", + "careContextReference": context["consultation_id"], + }, + ) + ), + "keyMaterial": data["key_material"], + } + + response = requests.post(data["data_push_url"], payload, headers=headers) + return response + + def data_notify(self, data): + path = "/v0.5/health-information/notify" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "notification": { + "consentId": data["consent_id"], + "transactionId": data["transaction_id"], + "doneAt": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "notifier": {"type": "HIP", "id": self.hip_id}, + "statusNotification": { + "sessionStatus": "TRANSFERRED", + "hipId": self.hip_id, + "statusResponses": list( + map( + lambda context: { + "careContextReference": context["id"], + "hiStatus": "OK", + "description": "success", # not sure what to put + }, + data["care_contexts"], + ) + ), + }, + }, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py new file mode 100644 index 0000000000..6924c537ff --- /dev/null +++ b/care/abdm/utils/fhir.py @@ -0,0 +1,23 @@ +from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.domainresource import DomainResource +from fhir.resources.encounter import Encounter + + +def get_reference_url(self, resource: DomainResource): + return f"{resource.resource_type}/{resource.id}" + + +def create_encounter(consultation): + return Encounter( + id=consultation.external_id, + status="discharged" if consultation.discharge_date else "in-progress", + ) + + +def create_consultation_bundle(consultation): + encounter = create_encounter(consultation) + + return Bundle( + id=consultation.patient.external_id, + entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], + ) diff --git a/config/urls.py b/config/urls.py index 64c58c67e4..5353e33294 100644 --- a/config/urls.py +++ b/config/urls.py @@ -18,6 +18,7 @@ OnConfirmView, OnFetchView, OnInitView, + RequestDataView, ) from care.facility.api.viewsets.open_id import OpenIdConfigView from care.users.api.viewsets.change_password import ChangePasswordView @@ -132,6 +133,11 @@ NotifyView.as_view(), name="abdm_notify_view", ), + path( + "v0.5/health-information/hip/request", + RequestDataView.as_view(), + name="abdm_request_data_view", + ), # Health check urls url(r"^watchman/", include("watchman.urls")), path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), diff --git a/requirements/base.txt b/requirements/base.txt index 057c1fe19c..d7ae7b383a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -64,3 +64,4 @@ typed-ast==1.5.0 littletable==2.0.7 pycryptodome==3.16.0 pycryptodomex==3.16.0 +fhir.resources==6.5.0 From 918d7d1c7994be6b730ff2f5752d9026261b1c73 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 3 Apr 2023 12:59:44 +0530 Subject: [PATCH 057/180] changed encryption according to abdm docs --- care/abdm/api/viewsets/auth.py | 193 ++++++++++++++++----------------- care/abdm/utils/api_call.py | 4 + care/abdm/utils/fhir.py | 13 ++- 3 files changed, 108 insertions(+), 102 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 5b3031da59..d43c850229 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,9 +1,14 @@ +import base64 import json from datetime import datetime, timezone import nacl.utils +from Crypto.Cipher import AES +from Crypto.Hash import SHA512 +from Crypto.Protocol.KDF import HKDF from django.core.cache import cache -from nacl.public import Box, PrivateKey +from nacl.encoding import Base64Encoder +from nacl.public import Box, PrivateKey, PublicKey from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny @@ -189,43 +194,7 @@ def post(self, request, *args, **kwargs): print(data) # TODO: create a seperate cache and also add a expiration time - cache.set(data["notification"]["consentId"], json.dumps(data)) - - # data = { - # "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", - # "timestamp": "2023-03-30T05:00:31.288Z", - # "notification": { - # "status": "GRANTED", - # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - # "consentDetail": { - # "schemaVersion": "string", - # "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", - # "createdAt": "2023-03-30T05:00:31.288Z", - # "patient": {"id": "hinapatel79@ndhm"}, - # "careContexts": [ - # { - # "patientReference": "hinapatel79@hospital", - # "careContextReference": "Episode1", - # } - # ], - # "purpose": {"text": "string", "code": "string", "refUri": "string"}, - # "hip": {"id": "string", "name": "TESI-HIP"}, - # "consentManager": {"id": "string"}, - # "hiTypes": ["OPConsultation"], - # "permission": { - # "accessMode": "VIEW", - # "dateRange": { - # "from": "2023-03-30T05:00:31.288Z", - # "to": "2023-03-30T05:00:31.288Z", - # }, - # "dataEraseAt": "2023-03-30T05:00:31.288Z", - # "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, - # }, - # }, - # "signature": "Signature of CM as defined in W3C standards; Base64 encoded", - # "grantAcknowledgement": False, - # }, - # } + cache.set(data["notification"]["consentDetail"]["consentId"], json.dumps(data)) AbdmGateway().on_notify( { @@ -244,36 +213,50 @@ def post(self, request, *args, **kwargs): data = request.data print(data) + # TODO: uncomment later consent_id = data["hiRequest"]["consent"]["id"] - consent = cache[consent_id] if consent_id not in cache else None + consent = json.loads(cache.get(consent_id)) if consent_id in cache else None if not consent or not consent["notification"]["status"] == "GRANTED": return Response({}, status=status.HTTP_401_UNAUTHORIZED) - consent_from = datetime.fromisoformat( - consent["notification"]["permission"]["dateRange"]["from"][:-1] - ) - consent_to = datetime.fromisoformat( - consent["notification"]["permission"]["dateRange"]["to"][:-1] - ) - now = datetime.now() - if not consent_from < now and now > consent_to: - return Response({}, status=status.HTTP_403_FORBIDDEN) - - AbdmGateway.on_data_request( + # TODO: check if from and to are in range and consent expiry is greater than today + # consent_from = datetime.fromisoformat( + # consent["notification"]["permission"]["dateRange"]["from"][:-1] + # ) + # consent_to = datetime.fromisoformat( + # consent["notification"]["permission"]["dateRange"]["to"][:-1] + # ) + # now = datetime.now() + # if not consent_from < now and now > consent_to: + # return Response({}, status=status.HTTP_403_FORBIDDEN) + + AbdmGateway().on_data_request( {"request_id": data["requestId"], "transaction_id": data["transactionId"]} ) + hiu_public_key_b64 = data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] + hiu_public_key_hex = base64.b64decode(hiu_public_key_b64).hex()[2:] + hiu_public_key_hex_x = hiu_public_key_hex[:64] + # hiu_public_key_hex_y = hiu_public_key_hex[64:] + hiu_public_key = PublicKey(bytes.fromhex(hiu_public_key_hex_x)) + hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"] + secret_key = PrivateKey.generate() - public_key = secret_key.public_key.encode().hex() - hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"].replace("-", "") + public_key = secret_key.public_key.encode(Base64Encoder) nonce = nacl.utils.random(32).hex() - xor_nonce = hex(int(hiu_nonce, base=16) ^ int(nonce, base=16))[2:] - shared_key = Box( - secret_key, data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] - ) + xored_nonce = hex( + int(base64.b64decode(hiu_nonce).hex(), base=16) ^ int(nonce, base=16) + )[2:] + salt = xored_nonce[:40] + iv = xored_nonce[40:] + shared_key = Box(secret_key, hiu_public_key).encode(Base64Encoder).hex() + + hkdf_key = HKDF(bytes.fromhex(shared_key), 32, bytes.fromhex(salt), SHA512) + + cipher = AES.new(hkdf_key, AES.MODE_GCM, iv.encode("utf8")) - AbdmGateway.data_transfer( + AbdmGateway().data_transfer( { "transaction_id": data["transactionId"], "data_push_url": data["hiRequest"]["dataPushUrl"], @@ -282,16 +265,17 @@ def post(self, request, *args, **kwargs): lambda context: { "patient_id": context["patientReference"], "consultation_id": context["careContextReference"], - "data": shared_key.encrypt( + "data": cipher.encrypt( create_consultation_bundle( PatientConsultation.objects.get( - external_id=context["consultation_id"] + external_id=context["careContextReference"] ) - ), - xor_nonce, + ) + .json() + .encode("utf8") ), }, - consent["notification"]["consentDetail"]["careContexts"], + consent["notification"]["consentDetail"]["careContexts"][3:], ) ), "key_material": { @@ -328,61 +312,74 @@ def post(self, request, *args, **kwargs): # consent = { -# "requestId": "5f7a535d-a3fd-416b-b069-c97d021fbacd", -# "timestamp": "2023-03-30T05:00:31.288Z", # "notification": { -# "status": "GRANTED", -# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", # "consentDetail": { -# "schemaVersion": "string", -# "consentId": "3fa85f64-5717-4562-b3fc-2c963f66afa6", -# "createdAt": "2023-03-30T05:00:31.288Z", -# "patient": {"id": "hinapatel79@ndhm"}, -# "careContexts": [ -# { -# "patientReference": "hinapatel79@hospital", -# "careContextReference": "Episode1", -# } -# ], -# "purpose": {"text": "string", "code": "string", "refUri": "string"}, -# "hip": {"id": "string", "name": "TESI-HIP"}, -# "consentManager": {"id": "string"}, +# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", +# "createdAt": "2023-03-31T15:30:58.212283603", +# "purpose": {"text": "Self Requested", "code": "PATRQT", "refUri": None}, +# "patient": {"id": "khavinshankar@sbx"}, +# "consentManager": {"id": "sbx"}, +# "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, # "hiTypes": ["OPConsultation"], # "permission": { # "accessMode": "VIEW", # "dateRange": { -# "from": "2023-03-30T05:00:31.288Z", -# "to": "2023-03-30T05:00:31.288Z", +# "from": "2023-03-29T15:28:00", +# "to": "2023-03-31T15:28:00", # }, -# "dataEraseAt": "2023-03-30T05:00:31.288Z", -# "frequency": {"unit": "HOUR", "value": 0, "repeats": 0}, +# "dataEraseAt": "2023-04-01T15:28:18.501", +# "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, # }, +# "careContexts": [ +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", +# }, +# { +# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", +# "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", +# }, +# ], # }, -# "signature": "Signature of CM as defined in W3C standards; Base64 encoded", +# "status": "GRANTED", +# "signature": "jnG9oxlV6jRfxqXgW781ehe5/VYyzG5Z3aWNsgMJB2GB4IGKRu6ZqMu82WYJOKJqY62Oy7J90XBPAWWacQJVoa1eq1qcw6Fgc7pejihVVN/Ohdu2S6LSIi27DdVRQLR//7bCTSfe1P+3qCj+GkMVGgX0LbtYp2n3awZ0kRZFDt5JUI1oqWItx4Zz8pOF+1zjhD+AdzydE4JrKl3o/qICsb6+C9Iqe0ZrfqWAOmpESD17Z0p6trzkbHgeWXW/7S4Fg27cAJt9Z+HCa4PZLTOm5yx231QXyTRKCPrSQsZDe/OR5fUu3b0bDWf4F1FIJKXLG8ZmlsCs0T1gs3n8MkWYmQ==", +# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", # "grantAcknowledgement": False, # }, +# "requestId": "99b5e499-c81f-42f9-a550-e0eef2b1e2c1", +# "timestamp": "2023-03-31T15:30:58.236624856", # } + # data = { -# "requestId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", -# "timestamp": "2023-03-30T06:37:05.476Z", -# "transactionId": "a1s2c932-2f70-3ds3-a3b5-2sfd46b12a18d", +# "transactionId": "2839dccc-c9e5-4e29-8904-440a1dc7f0cf", +# "requestId": "87e509d3-c43e-4da5-a39c-296c01740a79", +# "timestamp": "2023-03-31T15:31:28.587999924", # "hiRequest": { -# "consent": {"id": "string"}, -# "dateRange": { -# "from": "2023-03-30T06:37:05.476Z", -# "to": "2023-03-30T06:37:05.476Z", -# }, -# "dataPushUrl": "string", +# "consent": {"id": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218"}, +# "dateRange": {"from": "2023-03-29T15:28:00", "to": "2023-03-31T15:28:00"}, +# "dataPushUrl": "https://dev.abdm.gov.in/api-hiu/data/notification", # "keyMaterial": { # "cryptoAlg": "ECDH", -# "curve": "Curve25519", +# "curve": "curve25519", # "dhPublicKey": { -# "expiry": "2023-03-30T06:37:05.476Z", -# "parameters": "Curve25519/32byte random key", -# "keyValue": "string", +# "expiry": "2023-04-02T15:30:58.49682", +# "parameters": "Ephemeral public key", +# "keyValue": "BHkJo9SpkcGmxTNqo4pYdvGuZ/ELbwwCxoLbqyY5kuSyJ42FBfQUsLkg8prSQrzk5lIwQ3JEuXYsignQT5juGow=", # }, -# "nonce": "3fa85f64-5717-4562-b3fc-2c963f66afa6", +# "nonce": "EAeHOfrH6xNXxj2nM6TClwJ6k7FNWQ9UzAx2ylVyCzE=", # }, # }, # } diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b4ab714ff7..ae4c746c19 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -604,12 +604,16 @@ def data_transfer(self, data): "checksum": "string", "careContextReference": context["consultation_id"], }, + data["care_contexts"], ) ), "keyMaterial": data["key_material"], } response = requests.post(data["data_push_url"], payload, headers=headers) + print("-----------------------------------------") + print("data response", response.text, response.status_code) + print("-----------------------------------------") return response def data_notify(self, data): diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 6924c537ff..296038ae1c 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,16 +1,20 @@ from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.coding import Coding from fhir.resources.domainresource import DomainResource from fhir.resources.encounter import Encounter -def get_reference_url(self, resource: DomainResource): +def get_reference_url(resource: DomainResource): return f"{resource.resource_type}/{resource.id}" def create_encounter(consultation): return Encounter( - id=consultation.external_id, - status="discharged" if consultation.discharge_date else "in-progress", + **{ + "id": str(consultation.external_id), + "status": "discharged" if consultation.discharge_date else "in-progress", + "class": Coding(code="IMP", display="Inpatient Encounter"), + } ) @@ -18,6 +22,7 @@ def create_consultation_bundle(consultation): encounter = create_encounter(consultation) return Bundle( - id=consultation.patient.external_id, + id=str(consultation.patient.external_id), + type="collection", entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], ) From b8baf9ea3b6d1dabb7b4fdd2117b6b22f65cc802 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 6 Apr 2023 09:25:55 +0530 Subject: [PATCH 058/180] data transfer flow (working version with hardcoded data) --- care/abdm/api/viewsets/auth.py | 138 +++++------------------------ care/abdm/utils/api_call.py | 19 ++-- care/abdm/utils/cipher.py | 65 ++++++++++++++ care/abdm/utils/fhir.py | 154 ++++++++++++++++++++++++++++++++- 4 files changed, 252 insertions(+), 124 deletions(-) create mode 100644 care/abdm/utils/cipher.py diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index d43c850229..d3f02649e9 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -1,20 +1,14 @@ -import base64 import json -from datetime import datetime, timezone +from datetime import datetime, timedelta -import nacl.utils -from Crypto.Cipher import AES -from Crypto.Hash import SHA512 -from Crypto.Protocol.KDF import HKDF from django.core.cache import cache -from nacl.encoding import Base64Encoder -from nacl.public import Box, PrivateKey, PublicKey from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway +from care.abdm.utils.cipher import Cipher from care.abdm.utils.fhir import create_consultation_bundle from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation @@ -230,31 +224,21 @@ def post(self, request, *args, **kwargs): # if not consent_from < now and now > consent_to: # return Response({}, status=status.HTTP_403_FORBIDDEN) - AbdmGateway().on_data_request( + on_data_request_response = AbdmGateway().on_data_request( {"request_id": data["requestId"], "transaction_id": data["transactionId"]} ) - hiu_public_key_b64 = data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"] - hiu_public_key_hex = base64.b64decode(hiu_public_key_b64).hex()[2:] - hiu_public_key_hex_x = hiu_public_key_hex[:64] - # hiu_public_key_hex_y = hiu_public_key_hex[64:] - hiu_public_key = PublicKey(bytes.fromhex(hiu_public_key_hex_x)) - hiu_nonce = data["hiRequest"]["keyMaterial"]["nonce"] - - secret_key = PrivateKey.generate() - public_key = secret_key.public_key.encode(Base64Encoder) - nonce = nacl.utils.random(32).hex() - - xored_nonce = hex( - int(base64.b64decode(hiu_nonce).hex(), base=16) ^ int(nonce, base=16) - )[2:] - salt = xored_nonce[:40] - iv = xored_nonce[40:] - shared_key = Box(secret_key, hiu_public_key).encode(Base64Encoder).hex() + if not on_data_request_response.status_code == 202: + return Response( + on_data_request_response, status=status.HTTP_400_BAD_REQUEST + ) - hkdf_key = HKDF(bytes.fromhex(shared_key), 32, bytes.fromhex(salt), SHA512) + cipher = Cipher( + data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"], + data["hiRequest"]["keyMaterial"]["nonce"], + ) - cipher = AES.new(hkdf_key, AES.MODE_GCM, iv.encode("utf8")) + print(consent["notification"]["consentDetail"]["careContexts"][:1:-1]) AbdmGateway().data_transfer( { @@ -271,26 +255,22 @@ def post(self, request, *args, **kwargs): external_id=context["careContextReference"] ) ) - .json() - .encode("utf8") - ), + )["data"], }, - consent["notification"]["consentDetail"]["careContexts"][3:], + consent["notification"]["consentDetail"]["careContexts"][ + :-4:-1 + ], ) ), "key_material": { "cryptoAlg": "ECDH", "curve": "Curve25519", "dhPublicKey": { - "expiry": str( - datetime.now(tz=timezone.utc).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ) - ), # not sure what to put here - "parameters": f"Curve25519/{public_key}", # not sure what to put here - "keyValue": public_key, + "expiry": (datetime.now() + timedelta(days=2)).isoformat(), + "parameters": "Curve25519/32byte random key", + "keyValue": cipher.key_to_share, }, - "nonce": nonce, + "nonce": cipher.sender_nonce, }, } ) @@ -302,84 +282,12 @@ def post(self, request, *args, **kwargs): "care_contexts": list( map( lambda context: {"id": context["careContextReference"]}, - consent["notification"]["consentDetail"]["careContexts"], + consent["notification"]["consentDetail"]["careContexts"][ + :-4:-1 + ], ) ), } ) return Response({}, status=status.HTTP_202_ACCEPTED) - - -# consent = { -# "notification": { -# "consentDetail": { -# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", -# "createdAt": "2023-03-31T15:30:58.212283603", -# "purpose": {"text": "Self Requested", "code": "PATRQT", "refUri": None}, -# "patient": {"id": "khavinshankar@sbx"}, -# "consentManager": {"id": "sbx"}, -# "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, -# "hiTypes": ["OPConsultation"], -# "permission": { -# "accessMode": "VIEW", -# "dateRange": { -# "from": "2023-03-29T15:28:00", -# "to": "2023-03-31T15:28:00", -# }, -# "dataEraseAt": "2023-04-01T15:28:18.501", -# "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, -# }, -# "careContexts": [ -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", -# }, -# { -# "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", -# "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", -# }, -# ], -# }, -# "status": "GRANTED", -# "signature": "jnG9oxlV6jRfxqXgW781ehe5/VYyzG5Z3aWNsgMJB2GB4IGKRu6ZqMu82WYJOKJqY62Oy7J90XBPAWWacQJVoa1eq1qcw6Fgc7pejihVVN/Ohdu2S6LSIi27DdVRQLR//7bCTSfe1P+3qCj+GkMVGgX0LbtYp2n3awZ0kRZFDt5JUI1oqWItx4Zz8pOF+1zjhD+AdzydE4JrKl3o/qICsb6+C9Iqe0ZrfqWAOmpESD17Z0p6trzkbHgeWXW/7S4Fg27cAJt9Z+HCa4PZLTOm5yx231QXyTRKCPrSQsZDe/OR5fUu3b0bDWf4F1FIJKXLG8ZmlsCs0T1gs3n8MkWYmQ==", -# "consentId": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218", -# "grantAcknowledgement": False, -# }, -# "requestId": "99b5e499-c81f-42f9-a550-e0eef2b1e2c1", -# "timestamp": "2023-03-31T15:30:58.236624856", -# } - - -# data = { -# "transactionId": "2839dccc-c9e5-4e29-8904-440a1dc7f0cf", -# "requestId": "87e509d3-c43e-4da5-a39c-296c01740a79", -# "timestamp": "2023-03-31T15:31:28.587999924", -# "hiRequest": { -# "consent": {"id": "feb6a86a-3b8d-4c3b-9860-41f7b0ec1218"}, -# "dateRange": {"from": "2023-03-29T15:28:00", "to": "2023-03-31T15:28:00"}, -# "dataPushUrl": "https://dev.abdm.gov.in/api-hiu/data/notification", -# "keyMaterial": { -# "cryptoAlg": "ECDH", -# "curve": "curve25519", -# "dhPublicKey": { -# "expiry": "2023-04-02T15:30:58.49682", -# "parameters": "Ephemeral public key", -# "keyValue": "BHkJo9SpkcGmxTNqo4pYdvGuZ/ELbwwCxoLbqyY5kuSyJ42FBfQUsLkg8prSQrzk5lIwQ3JEuXYsignQT5juGow=", -# }, -# "nonce": "EAeHOfrH6xNXxj2nM6TClwJ6k7FNWQ9UzAx2ylVyCzE=", -# }, -# }, -# } diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ae4c746c19..09cfc9feb3 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -590,11 +590,11 @@ def on_data_request(self, data): return response def data_transfer(self, data): - headers = {"Authorization": f"Bearer {cache.get(ABDM_TOKEN_CACHE_KEY)}"} + headers = {"Content-Type": "application/json"} payload = { - "pageNumber": 0, - "pageCount": 0, + "pageNumber": 1, + "pageCount": 1, "transactionId": data["transaction_id"], "entries": list( map( @@ -610,9 +610,17 @@ def data_transfer(self, data): "keyMaterial": data["key_material"], } - response = requests.post(data["data_push_url"], payload, headers=headers) + response = requests.post( + data["data_push_url"], data=json.dumps(payload), headers=headers + ) print("-----------------------------------------") - print("data response", response.text, response.status_code) + print( + "data response", + len(data["care_contexts"]), + json.dumps(payload), + response.text, + response.status_code, + ) print("-----------------------------------------") return response @@ -632,7 +640,6 @@ def data_notify(self, data): "doneAt": str( datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") ), - "notifier": {"type": "HIP", "id": self.hip_id}, "statusNotification": { "sessionStatus": "TRANSFERRED", "hipId": self.hip_id, diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py new file mode 100644 index 0000000000..85e45435d0 --- /dev/null +++ b/care/abdm/utils/cipher.py @@ -0,0 +1,65 @@ +import json + +import requests + + +class Cipher: + server_url = "http://localhost:8090" + + def __init__(self, reciever_public_key, reciever_nonce): + self.reciever_public_key = reciever_public_key + self.reciever_nonce = reciever_nonce + + self.sender_private_key = None + self.sender_public_key = None + self.sender_nonce = None + + self.key_to_share = None + + def generate_key_pair(self): + response = requests.get(f"{self.server_url}/keys/generate") + + if response.status_code == 200: + key_material = response.json() + + self.sender_private_key = key_material["privateKey"] + self.sender_public_key = key_material["publicKey"] + self.sender_nonce = key_material["nonce"] + + return key_material + + return None + + def encrypt(self, paylaod): + if not self.sender_private_key: + key_material = self.generate_key_pair() + + if not key_material: + return None + + response = requests.post( + f"{self.server_url}/encrypt", + headers={"Content-Type": "application/json"}, + data=json.dumps( + { + "receiverPublicKey": self.reciever_public_key, + "receiverNonce": self.reciever_nonce, + "senderPrivateKey": self.sender_private_key, + "senderPublicKey": self.sender_public_key, + "senderNonce": self.sender_nonce, + "plainTextData": paylaod, + } + ), + ) + + if response.status_code == 200: + data = response.json() + self.key_to_share = data["keyToShare"] + + return { + "public_key": self.key_to_share, + "data": data["encryptedData"], + "nonce": self.sender_nonce, + } + + return None diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 296038ae1c..cbf31a8b62 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,3 +1,5 @@ +import json + from fhir.resources.bundle import Bundle, BundleEntry from fhir.resources.coding import Coding from fhir.resources.domainresource import DomainResource @@ -11,7 +13,7 @@ def get_reference_url(resource: DomainResource): def create_encounter(consultation): return Encounter( **{ - "id": str(consultation.external_id), + "id": str(str(consultation.external_id)), "status": "discharged" if consultation.discharge_date else "in-progress", "class": Coding(code="IMP", display="Inpatient Encounter"), } @@ -21,8 +23,154 @@ def create_encounter(consultation): def create_consultation_bundle(consultation): encounter = create_encounter(consultation) + return json.dumps( + { + "resourceType": "Bundle", + "id": "3739707e-1123-46fe-918f-b52d880e4e7f", + "meta": {"lastUpdated": "2016-08-07T00:00:00.000+05:30"}, + "identifier": { + "system": "https://www.max.in/bundle", + "value": "3739707e-1123-46fe-918f-b52d880e4e7f", + }, + "type": "document", + "timestamp": "2016-08-07T00:00:00.000+05:30", + "entry": [ + { + "fullUrl": "Composition/c63d1435-b6b6-46c4-8163-33133bf0d9bf", + "resource": { + "resourceType": "Composition", + "id": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", + "identifier": { + "system": "https://www.max.in/document", + "value": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", + }, + "status": "final", + "type": { + "coding": [ + { + "system": "https://projecteka.in/sct", + "code": "440545006", + "display": "Prescription record", + } + ] + }, + "subject": { + "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" + }, + "encounter": { + "reference": f"Encounter/{str(consultation.external_id)}" + }, + "date": "2016-08-07T00:00:00.605+05:30", + "author": [ + { + "reference": "Practitioner/MAX5001", + "display": "Dr Laxmikanth J", + } + ], + "title": "Prescription", + "section": [ + { + "title": "OPD Prescription", + "code": { + "coding": [ + { + "system": "https://projecteka.in/sct", + "code": "440545006", + "display": "Prescription record", + } + ] + }, + "entry": [ + { + "reference": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0" + } + ], + } + ], + }, + }, + { + "fullUrl": "Practitioner/MAX5001", + "resource": { + "resourceType": "Practitioner", + "id": "MAX5001", + "identifier": [ + { + "system": "https://www.mciindia.in/doctor", + "value": "MAX5001", + } + ], + "name": [ + {"text": "Laxmikanth J", "prefix": ["Dr"], "suffix": ["MD"]} + ], + }, + }, + { + "fullUrl": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4", + "resource": { + "resourceType": "Patient", + "id": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "name": [{"text": "KhavinShankar G"}], + "gender": "male", + }, + }, + { + "fullUrl": f"Encounter/{str(consultation.external_id)}", + "resource": { + "resourceType": "Encounter", + "id": str(consultation.external_id), + "status": "finished", + "class": { + "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", + "code": "AMB", + "display": "Outpatient visit", + }, + "subject": { + "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" + }, + "period": {"start": "2016-08-07T00:00:00+05:30"}, + }, + }, + { + "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", + "resource": { + "resourceType": "Medication", + "id": "54ab5657-5e79-4461-a823-20e522eb337d", + "code": { + "coding": [ + { + "system": "https://projecteka.in/act", + "code": "R05CB02", + "display": "bromhexine 24 mg", + } + ] + }, + }, + }, + { + "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", + "resource": { + "resourceType": "MedicationRequest", + "id": "68d9667c-00c3-455f-b75d-d580950498a0", + "status": "active", + "intent": "order", + "medicationReference": { + "reference": "Medication/54ab5657-5e79-4461-a823-20e522eb337d" + }, + "subject": { + "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" + }, + "authoredOn": "2016-08-07T00:00:00+05:30", + "requester": {"reference": "Practitioner/MAX5001"}, + "dosageInstruction": [{"text": "1 capsule 2 times a day"}], + }, + }, + ], + } + ) + return Bundle( id=str(consultation.patient.external_id), - type="collection", + type="document", entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], - ) + ).json() From 82514d807ef4456ad1ab010890d0c4aad2278b10 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 6 Apr 2023 14:22:52 +0530 Subject: [PATCH 059/180] added create_prescription_record in fhir --- care/abdm/api/viewsets/auth.py | 10 +- care/abdm/utils/fhir.py | 255 ++++++++++++++++++++++++++++++--- 2 files changed, 241 insertions(+), 24 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index d3f02649e9..62d532874c 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -9,7 +9,7 @@ from care.abdm.utils.api_call import AbdmGateway from care.abdm.utils.cipher import Cipher -from care.abdm.utils.fhir import create_consultation_bundle +from care.abdm.utils.fhir import Fhir from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation @@ -250,15 +250,15 @@ def post(self, request, *args, **kwargs): "patient_id": context["patientReference"], "consultation_id": context["careContextReference"], "data": cipher.encrypt( - create_consultation_bundle( + Fhir( PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ) + ).create_prescription_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ - :-4:-1 + :-2:-1 ], ) ), @@ -283,7 +283,7 @@ def post(self, request, *args, **kwargs): map( lambda context: {"id": context["careContextReference"]}, consent["notification"]["consentDetail"]["careContexts"][ - :-4:-1 + :-2:-1 ], ) ), diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index cbf31a8b62..62d482af6f 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,28 +1,251 @@ import json +from datetime import datetime, timezone +from uuid import uuid4 as uuid from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding -from fhir.resources.domainresource import DomainResource +from fhir.resources.composition import Composition, CompositionSection +from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter +from fhir.resources.humanname import HumanName +from fhir.resources.identifier import Identifier +from fhir.resources.medication import Medication +from fhir.resources.medicationrequest import MedicationRequest +from fhir.resources.meta import Meta +from fhir.resources.organization import Organization +from fhir.resources.patient import Patient +from fhir.resources.period import Period +from fhir.resources.practitioner import Practitioner +from fhir.resources.reference import Reference -def get_reference_url(resource: DomainResource): - return f"{resource.resource_type}/{resource.id}" +class Fhir: + def __init__(self, consultation): + self.consultation = consultation + self._patient_profile = None + self._practitioner_profile = None + self._organization_profile = None + self._encounter_profile = None + self._medication_profiles = [] + self._medication_request_profiles = [] -def create_encounter(consultation): - return Encounter( - **{ - "id": str(str(consultation.external_id)), - "status": "discharged" if consultation.discharge_date else "in-progress", - "class": Coding(code="IMP", display="Inpatient Encounter"), - } - ) + def _reference_url(self, resource=None): + if resource is None: + return "" + return f"{resource.resource_type}/{resource.id}" -def create_consultation_bundle(consultation): - encounter = create_encounter(consultation) + def _reference(self, resource=None): + if resource is None: + return None + + return Reference(reference=self._reference_url(resource)) + + def _patient(self): + if self._patient_profile is not None: + return self._patient_profile + + id = str(self.consultation.patient.external_id) + name = self.consultation.patient.name + gender = self.consultation.patient.gender + self._patient_profile = Patient( + id=id, + identifier=[Identifier(value=id)], + name=[HumanName(text=name)], + gender="male" if gender == 1 else "female" if gender == 2 else "other", + ) + + return self._patient_profile + + def _practioner(self): + if self._practitioner_profile is not None: + return self._practitioner_profile + + id = str(uuid()) + name = self.consultation.verified_by + self._practitioner_profile = Practitioner( + id=id, + identifier=[Identifier(value=id)], + name=[HumanName(text=name)], + ) + + return self._practitioner_profile + + def _organization(self): + if self._organization_profile is not None: + return self._organization_profile + + id = str(self.consultation.facility.external_id) + name = self.consultation.facility.name + self._organization_profile = Organization( + id=id, + identifier=[Identifier(value=id)], + name=name, + ) + + return self._organization_profile + + def _encounter(self): + if self._encounter_profile is not None: + return self._encounter_profile + + id = str(self.consultation.external_id) + status = "finished" if self.consultation.discharge_date else "in-progress" + period_start = self.consultation.admission_date.isoformat() + period_end = ( + self.consultation.discharge_date.isoformat() + if self.consultation.discharge_date + else None + ) + self._encounter_profile = Encounter( + **{ + "id": id, + "identifier": [Identifier(value=id)], + "status": status, + "class": Coding(code="IMP", display="Inpatient Encounter"), + "subject": self._reference(self._patient()), + "period": Period(start=period_start, end=period_end), + } + ) + + return self._encounter_profile + + def _medication(self, name): + medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name)) + self._medication_profiles.append(medication_profile) + return medication_profile + + def _medication_request(self, medicine): + id = str(uuid()) + prescription_date = ( + self.consultation.admission_date.isoformat() + ) # TODO: change to the time of prescription + status = "unknown" # TODO: get correct status active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown + dosage_text = ( + f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']}" + ) + + medication_profile = self._medication(medicine["medicine"]) + medication_request_profile = MedicationRequest( + id=id, + identifier=[Identifier(value=id)], + status=status, + intent="order", + authoredOn=prescription_date, + dosageInstruction=[Dosage(text=dosage_text)], + medicationReference=self._reference(medication_profile), + subject=self._reference(self._patient()), + requester=self._reference(self._practioner()), + ) + + self._medication_request_profiles.append(medication_request_profile) + return medication_profile, medication_request_profile + + def _composition(self, type): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="preliminary" or "final" or "amended", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="440545006", + display="Prescription record", + ) + ] + ), # TODO: make it dynamic + title=type, # "Prescription" + date=datetime.now(timezone.utc).isoformat(), + section=[ + CompositionSection( + title="In Patient Prescriptions", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="440545006", + display="Prescription record", + ) + ] + ), + entry=list( + map( + lambda medicine: self._reference( + self._medication_request(medicine)[1] + ), + self.consultation.discharge_advice, + ) + ), + ) + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter()), + author=[self._reference(self._organization())], + ) + + pr = { + "entry": [ + {"fullUrl": "Practitioner/MAX5001", "resource": ""}, + { + "fullUrl": "Patient/RVH9999", + "resource": "", + }, + { + "fullUrl": "Encounter/dab7fd2b-6a05-4adb-af35-bcffd6c85b81", + "resource": "", + }, + { + "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", + "resource": "", + }, + { + "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", + "resource": "", + }, + ], + } + + def _bundle_entry(self, resource): + return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) + + def create_prescription_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + composition_profile = self._composition("Prescription") + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(composition_profile), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._medication_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._medication_request_profiles, + ) + ), + ], + ).json() + + +def create_consultation_bundle(consultation): return json.dumps( { "resourceType": "Bundle", @@ -168,9 +391,3 @@ def create_consultation_bundle(consultation): ], } ) - - return Bundle( - id=str(consultation.patient.external_id), - type="document", - entry=[BundleEntry(fullUrl=get_reference_url(encounter), resource=encounter)], - ).json() From 9e2574cdf087e561d4e47f9d868921eb951021af Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 6 Apr 2023 14:25:59 +0530 Subject: [PATCH 060/180] removed unwanted variables --- care/abdm/utils/fhir.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 62d482af6f..e58453bee3 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -188,28 +188,6 @@ def _composition(self, type): author=[self._reference(self._organization())], ) - pr = { - "entry": [ - {"fullUrl": "Practitioner/MAX5001", "resource": ""}, - { - "fullUrl": "Patient/RVH9999", - "resource": "", - }, - { - "fullUrl": "Encounter/dab7fd2b-6a05-4adb-af35-bcffd6c85b81", - "resource": "", - }, - { - "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", - "resource": "", - }, - { - "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", - "resource": "", - }, - ], - } - def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) From b3b0439ee0d62eca1d1579944394b478faa07c1c Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Apr 2023 17:59:48 +0530 Subject: [PATCH 061/180] added wellness record --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/api_call.py | 9 - care/abdm/utils/fhir.py | 343 ++++++++++++++++++--------------- 3 files changed, 187 insertions(+), 167 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 62d532874c..a331195e52 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_prescription_record() + ).create_wellness_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 09cfc9feb3..47410fe729 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -613,15 +613,6 @@ def data_transfer(self, data): response = requests.post( data["data_push_url"], data=json.dumps(payload), headers=headers ) - print("-----------------------------------------") - print( - "data response", - len(data["care_contexts"]), - json.dumps(payload), - response.text, - response.status_code, - ) - print("-----------------------------------------") return response def data_notify(self, data): diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index e58453bee3..32c60cedc3 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,11 +1,12 @@ -import json from datetime import datetime, timezone from uuid import uuid4 as uuid +from fhir.resources.address import Address from fhir.resources.bundle import Bundle, BundleEntry from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding from fhir.resources.composition import Composition, CompositionSection +from fhir.resources.contactpoint import ContactPoint from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter from fhir.resources.humanname import HumanName @@ -13,10 +14,12 @@ from fhir.resources.medication import Medication from fhir.resources.medicationrequest import MedicationRequest from fhir.resources.meta import Meta +from fhir.resources.observation import Observation, ObservationComponent from fhir.resources.organization import Organization from fhir.resources.patient import Patient from fhir.resources.period import Period from fhir.resources.practitioner import Practitioner +from fhir.resources.quantity import Quantity from fhir.resources.reference import Reference @@ -30,6 +33,7 @@ def __init__(self, consultation): self._encounter_profile = None self._medication_profiles = [] self._medication_request_profiles = [] + self._observation_profiles = [] def _reference_url(self, resource=None): if resource is None: @@ -78,15 +82,122 @@ def _organization(self): return self._organization_profile id = str(self.consultation.facility.external_id) + hip_id = "IN3210000017" # TODO: make it dynamic name = self.consultation.facility.name + phone = self.consultation.facility.phone_number + address = self.consultation.facility.address + local_body = self.consultation.facility.local_body.name + district = self.consultation.facility.district.name + state = self.consultation.facility.state.name + pincode = self.consultation.facility.pincode self._organization_profile = Organization( id=id, - identifier=[Identifier(value=id)], + identifier=[ + Identifier(system="https://facilitysbx.ndhm.gov.in", value=hip_id) + ], name=name, + telecom=[ContactPoint(system="phone", value=phone)], + address=[ + Address( + line=[address, local_body], + district=district, + state=state, + postalCode=pincode, + country="INDIA", + ) + ], ) return self._organization_profile + def _observation(self, title, value, id, date): + if not value or (type(value) == dict and not value["value"]): + return + + return Observation( + id=f"{id}.{title.replace(' ', '')}" if id and title else str(uuid()), + status="final", + effectiveDateTime=date if date else None, + code=CodeableConcept(text=title), + valueQuantity=Quantity(value=str(value["value"]), unit=value["unit"]) + if type(value) == dict + else None, + valueString=value if type(value) == str else None, + component=list( + map( + lambda component: ObservationComponent( + code=CodeableConcept(text=component["title"]), + valueQuantity=Quantity( + value=component["value"], unit=component["unit"] + ) + if type(component) == dict + else None, + valueString=component if type(component) == str else None, + ), + value, + ) + ) + if type(value) == list + else None, + ) + + def _observations_from_daily_round(self, daily_round): + id = str(daily_round.external_id) + date = daily_round.created_date.isoformat() + observation_profiles = [ + self._observation( + "Temperature", + {"value": daily_round.temperature, "unit": "F"}, + id, + date, + ), + self._observation( + "SpO2", + {"value": daily_round.spo2, "unit": "%"}, + id, + date, + ), + self._observation( + "Pulse", + {"value": daily_round.pulse, "unit": "bpm"}, + id, + date, + ), + self._observation( + "Resp", + {"value": daily_round.resp, "unit": "bpm"}, + id, + date, + ), + self._observation( + "Blood Pressure", + [ + { + "title": "Systolic Blood Pressure", + "value": daily_round.bp["systolic"], + "unit": "mmHg", + }, + { + "title": "Diastolic Blood Pressure", + "value": daily_round.bp["diastolic"], + "unit": "mmHg", + }, + ] + if "systolic" in daily_round.bp and "diastolic" in daily_round.bp + else None, + id, + date, + ), + ] + + # TODO: do it for other fields like bp, pulse, spo2, ... + + observation_profiles = list( + filter(lambda profile: profile is not None, observation_profiles) + ) + self._observation_profiles.extend(observation_profiles) + return observation_profiles + def _encounter(self): if self._encounter_profile is not None: return self._encounter_profile @@ -124,9 +235,7 @@ def _medication_request(self, medicine): self.consultation.admission_date.isoformat() ) # TODO: change to the time of prescription status = "unknown" # TODO: get correct status active | on-hold | cancelled | completed | entered-in-error | stopped | draft | unknown - dosage_text = ( - f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']}" - ) + dosage_text = f"{medicine['dosage_new']} / {medicine['dosage']} for {medicine['days']} days" medication_profile = self._medication(medicine["medicine"]) medication_request_profile = MedicationRequest( @@ -144,12 +253,12 @@ def _medication_request(self, medicine): self._medication_request_profiles.append(medication_request_profile) return medication_profile, medication_request_profile - def _composition(self, type): + def _prescription_composition(self): id = str(uuid()) # TODO: use identifiable id return Composition( id=id, identifier=Identifier(value=id), - status="preliminary" or "final" or "amended", # TODO: use appropriate one + status="final", # TODO: use appropriate one type=CodeableConcept( coding=[ Coding( @@ -158,8 +267,8 @@ def _composition(self, type): display="Prescription record", ) ] - ), # TODO: make it dynamic - title=type, # "Prescription" + ), + title="Prescription", date=datetime.now(timezone.utc).isoformat(), section=[ CompositionSection( @@ -188,13 +297,57 @@ def _composition(self, type): author=[self._reference(self._organization())], ) + def _wellness_composition(self): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="final", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + display="Wellness Record", + ) + ] + ), + title="Wellness Record", + date=datetime.now(timezone.utc).isoformat(), + section=list( + map( + lambda daily_round: CompositionSection( + title=f"Daily Round - {daily_round.created_date}", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + display="Wellness Record", + ) + ] + ), + entry=list( + map( + lambda observation_profile: self._reference( + observation_profile + ), + self._observations_from_daily_round(daily_round), + ) + ), + ), + self.consultation.daily_rounds.all(), + ) + ), + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter()), + author=[self._reference(self._organization())], + ) + def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) def create_prescription_record(self): id = str(uuid()) now = datetime.now(timezone.utc).isoformat() - composition_profile = self._composition("Prescription") return Bundle( id=id, identifier=Identifier(value=id), @@ -202,7 +355,7 @@ def create_prescription_record(self): meta=Meta(lastUpdated=now), timestamp=now, entry=[ - self._bundle_entry(composition_profile), + self._bundle_entry(self._prescription_composition()), self._bundle_entry(self._practioner()), self._bundle_entry(self._patient()), self._bundle_entry(self._organization()), @@ -222,150 +375,26 @@ def create_prescription_record(self): ], ).json() - -def create_consultation_bundle(consultation): - return json.dumps( - { - "resourceType": "Bundle", - "id": "3739707e-1123-46fe-918f-b52d880e4e7f", - "meta": {"lastUpdated": "2016-08-07T00:00:00.000+05:30"}, - "identifier": { - "system": "https://www.max.in/bundle", - "value": "3739707e-1123-46fe-918f-b52d880e4e7f", - }, - "type": "document", - "timestamp": "2016-08-07T00:00:00.000+05:30", - "entry": [ - { - "fullUrl": "Composition/c63d1435-b6b6-46c4-8163-33133bf0d9bf", - "resource": { - "resourceType": "Composition", - "id": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", - "identifier": { - "system": "https://www.max.in/document", - "value": "c63d1435-b6b6-46c4-8163-33133bf0d9bf", - }, - "status": "final", - "type": { - "coding": [ - { - "system": "https://projecteka.in/sct", - "code": "440545006", - "display": "Prescription record", - } - ] - }, - "subject": { - "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" - }, - "encounter": { - "reference": f"Encounter/{str(consultation.external_id)}" - }, - "date": "2016-08-07T00:00:00.605+05:30", - "author": [ - { - "reference": "Practitioner/MAX5001", - "display": "Dr Laxmikanth J", - } - ], - "title": "Prescription", - "section": [ - { - "title": "OPD Prescription", - "code": { - "coding": [ - { - "system": "https://projecteka.in/sct", - "code": "440545006", - "display": "Prescription record", - } - ] - }, - "entry": [ - { - "reference": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0" - } - ], - } - ], - }, - }, - { - "fullUrl": "Practitioner/MAX5001", - "resource": { - "resourceType": "Practitioner", - "id": "MAX5001", - "identifier": [ - { - "system": "https://www.mciindia.in/doctor", - "value": "MAX5001", - } - ], - "name": [ - {"text": "Laxmikanth J", "prefix": ["Dr"], "suffix": ["MD"]} - ], - }, - }, - { - "fullUrl": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4", - "resource": { - "resourceType": "Patient", - "id": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "name": [{"text": "KhavinShankar G"}], - "gender": "male", - }, - }, - { - "fullUrl": f"Encounter/{str(consultation.external_id)}", - "resource": { - "resourceType": "Encounter", - "id": str(consultation.external_id), - "status": "finished", - "class": { - "system": "http://terminology.hl7.org/CodeSystem/v3-ActCode", - "code": "AMB", - "display": "Outpatient visit", - }, - "subject": { - "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" - }, - "period": {"start": "2016-08-07T00:00:00+05:30"}, - }, - }, - { - "fullUrl": "Medication/54ab5657-5e79-4461-a823-20e522eb337d", - "resource": { - "resourceType": "Medication", - "id": "54ab5657-5e79-4461-a823-20e522eb337d", - "code": { - "coding": [ - { - "system": "https://projecteka.in/act", - "code": "R05CB02", - "display": "bromhexine 24 mg", - } - ] - }, - }, - }, - { - "fullUrl": "MedicationRequest/68d9667c-00c3-455f-b75d-d580950498a0", - "resource": { - "resourceType": "MedicationRequest", - "id": "68d9667c-00c3-455f-b75d-d580950498a0", - "status": "active", - "intent": "order", - "medicationReference": { - "reference": "Medication/54ab5657-5e79-4461-a823-20e522eb337d" - }, - "subject": { - "reference": "Patient/1019f565-065a-4287-93fd-a3db4cda7fe4" - }, - "authoredOn": "2016-08-07T00:00:00+05:30", - "requester": {"reference": "Practitioner/MAX5001"}, - "dosageInstruction": [{"text": "1 capsule 2 times a day"}], - }, - }, + def create_wellness_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(self._wellness_composition()), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._observation_profiles, + ) + ), ], - } - ) + ).json() From b953073793143ac79e4f8e106fda6c03e857d302 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Apr 2023 18:49:39 +0530 Subject: [PATCH 062/180] added immunization profile --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/fhir.py | 98 ++++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index a331195e52..79dc795644 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_wellness_record() + ).create_immunization_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 32c60cedc3..293378cb6e 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -11,6 +11,7 @@ from fhir.resources.encounter import Encounter from fhir.resources.humanname import HumanName from fhir.resources.identifier import Identifier +from fhir.resources.immunization import Immunization, ImmunizationProtocolApplied from fhir.resources.medication import Medication from fhir.resources.medicationrequest import MedicationRequest from fhir.resources.meta import Meta @@ -223,6 +224,47 @@ def _encounter(self): return self._encounter_profile + def _immunization(self): + if not self.consultation.patient.is_vaccinated: + return + + return Immunization( + id=str(uuid()), + status="completed", + identifier=[ + Identifier( + type=CodeableConcept(text="Covin Id"), + value=self.consultation.patient.covin_id, + ) + ], + vaccineCode=CodeableConcept( + coding=[ + Coding( + system="http://snomed.info/sct", + code="1119305005", + display="COVID-19 antigen vaccine", + ) + ], + text=self.consultation.patient.vaccine_name, + ), + patient=self._reference(self._patient()), + route=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="47625008", + display="Intravenous route", + ) + ] + ), + occurrenceDateTime=self.consultation.patient.last_vaccinated_date.isoformat(), + protocolApplied=[ + ImmunizationProtocolApplied( + doseNumberPositiveInt=self.consultation.patient.number_of_doses + ) + ], + ) + def _medication(self, name): medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name)) @@ -342,6 +384,43 @@ def _wellness_composition(self): author=[self._reference(self._organization())], ) + def _immunization_composition(self): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="final", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="41000179103", + display="Immunization Record", + ), + ], + ), + title="Immunization", + date=datetime.now(timezone.utc).isoformat(), + section=[ + CompositionSection( + title="IPD Immunization", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="41000179103", + display="Immunization Record", + ), + ], + ), + entry=[self._reference(self._immunization())], + ), + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter()), + author=[self._reference(self._organization())], + ) + def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) @@ -398,3 +477,22 @@ def create_wellness_record(self): ), ], ).json() + + def create_immunization_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(self._immunization_composition()), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + self._bundle_entry(self._immunization()), + ], + ).json() From 477e642acba7064b8dc88b7db19bb75c0298745a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 10 Apr 2023 22:26:17 +0530 Subject: [PATCH 063/180] added health document profile --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/fhir.py | 97 +++++++++++++++++++++++++++++ care/facility/models/file_upload.py | 11 ++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 79dc795644..05b73aafd0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_immunization_record() + ).create_health_document_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 293378cb6e..b0b1621422 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,12 +1,15 @@ +import base64 from datetime import datetime, timezone from uuid import uuid4 as uuid from fhir.resources.address import Address +from fhir.resources.attachment import Attachment from fhir.resources.bundle import Bundle, BundleEntry from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding from fhir.resources.composition import Composition, CompositionSection from fhir.resources.contactpoint import ContactPoint +from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter from fhir.resources.humanname import HumanName @@ -23,6 +26,8 @@ from fhir.resources.quantity import Quantity from fhir.resources.reference import Reference +from care.facility.models.file_upload import FileUpload + class Fhir: def __init__(self, consultation): @@ -35,6 +40,7 @@ def __init__(self, consultation): self._medication_profiles = [] self._medication_request_profiles = [] self._observation_profiles = [] + self._document_reference_profiles = [] def _reference_url(self, resource=None): if resource is None: @@ -265,6 +271,27 @@ def _immunization(self): ], ) + def _document_reference(self, file): + id = str(file.external_id) + content_type, content = file.file_contents() + document_reference_profile = DocumentReference( + id=id, + identifier=[Identifier(value=id)], + status="current", + type=CodeableConcept(text=file.internal_name.split(".")[0]), + content=[ + DocumentReferenceContent( + attachment=Attachment( + contentType=content_type, data=base64.b64encode(content) + ) + ) + ], + author=[self._reference(self._organization())], + ) + + self._document_reference_profiles.append(document_reference_profile) + return document_reference_profile + def _medication(self, name): medication_profile = Medication(id=str(uuid()), code=CodeableConcept(text=name)) @@ -339,6 +366,52 @@ def _prescription_composition(self): author=[self._reference(self._organization())], ) + def _health_document_composition(self): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="final", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="419891008", + display="Record artifact", + ) + ] + ), + title="Health Document Record", + date=datetime.now(timezone.utc).isoformat(), + section=[ + CompositionSection( + title="Health Document Record", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="419891008", + display="Record artifact", + ) + ] + ), + entry=list( + map( + lambda file: self._reference( + self._document_reference(file) + ), + FileUpload.objects.filter( + associating_id=self.consultation.id + ), + ) + ), + ) + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter()), + author=[self._reference(self._organization())], + ) + def _wellness_composition(self): id = str(uuid()) # TODO: use identifiable id return Composition( @@ -496,3 +569,27 @@ def create_immunization_record(self): self._bundle_entry(self._immunization()), ], ).json() + + def create_health_document_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(self._health_document_composition()), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._document_reference_profiles, + ) + ), + ], + ).json() diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index b72378b0f8..21cdcb4a73 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -103,3 +103,14 @@ def read_signed_url(self): ExpiresIn=60 * 60, # One Hour ) return signed_url + + def file_contents(self): + s3Client = boto3.client("s3", **cs_provider.get_client_config()) + response = s3Client.get_object( + Bucket=settings.FILE_UPLOAD_BUCKET, + Key=self.FileType(self.file_type).name + "/" + self.internal_name, + ) + + content_type = response["ContentType"] + content = response["Body"].read() + return content_type, content From ecb674505d6916f88d121d434de0c1e32890903e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 11 Apr 2023 11:53:11 +0530 Subject: [PATCH 064/180] added discharge summary profile --- care/abdm/api/viewsets/auth.py | 2 +- care/abdm/utils/fhir.py | 298 ++++++++++++++++++++++++++++++++- 2 files changed, 297 insertions(+), 3 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 05b73aafd0..7c315b3379 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -254,7 +254,7 @@ def post(self, request, *args, **kwargs): PatientConsultation.objects.get( external_id=context["careContextReference"] ) - ).create_health_document_record() + ).create_discharge_summary_record() )["data"], }, consent["notification"]["consentDetail"]["careContexts"][ diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index b0b1621422..3e735c5f14 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -3,15 +3,18 @@ from uuid import uuid4 as uuid from fhir.resources.address import Address +from fhir.resources.annotation import Annotation from fhir.resources.attachment import Attachment from fhir.resources.bundle import Bundle, BundleEntry +from fhir.resources.careplan import CarePlan from fhir.resources.codeableconcept import CodeableConcept from fhir.resources.coding import Coding from fhir.resources.composition import Composition, CompositionSection +from fhir.resources.condition import Condition from fhir.resources.contactpoint import ContactPoint from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent from fhir.resources.dosage import Dosage -from fhir.resources.encounter import Encounter +from fhir.resources.encounter import Encounter, EncounterDiagnosis from fhir.resources.humanname import HumanName from fhir.resources.identifier import Identifier from fhir.resources.immunization import Immunization, ImmunizationProtocolApplied @@ -23,10 +26,12 @@ from fhir.resources.patient import Patient from fhir.resources.period import Period from fhir.resources.practitioner import Practitioner +from fhir.resources.procedure import Procedure from fhir.resources.quantity import Quantity from fhir.resources.reference import Reference from care.facility.models.file_upload import FileUpload +from care.facility.static_data.icd11 import ICDDiseases class Fhir: @@ -37,10 +42,13 @@ def __init__(self, consultation): self._practitioner_profile = None self._organization_profile = None self._encounter_profile = None + self._careplan_profile = None self._medication_profiles = [] self._medication_request_profiles = [] self._observation_profiles = [] self._document_reference_profiles = [] + self._condition_profiles = [] + self._procedure_profiles = [] def _reference_url(self, resource=None): if resource is None: @@ -117,6 +125,94 @@ def _organization(self): return self._organization_profile + def _condition(self, diagnosis_id, provisional=False): + diagnosis = ICDDiseases.by.id[diagnosis_id] + [code, label] = diagnosis.label.split(" ", 1) + condition_profile = Condition( + id=diagnosis_id, + identifier=[Identifier(value=diagnosis_id)], + category=[ + CodeableConcept( + coding=[ + Coding( + system="http://terminology.hl7.org/CodeSystem/condition-category", + code="encounter-diagnosis", + display="Encounter Diagnosis", + ) + ], + text="Encounter Diagnosis", + ) + ], + verificationStatus=CodeableConcept( + coding=[ + Coding( + system="http://terminology.hl7.org/CodeSystem/condition-ver-status", + code="provisional" if provisional else "confirmed", + display="Provisional" if provisional else "Confirmed", + ) + ] + ), + code=CodeableConcept( + coding=[ + Coding( + system="http://id.who.int/icd/release/11/mms", + code=code, + display=label, + ) + ], + text=diagnosis.label, + ), + subject=self._reference(self._patient()), + ) + + self._condition_profiles.append(condition_profile) + return condition_profile + + def _procedure(self, procedure): + procedure_profile = Procedure( + id=str(uuid()), + status="completed", + code=CodeableConcept( + text=procedure["procedure"], + ), + subject=self._reference(self._patient()), + performedDateTime=f"{procedure['time']}:00+05:30" + if not procedure["repetitive"] + else None, + performedString=f"Every {procedure['frequency']}" + if procedure["repetitive"] + else None, + ) + + self._procedure_profiles.append(procedure_profile) + return procedure_profile + + def _careplan(self): + if self._careplan_profile: + return self._careplan_profile + + self._careplan_profile = CarePlan( + id=str(uuid()), + status="completed", + intent="plan", + title="Care Plan", + description="This includes Treatment Summary, Prescribed Medication, General Notes and Special Instructions", + period=Period( + start=self.consultation.admission_date.isoformat(), + end=self.consultation.discharge_date.isoformat() + if self.consultation.discharge_date + else None, + ), + note=[ + Annotation(text=self.consultation.prescribed_medication), + Annotation(text=self.consultation.consultation_notes), + Annotation(text=self.consultation.special_instruction), + ], + subject=self._reference(self._patient()), + ) + + return self._careplan_profile + def _observation(self, title, value, id, date): if not value or (type(value) == dict and not value["value"]): return @@ -205,7 +301,7 @@ def _observations_from_daily_round(self, daily_round): self._observation_profiles.extend(observation_profiles) return observation_profiles - def _encounter(self): + def _encounter(self, include_diagnosis=False): if self._encounter_profile is not None: return self._encounter_profile @@ -225,6 +321,26 @@ def _encounter(self): "class": Coding(code="IMP", display="Inpatient Encounter"), "subject": self._reference(self._patient()), "period": Period(start=period_start, end=period_end), + "diagnosis": list( + map( + lambda diagnosis: EncounterDiagnosis( + condition=self._reference( + self._condition(diagnosis), + ) + ), + self.consultation.icd11_diagnoses, + ) + ) + + list( + map( + lambda diagnosis: EncounterDiagnosis( + condition=self._reference(self._condition(diagnosis)) + ), + self.consultation.icd11_provisional_diagnoses, + ) + ) + if include_diagnosis + else None, } ) @@ -494,6 +610,129 @@ def _immunization_composition(self): author=[self._reference(self._organization())], ) + def _discharge_summary_composition(self): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="final", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="373942005", + display="Discharge Summary Record", + ) + ] + ), + title="Discharge Summary Document", + date=datetime.now(timezone.utc).isoformat(), + section=[ + CompositionSection( + title="Prescribed medications", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="440545006", + display="Prescription", + ) + ] + ), + entry=list( + map( + lambda medicine: self._reference( + self._medication_request(medicine)[1] + ), + self.consultation.discharge_advice, + ) + ), + ), + CompositionSection( + title="Health Documents", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="419891008", + display="Record", + ) + ] + ), + entry=list( + map( + lambda file: self._reference( + self._document_reference(file) + ), + FileUpload.objects.filter( + associating_id=self.consultation.id + ), + ) + ), + ), + *list( + map( + lambda daily_round: CompositionSection( + title=f"Daily Round - {daily_round.created_date}", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + display="Wellness Record", + ) + ] + ), + entry=list( + map( + lambda observation_profile: self._reference( + observation_profile + ), + self._observations_from_daily_round(daily_round), + ) + ), + ), + self.consultation.daily_rounds.all(), + ) + ), + CompositionSection( + title="Procedures", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="371525003", + display="Clinical procedure report", + ) + ] + ), + entry=list( + map( + lambda procedure: self._reference( + self._procedure(procedure) + ), + self.consultation.procedure, + ) + ), + ), + CompositionSection( + title="Care Plan", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="734163000", + display="Care Plan", + ) + ] + ), + entry=[self._reference(self._careplan())], + ), + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter(include_diagnosis=True)), + author=[self._reference(self._organization())], + ) + def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) @@ -593,3 +832,58 @@ def create_health_document_record(self): ), ], ).json() + + def create_discharge_summary_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(self._discharge_summary_composition()), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + self._bundle_entry(self._careplan()), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._medication_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._medication_request_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._condition_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._procedure_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._document_reference_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._observation_profiles, + ) + ), + ], + ).json() From d1ea874c3667edc3b9db76698166af01c836dd79 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 11 Apr 2023 13:40:39 +0530 Subject: [PATCH 065/180] support sending multiple profiles --- care/abdm/api/viewsets/auth.py | 122 ++++++++++++++++++++++++++++----- care/abdm/utils/fhir.py | 18 +++++ 2 files changed, 123 insertions(+), 17 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7c315b3379..2f3c5c33ce 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -244,23 +244,35 @@ def post(self, request, *args, **kwargs): { "transaction_id": data["transactionId"], "data_push_url": data["hiRequest"]["dataPushUrl"], - "care_contexts": list( - map( - lambda context: { - "patient_id": context["patientReference"], - "consultation_id": context["careContextReference"], - "data": cipher.encrypt( - Fhir( - PatientConsultation.objects.get( - external_id=context["careContextReference"] - ) - ).create_discharge_summary_record() - )["data"], - }, - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) + "care_contexts": sum( + list( + map( + lambda context: list( + map( + lambda record: { + "patient_id": context["patientReference"], + "consultation_id": context[ + "careContextReference" + ], + "data": cipher.encrypt( + Fhir( + PatientConsultation.objects.get( + external_id=context[ + "careContextReference" + ] + ) + ).create_record(record) + )["data"], + }, + consent["notification"]["consentDetail"]["hiTypes"], + ) + ), + consent["notification"]["consentDetail"]["careContexts"][ + :-2:-1 + ], + ) + ), + [], ), "key_material": { "cryptoAlg": "ECDH", @@ -291,3 +303,79 @@ def post(self, request, *args, **kwargs): ) return Response({}, status=status.HTTP_202_ACCEPTED) + + +consent = { + "notification": { + "consentDetail": { + "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", + "createdAt": "2023-04-11T08:01:55.554799671", + "purpose": {"text": "Care Management", "code": "CAREMGT", "refUri": None}, + "patient": {"id": "khavinshankar@sbx"}, + "consentManager": {"id": "sbx"}, + "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, + "hiTypes": [ + "DiagnosticReport", + "DischargeSummary", + "HealthDocumentRecord", + "ImmunizationRecord", + "OPConsultation", + "Prescription", + "WellnessRecord", + ], + "permission": { + "accessMode": "VIEW", + "dateRange": { + "from": "2023-04-11T08:01:32.774", + "to": "2023-04-11T08:01:32.774", + }, + "dataEraseAt": "2023-04-12T08:01:32.774", + "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, + }, + "careContexts": [ + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", + }, + { + "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", + "careContextReference": "66d32279-b3b3-43e1-971e-eadc5da67e95", + }, + ], + }, + "status": "GRANTED", + "signature": "oB2JZD5CMecjW3y817dYK9pqE36yE3W+jUPtc0vfrPOMEFoYftdXAnNHxBUYZ0FKKIAJGf3erLxkzx0KE+ISFJyXX4U8OzKTBGJEjjJJ7/reDRSWnXS41D89/l8kmZHtVNsqmkje4BMKAjQylw9i8js+VaVpbgC7+NtYcSfLPWqPLnw+ppFJVKM3vrL7/w1UUrrSWB27YOX02XYj4eBtjxiLneG6fTzTT7QqrUtaYYFTU7CY1Ujwx+/q82J1sz3FGszFBe4c+1orqs2jwyLSgu73qmsySdJM1ugjjWs2Y/EBG6SnWjvfz7rvfDZ0KLfcUnWfEU5FVj8umGucAshnXA==", + "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", + "grantAcknowledgement": False, + }, + "requestId": "12fa9195-031a-4d89-b96c-0d7e3cfd009f", + "timestamp": "2023-04-11T08:01:55.570970194", +} diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 3e735c5f14..c9cde0d4ed 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -887,3 +887,21 @@ def create_discharge_summary_record(self): ), ], ).json() + + def create_record(self, record_type): + if record_type == "Prescription": + return self.create_prescription_record() + elif record_type == "WellnessRecord": + return self.create_wellness_record() + elif record_type == "ImmunizationRecord": + return self.create_immunization_record() + elif record_type == "HealthDocumentRecord": + return self.create_health_document_record() + elif record_type == "DiagnosticReport": + return self.create_discharge_summary_record() + elif record_type == "DischargeSummary": + return self.create_discharge_summary_record() + elif record_type == "OPConsultation": + return self.create_discharge_summary_record() + else: + return self.create_discharge_summary_record() From b9e94a9048d8941fbf559c0f2a4022f56ead40b4 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 13 Apr 2023 11:21:07 +0530 Subject: [PATCH 066/180] merged conflicting migrations --- .../migrations/0344_merge_20230413_1120.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 care/facility/migrations/0344_merge_20230413_1120.py diff --git a/care/facility/migrations/0344_merge_20230413_1120.py b/care/facility/migrations/0344_merge_20230413_1120.py new file mode 100644 index 0000000000..96acc3ff38 --- /dev/null +++ b/care/facility/migrations/0344_merge_20230413_1120.py @@ -0,0 +1,14 @@ +# Generated by Django 2.2.11 on 2023-04-13 05:50 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('facility', '0343_auto_20230407_1850'), + ('facility', '0331_auto_20230130_1652'), + ] + + operations = [ + ] From c0cbad9059050a6c16e7f832330fefbe342a11d0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 13 Apr 2023 13:53:24 +0530 Subject: [PATCH 067/180] integrated fidelius as a docker service --- care/abdm/utils/cipher.py | 3 ++- config/settings/base.py | 1 + docker-compose.yaml | 7 ++++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py index 85e45435d0..2401d1e1ed 100644 --- a/care/abdm/utils/cipher.py +++ b/care/abdm/utils/cipher.py @@ -1,10 +1,11 @@ import json import requests +from django.conf import settings class Cipher: - server_url = "http://localhost:8090" + server_url = settings.FIDELIUS_URL def __init__(self, reciever_public_key, reciever_nonce): self.reciever_public_key = reciever_public_key diff --git a/config/settings/base.py b/config/settings/base.py index 2661552dc2..da7fd307f8 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -515,3 +515,4 @@ def GETKEY(group, request): ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") X_CM_ID = env("X_CM_ID", default="sbx") +FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") diff --git a/docker-compose.yaml b/docker-compose.yaml index bac47aa106..495805aefb 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -38,5 +38,10 @@ services: - "${TEMPDIR:-/tmp/localstack}:/tmp/localstack" - "./docker/awslocal:/docker-entrypoint-initaws.d" + fidelius: + container_name: care_fidelius + image: khavinshankar/fidelius:v1.0 + restart: always + volumes: - postgres-data: \ No newline at end of file + postgres-data: From 7dcac1db90ad99e319ac27453fc5e3fa76d02080 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 14 Apr 2023 13:56:29 +0530 Subject: [PATCH 068/180] added op consultation profile --- care/abdm/utils/fhir.py | 180 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 1 deletion(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c9cde0d4ed..6c9b999936 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -733,6 +733,129 @@ def _discharge_summary_composition(self): author=[self._reference(self._organization())], ) + def _op_consultation_composition(self): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="final", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="371530004", + display="Clinical consultation report", + ) + ] + ), + title="OP Consultation Document", + date=datetime.now(timezone.utc).isoformat(), + section=[ + CompositionSection( + title="Prescribed medications", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="440545006", + display="Prescription", + ) + ] + ), + entry=list( + map( + lambda medicine: self._reference( + self._medication_request(medicine)[1] + ), + self.consultation.discharge_advice, + ) + ), + ), + CompositionSection( + title="Health Documents", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="419891008", + display="Record", + ) + ] + ), + entry=list( + map( + lambda file: self._reference( + self._document_reference(file) + ), + FileUpload.objects.filter( + associating_id=self.consultation.id + ), + ) + ), + ), + *list( + map( + lambda daily_round: CompositionSection( + title=f"Daily Round - {daily_round.created_date}", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + display="Wellness Record", + ) + ] + ), + entry=list( + map( + lambda observation_profile: self._reference( + observation_profile + ), + self._observations_from_daily_round(daily_round), + ) + ), + ), + self.consultation.daily_rounds.all(), + ) + ), + CompositionSection( + title="Procedures", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="371525003", + display="Clinical procedure report", + ) + ] + ), + entry=list( + map( + lambda procedure: self._reference( + self._procedure(procedure) + ), + self.consultation.procedure, + ) + ), + ), + CompositionSection( + title="Care Plan", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="734163000", + display="Care Plan", + ) + ] + ), + entry=[self._reference(self._careplan())], + ), + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter(include_diagnosis=True)), + author=[self._reference(self._organization())], + ) + def _bundle_entry(self, resource): return BundleEntry(fullUrl=self._reference_url(resource), resource=resource) @@ -888,6 +1011,61 @@ def create_discharge_summary_record(self): ], ).json() + def create_op_consultation_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(self._op_consultation_composition()), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + self._bundle_entry(self._careplan()), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._medication_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._medication_request_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._condition_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._procedure_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._document_reference_profiles, + ) + ), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._observation_profiles, + ) + ), + ], + ).json() + def create_record(self, record_type): if record_type == "Prescription": return self.create_prescription_record() @@ -902,6 +1080,6 @@ def create_record(self, record_type): elif record_type == "DischargeSummary": return self.create_discharge_summary_record() elif record_type == "OPConsultation": - return self.create_discharge_summary_record() + return self.create_op_consultation_record() else: return self.create_discharge_summary_record() From 4608e92b15156c578b437feac5688997eaac95fc Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 14 Apr 2023 17:25:50 +0530 Subject: [PATCH 069/180] added diagnostic report profile --- care/abdm/utils/fhir.py | 102 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 100 insertions(+), 2 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 6c9b999936..c71d5cd811 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -12,6 +12,7 @@ from fhir.resources.composition import Composition, CompositionSection from fhir.resources.condition import Condition from fhir.resources.contactpoint import ContactPoint +from fhir.resources.diagnosticreport import DiagnosticReport from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent from fhir.resources.dosage import Dosage from fhir.resources.encounter import Encounter, EncounterDiagnosis @@ -31,6 +32,7 @@ from fhir.resources.reference import Reference from care.facility.models.file_upload import FileUpload +from care.facility.models.patient_investigation import InvestigationValue from care.facility.static_data.icd11 import ICDDiseases @@ -43,6 +45,7 @@ def __init__(self, consultation): self._organization_profile = None self._encounter_profile = None self._careplan_profile = None + self._diagnostic_report_profile = None self._medication_profiles = [] self._medication_request_profiles = [] self._observation_profiles = [] @@ -213,12 +216,46 @@ def _careplan(self): return self._careplan_profile + def _diagnostic_report(self): + if self._diagnostic_report_profile: + return self._diagnostic_report_profile + + self._diagnostic_report_profile = DiagnosticReport( + id=str(uuid()), + status="final", + code=CodeableConcept(text="Investigation/Test Results"), + result=list( + map( + lambda investigation: self._reference( + self._observation( + title=investigation.investigation.name, + value={ + "value": investigation.value, + "unit": investigation.investigation.unit, + }, + id=str(investigation.external_id), + date=investigation.created_date.isoformat(), + ) + ), + InvestigationValue.objects.filter(consultation=self.consultation), + ) + ), + subject=self._reference(self._patient()), + performer=[self._reference(self._organization())], + resultsInterpreter=[self._reference(self._organization())], + conclusion="Refer to Doctor. To be correlated with further study.", + ) + + return self._diagnostic_report_profile + def _observation(self, title, value, id, date): if not value or (type(value) == dict and not value["value"]): return return Observation( - id=f"{id}.{title.replace(' ', '')}" if id and title else str(uuid()), + id=f"{id}.{title.replace(' ', '').replace('_', '-')}" + if id and title + else str(uuid()), status="final", effectiveDateTime=date if date else None, code=CodeableConcept(text=title), @@ -610,6 +647,43 @@ def _immunization_composition(self): author=[self._reference(self._organization())], ) + def _diagnostic_report_composition(self): + id = str(uuid()) # TODO: use identifiable id + return Composition( + id=id, + identifier=Identifier(value=id), + status="final", # TODO: use appropriate one + type=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="721981007", + display="Diagnostic Report", + ), + ], + ), + title="Diagnostic Report", + date=datetime.now(timezone.utc).isoformat(), + section=[ + CompositionSection( + title="Investigation Report", + code=CodeableConcept( + coding=[ + Coding( + system="https://projecteka.in/sct", + code="721981007", + display="Diagnostic Report", + ), + ], + ), + entry=[self._reference(self._diagnostic_report())], + ), + ], + subject=self._reference(self._patient()), + encounter=self._reference(self._encounter()), + author=[self._reference(self._organization())], + ) + def _discharge_summary_composition(self): id = str(uuid()) # TODO: use identifiable id return Composition( @@ -932,6 +1006,30 @@ def create_immunization_record(self): ], ).json() + def create_diagnostic_report_record(self): + id = str(uuid()) + now = datetime.now(timezone.utc).isoformat() + return Bundle( + id=id, + identifier=Identifier(value=id), + type="document", + meta=Meta(lastUpdated=now), + timestamp=now, + entry=[ + self._bundle_entry(self._diagnostic_report_composition()), + self._bundle_entry(self._practioner()), + self._bundle_entry(self._patient()), + self._bundle_entry(self._organization()), + self._bundle_entry(self._encounter()), + *list( + map( + lambda resource: self._bundle_entry(resource), + self._observation_profiles, + ) + ), + ], + ).json() + def create_health_document_record(self): id = str(uuid()) now = datetime.now(timezone.utc).isoformat() @@ -1076,7 +1174,7 @@ def create_record(self, record_type): elif record_type == "HealthDocumentRecord": return self.create_health_document_record() elif record_type == "DiagnosticReport": - return self.create_discharge_summary_record() + return self.create_diagnostic_report_record() elif record_type == "DischargeSummary": return self.create_discharge_summary_record() elif record_type == "OPConsultation": From 19c6819d4d177d8714bf353d46e6f9be96b6fca1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 21 Apr 2023 12:10:41 +0530 Subject: [PATCH 070/180] auto link care context while discharge --- care/abdm/api/viewsets/auth.py | 109 ++++++-------------------- care/abdm/api/viewsets/hip.py | 16 ++-- care/abdm/utils/api_call.py | 30 ++++++- care/facility/api/viewsets/patient.py | 15 ++++ 4 files changed, 76 insertions(+), 94 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 2f3c5c33ce..2fa8782e9b 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -43,11 +43,30 @@ class OnConfirmView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data print(data) - AbdmGateway().save_linking_token( - data["auth"]["patient"], - data["auth"]["accessToken"], - data["resp"]["requestId"], - ) + + if "validity" in data["auth"]: + if data["auth"]["validity"]["purpose"] == "LINK": + AbdmGateway().add_care_context( + data["auth"]["accessToken"], + data["resp"]["requestId"], + ) + else: + AbdmGateway().save_linking_token( + data["auth"]["patient"], + data["auth"]["accessToken"], + data["resp"]["requestId"], + ) + else: + AbdmGateway().save_linking_token( + data["auth"]["patient"], + data["auth"]["accessToken"], + data["resp"]["requestId"], + ) + AbdmGateway().add_care_context( + data["auth"]["accessToken"], + data["resp"]["requestId"], + ) + return Response({}, status=status.HTTP_202_ACCEPTED) @@ -80,7 +99,7 @@ def post(self, request, *args, **kwargs): for identifier in verified_identifiers: if identifier["type"] == "MOBILE": matched_by.append(identifier["value"]) - patients = patients.filter(phone_number=identifier["value"]) + patients = patients.filter(phone_number=f"+91{identifier['value']}") if identifier["type"] == "NDHM_HEALTH_NUMBER": matched_by.append(identifier["value"]) @@ -188,7 +207,7 @@ def post(self, request, *args, **kwargs): print(data) # TODO: create a seperate cache and also add a expiration time - cache.set(data["notification"]["consentDetail"]["consentId"], json.dumps(data)) + cache.set(data["notification"]["consentId"], json.dumps(data)) AbdmGateway().on_notify( { @@ -303,79 +322,3 @@ def post(self, request, *args, **kwargs): ) return Response({}, status=status.HTTP_202_ACCEPTED) - - -consent = { - "notification": { - "consentDetail": { - "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", - "createdAt": "2023-04-11T08:01:55.554799671", - "purpose": {"text": "Care Management", "code": "CAREMGT", "refUri": None}, - "patient": {"id": "khavinshankar@sbx"}, - "consentManager": {"id": "sbx"}, - "hip": {"id": "IN3210000017", "name": "Coronasafe Care 01"}, - "hiTypes": [ - "DiagnosticReport", - "DischargeSummary", - "HealthDocumentRecord", - "ImmunizationRecord", - "OPConsultation", - "Prescription", - "WellnessRecord", - ], - "permission": { - "accessMode": "VIEW", - "dateRange": { - "from": "2023-04-11T08:01:32.774", - "to": "2023-04-11T08:01:32.774", - }, - "dataEraseAt": "2023-04-12T08:01:32.774", - "frequency": {"unit": "HOUR", "value": 1, "repeats": 0}, - }, - "careContexts": [ - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "c7134ba2-692a-40f5-a143-d306896436dd", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "56015494-bac8-486d-85b6-6f67d1708764", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "140f79f9-4e4e-4bc1-b43e-ebce3c9313a5", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "90742c64-ac7b-4806-bcb6-2f8418d0bd5b", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "829cf90f-23c0-4978-be5c-94131be5d2f9", - }, - { - "patientReference": "1019f565-065a-4287-93fd-a3db4cda7fe4", - "careContextReference": "66d32279-b3b3-43e1-971e-eadc5da67e95", - }, - ], - }, - "status": "GRANTED", - "signature": "oB2JZD5CMecjW3y817dYK9pqE36yE3W+jUPtc0vfrPOMEFoYftdXAnNHxBUYZ0FKKIAJGf3erLxkzx0KE+ISFJyXX4U8OzKTBGJEjjJJ7/reDRSWnXS41D89/l8kmZHtVNsqmkje4BMKAjQylw9i8js+VaVpbgC7+NtYcSfLPWqPLnw+ppFJVKM3vrL7/w1UUrrSWB27YOX02XYj4eBtjxiLneG6fTzTT7QqrUtaYYFTU7CY1Ujwx+/q82J1sz3FGszFBe4c+1orqs2jwyLSgu73qmsySdJM1ugjjWs2Y/EBG6SnWjvfz7rvfDZ0KLfcUnWfEU5FVj8umGucAshnXA==", - "consentId": "0ad38ac1-5f61-480a-b9d6-6ace3e2d2139", - "grantAcknowledgement": False, - }, - "requestId": "12fa9195-031a-4d89-b96c-0d7e3cfd009f", - "timestamp": "2023-04-11T08:01:55.570970194", -} diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index c218d657f0..6b6d87d201 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -159,13 +159,15 @@ def add_care_context(self, request, *args, **kwargs): status=status.HTTP_404_NOT_FOUND, ) - response = AbdmGateway().add_contexts( + AbdmGateway().fetch_modes( { - "access_token": consultation.patient.abha_number.access_token, - "patient_id": str(consultation.patient.external_id), - "patient_name": consultation.patient.name, - "context_id": str(consultation.external_id), - "context_name": f"Encounter: {str(consultation.created_date.date())}", + "healthId": consultation.patient.abha_number.abha_number, + "name": consultation.patient.abha_number.name, + "gender": consultation.patient.abha_number.gender, + "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "consultationId": consultation_id, + "purpose": "LINK", } ) - return Response(response) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 47410fe729..2fc3173962 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -11,6 +11,7 @@ from django.db.models import Q from care.abdm.models import AbhaNumber +from care.facility.models.patient_consultation import PatientConsultation GATEWAY_API_URL = "https://dev.abdm.gov.in/" HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" @@ -325,9 +326,31 @@ class AbdmGateway: def __init__(self): self.api = APIGateway("abdm_gateway", None) + def add_care_context(self, access_token, request_id): + data = self.temp_memory[request_id] + + if "consultationId" in data: + consultation = PatientConsultation.objects.get( + external_id=data["consultationId"] + ) + + response = self.add_contexts( + { + "access_token": access_token, + "patient_id": str(consultation.patient.external_id), + "patient_name": consultation.patient.name, + "context_id": str(consultation.external_id), + "context_name": f"Encounter: {str(consultation.created_date.date())}", + } + ) + + return response + + return False + def save_linking_token(self, patient, access_token, request_id): data = self.temp_memory[request_id] - health_id = patient["id"] or data["healthId"] + health_id = patient and patient["id"] or data["healthId"] abha_object = AbhaNumber.objects.filter( Q(abha_number=health_id) | Q(health_id=health_id) @@ -352,7 +375,6 @@ def fetch_modes(self, data): name, gender, dateOfBirth, - patientId } """ self.temp_memory[request_id] = data @@ -364,7 +386,7 @@ def fetch_modes(self, data): ), "query": { "id": data["healthId"], - "purpose": "KYC_AND_LINK", + "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", "requester": {"type": "HIP", "id": self.hip_id}, }, } @@ -388,7 +410,7 @@ def init(self, prev_request_id): ), "query": { "id": data["healthId"], - "purpose": "KYC_AND_LINK", + "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", "authMode": "DEMOGRAPHICS", "requester": {"type": "HIP", "id": self.hip_id}, }, diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index ced0af6f4a..dca170044a 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -24,6 +24,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from care.abdm.utils.api_call import AbdmGateway from care.facility.api.serializers.patient import ( FacilityPatientStatsHistorySerializer, PatientDetailSerializer, @@ -454,6 +455,20 @@ def discharge_patient(self, request, *args, **kwargs): consultation=last_consultation, end_date__isnull=True ).update(end_date=current_time) + if last_consultation.patient.abha_number: + AbdmGateway().fetch_modes( + { + "healthId": last_consultation.patient.abha_number.abha_number, + "name": last_consultation.patient.abha_number.name, + "gender": last_consultation.patient.abha_number.gender, + "dateOfBirth": str( + last_consultation.patient.abha_number.date_of_birth + ), + "consultationId": last_consultation.external_id, + "purpose": "LINK", + } + ) + return Response(status=status.HTTP_200_OK) @action(detail=True, methods=["POST"]) From b9ea2a0aaf70f5ff7737148231fde8bc4ed8ebf3 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 24 Apr 2023 16:06:11 +0530 Subject: [PATCH 071/180] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 60dc5f6b00..08cda88a3d 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -5,12 +5,11 @@ on: push: branches: - - abdm-m1-vignesh + - abdm-m2 paths-ignore: - "docs/**" -env: - IMAGE_NAME: care-${{ github.ref_name}} + jobs: build-image: name: Build & Push Staging to container registries @@ -25,8 +24,8 @@ jobs: images: | ghcr.io/${{ github.repository }} tags: | - type=raw,value=latest-${{ github.run_number }} - type=raw,value=latest + type=raw,value=${{ github.ref_name}}-${{ github.run_number }} + type=raw,value=${{ github.ref_name}} type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} flavor: | From b6b8ac233d9e89c1a4caa897d14e81d3429c6e57 Mon Sep 17 00:00:00 2001 From: Mathew Date: Mon, 24 Apr 2023 16:12:11 +0530 Subject: [PATCH 072/180] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 08cda88a3d..0ebe252197 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -26,10 +26,7 @@ jobs: tags: | type=raw,value=${{ github.ref_name}}-${{ github.run_number }} type=raw,value=${{ github.ref_name}} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=true + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 From 2d20aa0d90381dec3a120f9074c2221835002167 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 18:50:54 +0530 Subject: [PATCH 073/180] disable existing abha number check --- care/abdm/api/viewsets/healthid.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 553de6278e..142c324a52 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -296,13 +296,13 @@ def link_via_qr(self, request): patient = PatientRegistration.objects.filter( abha_number__abha_number=data["hidn"] ).first() - if patient: - return Response( - { - "message": "A patient is already associated with the provided Abha Number" - }, - status=status.HTTP_400_BAD_REQUEST, - ) + # if patient: + # return Response( + # { + # "message": "A patient is already associated with the provided Abha Number" + # }, + # status=status.HTTP_400_BAD_REQUEST, + # ) if ( "facilityId" not in data From 9e32644500c2470f7b2326ee704293beaa55b27a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 19:14:18 +0530 Subject: [PATCH 074/180] pick latest patient if more than 1 in discover api --- care/abdm/api/viewsets/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 2fa8782e9b..458c245a27 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -113,11 +113,11 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patients.filter( + patients = patients.filter( abha_number__name=data["patient"]["name"], abha_number__gender=data["patient"]["gender"], # TODO: check date also - ) + ).last() if len(patients) != 1: return Response( From 6bf96a0f727adca11bd27a478944e6f34d114e8e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 19:25:08 +0530 Subject: [PATCH 075/180] removed multiple patient condition check in discover --- care/abdm/api/viewsets/auth.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 458c245a27..afd66989a0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -113,31 +113,31 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patients = patients.filter( + patient = patients.filter( abha_number__name=data["patient"]["name"], abha_number__gender=data["patient"]["gender"], # TODO: check date also ).last() - if len(patients) != 1: - return Response( - "No matching records found, need more data", - status=status.HTTP_404_NOT_FOUND, - ) + # if len(patients) != 1: + # return Response( + # "No matching records found, need more data", + # status=status.HTTP_404_NOT_FOUND, + # ) AbdmGateway().on_discover( { "request_id": data["requestId"], "transaction_id": data["transactionId"], - "patient_id": str(patients[0].external_id), - "patient_name": patients[0].name, + "patient_id": str(patient.external_id), + "patient_name": patient.name, "care_contexts": list( map( lambda consultation: { "id": str(consultation.external_id), "name": f"Encounter: {str(consultation.created_date.date())}", }, - PatientConsultation.objects.filter(patient=patients[0]), + PatientConsultation.objects.filter(patient=patient), ) ), "matched_by": matched_by, From c5ff93bc8a5cb27dcb615635deef946cdc714822 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 19:39:33 +0530 Subject: [PATCH 076/180] select last patient directly in discover --- care/abdm/api/viewsets/auth.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index afd66989a0..6688421c22 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -113,11 +113,13 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patient = patients.filter( - abha_number__name=data["patient"]["name"], - abha_number__gender=data["patient"]["gender"], - # TODO: check date also - ).last() + # patient = patients.filter( + # abha_number__name=data["patient"]["name"], + # abha_number__gender=data["patient"]["gender"], + # # TODO: check date also + # ).last() + + patient = patients.last() # if len(patients) != 1: # return Response( From 307077d4707a3149c952ba7770a494cf6e4a9135 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 20:00:47 +0530 Subject: [PATCH 077/180] added prints --- care/abdm/api/viewsets/auth.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 6688421c22..9cf9a263c0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -88,6 +88,7 @@ def post(self, request, *args, **kwargs): data = request.data patients = PatientRegistration.objects.all() + print(data, patients) verified_identifiers = data["patient"]["verifiedIdentifiers"] matched_by = [] if len(verified_identifiers) == 0: @@ -118,8 +119,10 @@ def post(self, request, *args, **kwargs): # abha_number__gender=data["patient"]["gender"], # # TODO: check date also # ).last() + print(patients) patient = patients.last() + print(patient) # if len(patients) != 1: # return Response( From 3136a9e8bcd1dfb436f62ba70759c9f78cad577a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 20:12:10 +0530 Subject: [PATCH 078/180] improved checks for discover api --- care/abdm/api/viewsets/auth.py | 33 ++++++++++++++++----------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 9cf9a263c0..48ec6bfd20 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -2,6 +2,7 @@ from datetime import datetime, timedelta from django.core.cache import cache +from django.db.models import Q from rest_framework import status from rest_framework.generics import GenericAPIView from rest_framework.permissions import AllowAny @@ -88,7 +89,6 @@ def post(self, request, *args, **kwargs): data = request.data patients = PatientRegistration.objects.all() - print(data, patients) verified_identifiers = data["patient"]["verifiedIdentifiers"] matched_by = [] if len(verified_identifiers) == 0: @@ -100,7 +100,10 @@ def post(self, request, *args, **kwargs): for identifier in verified_identifiers: if identifier["type"] == "MOBILE": matched_by.append(identifier["value"]) - patients = patients.filter(phone_number=f"+91{identifier['value']}") + patients = patients.filter( + Q(phone_number=f"+91{identifier['value']}") + | Q(health_id=identifier["value"]) + ) if identifier["type"] == "NDHM_HEALTH_NUMBER": matched_by.append(identifier["value"]) @@ -114,21 +117,17 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - # patient = patients.filter( - # abha_number__name=data["patient"]["name"], - # abha_number__gender=data["patient"]["gender"], - # # TODO: check date also - # ).last() - print(patients) - - patient = patients.last() - print(patient) - - # if len(patients) != 1: - # return Response( - # "No matching records found, need more data", - # status=status.HTTP_404_NOT_FOUND, - # ) + patient = patients.filter( + abha_number__name=data["patient"]["name"], + abha_number__gender=data["patient"]["gender"], + # TODO: check date also + ).last() + + if not patient: + return Response( + "No matching records found, need more data", + status=status.HTTP_404_NOT_FOUND, + ) AbdmGateway().on_discover( { From 2d52f76de6fe7ca352926188edad967e0c5c3e35 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 24 Apr 2023 20:18:12 +0530 Subject: [PATCH 079/180] fixed a bug in query in discover api --- care/abdm/api/viewsets/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 48ec6bfd20..74375ea759 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -102,7 +102,7 @@ def post(self, request, *args, **kwargs): matched_by.append(identifier["value"]) patients = patients.filter( Q(phone_number=f"+91{identifier['value']}") - | Q(health_id=identifier["value"]) + | Q(phone_number=identifier["value"]) ) if identifier["type"] == "NDHM_HEALTH_NUMBER": From cec1ce23db7d2a865d607a19254de3b24ffe3214 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Tue, 25 Apr 2023 10:58:15 +0530 Subject: [PATCH 080/180] turned on debug and linked a s3 bucket --- config/settings/base.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index da7fd307f8..fe860d98c4 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,7 +29,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", False) +DEBUG = env.bool("DJANGO_DEBUG", True) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. @@ -411,15 +411,20 @@ def GETKEY(group, request): ####################### # File Upload Parameters - -FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") -# FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") -FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") -FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") -FILE_UPLOAD_BUCKET_ENDPOINT = env( - "FILE_UPLOAD_BUCKET_ENDPOINT", - default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", -) +FILE_UPLOAD_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" +FILE_UPLOAD_KEY = "AKIAULRENCOFV7LFUO74" +FILE_UPLOAD_SECRET = "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" +FILE_UPLOAD_BUCKET = "care-s3-dev" +CLOUD_REGION = "ap-south-1" +FACILITY_S3_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" +# FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") +# # FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") +# FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") +# FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") +# FILE_UPLOAD_BUCKET_ENDPOINT = env( +# "FILE_UPLOAD_BUCKET_ENDPOINT", +# default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", +# ) FACILITY_S3_BUCKET = env("FACILITY_S3_BUCKET", default="") FACILITY_S3_REGION = env("FACILITY_S3_REGION_CODE", default="ap-south-1") From 85d0ed7ef368f73bbd3aea3da7f57185ab577eb9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 26 Apr 2023 09:18:57 +0530 Subject: [PATCH 081/180] handled None case in immunization record and empty string case in practitioner name --- care/abdm/utils/fhir.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c71d5cd811..b28ab3eb70 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -46,6 +46,7 @@ def __init__(self, consultation): self._encounter_profile = None self._careplan_profile = None self._diagnostic_report_profile = None + self._immunization_profile = None self._medication_profiles = [] self._medication_request_profiles = [] self._observation_profiles = [] @@ -86,7 +87,10 @@ def _practioner(self): return self._practitioner_profile id = str(uuid()) - name = self.consultation.verified_by + name = ( + self.consultation.verified_by + or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}" + ) self._practitioner_profile = Practitioner( id=id, identifier=[Identifier(value=id)], @@ -384,10 +388,13 @@ def _encounter(self, include_diagnosis=False): return self._encounter_profile def _immunization(self): + if self._immunization_profile: + return self._immunization_profile + if not self.consultation.patient.is_vaccinated: return - return Immunization( + self._immunization_profile = Immunization( id=str(uuid()), status="completed", identifier=[ @@ -639,7 +646,18 @@ def _immunization_composition(self): ), ], ), - entry=[self._reference(self._immunization())], + entry=[ + *( + [self._reference(self._immunization())] + if self._immunization() + else [] + ) + ], + emptyReason=None + if self._immunization() + else CodeableConcept( + coding=[Coding(code="notasked", display="Not Asked")] + ), ), ], subject=self._reference(self._patient()), From 11cfa4d0293efffe6f840a4e9f1dec9cf9a5f203 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 26 Apr 2023 09:25:03 +0530 Subject: [PATCH 082/180] moved add_are_context inti healthid viewset --- care/abdm/api/viewsets/healthid.py | 27 ++++++++++++++++++++++++++- care/abdm/api/viewsets/hip.py | 28 +--------------------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 142c324a52..50c1bf864e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -26,7 +26,7 @@ from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.api.serializers.patient import PatientDetailSerializer from care.facility.models.facility import Facility -from care.facility.models.patient import PatientRegistration +from care.facility.models.patient import PatientConsultation, PatientRegistration from care.utils.queryset.patient import get_patient_queryset from config.auth_views import CaptchaRequiredException from config.ratelimit import ratelimit @@ -408,6 +408,31 @@ def get_new_linking_token(self, request): return Response({}, status=status.HTTP_200_OK) + @action(detail=False, methods=["POST"]) + def add_care_context(self, request, *args, **kwargs): + consultation_id = request.data["consultation"] + + consultation = PatientConsultation.objects.get(external_id=consultation_id) + + if not consultation: + return Response( + {"consultation": "No matching records found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + AbdmGateway().fetch_modes( + { + "healthId": consultation.patient.abha_number.abha_number, + "name": consultation.patient.abha_number.name, + "gender": consultation.patient.abha_number.gender, + "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "consultationId": consultation_id, + "purpose": "LINK", + } + ) + + return Response(status=status.HTTP_202_ACCEPTED) + # auth/init @swagger_auto_schema( # /v1/auth/init diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 6b6d87d201..09945866f9 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -11,7 +11,7 @@ from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility -from care.facility.models.patient import PatientConsultation, PatientRegistration +from care.facility.models.patient import PatientRegistration class HipViewSet(GenericViewSet): @@ -145,29 +145,3 @@ def share(self, request, *args, **kwargs): }, status=status.HTTP_401_UNAUTHORIZED, ) - - # TODO: move it somewhere appropriate - @action(detail=False, methods=["POST"]) - def add_care_context(self, request, *args, **kwargs): - consultation_id = request.data["consultation"] - - consultation = PatientConsultation.objects.get(external_id=consultation_id) - - if not consultation: - return Response( - {"consultation": "No matching records found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - AbdmGateway().fetch_modes( - { - "healthId": consultation.patient.abha_number.abha_number, - "name": consultation.patient.abha_number.name, - "gender": consultation.patient.abha_number.gender, - "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), - "consultationId": consultation_id, - "purpose": "LINK", - } - ) - - return Response(status=status.HTTP_202_ACCEPTED) From 67c4be5ffe1e93b2840df92819d0cdf06e10d8a9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 27 Apr 2023 08:46:01 +0530 Subject: [PATCH 083/180] added heartbeat api --- care/abdm/api/viewsets/monitoring.py | 23 +++++++++++++++++++++++ config/urls.py | 6 ++++++ 2 files changed, 29 insertions(+) create mode 100644 care/abdm/api/viewsets/monitoring.py diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py new file mode 100644 index 0000000000..54cfe30069 --- /dev/null +++ b/care/abdm/api/viewsets/monitoring.py @@ -0,0 +1,23 @@ +from datetime import datetime, timezone + +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + + +class HeartbeatView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def get(self, request, *args, **kwargs): + return Response( + { + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "status": "UP", + "error": None, + }, + status=status.HTTP_200_OK, + ) diff --git a/config/urls.py b/config/urls.py index 280d6509a1..8050b1aaa8 100644 --- a/config/urls.py +++ b/config/urls.py @@ -20,6 +20,7 @@ OnInitView, RequestDataView, ) +from care.abdm.api.viewsets.monitoring import HeartbeatView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -129,6 +130,11 @@ RequestDataView.as_view(), name="abdm_request_data_view", ), + path( + "v0.5/heartbeat", + HeartbeatView.as_view(), + name="abdm_monitoring_heartbeat_view", + ), # Hcx Listeners path( "coverageeligibility/on_check", From 949f079897caa1c9e1213ca79826d25a9d56032a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 27 Apr 2023 09:23:44 +0530 Subject: [PATCH 084/180] added abha opt out flow --- care/abdm/api/viewsets/status.py | 28 ++++++++++++++++++++++++++++ care/abdm/utils/api_call.py | 18 ++++++++++++++++++ config/urls.py | 6 ++++++ 3 files changed, 52 insertions(+) create mode 100644 care/abdm/api/viewsets/status.py diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py new file mode 100644 index 0000000000..a1f56ab96e --- /dev/null +++ b/care/abdm/api/viewsets/status.py @@ -0,0 +1,28 @@ +from rest_framework import status +from rest_framework.generics import GenericAPIView +from rest_framework.permissions import AllowAny +from rest_framework.response import Response + +from care.abdm.models import AbhaNumber +from care.abdm.utils.api_call import AbdmGateway +from care.facility.models.patient import PatientRegistration + + +class NotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("patient_status_notify", data) + + PatientRegistration.objects.filter( + abha_number__health_id=data["notification"]["patient"]["id"] + ).update(abha_number=None) + AbhaNumber.objects.filter( + health_id=data["notification"]["patient"]["id"] + ).delete() + + AbdmGateway().patient_status_on_notify({"request_id": data["requestId"]}) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 2fc3173962..8923050bee 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -673,6 +673,24 @@ def data_notify(self, data): response = self.api.post(path, payload, None, additional_headers) return response + def patient_status_on_notify(self, data): + path = "/v0.5/patients/status/on-notify" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "acknowledgement": {"status": "OK"}, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index 8050b1aaa8..23c4712911 100644 --- a/config/urls.py +++ b/config/urls.py @@ -21,6 +21,7 @@ RequestDataView, ) from care.abdm.api.viewsets.monitoring import HeartbeatView +from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -130,6 +131,11 @@ RequestDataView.as_view(), name="abdm_request_data_view", ), + path( + "v0.5/patients/status/notify", + PatientStatusNotifyView.as_view(), + name="abdm_patient_status_notify_view", + ), path( "v0.5/heartbeat", HeartbeatView.as_view(), From 48cb737fa842c12efffd5af106b69b9ca841108a Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 27 Apr 2023 10:13:39 +0530 Subject: [PATCH 085/180] added sms apis --- care/abdm/api/viewsets/status.py | 11 +++++++++++ care/abdm/utils/api_call.py | 20 ++++++++++++++++++++ config/urls.py | 6 ++++++ 3 files changed, 37 insertions(+) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index a1f56ab96e..a52044c5e4 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -26,3 +26,14 @@ def post(self, request, *args, **kwargs): AbdmGateway().patient_status_on_notify({"request_id": data["requestId"]}) return Response(status=status.HTTP_202_ACCEPTED) + + +class SMSOnNotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [] + + def post(self, request, *args, **kwargs): + data = request.data + print("patient_sms_on_notify", data) + + return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 8923050bee..b02746e4ec 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -321,6 +321,7 @@ def verify_document_mobile_otp(self, data): class AbdmGateway: # TODO: replace this with in-memory db (redis) temp_memory = {} + hip_name = "Coronasafe Care 01" hip_id = "IN3210000017" def __init__(self): @@ -691,6 +692,25 @@ def patient_status_on_notify(self, data): response = self.api.post(path, payload, None, additional_headers) return response + def patient_sms_notify(self, data): + path = "/v0.5/patients/sms/notify2" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "notification": { + "phoneNo": f"+91-{data['phone']}", + "hip": {"name": self.hip_name, "id": self.hip_id}, + }, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + # /v1.0/patients/profile/on-share def on_share(self, data): path = "/v1.0/patients/profile/on-share" diff --git a/config/urls.py b/config/urls.py index 23c4712911..0e325edc4a 100644 --- a/config/urls.py +++ b/config/urls.py @@ -22,6 +22,7 @@ ) from care.abdm.api.viewsets.monitoring import HeartbeatView from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView +from care.abdm.api.viewsets.status import SMSOnNotifyView from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -136,6 +137,11 @@ PatientStatusNotifyView.as_view(), name="abdm_patient_status_notify_view", ), + path( + "v0.5/patients/sms/on-notify", + SMSOnNotifyView.as_view(), + name="abdm_patient_status_notify_view", + ), path( "v0.5/heartbeat", HeartbeatView.as_view(), From b37d44e2c0dfa48381d6f72c80df44ca6e484d5f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 24 May 2023 12:19:14 +0530 Subject: [PATCH 086/180] added ratelimiting to m2 apis --- care/abdm/api/viewsets/healthid.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 50c1bf864e..b28d4ce869 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -287,6 +287,12 @@ def search_by_health_id(self, request): def link_via_qr(self, request): data = request.data + if ratelimit(request, "link_via_qr", [data["hdin"]], increment=False): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + serializer = QRContentSerializer(data=data) serializer.is_valid(raise_exception=True) @@ -393,6 +399,14 @@ def link_via_qr(self, request): def get_new_linking_token(self, request): data = request.data + if ratelimit( + request, "get_new_linking_token", [data["patient"]], increment=False + ): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + patient = PatientDetailSerializer( PatientRegistration.objects.get(external_id=data["patient"]) ).data @@ -412,6 +426,12 @@ def get_new_linking_token(self, request): def add_care_context(self, request, *args, **kwargs): consultation_id = request.data["consultation"] + if ratelimit(request, "add_care_context", [consultation_id], increment=False): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + consultation = PatientConsultation.objects.get(external_id=consultation_id) if not consultation: From a8a416282979aae08e59f2d3d90875243d95fe62 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 3 Jun 2023 12:21:45 +0530 Subject: [PATCH 087/180] added patient_sms_notify --- care/abdm/api/viewsets/healthid.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b28d4ce869..14b854212c 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -453,6 +453,28 @@ def add_care_context(self, request, *args, **kwargs): return Response(status=status.HTTP_202_ACCEPTED) + @action(detail=False, methods=["POST"]) + def patient_sms_notify(self, request, *args, **kwargs): + patient_id = request.data["patient"] + + if ratelimit(request, "patient_sms_notify", [patient_id], increment=False): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + patient = PatientRegistration.objects.get(external_id=patient_id) + + if not patient: + return Response( + {"consultation": "No matching records found"}, + status=status.HTTP_404_NOT_FOUND, + ) + + AbdmGateway().patient_sms_notify({"phone": patient.phone_number}) + + return Response(status=status.HTTP_202_ACCEPTED) + # auth/init @swagger_auto_schema( # /v1/auth/init From 8ccc1b7733dd57c8ecbdfbf93dee2992990fa28f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 3 Jun 2023 12:44:06 +0530 Subject: [PATCH 088/180] return abdm response in patient_sms_notify --- care/abdm/api/viewsets/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 14b854212c..305827da33 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -471,9 +471,9 @@ def patient_sms_notify(self, request, *args, **kwargs): status=status.HTTP_404_NOT_FOUND, ) - AbdmGateway().patient_sms_notify({"phone": patient.phone_number}) + response = AbdmGateway().patient_sms_notify({"phone": patient.phone_number}) - return Response(status=status.HTTP_202_ACCEPTED) + return Response(response, status=status.HTTP_202_ACCEPTED) # auth/init @swagger_auto_schema( From 3cd09d818d242ceb0738a4558ddef981a90a4bb0 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 4 Jun 2023 08:43:26 +0530 Subject: [PATCH 089/180] added key chech in auth/init and auth/confirm --- care/abdm/utils/api_call.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b02746e4ec..ba1ff86fc4 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -396,6 +396,9 @@ def fetch_modes(self, data): # "/v0.5/users/auth/init" def init(self, prev_request_id): + if prev_request_id not in self.temp_memory: + return + path = "/v0.5/users/auth/init" additional_headers = {"X-CM-ID": settings.X_CM_ID} @@ -421,6 +424,9 @@ def init(self, prev_request_id): # "/v0.5/users/auth/confirm" def confirm(self, transaction_id, prev_request_id): + if prev_request_id not in self.temp_memory: + return + path = "/v0.5/users/auth/confirm" additional_headers = {"X-CM-ID": settings.X_CM_ID} From 66b231638bcfe7df1675f4d5e287ae557753dde7 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 4 Jun 2023 08:58:17 +0530 Subject: [PATCH 090/180] added key chech in save_linking_token and add_care_context --- care/abdm/utils/api_call.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ba1ff86fc4..201bc47854 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -328,6 +328,9 @@ def __init__(self): self.api = APIGateway("abdm_gateway", None) def add_care_context(self, access_token, request_id): + if request_id not in self.temp_memory: + return + data = self.temp_memory[request_id] if "consultationId" in data: @@ -350,6 +353,9 @@ def add_care_context(self, access_token, request_id): return False def save_linking_token(self, patient, access_token, request_id): + if request_id not in self.temp_memory: + return + data = self.temp_memory[request_id] health_id = patient and patient["id"] or data["healthId"] From 72240a2803d162c4073a4a70791fe6ba8a8ad4f1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sun, 4 Jun 2023 10:35:27 +0530 Subject: [PATCH 091/180] minor fail safes added --- care/abdm/api/viewsets/auth.py | 8 +++++--- care/abdm/api/viewsets/healthid.py | 2 +- care/abdm/api/viewsets/hip.py | 1 + 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 74375ea759..0fdcf145ea 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -4,7 +4,7 @@ from django.core.cache import cache from django.db.models import Q from rest_framework import status -from rest_framework.generics import GenericAPIView +from rest_framework.generics import GenericAPIView, get_object_or_404 from rest_framework.permissions import AllowAny from rest_framework.response import Response @@ -179,8 +179,10 @@ def post(self, request, *args, **kwargs): # TODO: verify otp - patient = PatientRegistration.objects.get( - external_id=data["confirmation"]["linkRefNumber"] + patient = get_object_or_404( + PatientRegistration.objects.filter( + external_id=data["confirmation"]["linkRefNumber"] + ).first() ) AbdmGateway().on_link_confirm( { diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 305827da33..f583baca8d 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -463,7 +463,7 @@ def patient_sms_notify(self, request, *args, **kwargs): code=status.HTTP_429_TOO_MANY_REQUESTS, ) - patient = PatientRegistration.objects.get(external_id=patient_id) + patient = PatientRegistration.objects.filter(external_id=patient_id).first() if not patient: return Response( diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 09945866f9..f22415f72b 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -25,6 +25,7 @@ def get_linking_token(self, data): @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data + print(data) patient_data = data["profile"]["patient"] counter_id = ( From b562bb52788599678593d38e012716ff74ccf1e2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 5 Jun 2023 14:45:32 +0530 Subject: [PATCH 092/180] enabled ratelimiting --- config/settings/local.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/settings/local.py b/config/settings/local.py index 288db0a97a..7c98c8ce5f 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -100,7 +100,7 @@ AUDIT_LOG_ENABLED = True -DISABLE_RATELIMIT = True +DISABLE_RATELIMIT = False FILE_UPLOAD_BUCKET_ENDPOINT = "http://localstack:4566" From 7c574719b4f42c30c90da24a2e088cf198017229 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 5 Jun 2023 15:07:47 +0530 Subject: [PATCH 093/180] debug: ratelimit --- config/ratelimit.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/ratelimit.py b/config/ratelimit.py index 435076c726..cd751b7a47 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -25,9 +25,15 @@ def validatecaptcha(request): def ratelimit( request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True ): + print("---------------------------------------------------------") + print(settings.DISABLE_RATELIMIT) + print("---------------------------------------------------------") if settings.DISABLE_RATELIMIT: return False + print("---------------------------------------------------------") + print(group, keys, rate) + print("---------------------------------------------------------") checkcaptcha = False for key in keys: if key == "ip": From c71012b99811c7277da73ebf27d41bebe665055d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 5 Jun 2023 15:16:33 +0530 Subject: [PATCH 094/180] removed increament in ratelimiting --- care/abdm/api/viewsets/healthid.py | 8 +++----- config/ratelimit.py | 6 ------ config/settings/local.py | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index f583baca8d..9a75bcda5e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -399,9 +399,7 @@ def link_via_qr(self, request): def get_new_linking_token(self, request): data = request.data - if ratelimit( - request, "get_new_linking_token", [data["patient"]], increment=False - ): + if ratelimit(request, "get_new_linking_token", [data["patient"]]): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, @@ -426,7 +424,7 @@ def get_new_linking_token(self, request): def add_care_context(self, request, *args, **kwargs): consultation_id = request.data["consultation"] - if ratelimit(request, "add_care_context", [consultation_id], increment=False): + if ratelimit(request, "add_care_context", [consultation_id]): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, @@ -457,7 +455,7 @@ def add_care_context(self, request, *args, **kwargs): def patient_sms_notify(self, request, *args, **kwargs): patient_id = request.data["patient"] - if ratelimit(request, "patient_sms_notify", [patient_id], increment=False): + if ratelimit(request, "patient_sms_notify", [patient_id]): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, diff --git a/config/ratelimit.py b/config/ratelimit.py index cd751b7a47..435076c726 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -25,15 +25,9 @@ def validatecaptcha(request): def ratelimit( request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True ): - print("---------------------------------------------------------") - print(settings.DISABLE_RATELIMIT) - print("---------------------------------------------------------") if settings.DISABLE_RATELIMIT: return False - print("---------------------------------------------------------") - print(group, keys, rate) - print("---------------------------------------------------------") checkcaptcha = False for key in keys: if key == "ip": diff --git a/config/settings/local.py b/config/settings/local.py index 7c98c8ce5f..288db0a97a 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -100,7 +100,7 @@ AUDIT_LOG_ENABLED = True -DISABLE_RATELIMIT = False +DISABLE_RATELIMIT = True FILE_UPLOAD_BUCKET_ENDPOINT = "http://localstack:4566" From d0c2e2d2f3cb5f63b0b7887fd1f73fa243037d6e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 7 Jun 2023 11:17:36 +0530 Subject: [PATCH 095/180] added ABDMAuthentication for authentication the callbacks --- care/abdm/api/viewsets/auth.py | 19 ++++++------ care/abdm/api/viewsets/hip.py | 3 +- care/abdm/api/viewsets/status.py | 5 +-- config/authentication.py | 52 ++++++++++++++++++++++++++++++++ config/settings/base.py | 2 ++ 5 files changed, 69 insertions(+), 12 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 0fdcf145ea..b978c835d1 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -13,11 +13,12 @@ from care.abdm.utils.fhir import Fhir from care.facility.models.patient import PatientRegistration from care.facility.models.patient_consultation import PatientConsultation +from config.authentication import ABDMAuthentication class OnFetchView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -28,7 +29,7 @@ def post(self, request, *args, **kwargs): class OnInitView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -39,7 +40,7 @@ def post(self, request, *args, **kwargs): class OnConfirmView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -73,7 +74,7 @@ def post(self, request, *args, **kwargs): class OnAddContextsView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -83,7 +84,7 @@ def post(self, request, *args, **kwargs): class DiscoverView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -152,7 +153,7 @@ def post(self, request, *args, **kwargs): class LinkInitView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -172,7 +173,7 @@ def post(self, request, *args, **kwargs): class LinkConfirmView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -206,7 +207,7 @@ def post(self, request, *args, **kwargs): class NotifyView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -226,7 +227,7 @@ def post(self, request, *args, **kwargs): class RequestDataView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index f22415f72b..cd625fb246 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -12,11 +12,12 @@ from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.models.facility import Facility from care.facility.models.patient import PatientRegistration +from config.authentication import ABDMAuthentication class HipViewSet(GenericViewSet): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def get_linking_token(self, data): AbdmGateway().fetch_modes(data) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index a52044c5e4..3da724c722 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -6,11 +6,12 @@ from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway from care.facility.models.patient import PatientRegistration +from config.authentication import ABDMAuthentication class NotifyView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data @@ -30,7 +31,7 @@ def post(self, request, *args, **kwargs): class SMSOnNotifyView(GenericAPIView): permission_classes = (AllowAny,) - authentication_classes = [] + authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): data = request.data diff --git a/config/authentication.py b/config/authentication.py index 8a68ca592e..a03e2daa2b 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -2,6 +2,7 @@ import jwt import requests +from django.conf import settings from django.contrib.auth import authenticate from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ @@ -178,3 +179,54 @@ def get_user(self, validated_token, facility): ) asset_user.save() return asset_user + + +class ABDMAuthentication(JWTAuthentication): + def open_id_authenticate(self, url, token): + public_key = requests.get(url) + jwk = public_key.json()["keys"][0] + public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) + return jwt.decode( + token, key=public_key, audience="account", algorithms=["RS256"] + ) + + def authenticate_header(self, request): + return "Bearer" + + def authenticate(self, request): + jwt_token = request.META.get("HTTP_AUTHORIZATION") + if jwt_token is None: + return None + jwt_token = self.get_jwt_token(jwt_token) + + abdm_cert_url = f"{settings.ABDM_URL}/gateway/v0.5/certs" + validated_token = self.get_validated_token(abdm_cert_url, jwt_token) + + return self.get_user(validated_token), validated_token + + def get_jwt_token(self, token): + return token.replace("Bearer", "").replace(" ", "") + + def get_validated_token(self, url, token): + try: + return self.open_id_authenticate(url, token) + except Exception as e: + print(e) + raise InvalidToken({"detail": f"Invalid Authorization token: {e}"}) + + def get_user(self, validated_token): + user = User.objects.filter(username=settings.ABDM_USERNAME).first() + if not user: + password = User.objects.make_random_password() + user = User( + username=settings.ABDM_USERNAME, + email="hcx@coronasafe.network", + password=f"{password}123", + gender=3, + phone_number="917777777777", + user_type=User.TYPE_VALUE_MAP["Volunteer"], + verified=True, + age=10, + ) + user.save() + return user diff --git a/config/settings/base.py b/config/settings/base.py index fe860d98c4..a6c49d6ea5 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -519,5 +519,7 @@ def GETKEY(group, request): ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") +ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in") +ABDM_USERNAME = env("ABDM_USERNAME", default="abdm_user_internal") X_CM_ID = env("X_CM_ID", default="sbx") FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") From e561bc26a4ab7381a22c5d5a32eb65d4abafa353 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 7 Jun 2023 12:23:39 +0530 Subject: [PATCH 096/180] set jwt as default auth in production --- config/settings/production.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/config/settings/production.py b/config/settings/production.py index d8580b51cd..d48e5fcdd4 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -1,7 +1,8 @@ from .deployment import * # noqa -# Your stuff... -# ------------------------------------------------------------------------------ +REST_FRAMEWORK["DEFAULT_AUTHENTICATION_CLASSES"] = ( # noqa F405 + "config.authentication.CustomJWTAuthentication", +) IS_PRODUCTION = True USE_SMS = True From 3e33b1d5c78526c08d75e33b5d8312f99021c204 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 8 Jun 2023 11:25:26 +0530 Subject: [PATCH 097/180] fixed issue in link/confirm callback --- care/abdm/api/viewsets/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index b978c835d1..7f7be41f94 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -183,7 +183,7 @@ def post(self, request, *args, **kwargs): patient = get_object_or_404( PatientRegistration.objects.filter( external_id=data["confirmation"]["linkRefNumber"] - ).first() + ) ) AbdmGateway().on_link_confirm( { From f9303be540ef305c60cd5b74b48564fbeb499aa1 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 8 Jun 2023 11:30:45 +0530 Subject: [PATCH 098/180] temp: send 202 for hip/request and profile/share --- care/abdm/api/viewsets/auth.py | 5 +++-- care/abdm/api/viewsets/hip.py | 8 ++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7f7be41f94..892b608e01 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -236,8 +236,8 @@ def post(self, request, *args, **kwargs): # TODO: uncomment later consent_id = data["hiRequest"]["consent"]["id"] consent = json.loads(cache.get(consent_id)) if consent_id in cache else None - if not consent or not consent["notification"]["status"] == "GRANTED": - return Response({}, status=status.HTTP_401_UNAUTHORIZED) + # if not consent or not consent["notification"]["status"] == "GRANTED": + # return Response({}, status=status.HTTP_401_UNAUTHORIZED) # TODO: check if from and to are in range and consent expiry is greater than today # consent_from = datetime.fromisoformat( @@ -255,6 +255,7 @@ def post(self, request, *args, **kwargs): ) if not on_data_request_response.status_code == 202: + return Response({}, status=status.HTTP_202_ACCEPTED) return Response( on_data_request_response, status=status.HTTP_400_BAD_REQUEST ) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index cd625fb246..4958e23d94 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -140,6 +140,14 @@ def share(self, request, *args, **kwargs): status=status.HTTP_202_ACCEPTED, ) + return Response( + { + "status": "ACCEPTED", + "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], + }, + status=status.HTTP_202_ACCEPTED, + ) + return Response( { "status": "FAILURE", From be6968f48cec8e89f57a44bff644c6dd6fce68c2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 8 Jun 2023 18:32:24 +0530 Subject: [PATCH 099/180] set debug to false --- 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 a6c49d6ea5..db342ce483 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,7 +29,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", True) +DEBUG = env.bool("DJANGO_DEBUG", False) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. From e57e8178df8b2f2d85e1db37dc314010bb68193e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 13:47:35 +0530 Subject: [PATCH 100/180] add external_id field to users --- .../users/migrations/0047_user_external_id.py | 19 ++++++++++++++++ .../migrations/0048_auto_20230609_1411.py | 22 +++++++++++++++++++ .../migrations/0049_auto_20230609_1413.py | 19 ++++++++++++++++ care/users/models.py | 3 +++ 4 files changed, 63 insertions(+) create mode 100644 care/users/migrations/0047_user_external_id.py create mode 100644 care/users/migrations/0048_auto_20230609_1411.py create mode 100644 care/users/migrations/0049_auto_20230609_1413.py diff --git a/care/users/migrations/0047_user_external_id.py b/care/users/migrations/0047_user_external_id.py new file mode 100644 index 0000000000..86b0ecc94f --- /dev/null +++ b/care/users/migrations/0047_user_external_id.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.11 on 2023-06-09 08:41 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0046_auto_20230204_1733"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="external_id", + field=models.UUIDField(default=uuid.uuid4, null=True), + ), + ] diff --git a/care/users/migrations/0048_auto_20230609_1411.py b/care/users/migrations/0048_auto_20230609_1411.py new file mode 100644 index 0000000000..7b2f7fcee3 --- /dev/null +++ b/care/users/migrations/0048_auto_20230609_1411.py @@ -0,0 +1,22 @@ +# Generated by Django 2.2.11 on 2023-06-09 08:41 + +import uuid + +from django.db import migrations + + +def gen_uuid(apps, schema_editor): + User = apps.get_model("users", "User") + for user in User.objects.get_entire_queryset(): + user.external_id = uuid.uuid4() + user.save(update_fields=["external_id"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0047_user_external_id"), + ] + + operations = [ + migrations.RunPython(gen_uuid, migrations.RunPython.noop), + ] diff --git a/care/users/migrations/0049_auto_20230609_1413.py b/care/users/migrations/0049_auto_20230609_1413.py new file mode 100644 index 0000000000..a1789c58bf --- /dev/null +++ b/care/users/migrations/0049_auto_20230609_1413.py @@ -0,0 +1,19 @@ +# Generated by Django 2.2.11 on 2023-06-09 08:43 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0048_auto_20230609_1411"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="external_id", + field=models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ] diff --git a/care/users/models.py b/care/users/models.py index a9401ede93..166460aa0f 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -1,3 +1,5 @@ +import uuid + from django.contrib.auth.models import AbstractUser, UserManager from django.contrib.auth.validators import UnicodeUsernameValidator from django.core.validators import MaxValueValidator, MinValueValidator, RegexValidator @@ -172,6 +174,7 @@ class Meta: class User(AbstractUser): + external_id = models.UUIDField(default=uuid.uuid4, unique=True, db_index=True) username_validator = UsernameValidator() username = models.CharField( _("username"), From 69b3996767e19d8a6a0d72f23625f3178a443a2b Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 14:21:49 +0530 Subject: [PATCH 101/180] use uuid for jwt --- config/settings/base.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/base.py b/config/settings/base.py index db342ce483..d6b91fc5d9 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -318,6 +318,7 @@ minutes=env("JWT_REFRESH_TOKEN_LIFETIME", default=30) ), "ROTATE_REFRESH_TOKENS": True, + "USER_ID_FIELD": "external_id", } From 35170453411e0df0effebd579782861410598f4d Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Fri, 9 Jun 2023 14:59:43 +0530 Subject: [PATCH 102/180] use uuid for user_id in jwt in local conf --- config/settings/local.py | 1 + 1 file changed, 1 insertion(+) diff --git a/config/settings/local.py b/config/settings/local.py index 288db0a97a..3e917901d8 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -94,6 +94,7 @@ minutes=env("JWT_REFRESH_TOKEN_LIFETIME", default=3000000000) ), "ROTATE_REFRESH_TOKENS": True, + "USER_ID_FIELD": "external_id", } RUNSERVER_PLUS_PRINT_SQL_TRUNCATE = 100000 From 08e38501fced37bcf6560ccdedc219a1f94a4c1c Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 20:16:40 +0530 Subject: [PATCH 103/180] fix merge issues --- .../migrations/0361_merge_20230609_2014.py | 12 +++++++ config/settings/base.py | 31 ++++++++++--------- 2 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 care/facility/migrations/0361_merge_20230609_2014.py diff --git a/care/facility/migrations/0361_merge_20230609_2014.py b/care/facility/migrations/0361_merge_20230609_2014.py new file mode 100644 index 0000000000..66a67446c3 --- /dev/null +++ b/care/facility/migrations/0361_merge_20230609_2014.py @@ -0,0 +1,12 @@ +# Generated by Django 2.2.11 on 2023-06-09 14:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0360_auto_20230608_1750"), + ("facility", "0344_merge_20230413_1120"), + ] + + operations = [] diff --git a/config/settings/base.py b/config/settings/base.py index 1c0d4173a4..18b50651bf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -412,20 +412,14 @@ def GETKEY(group, request): ####################### # File Upload Parameters -FILE_UPLOAD_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" -FILE_UPLOAD_KEY = "AKIAULRENCOFV7LFUO74" -FILE_UPLOAD_SECRET = "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" -FILE_UPLOAD_BUCKET = "care-s3-dev" -CLOUD_REGION = "ap-south-1" -FACILITY_S3_BUCKET_ENDPOINT = "https://care-s3-dev.s3.amazonaws.com" -# FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") -# # FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") -# FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") -# FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") -# FILE_UPLOAD_BUCKET_ENDPOINT = env( -# "FILE_UPLOAD_BUCKET_ENDPOINT", -# default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", -# ) +FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") +FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default="care-patient-staging") +FILE_UPLOAD_KEY = env("FILE_UPLOAD_KEY", default="") +FILE_UPLOAD_SECRET = env("FILE_UPLOAD_SECRET", default="") +FILE_UPLOAD_BUCKET_ENDPOINT = env( + "FILE_UPLOAD_BUCKET_ENDPOINT", + default=f"https://{FILE_UPLOAD_BUCKET}.s3.amazonaws.com", +) FACILITY_S3_BUCKET = env("FACILITY_S3_BUCKET", default="") FACILITY_S3_REGION = env("FACILITY_S3_REGION_CODE", default="ap-south-1") @@ -517,3 +511,12 @@ def GETKEY(group, request): JWKS = JsonWebKey.import_key_set( json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks()))) ) + +PEACETIME_MODE = env.bool("PEACETIME_MODE", default=True) + +ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") +ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") +ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in") +ABDM_USERNAME = env("ABDM_USERNAME", default="abdm_user_internal") +X_CM_ID = env("X_CM_ID", default="sbx") +FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") From bfbc965881953ee6b25b871c3b4add1757b8bd7c Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 9 Jun 2023 20:20:33 +0530 Subject: [PATCH 104/180] fix lint issues --- care/abdm/migrations/0001_initial.py | 56 ++++++++++++------- .../migrations/0002_auto_20221220_2312.py | 19 +++---- .../migrations/0003_auto_20221220_2321.py | 11 ++-- .../migrations/0004_auto_20221220_2325.py | 35 ++++++------ .../migrations/0005_auto_20221220_2327.py | 43 +++++++------- .../migrations/0006_auto_20230208_0915.py | 1 - .../migrations/0329_auto_20221219_1936.py | 28 +++++++--- .../migrations/0330_auto_20221220_2312.py | 31 ++++++---- .../migrations/0331_auto_20230130_1652.py | 7 +-- .../migrations/0344_merge_20230413_1120.py | 8 +-- 10 files changed, 133 insertions(+), 106 deletions(-) diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py index cb8481db18..bcd8410442 100644 --- a/care/abdm/migrations/0001_initial.py +++ b/care/abdm/migrations/0001_initial.py @@ -8,32 +8,48 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( - name='AbhaNumber', + name="AbhaNumber", fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('external_id', models.UUIDField(db_index=True, default=uuid.uuid4, unique=True)), - ('created_date', models.DateTimeField(auto_now_add=True, db_index=True, null=True)), - ('modified_date', models.DateTimeField(auto_now=True, db_index=True, null=True)), - ('deleted', models.BooleanField(db_index=True, default=False)), - ('abha_number', models.CharField(max_length=50)), - ('email', models.CharField(max_length=50)), - ('first_name', models.CharField(max_length=50)), - ('health_id', models.CharField(max_length=50)), - ('last_name', models.CharField(max_length=50)), - ('middle_name', models.CharField(max_length=50)), - ('password', models.CharField(max_length=50)), - ('profile_photo', models.CharField(max_length=50)), - ('txn_id', models.CharField(max_length=50)), - ('access_token', models.CharField(max_length=50)), - ('refresh_token', models.CharField(max_length=50)), + ( + "id", + models.AutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("abha_number", models.CharField(max_length=50)), + ("email", models.CharField(max_length=50)), + ("first_name", models.CharField(max_length=50)), + ("health_id", models.CharField(max_length=50)), + ("last_name", models.CharField(max_length=50)), + ("middle_name", models.CharField(max_length=50)), + ("password", models.CharField(max_length=50)), + ("profile_photo", models.CharField(max_length=50)), + ("txn_id", models.CharField(max_length=50)), + ("access_token", models.CharField(max_length=50)), + ("refresh_token", models.CharField(max_length=50)), ], options={ - 'abstract': False, + "abstract": False, }, ), ] diff --git a/care/abdm/migrations/0002_auto_20221220_2312.py b/care/abdm/migrations/0002_auto_20221220_2312.py index fff590d3d5..73253c5c75 100644 --- a/care/abdm/migrations/0002_auto_20221220_2312.py +++ b/care/abdm/migrations/0002_auto_20221220_2312.py @@ -4,30 +4,29 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0001_initial'), + ("abdm", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='abhanumber', - name='email', + model_name="abhanumber", + name="email", field=models.EmailField(max_length=254), ), migrations.AlterField( - model_name='abhanumber', - name='first_name', + model_name="abhanumber", + name="first_name", field=models.CharField(max_length=512), ), migrations.AlterField( - model_name='abhanumber', - name='last_name', + model_name="abhanumber", + name="last_name", field=models.CharField(max_length=512), ), migrations.AlterField( - model_name='abhanumber', - name='middle_name', + model_name="abhanumber", + name="middle_name", field=models.CharField(max_length=512), ), ] diff --git a/care/abdm/migrations/0003_auto_20221220_2321.py b/care/abdm/migrations/0003_auto_20221220_2321.py index 7e2fd3121b..aaa8357d2c 100644 --- a/care/abdm/migrations/0003_auto_20221220_2321.py +++ b/care/abdm/migrations/0003_auto_20221220_2321.py @@ -4,19 +4,18 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0002_auto_20221220_2312'), + ("abdm", "0002_auto_20221220_2312"), ] operations = [ migrations.RemoveField( - model_name='abhanumber', - name='password', + model_name="abhanumber", + name="password", ), migrations.AlterField( - model_name='abhanumber', - name='profile_photo', + model_name="abhanumber", + name="profile_photo", field=models.TextField(), ), ] diff --git a/care/abdm/migrations/0004_auto_20221220_2325.py b/care/abdm/migrations/0004_auto_20221220_2325.py index 7e90626f80..613d539e15 100644 --- a/care/abdm/migrations/0004_auto_20221220_2325.py +++ b/care/abdm/migrations/0004_auto_20221220_2325.py @@ -4,50 +4,49 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0003_auto_20221220_2321'), + ("abdm", "0003_auto_20221220_2321"), ] operations = [ migrations.AlterField( - model_name='abhanumber', - name='abha_number', + model_name="abhanumber", + name="abha_number", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='access_token', + model_name="abhanumber", + name="access_token", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='first_name', + model_name="abhanumber", + name="first_name", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='health_id', + model_name="abhanumber", + name="health_id", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='last_name', + model_name="abhanumber", + name="last_name", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='middle_name', + model_name="abhanumber", + name="middle_name", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='refresh_token', + model_name="abhanumber", + name="refresh_token", field=models.TextField(), ), migrations.AlterField( - model_name='abhanumber', - name='txn_id', + model_name="abhanumber", + name="txn_id", field=models.TextField(), ), ] diff --git a/care/abdm/migrations/0005_auto_20221220_2327.py b/care/abdm/migrations/0005_auto_20221220_2327.py index f9781a1dbf..1c0b9bc736 100644 --- a/care/abdm/migrations/0005_auto_20221220_2327.py +++ b/care/abdm/migrations/0005_auto_20221220_2327.py @@ -4,60 +4,59 @@ class Migration(migrations.Migration): - dependencies = [ - ('abdm', '0004_auto_20221220_2325'), + ("abdm", "0004_auto_20221220_2325"), ] operations = [ migrations.AlterField( - model_name='abhanumber', - name='abha_number', + model_name="abhanumber", + name="abha_number", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='access_token', + model_name="abhanumber", + name="access_token", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='email', + model_name="abhanumber", + name="email", field=models.EmailField(blank=True, max_length=254, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='first_name', + model_name="abhanumber", + name="first_name", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='health_id', + model_name="abhanumber", + name="health_id", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='last_name', + model_name="abhanumber", + name="last_name", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='middle_name', + model_name="abhanumber", + name="middle_name", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='profile_photo', + model_name="abhanumber", + name="profile_photo", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='refresh_token', + model_name="abhanumber", + name="refresh_token", field=models.TextField(blank=True, null=True), ), migrations.AlterField( - model_name='abhanumber', - name='txn_id', + model_name="abhanumber", + name="txn_id", field=models.TextField(blank=True, null=True), ), ] diff --git a/care/abdm/migrations/0006_auto_20230208_0915.py b/care/abdm/migrations/0006_auto_20230208_0915.py index ed00f037cd..940ed863c8 100644 --- a/care/abdm/migrations/0006_auto_20230208_0915.py +++ b/care/abdm/migrations/0006_auto_20230208_0915.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ("abdm", "0005_auto_20221220_2327"), ] diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py index c0b161de69..589981551d 100644 --- a/care/facility/migrations/0329_auto_20221219_1936.py +++ b/care/facility/migrations/0329_auto_20221219_1936.py @@ -7,20 +7,30 @@ class Migration(migrations.Migration): dependencies = [ - ('facility', '0328_merge_20221208_1110'), + ("facility", "0328_merge_20221208_1110"), ] operations = [ migrations.AlterField( - model_name='fileupload', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - related_name='archived_by', to=settings.AUTH_USER_MODEL), + model_name="fileupload", + name="archived_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="archived_by", + to=settings.AUTH_USER_MODEL, + ), ), migrations.AlterField( - model_name='fileupload', - name='uploaded_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, - related_name='uploaded_by', to=settings.AUTH_USER_MODEL), + model_name="fileupload", + name="uploaded_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="uploaded_by", + to=settings.AUTH_USER_MODEL, + ), ), ] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0330_auto_20221220_2312.py index 53e3038ed7..b0808c90ea 100644 --- a/care/facility/migrations/0330_auto_20221220_2312.py +++ b/care/facility/migrations/0330_auto_20221220_2312.py @@ -6,22 +6,31 @@ class Migration(migrations.Migration): dependencies = [ - ('abdm', '0002_auto_20221220_2312'), - ('facility', '0329_auto_20221219_1936'), + ("abdm", "0002_auto_20221220_2312"), + ("facility", "0329_auto_20221219_1936"), ] operations = [ migrations.AddField( - model_name='historicalpatientregistration', - name='abha_number', - field=models.ForeignKey(blank=True, db_constraint=False, null=True, - on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', - to='abdm.AbhaNumber'), + model_name="historicalpatientregistration", + name="abha_number", + field=models.ForeignKey( + blank=True, + db_constraint=False, + null=True, + on_delete=django.db.models.deletion.DO_NOTHING, + related_name="+", + to="abdm.AbhaNumber", + ), ), migrations.AddField( - model_name='patientregistration', - name='abha_number', - field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to='abdm.AbhaNumber'), + model_name="patientregistration", + name="abha_number", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="abdm.AbhaNumber", + ), ), ] diff --git a/care/facility/migrations/0331_auto_20230130_1652.py b/care/facility/migrations/0331_auto_20230130_1652.py index 7b96181775..334743b8c0 100644 --- a/care/facility/migrations/0331_auto_20230130_1652.py +++ b/care/facility/migrations/0331_auto_20230130_1652.py @@ -4,15 +4,14 @@ class Migration(migrations.Migration): - dependencies = [ - ('facility', '0330_auto_20221220_2312'), + ("facility", "0330_auto_20221220_2312"), ] operations = [ migrations.AlterField( - model_name='patientsearch', - name='state_id', + model_name="patientsearch", + name="state_id", field=models.IntegerField(null=True), ), ] diff --git a/care/facility/migrations/0344_merge_20230413_1120.py b/care/facility/migrations/0344_merge_20230413_1120.py index 96acc3ff38..79bc41b1d5 100644 --- a/care/facility/migrations/0344_merge_20230413_1120.py +++ b/care/facility/migrations/0344_merge_20230413_1120.py @@ -4,11 +4,9 @@ class Migration(migrations.Migration): - dependencies = [ - ('facility', '0343_auto_20230407_1850'), - ('facility', '0331_auto_20230130_1652'), + ("facility", "0343_auto_20230407_1850"), + ("facility", "0331_auto_20230130_1652"), ] - operations = [ - ] + operations = [] From c580e4b77c594bb997abef569e3ffe4cbc3a8025 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 21 Jun 2023 08:42:19 +0530 Subject: [PATCH 105/180] set debug to True --- 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 18b50651bf..e8f38e7732 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -29,7 +29,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", False) +DEBUG = env.bool("DJANGO_DEBUG", True) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. From 87d5e583386363cef73048aa7123ab2aa7b28d2f Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 21 Jun 2023 10:13:38 +0530 Subject: [PATCH 106/180] added check_and_generate_mobile_otp --- care/abdm/api/viewsets/healthid.py | 2 +- care/abdm/utils/api_call.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 9a75bcda5e..dc7f191e8d 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -626,7 +626,7 @@ def confirm_with_demographics(self, request): ############################################################################################################ # HealthID V2 APIs @swagger_auto_schema( - # /v1/registration/aadhaar/checkAndGenerateMobileOTP + # /v2/registration/aadhaar/checkAndGenerateMobileOTP operation_id="check_and_generate_mobile_otp", request_body=GenerateMobileOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 201bc47854..a84e5ff258 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -161,8 +161,13 @@ def verify_aadhaar_otp(self, data): response = self.api.post(path, data) return response.json() + def check_and_generate_mobile_otp(self, data): + path = "/v2/registration/aadhaar/checkAndGenerateMobileOTP" + response = self.api.post(path, data) + return response.json() + def generate_mobile_otp(self, data): - path = "/v1/registration/aadhaar/generateMobileOTP" + path = "/v2/registration/aadhaar/generateMobileOTP" response = self.api.post(path, data) return response.json() From bffc433ff3f919dbb5c333efc760c44e6c88c67c Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 09:11:45 +0530 Subject: [PATCH 107/180] added validation in link_via_qr --- care/abdm/api/viewsets/healthid.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index dc7f191e8d..0ee8287f7e 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -302,13 +302,13 @@ def link_via_qr(self, request): patient = PatientRegistration.objects.filter( abha_number__abha_number=data["hidn"] ).first() - # if patient: - # return Response( - # { - # "message": "A patient is already associated with the provided Abha Number" - # }, - # status=status.HTTP_400_BAD_REQUEST, - # ) + if patient: + return Response( + { + "message": "A patient is already associated with the provided Abha Number" + }, + status=status.HTTP_400_BAD_REQUEST, + ) if ( "facilityId" not in data From ea1167fadb850bbb93f3e1609fc0852ad0d3a817 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 09:27:31 +0530 Subject: [PATCH 108/180] fixed a typo in link_via_qr --- care/abdm/api/viewsets/healthid.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 0ee8287f7e..b05fe53074 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -287,7 +287,7 @@ def search_by_health_id(self, request): def link_via_qr(self, request): data = request.data - if ratelimit(request, "link_via_qr", [data["hdin"]], increment=False): + if ratelimit(request, "link_via_qr", [data["hidn"]], increment=False): raise CaptchaRequiredException( detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, code=status.HTTP_429_TOO_MANY_REQUESTS, @@ -320,7 +320,7 @@ def link_via_qr(self, request): ) if not HealthIdGateway().verify_demographics( - data["phr"] or data["hdin"], + data["phr"] or data["hidn"], data["name"], data["gender"], str(dob.year), From e623cd465b311e5c7947e4e51c820c9edcefd3e6 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 10:23:09 +0530 Subject: [PATCH 109/180] added a patientId is None validation in link_via_qr --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b05fe53074..8b97de5d11 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -298,7 +298,7 @@ def link_via_qr(self, request): dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - if "patientId" not in data: + if "patientId" not in data or data["patientId"] is None: patient = PatientRegistration.objects.filter( abha_number__abha_number=data["hidn"] ).first() From 6e9a40b7f118f3599120c35b55dda07474013eb3 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 22 Jun 2023 12:44:20 +0530 Subject: [PATCH 110/180] return error message in create_health_id --- care/abdm/api/viewsets/healthid.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 8b97de5d11..f80e8d5270 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -219,6 +219,12 @@ def create_health_id(self, request): serializer.is_valid(raise_exception=True) abha_profile = HealthIdGateway().create_health_id(data) + if "token" not in abha_profile: + return Response( + abha_profile, + status=status.HTTP_400_BAD_REQUEST, + ) + # have a serializer to verify data of abha_profile abha_object = self.create_abha( abha_profile, From c715768c07af388395eee5477340a94516f8ef0d Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 28 Jun 2023 12:09:45 +0530 Subject: [PATCH 111/180] removed conflicting imports --- requirements/local.txt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/requirements/local.txt b/requirements/local.txt index 54a0308b01..cd49334034 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -5,12 +5,6 @@ ipdb==0.13.2 # https://github.com/gotcha/ipdb psycopg2-binary==2.8.6 # https://github.com/psycopg/psycopg2 # Code quality # ------------------------------------------------------------------------------ -isort==5.11.5 # https://github.com/PyCQA/isort -flake8==4.0.1 # https://github.com/PyCQA/flake8 -flake8-isort==4.1.1 # https://github.com/gforcada/flake8-isort -black==22.3.0 # https://github.com/ambv/black -pre-commit==2.19.0 # https://github.com/pre-commit/pre-commit - Werkzeug==2.3.6 # https://werkzeug.palletsprojects.com/en/latest/changes/ # Django From fb19c805343d98df4cffee6aee5c752e6ba46f20 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 28 Jun 2023 13:56:49 +0530 Subject: [PATCH 112/180] fixed issues related to django4.2 upgrade --- care/abdm/api/viewsets/healthid.py | 57 +++++++++++++++--------------- care/abdm/apps.py | 8 +---- config/api_router.py | 2 +- 3 files changed, 30 insertions(+), 37 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index f80e8d5270..4b2b6ef39b 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -2,7 +2,7 @@ from datetime import datetime -from drf_yasg.utils import swagger_auto_schema +from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import ValidationError @@ -38,10 +38,9 @@ class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): model = AbhaNumber permission_classes = (IsAuthenticated,) - # TODO: Ratelimiting for all endpoints that generate OTP's / Critical API's - @swagger_auto_schema( + @extend_schema( operation_id="generate_aadhaar_otp", - request_body=AadharOtpGenerateRequestPayloadSerializer, + request=AadharOtpGenerateRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -60,10 +59,10 @@ def generate_aadhaar_otp(self, request): response = HealthIdGateway().generate_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/resendAadhaarOtp operation_id="resend_aadhaar_otp", - request_body=AadharOtpResendRequestPayloadSerializer, + request=AadharOtpResendRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -82,10 +81,10 @@ def resend_aadhaar_otp(self, request): response = HealthIdGateway().resend_aadhaar_otp(data) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/verifyAadhaarOtp operation_id="verify_aadhaar_otp", - request_body=VerifyOtpRequestPayloadSerializer, + request=VerifyOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -106,10 +105,10 @@ def verify_aadhaar_otp(self, request): ) # HealthIdGatewayV2().verify_document_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/generateMobileOTP operation_id="generate_mobile_otp", - request_body=GenerateMobileOtpRequestPayloadSerializer, + request=GenerateMobileOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -128,10 +127,10 @@ def generate_mobile_otp(self, request): response = HealthIdGateway().generate_mobile_otp(data) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/verifyMobileOTP operation_id="verify_mobile_otp", - request_body=VerifyOtpRequestPayloadSerializer, + request=VerifyOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -198,10 +197,10 @@ def add_abha_details_to_patient(self, abha_object, patient_object): patient_object.save() return True - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/createHealthId operation_id="create_health_id", - request_body=CreateHealthIdSerializer, + request=CreateHealthIdSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -258,10 +257,10 @@ def create_health_id(self, request): # APIs to Find & Link Existing HealthID # searchByHealthId - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/searchByHealthId operation_id="search_by_health_id", - request_body=HealthIdSerializer, + request=HealthIdSerializer, responses={"200": "{'status': 'boolean'}"}, tags=["ABDM HealthID"], ) @@ -282,10 +281,10 @@ def search_by_health_id(self, request): response = HealthIdGateway().search_by_health_id(data) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( + @extend_schema( # /v1/registration/aadhaar/searchByHealthId operation_id="link_via_qr", - request_body=HealthIdSerializer, + request=HealthIdSerializer, responses={"200": "{'status': 'boolean'}"}, tags=["ABDM HealthID"], ) @@ -396,7 +395,7 @@ def link_via_qr(self, request): patient_serialized = PatientDetailSerializer(patient).data return Response(patient_serialized, status=status.HTTP_200_OK) - @swagger_auto_schema( + @extend_schema( operation_id="get_new_linking_token", responses={"200": "{'status': 'boolean'}"}, tags=["ABDM HealthID"], @@ -480,10 +479,10 @@ def patient_sms_notify(self, request, *args, **kwargs): return Response(response, status=status.HTTP_202_ACCEPTED) # auth/init - @swagger_auto_schema( + @extend_schema( # /v1/auth/init operation_id="auth_init", - request_body=HealthIdAuthSerializer, + request=HealthIdAuthSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -503,9 +502,9 @@ def auth_init(self, request): return Response(response, status=status.HTTP_200_OK) # /v1/auth/confirmWithAadhaarOtp - @swagger_auto_schema( + @extend_schema( operation_id="confirm_with_aadhaar_otp", - request_body=VerifyOtpRequestPayloadSerializer, + request=VerifyOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -556,9 +555,9 @@ def confirm_with_aadhaar_otp(self, request): ) # /v1/auth/confirmWithMobileOtp - @swagger_auto_schema( + @extend_schema( operation_id="confirm_with_mobile_otp", - request_body=VerifyOtpRequestPayloadSerializer, + request=VerifyOtpRequestPayloadSerializer, # responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID"], ) @@ -608,9 +607,9 @@ def confirm_with_mobile_otp(self, request): status=status.HTTP_200_OK, ) - @swagger_auto_schema( + @extend_schema( operation_id="confirm_with_demographics", - request_body=VerifyDemographicsRequestPayloadSerializer, + request=VerifyDemographicsRequestPayloadSerializer, responses={"200": "{'status': true}"}, tags=["ABDM HealthID"], ) @@ -631,10 +630,10 @@ def confirm_with_demographics(self, request): ############################################################################################################ # HealthID V2 APIs - @swagger_auto_schema( + @extend_schema( # /v2/registration/aadhaar/checkAndGenerateMobileOTP operation_id="check_and_generate_mobile_otp", - request_body=GenerateMobileOtpRequestPayloadSerializer, + request=GenerateMobileOtpRequestPayloadSerializer, responses={"200": "{'txnId': 'string'}"}, tags=["ABDM HealthID V2"], ) diff --git a/care/abdm/apps.py b/care/abdm/apps.py index f00da32cb9..54e278d631 100644 --- a/care/abdm/apps.py +++ b/care/abdm/apps.py @@ -3,11 +3,5 @@ class AbdmConfig(AppConfig): - name = "abdm" + name = "care.abdm" verbose_name = _("ABDM Integration") - - def ready(self): - try: - import care.abdm.signals # noqa F401 - except ImportError: - pass diff --git a/config/api_router.py b/config/api_router.py index 1b1ddaa081..e1883b46d2 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -238,5 +238,5 @@ def __init__(self): ] abdm_urlpatterns = [ - url(r"^", include(abdm_router.urls)), + path("", include(abdm_router.urls)), ] From 5d581cb8c1325d898f47736d53815a6d67e0a018 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 28 Jun 2023 14:33:10 +0530 Subject: [PATCH 113/180] fix migrations --- .../migrations/0007_alter_abhanumber_id.py | 19 +++++++++++++++++++ .../migrations/0003_auto_20230614_1048.py | 4 ++++ .../migrations/0331_auto_20230130_1652.py | 17 ----------------- .../migrations/0344_merge_20230413_1120.py | 12 ------------ .../migrations/0361_merge_20230609_2014.py | 12 ------------ .../migrations/0366_merge_20230628_1428.py | 12 ++++++++++++ 6 files changed, 35 insertions(+), 41 deletions(-) create mode 100644 care/abdm/migrations/0007_alter_abhanumber_id.py delete mode 100644 care/facility/migrations/0331_auto_20230130_1652.py delete mode 100644 care/facility/migrations/0344_merge_20230413_1120.py delete mode 100644 care/facility/migrations/0361_merge_20230609_2014.py create mode 100644 care/facility/migrations/0366_merge_20230628_1428.py diff --git a/care/abdm/migrations/0007_alter_abhanumber_id.py b/care/abdm/migrations/0007_alter_abhanumber_id.py new file mode 100644 index 0000000000..ef9ec6422c --- /dev/null +++ b/care/abdm/migrations/0007_alter_abhanumber_id.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.2 on 2023-06-28 09:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("abdm", "0006_auto_20230208_0915"), + ] + + operations = [ + migrations.AlterField( + model_name="abhanumber", + name="id", + field=models.BigAutoField( + auto_created=True, primary_key=True, serialize=False, verbose_name="ID" + ), + ), + ] diff --git a/care/facility/migrations/0003_auto_20230614_1048.py b/care/facility/migrations/0003_auto_20230614_1048.py index ec6a7f9d74..f3d516a7da 100644 --- a/care/facility/migrations/0003_auto_20230614_1048.py +++ b/care/facility/migrations/0003_auto_20230614_1048.py @@ -8,6 +8,10 @@ class Migration(migrations.Migration): ("facility", "0002_auto_20230613_1657"), ] + replaces = [ + ("facility", "0331_auto_20230130_1652"), + ] + operations = [ migrations.RemoveField( model_name="patientregistration", diff --git a/care/facility/migrations/0331_auto_20230130_1652.py b/care/facility/migrations/0331_auto_20230130_1652.py deleted file mode 100644 index 334743b8c0..0000000000 --- a/care/facility/migrations/0331_auto_20230130_1652.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 2.2.11 on 2023-01-30 11:22 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0330_auto_20221220_2312"), - ] - - operations = [ - migrations.AlterField( - model_name="patientsearch", - name="state_id", - field=models.IntegerField(null=True), - ), - ] diff --git a/care/facility/migrations/0344_merge_20230413_1120.py b/care/facility/migrations/0344_merge_20230413_1120.py deleted file mode 100644 index 79bc41b1d5..0000000000 --- a/care/facility/migrations/0344_merge_20230413_1120.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 2.2.11 on 2023-04-13 05:50 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0343_auto_20230407_1850"), - ("facility", "0331_auto_20230130_1652"), - ] - - operations = [] diff --git a/care/facility/migrations/0361_merge_20230609_2014.py b/care/facility/migrations/0361_merge_20230609_2014.py deleted file mode 100644 index 66a67446c3..0000000000 --- a/care/facility/migrations/0361_merge_20230609_2014.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 2.2.11 on 2023-06-09 14:44 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0360_auto_20230608_1750"), - ("facility", "0344_merge_20230413_1120"), - ] - - operations = [] diff --git a/care/facility/migrations/0366_merge_20230628_1428.py b/care/facility/migrations/0366_merge_20230628_1428.py new file mode 100644 index 0000000000..a8607a065d --- /dev/null +++ b/care/facility/migrations/0366_merge_20230628_1428.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.2 on 2023-06-28 08:58 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0330_auto_20221220_2312"), + ("facility", "0365_merge_20230626_1834"), + ] + + operations = [] From 42a978a34bc1a07cbc72d45ea0487644120981d2 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 28 Jun 2023 14:34:01 +0530 Subject: [PATCH 114/180] revert unwanted changes --- requirements/local.txt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/requirements/local.txt b/requirements/local.txt index cd49334034..13ad266042 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,10 +1,6 @@ -r ./base.txt -r ./docs.txt -ipdb==0.13.2 # https://github.com/gotcha/ipdb -psycopg2-binary==2.8.6 # https://github.com/psycopg/psycopg2 -# Code quality -# ------------------------------------------------------------------------------ Werkzeug==2.3.6 # https://werkzeug.palletsprojects.com/en/latest/changes/ # Django From 89d3cebdca02b8fc6e4d64697cb1c47ff7b58d88 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 07:53:32 +0530 Subject: [PATCH 115/180] added direct auth --- care/abdm/api/viewsets/auth.py | 19 +++++++++++++++++++ care/abdm/api/viewsets/healthid.py | 9 +++++---- care/abdm/utils/api_call.py | 24 +++++++++++++++++++++++- config/urls.py | 6 ++++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 892b608e01..4b8e49389e 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -72,6 +72,25 @@ def post(self, request, *args, **kwargs): return Response({}, status=status.HTTP_202_ACCEPTED) +class AuthNotifyView(GenericAPIView): + permission_classes = (AllowAny,) + authentication_classes = [ABDMAuthentication] + + def post(self, request, *args, **kwargs): + data = request.data + print(data) + + if data["auth"]["status"] != "GRANTED": + return + + AbdmGateway.auth_on_notify({"request_id": data["auth"]["transactionId"]}) + + # AbdmGateway().add_care_context( + # data["auth"]["accessToken"], + # data["resp"]["requestId"], + # ) + + class OnAddContextsView(GenericAPIView): permission_classes = (AllowAny,) authentication_classes = [ABDMAuthentication] diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index f80e8d5270..e3d0b33711 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -447,10 +447,11 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { "healthId": consultation.patient.abha_number.abha_number, - "name": consultation.patient.abha_number.name, - "gender": consultation.patient.abha_number.gender, - "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), - "consultationId": consultation_id, + # "name": consultation.patient.abha_number.name, + # "gender": consultation.patient.abha_number.gender, + # "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + # "consultationId": consultation_id, + "authMode": "DIRECT", "purpose": "LINK", } ) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index a84e5ff258..3b5a9523c4 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -391,6 +391,10 @@ def fetch_modes(self, data): """ self.temp_memory[request_id] = data + if "authMode" in data and data["authMode"] == "DIRECT": + self.init(request_id) + return + payload = { "requestId": request_id, "timestamp": str( @@ -426,7 +430,7 @@ def init(self, prev_request_id): "query": { "id": data["healthId"], "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "authMode": "DEMOGRAPHICS", + "authMode": data["authMode"] if "authMode" in data else "DEMOGRAPHICS", "requester": {"type": "HIP", "id": self.hip_id}, }, } @@ -467,6 +471,24 @@ def confirm(self, transaction_id, prev_request_id): response = self.api.post(path, payload, None, additional_headers) return response + def auth_on_notify(self, data): + path = "/v0.5/links/link/on-init" + additional_headers = {"X-CM-ID": settings.X_CM_ID} + + request_id = str(uuid.uuid4()) + payload = { + "requestId": request_id, + "timestamp": str( + datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + ), + "acknowledgement": {"status": "OK"}, + # "error": {"code": 1000, "message": "string"}, + "resp": {"requestId": data["request_id"]}, + } + + response = self.api.post(path, payload, None, additional_headers) + return response + # TODO: make it dynamic and call it at discharge (call it from on_confirm) def add_contexts(self, data): path = "/v0.5/links/link/add-contexts" diff --git a/config/urls.py b/config/urls.py index 0e325edc4a..0d891e1a37 100644 --- a/config/urls.py +++ b/config/urls.py @@ -10,6 +10,7 @@ from rest_framework_simplejwt.views import TokenVerifyView from care.abdm.api.viewsets.auth import ( + AuthNotifyView, DiscoverView, LinkConfirmView, LinkInitView, @@ -102,6 +103,11 @@ OnConfirmView.as_view(), name="abdm_on_confirm_view", ), + path( + "v0.5/users/auth/notify", + AuthNotifyView.as_view(), + name="abdm_auth_notify_view", + ), path( "v0.5/links/link/on-add-contexts", OnAddContextsView.as_view(), From 24dd683c46301e835687b4fb6bd0740db1c39612 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:02:22 +0530 Subject: [PATCH 116/180] resolve dependency issue in fhir --- requirements/base.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements/base.txt b/requirements/base.txt index bff77ed5ab..c3a351f815 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -70,5 +70,6 @@ pycryptodome==3.16.0 pycryptodomex==3.16.0 # HCX fhir.resources==6.5.0 +pydantic==1.* jwcrypto==1.4.2 requests==2.31.0 From 6f91d09aeb1f06e1daf653a1ebe6f1880d4e28b1 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:33:11 +0530 Subject: [PATCH 117/180] debug --- care/abdm/utils/api_call.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 3b5a9523c4..8ef9c39e66 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -395,6 +395,8 @@ def fetch_modes(self, data): self.init(request_id) return + print("auth-init", data) + payload = { "requestId": request_id, "timestamp": str( From a0ddbb03f8fb6a04112fe9b0ca34e1c092cca3ee Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:36:41 +0530 Subject: [PATCH 118/180] debug --- care/abdm/utils/api_call.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 8ef9c39e66..ca5b24b218 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -395,8 +395,6 @@ def fetch_modes(self, data): self.init(request_id) return - print("auth-init", data) - payload = { "requestId": request_id, "timestamp": str( @@ -424,6 +422,8 @@ def init(self, prev_request_id): data = self.temp_memory[prev_request_id] self.temp_memory[request_id] = data + print("auth-init", data) + payload = { "requestId": request_id, "timestamp": str( From 6095d2c52cf825d5781c9ab57492ffb6f21b8ff5 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:44:54 +0530 Subject: [PATCH 119/180] send health id for direct auth --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e3d0b33711..abeb91de21 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -446,7 +446,7 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { - "healthId": consultation.patient.abha_number.abha_number, + "healthId": consultation.patient.abha_number.health_id, # "name": consultation.patient.abha_number.name, # "gender": consultation.patient.abha_number.gender, # "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), From d7ded21a1f41deab6d3d71def3c534944abda6b5 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 08:55:41 +0530 Subject: [PATCH 120/180] fix error in oninit on direct auth --- care/abdm/api/viewsets/auth.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 4b8e49389e..80f98ef537 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -34,7 +34,12 @@ class OnInitView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data print("on-init", data) - AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) + + if data["auth"]["transactionId"]["mode"] != "DIRECT": + AbdmGateway().confirm( + data["auth"]["transactionId"], data["resp"]["requestId"] + ) + return Response({}, status=status.HTTP_202_ACCEPTED) @@ -78,7 +83,7 @@ class AuthNotifyView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print(data) + print("auth-notify", data) if data["auth"]["status"] != "GRANTED": return From def4b39927fd160e863296f99f3ef4ad6c323d98 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 09:02:27 +0530 Subject: [PATCH 121/180] fixed a typo --- care/abdm/api/viewsets/auth.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 80f98ef537..2903b6d73a 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -35,7 +35,7 @@ def post(self, request, *args, **kwargs): data = request.data print("on-init", data) - if data["auth"]["transactionId"]["mode"] != "DIRECT": + if data["auth"]["mode"] != "DIRECT": AbdmGateway().confirm( data["auth"]["transactionId"], data["resp"]["requestId"] ) From fa3075826cd1473038dd704f9386646ae8f4fd1d Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 09:10:39 +0530 Subject: [PATCH 122/180] do confirm to direct auth --- care/abdm/api/viewsets/auth.py | 5 +---- care/abdm/api/viewsets/healthid.py | 8 ++++---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 2903b6d73a..d0ee48d36d 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -35,10 +35,7 @@ def post(self, request, *args, **kwargs): data = request.data print("on-init", data) - if data["auth"]["mode"] != "DIRECT": - AbdmGateway().confirm( - data["auth"]["transactionId"], data["resp"]["requestId"] - ) + AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index abeb91de21..b8a242c00b 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -447,10 +447,10 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { "healthId": consultation.patient.abha_number.health_id, - # "name": consultation.patient.abha_number.name, - # "gender": consultation.patient.abha_number.gender, - # "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), - # "consultationId": consultation_id, + "name": consultation.patient.abha_number.name, + "gender": consultation.patient.abha_number.gender, + "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "consultationId": consultation_id, "authMode": "DIRECT", "purpose": "LINK", } From 767c419ad319c92eb8dbdb73663230b97113295e Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 09:23:38 +0530 Subject: [PATCH 123/180] reverted back to demographics --- care/abdm/api/viewsets/healthid.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index b8a242c00b..ca06b938c7 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -447,11 +447,17 @@ def add_care_context(self, request, *args, **kwargs): AbdmGateway().fetch_modes( { "healthId": consultation.patient.abha_number.health_id, - "name": consultation.patient.abha_number.name, - "gender": consultation.patient.abha_number.gender, - "dateOfBirth": str(consultation.patient.abha_number.date_of_birth), + "name": request.data["name"] + if "name" in request.data + else consultation.patient.abha_number.name, + "gender": request.data["gender"] + if "gender" in request.data + else consultation.patient.abha_number.gender, + "dateOfBirth": request.data["dob"] + if "dob" in request.data + else str(consultation.patient.abha_number.date_of_birth), "consultationId": consultation_id, - "authMode": "DIRECT", + # "authMode": "DIRECT", "purpose": "LINK", } ) From a94442c70bf0a81825693117db89ad670d0a0aa3 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 11:03:31 +0530 Subject: [PATCH 124/180] send healthId --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ca5b24b218..f544410c33 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -181,7 +181,7 @@ def verify_mobile_otp(self, data): def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" print("Creating Health ID with data: {}".format(data)) - data.pop("healthId", None) + # data.pop("healthId", None) response = self.api.post(path, data) return response.json() From 30ddd72b1411bf0e779678a5f783e0e9cf1c9440 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 12:24:15 +0530 Subject: [PATCH 125/180] remove abha address --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index f544410c33..ca5b24b218 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -181,7 +181,7 @@ def verify_mobile_otp(self, data): def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" print("Creating Health ID with data: {}".format(data)) - # data.pop("healthId", None) + data.pop("healthId", None) response = self.api.post(path, data) return response.json() From 92a4548f3e9c1cb5ba98935b6e3b3917dc03520c Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 13:49:09 +0530 Subject: [PATCH 126/180] debug data --- care/abdm/api/viewsets/auth.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index d0ee48d36d..87c16ca750 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -335,6 +335,10 @@ def post(self, request, *args, **kwargs): } ) + print("______________________________________________") + print(consent["notification"]["consentDetail"]["careContexts"][:-2:-1]) + print("______________________________________________") + AbdmGateway().data_notify( { "consent_id": data["hiRequest"]["consent"]["id"], From c7c326d70e5d10435f390ff69a7a79e795c32b29 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 14:09:13 +0530 Subject: [PATCH 127/180] hardcode consultation id for abdm data --- care/abdm/api/viewsets/auth.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 87c16ca750..7ca0af9519 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -305,9 +305,8 @@ def post(self, request, *args, **kwargs): "data": cipher.encrypt( Fhir( PatientConsultation.objects.get( - external_id=context[ - "careContextReference" - ] + external_id="6ddf9a81-e6f6-47c2-a2d0-63f64f66fcfd" + or context["careContextReference"] ) ).create_record(record) )["data"], From 70ad08b655183b24c4439e1d9c8cc8d3f22f61ce Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Tue, 4 Jul 2023 14:18:07 +0530 Subject: [PATCH 128/180] removed hard coding --- care/abdm/api/viewsets/auth.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7ca0af9519..87c16ca750 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -305,8 +305,9 @@ def post(self, request, *args, **kwargs): "data": cipher.encrypt( Fhir( PatientConsultation.objects.get( - external_id="6ddf9a81-e6f6-47c2-a2d0-63f64f66fcfd" - or context["careContextReference"] + external_id=context[ + "careContextReference" + ] ) ).create_record(record) )["data"], From 0be66c97f63f18e338cd44bb23a9a7b7440bb7d7 Mon Sep 17 00:00:00 2001 From: Mathew Date: Tue, 4 Jul 2023 17:53:48 +0530 Subject: [PATCH 129/180] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 33de64a0b2..8881bd107a 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -28,7 +28,7 @@ jobs: type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} flavor: | - latest=true + latest=false - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 From b96cf47a06cd500d03503346f99ef72a22d126e4 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 5 Jul 2023 09:37:29 +0530 Subject: [PATCH 130/180] added support for abha card --- care/abdm/api/viewsets/healthid.py | 23 +++++++++++++++++++++++ care/abdm/utils/api_call.py | 7 +++++++ 2 files changed, 30 insertions(+) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index ca06b938c7..68fed4e250 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -282,6 +282,29 @@ def search_by_health_id(self, request): response = HealthIdGateway().search_by_health_id(data) return Response(response, status=status.HTTP_200_OK) + @action(detail=False, methods=["post"]) + def get_abha_card(self, request): + data = request.data + + if ratelimit(request, "get_abha_card", [data["patient"]], increment=False): + raise CaptchaRequiredException( + detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, + code=status.HTTP_429_TOO_MANY_REQUESTS, + ) + + allowed_patients = get_patient_queryset(request.user) + patient = allowed_patients.filter(external_id=data["patient"]).first() + if not patient: + raise ValidationError({"patient": "Not Found"}) + + if not patient.abha_number: + raise ValidationError({"abha": "Patient hasn't linked thier abha"}) + + response = HealthIdGateway().get_abha_card_png( + {"refreshToken": patient.abha_number.refresh_token} + ) + return Response(response, status=status.HTTP_200_OK) + @swagger_auto_schema( # /v1/registration/aadhaar/searchByHealthId operation_id="link_via_qr", diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index ca5b24b218..5124f44c6a 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -286,6 +286,13 @@ def get_profile(self, data): response = self.api.get(path, {}, access_token) return response.json() + # /v1/account/getPngCard + def get_abha_card_png(self, data): + path = "/v1/account/getPngCard" + access_token = self.generate_access_token(data) + response = self.api.get(path, {}, access_token) + return response.json() + # /v1/account/qrCode def get_qr_code(self, data, auth): path = "/v1/account/qrCode" From 1ffe7b8daa633f8657680d367176b393f03bccd6 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 5 Jul 2023 10:11:27 +0530 Subject: [PATCH 131/180] store refresh token while abha creation --- care/abdm/api/viewsets/healthid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 68fed4e250..2f1c2c6164 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -231,7 +231,7 @@ def create_health_id(self, request): { "txn_id": data["txnId"], "access_token": abha_profile["token"], - "refresh_token": None, + "refresh_token": abha_profile["refreshToken"], }, ) From 5af1584256ea0091e6fa36685b25fe6f2f8a871f Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 5 Jul 2023 10:21:20 +0530 Subject: [PATCH 132/180] get_abha_card_png return original response --- care/abdm/utils/api_call.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 5124f44c6a..c041f051bc 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -291,7 +291,7 @@ def get_abha_card_png(self, data): path = "/v1/account/getPngCard" access_token = self.generate_access_token(data) response = self.api.get(path, {}, access_token) - return response.json() + return response # /v1/account/qrCode def get_qr_code(self, data, auth): From 5205e18cc39517b0a1169b0397678f1ee38ae96b Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 5 Jul 2023 07:00:55 +0000 Subject: [PATCH 133/180] added pdf and png downloads for abha card --- care/abdm/api/viewsets/healthid.py | 13 ++++++++++--- care/abdm/utils/api_call.py | 11 ++++++++++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 2f1c2c6164..7f29fd85a3 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -300,10 +300,17 @@ def get_abha_card(self, request): if not patient.abha_number: raise ValidationError({"abha": "Patient hasn't linked thier abha"}) - response = HealthIdGateway().get_abha_card_png( - {"refreshToken": patient.abha_number.refresh_token} - ) + if data["type"] == "png": + response = HealthIdGateway().get_abha_card_png( + {"refreshToken": patient.abha_number.refresh_token} + ) + return Response(response, status=status.HTTP_200_OK) + + response = HealthIdGateway().get_abha_card_pdf( + {"refreshToken": patient.abha_number.refresh_token} + ) return Response(response, status=status.HTTP_200_OK) + @swagger_auto_schema( # /v1/registration/aadhaar/searchByHealthId diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index c041f051bc..53ce22b426 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -291,7 +291,16 @@ def get_abha_card_png(self, data): path = "/v1/account/getPngCard" access_token = self.generate_access_token(data) response = self.api.get(path, {}, access_token) - return response + + return b64encode(response.content) + + def get_abha_card_pdf(self, data): + path = "/v1/account/getCard" + access_token = self.generate_access_token(data) + response = self.api.get(path, {}, access_token) + + return b64encode(response.content) + # /v1/account/qrCode def get_qr_code(self, data, auth): From 53f9586b60c3addf14d5eb7eb96aba2011d80884 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 5 Jul 2023 07:23:23 +0000 Subject: [PATCH 134/180] send 401 when consent is denied in data request --- care/abdm/api/viewsets/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 87c16ca750..7d9305ec01 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -257,8 +257,8 @@ def post(self, request, *args, **kwargs): # TODO: uncomment later consent_id = data["hiRequest"]["consent"]["id"] consent = json.loads(cache.get(consent_id)) if consent_id in cache else None - # if not consent or not consent["notification"]["status"] == "GRANTED": - # return Response({}, status=status.HTTP_401_UNAUTHORIZED) + if not consent or not consent["notification"]["status"] == "GRANTED": + return Response({}, status=status.HTTP_401_UNAUTHORIZED) # TODO: check if from and to are in range and consent expiry is greater than today # consent_from = datetime.fromisoformat( From 42ed78889ca86e42180a5a813040354e043684ee Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Wed, 5 Jul 2023 09:39:07 +0000 Subject: [PATCH 135/180] Trigger Build From eb98ba0f6ef12caf06b6e47cb8b75d33fdec8381 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Tue, 11 Jul 2023 08:58:07 +0000 Subject: [PATCH 136/180] Medibase: Drop littletable; instead store as list from ORM's `values_list` (#1443) * custom in memory search * as tuple * fix tests * remove db queryset reference after loading to in-memory * minor optimization * remove commented code * load to in-mem using ORM's `values_list` * Update care/facility/api/viewsets/prescription.py Co-authored-by: Aakash Singh --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/prescription.py | 50 +++++++++------------- care/facility/static_data/medibase.py | 48 ++++++++++++--------- config/wsgi.py | 2 + 3 files changed, 50 insertions(+), 50 deletions(-) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index ff1dfaa3a1..e6007e72c3 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -1,5 +1,3 @@ -from re import IGNORECASE - from django.shortcuts import get_object_or_404 from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema @@ -140,36 +138,30 @@ class MedibaseViewSet(ViewSet): permission_classes = (IsAuthenticated,) def serailize_data(self, objects): - result = [] - for object in objects: - if type(object) == tuple: - object = object[0] - result.append( - { - "id": object.external_id, - "name": object.name, - "type": object.type, - "generic": object.generic, - "company": object.company, - "contents": object.contents, - "cims_class": object.cims_class, - "atc_classification": object.atc_classification, - } - ) - return result + return [ + { + "id": x[0], + "name": x[1], + "type": x[2], + "generic": x[3], + "company": x[4], + "contents": x[5], + "cims_class": x[6], + "atc_classification": x[7], + } + for x in objects + ] def sort(self, query, results): exact_matches = [] partial_matches = [] - for result in results: - if type(result) == tuple: - result = result[0] - words = result.searchable.lower().split() + for x in results: + words = f"{x[1]} {x[3]} {x[4]}".lower().split() if query in words: - exact_matches.append(result) + exact_matches.append(x) else: - partial_matches.append(result) + partial_matches.append(x) return exact_matches + partial_matches @@ -178,10 +170,8 @@ def list(self, request): queryset = MedibaseMedicineTable - if request.GET.get("query", False): - query = request.GET.get("query").strip().lower() - queryset = queryset.where( - searchable=queryset.re_match(r".*" + query + r".*", IGNORECASE) - ) + if query := request.query_params.get("query"): + query = query.strip().lower() + queryset = [x for x in queryset if query in f"{x[1]} {x[3]} {x[4]}".lower()] queryset = self.sort(query, queryset) return Response(self.serailize_data(queryset[:15])) diff --git a/care/facility/static_data/medibase.py b/care/facility/static_data/medibase.py index 946360635f..fb5718c3df 100644 --- a/care/facility/static_data/medibase.py +++ b/care/facility/static_data/medibase.py @@ -1,26 +1,34 @@ -from littletable import Table +from django.db.models import CharField, TextField, Value +from django.db.models.functions import Coalesce from care.facility.models.prescription import MedibaseMedicine -MedibaseMedicineTable = Table("MedibaseMedicine") -medibase_objects = MedibaseMedicine.objects.all() - -for obj in medibase_objects: - MedibaseMedicineTable.insert( - { - "id": obj.id, - "external_id": obj.external_id, - "name": obj.name, - "type": obj.type, - "generic": obj.generic or "", - "company": obj.company or "", - "contents": obj.contents or "", - "cims_class": obj.cims_class or "", - "atc_classification": obj.atc_classification or "", - "searchable": f"{obj.name} {obj.generic} {obj.company}", - } +def load_medibase_in_memory(): + return ( + MedibaseMedicine.objects.all() + .annotate( + generic_pretty=Coalesce("generic", Value(""), output_field=CharField()), + company_pretty=Coalesce("company", Value(""), output_field=CharField()), + contents_pretty=Coalesce("contents", Value(""), output_field=TextField()), + cims_class_pretty=Coalesce( + "cims_class", Value(""), output_field=CharField() + ), + atc_classification_pretty=Coalesce( + "atc_classification", Value(""), output_field=TextField() + ), + ) + .values_list( + "external_id", + "name", + "type", + "generic_pretty", + "company_pretty", + "contents_pretty", + "cims_class_pretty", + "atc_classification_pretty", + ) ) -MedibaseMedicineTable.create_index("id", unique=True) -MedibaseMedicineTable.create_search_index("searchable") + +MedibaseMedicineTable = load_medibase_in_memory() diff --git a/config/wsgi.py b/config/wsgi.py index 835d5c2ece..9827d177fd 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -36,3 +36,5 @@ # Apply WSGI middleware here. # from helloworld.wsgi import HelloWorldApplication # application = HelloWorldApplication(application) + +from care.facility.static_data.medibase import MedibaseMedicineTable # noqa From e9d6be920512d1ebaa0127c7545e86ea07da825a Mon Sep 17 00:00:00 2001 From: Mathew Date: Tue, 11 Jul 2023 16:38:07 +0530 Subject: [PATCH 137/180] Update celery.json --- aws/celery.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/aws/celery.json b/aws/celery.json index e79e4f66be..52cbb8b8f7 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -96,8 +96,8 @@ "repositoryCredentials": { "credentialsParameter": "arn:aws:secretsmanager:ap-south-1:299650323339:secret:/github/pat-UHmr0B" }, - "memory": 256, - "memoryReservation": 256, + "memory": 512, + "memoryReservation": 512, "workingDirectory": "/app", "secrets": [ { @@ -232,8 +232,8 @@ "/app/celery_worker-ecs.sh" ], "cpu": 384, - "memory": 768, - "memoryReservation": 768, + "memory": 1536, + "memoryReservation": 1536, "environment": [ { "name": "AUDIT_LOG_ENABLED", @@ -437,7 +437,7 @@ "name": "care-celery-worker" } ], - "memory": "1024", + "memory": "2048", "taskRoleArn": "arn:aws:iam::299650323339:role/ecsTaskExecutionRole", "family": "care-celery", "requiresCompatibilities": [ From 0f4486ae9346bcea36f9ff0789fae647a0451f4a Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Thu, 13 Jul 2023 17:52:37 +0000 Subject: [PATCH 138/180] Adds `chapter`, `root_block`, `root_category` columns to `meta_icd11_diagnosis` DB table (#1448) * add `root_label` column to `meta_icd11_diagnosis` db table * adds `chapter`, `root_block`, `root_category` * fix category being None --- .../commands/load_meta_icd11_diagnosis.py | 67 ++++++++++++++++++- ...371_metaicd11diagnosis_chapter_and_more.py | 28 ++++++++ care/facility/models/meta_icd11_diagnosis.py | 7 ++ 3 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py diff --git a/care/facility/management/commands/load_meta_icd11_diagnosis.py b/care/facility/management/commands/load_meta_icd11_diagnosis.py index 9a339df9bb..2895608657 100644 --- a/care/facility/management/commands/load_meta_icd11_diagnosis.py +++ b/care/facility/management/commands/load_meta_icd11_diagnosis.py @@ -13,10 +13,70 @@ class Command(BaseCommand): help = "Loads ICD11 data to a table in to database." + data = [] + roots_lookup = {} + """ + Eg: + ``` + { + "http://id.who.int/icd/entity/594985340": { + "chapter": "Certain infectious or parasitic diseases", + "block": "Intestinal infectious diseases", + "category": None, + }, + } + ``` + """ + + CLASS_KIND_DB_KEYS = { + "block": "root_block", + "category": "root_category", + } + + def find_roots(self, item): + id = item["ID"] + + if id in self.roots_lookup: + return self.roots_lookup[id] + + if not item["parentId"]: + self.roots_lookup[id] = {item["classKind"]: item["label"]} + return self.roots_lookup[id] + + if parent := self.roots_lookup.get(item["parentId"]): + + def my(x): + return item["label"] if item["classKind"] == x else None + + self.roots_lookup[id] = { + "chapter": parent.get("chapter") or my("chapter"), + "block": parent.get("block") or my("block"), + "category": parent.get("category") or my("category"), + } + return self.roots_lookup[id] + + # The following code is never executed as the `icd11.json` file is + # pre-sorted and hence the parent is always present before the child. + print("Full-scan for", id, item["label"]) + return self.find_roots( + [ + icd11_object + for icd11_object in self.data + if icd11_object["ID"] == item["parentId"] + ][0] + ) + def handle(self, *args, **options): - print("Loading ICD11 data to database...") + print("Loading ICD11 data to DB Table (meta_icd11_diagnosis)...") try: - icd11_objects = fetch_data() + self.data = fetch_data() + + def roots(item): + return { + self.CLASS_KIND_DB_KEYS.get(k, k): v + for k, v in self.find_roots(item).items() + } + MetaICD11Diagnosis.objects.all().delete() MetaICD11Diagnosis.objects.bulk_create( [ @@ -30,8 +90,9 @@ def handle(self, *args, **options): is_leaf=icd11_object["isLeaf"], label=icd11_object["label"], breadth_value=icd11_object["breadthValue"], + **roots(icd11_object), ) - for icd11_object in icd11_objects + for icd11_object in self.data if icd11_object["ID"].split("/")[-1].isnumeric() ] ) diff --git a/care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py b/care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py new file mode 100644 index 0000000000..342c2014d3 --- /dev/null +++ b/care/facility/migrations/0371_metaicd11diagnosis_chapter_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 4.2.2 on 2023-07-12 13:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0370_merge_20230705_1500"), + ] + + operations = [ + migrations.AddField( + model_name="metaicd11diagnosis", + name="chapter", + field=models.CharField(default="", max_length=255), + preserve_default=False, + ), + migrations.AddField( + model_name="metaicd11diagnosis", + name="root_block", + field=models.CharField(max_length=255, null=True), + ), + migrations.AddField( + model_name="metaicd11diagnosis", + name="root_category", + field=models.CharField(max_length=255, null=True), + ), + ] diff --git a/care/facility/models/meta_icd11_diagnosis.py b/care/facility/models/meta_icd11_diagnosis.py index d59cc4ebfd..a3ea15f5b5 100644 --- a/care/facility/models/meta_icd11_diagnosis.py +++ b/care/facility/models/meta_icd11_diagnosis.py @@ -2,6 +2,10 @@ class MetaICD11Diagnosis(models.Model): + """ + Not for production use. For Metabase purposes only. Do not build relations to this model. + """ + id = models.CharField(max_length=255, primary_key=True) _id = models.IntegerField() average_depth = models.IntegerField() @@ -11,6 +15,9 @@ class MetaICD11Diagnosis(models.Model): is_leaf = models.BooleanField() label = models.CharField(max_length=255) breadth_value = models.DecimalField(max_digits=24, decimal_places=22) + chapter = models.CharField(max_length=255) + root_block = models.CharField(max_length=255, null=True) + root_category = models.CharField(max_length=255, null=True) class Meta: db_table = "meta_icd11_diagnosis" From 2933234dec6706aae12b8c9fcfe5ebbd9e299a43 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 13 Jul 2023 23:26:30 +0530 Subject: [PATCH 139/180] Update branding from CoronaSafe Network to Open Healthcare Network (#1449) Rename Coronasafe to OHC --- care/facility/api/serializers/patient_otp.py | 2 +- care/templates/base.html | 2 +- care/templates/email/user_reset_password.html | 2 +- care/templates/email/user_reset_password.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/care/facility/api/serializers/patient_otp.py b/care/facility/api/serializers/patient_otp.py index 9d6951e883..7457ac8454 100644 --- a/care/facility/api/serializers/patient_otp.py +++ b/care/facility/api/serializers/patient_otp.py @@ -26,7 +26,7 @@ def send_sms(otp, phone_number): sendSMS( phone_number, ( - f"CoronaSafe Network Patient Management System Login, OTP is {otp} . " + f"Open Healthcare Network Patient Management System Login, OTP is {otp} . " "Please do not share this Confidential Login Token with anyone else" ), ) diff --git a/care/templates/base.html b/care/templates/base.html index 9270456239..53b13042d1 100644 --- a/care/templates/base.html +++ b/care/templates/base.html @@ -104,7 +104,7 @@ alt="Digital Public Goods logo" /> - CoronaSafe Network is an open-source digital public good designed by + Open Healthcare Network is an open-source digital public good designed by a multi-disciplinary team of innovators and volunteers who are working on a model to support Government efforts.  (Github) diff --git a/care/templates/email/user_reset_password.html b/care/templates/email/user_reset_password.html index 7c5e29a087..8eb0100c40 100644 --- a/care/templates/email/user_reset_password.html +++ b/care/templates/email/user_reset_password.html @@ -1,5 +1,5 @@ Hi, -Greetings from Coronasafe Network, +Greetings from Open Healthcare Network, Please click the following link to reset your password for your account with username {{username}} Click Here diff --git a/care/templates/email/user_reset_password.txt b/care/templates/email/user_reset_password.txt index df328882a5..2a2e8a703f 100644 --- a/care/templates/email/user_reset_password.txt +++ b/care/templates/email/user_reset_password.txt @@ -1,5 +1,5 @@ Hi, -Greetings from Coronasafe Network, +Greetings from Open Healthcare Network, Please click the following link to reset your password {{reset_password_url}} From 43a47ea91bdac47696b8145b2349415500943065 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 14 Jul 2023 07:09:27 +0000 Subject: [PATCH 140/180] Fixes string representation of `MedibaseMedicine` model (#1452) * fix repr of `MedibaseMedicine` * improve string repr * make it prettier :) --- care/facility/models/prescription.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index 93221302ad..b6aca21eab 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -64,7 +64,7 @@ class MedibaseMedicine(BaseModel): atc_classification = models.TextField(blank=True, null=True) def __str__(self): - return " - ".join([self.name, self.generic, self.company]) + return " - ".join(filter(None, [self.name, self.generic, self.company])) class Prescription(BaseModel): From 9f84bd4cba9512cc4dae2f6be46338c335f4f881 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 18 Jul 2023 19:37:55 +0530 Subject: [PATCH 141/180] Asset Tracking and Monitoring Enhancements (#1454) * Rename Coronasafe to OHC * Store asset uptime * Adapt to new API format * Add endpoint to fetch availability records * Merge migration conflict * Fix crash when running celery task for asset uptime * Update care/facility/models/asset.py Co-authored-by: Aakash Singh * Update care/facility/tasks/asset_monitor.py Co-authored-by: Aakash Singh * use IntegerChoices instead of Enum for assetstatus * Merge migrations * Switch to string values for AvailabilityRecord * use logging instead of print * Switch to string values * Add tests * fix tests * Remove print statement --------- Co-authored-by: Aakash Singh --- care/facility/api/serializers/asset.py | 10 ++ care/facility/api/viewsets/asset.py | 13 +++ .../0372_assetavailabilityrecord.py | 66 +++++++++++++ care/facility/models/asset.py | 35 +++++++ care/facility/tasks/__init__.py | 14 ++- care/facility/tasks/asset_monitor.py | 96 +++++++++++++++++++ care/facility/tests/test_asset_api.py | 20 ++-- .../tests/test_asset_availability_api.py | 57 +++++++++++ care/facility/tests/test_medibase_api.py | 16 ++-- .../tests/test_patient_daily_rounds_api.py | 2 +- care/users/tests/test_facility_user_create.py | 4 +- care/utils/assetintegration/base.py | 4 +- config/api_router.py | 2 + 13 files changed, 313 insertions(+), 26 deletions(-) create mode 100644 care/facility/migrations/0372_assetavailabilityrecord.py create mode 100644 care/facility/tasks/asset_monitor.py create mode 100644 care/facility/tests/test_asset_availability_api.py diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index fbb9f44970..7e672c4a01 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -18,6 +18,7 @@ from care.facility.api.serializers.facility import FacilityBareMinimumSerializer from care.facility.models.asset import ( Asset, + AssetAvailabilityRecord, AssetLocation, AssetTransaction, UserDefaultAssetLocation, @@ -165,6 +166,15 @@ class Meta: exclude = ("deleted", "external_id") +class AssetAvailabilitySerializer(ModelSerializer): + id = UUIDField(source="external_id", read_only=True) + asset = AssetBareMinimumSerializer(read_only=True) + + class Meta: + model = AssetAvailabilityRecord + exclude = ("deleted", "external_id") + + class UserDefaultAssetLocationSerializer(ModelSerializer): location_object = AssetLocationSerializer(source="location", read_only=True) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 57ec16da51..2b5fc6e6b1 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -23,6 +23,7 @@ from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.asset import ( + AssetAvailabilitySerializer, AssetLocationSerializer, AssetSerializer, AssetTransactionSerializer, @@ -32,6 +33,7 @@ ) from care.facility.models.asset import ( Asset, + AssetAvailabilityRecord, AssetLocation, AssetTransaction, UserDefaultAssetLocation, @@ -128,6 +130,17 @@ def retrieve(self, request, *args, **kwargs): return Response(hit) +class AssetAvailabilityFilter(filters.FilterSet): + external_id = filters.CharFilter(field_name="asset__external_id") + + +class AssetAvailabilityViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): + queryset = AssetAvailabilityRecord.objects.all().select_related("asset") + serializer_class = AssetAvailabilitySerializer + filter_backends = (filters.DjangoFilterBackend,) + filterset_class = AssetAvailabilityFilter + + class AssetViewSet( ListModelMixin, RetrieveModelMixin, diff --git a/care/facility/migrations/0372_assetavailabilityrecord.py b/care/facility/migrations/0372_assetavailabilityrecord.py new file mode 100644 index 0000000000..2d3dc2daf4 --- /dev/null +++ b/care/facility/migrations/0372_assetavailabilityrecord.py @@ -0,0 +1,66 @@ +# Generated by Django 4.2.2 on 2023-07-18 05:00 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0371_metaicd11diagnosis_chapter_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="AssetAvailabilityRecord", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ( + "status", + models.CharField( + choices=[ + ("Not Monitored", "Not Monitored"), + ("Operational", "Operational"), + ("Down", "Down"), + ("Under Maintenance", "Under Maintenance"), + ], + default="Not Monitored", + max_length=20, + ), + ), + ("timestamp", models.DateTimeField()), + ( + "asset", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, to="facility.asset" + ), + ), + ], + options={ + "ordering": ["-timestamp"], + "unique_together": {("asset", "timestamp")}, + }, + ), + ] diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index ac14fba3a0..fdeaff4263 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -17,6 +17,13 @@ def get_random_asset_id(): return str(uuid.uuid4()) +class AvailabilityStatus(models.TextChoices): + NOT_MONITORED = "Not Monitored" + OPERATIONAL = "Operational" + DOWN = "Down" + UNDER_MAINTENANCE = "Under Maintenance" + + class AssetLocation(BaseModel, AssetsPermissionMixin): """ This model is also used to store rooms that the assets are in, Since these rooms are mapped to @@ -105,6 +112,34 @@ def __str__(self): return self.name +class AssetAvailabilityRecord(BaseModel): + """ + Model to store the availability status of an asset at a particular timestamp. + + Fields: + - asset: ForeignKey to Asset model + - status: CharField with choices from AvailabilityStatus + - timestamp: DateTimeField to store the timestamp of the availability record + + Note: A pair of asset and timestamp together should be unique, not just the timestamp alone. + """ + + asset = models.ForeignKey(Asset, on_delete=models.PROTECT, null=False, blank=False) + status = models.CharField( + choices=AvailabilityStatus.choices, + default=AvailabilityStatus.NOT_MONITORED, + max_length=20, + ) + timestamp = models.DateTimeField(null=False, blank=False) + + class Meta: + unique_together = (("asset", "timestamp"),) + ordering = ["-timestamp"] + + def __str__(self): + return f"{self.asset.name} - {self.status} - {self.timestamp}" + + class UserDefaultAssetLocation(BaseModel): user = models.ForeignKey(User, on_delete=models.PROTECT, null=False, blank=False) location = models.ForeignKey( diff --git a/care/facility/tasks/__init__.py b/care/facility/tasks/__init__.py index 7ebf63cdaa..1a9383d32d 100644 --- a/care/facility/tasks/__init__.py +++ b/care/facility/tasks/__init__.py @@ -1,6 +1,7 @@ from celery import current_app from celery.schedules import crontab +from care.facility.tasks.asset_monitor import check_asset_status from care.facility.tasks.cleanup import delete_old_notifications from care.facility.tasks.summarisation import ( summarise_district_patient, @@ -19,12 +20,12 @@ def setup_periodic_tasks(sender, **kwargs): name="delete_old_notifications", ) sender.add_periodic_task( - crontab(hour="*/4", minute=59), + crontab(hour="*/4", minute="59"), summarise_triage.s(), name="summarise_triage", ) sender.add_periodic_task( - crontab(hour=23, minute=59), + crontab(hour="23", minute="59"), summarise_tests.s(), name="summarise_tests", ) @@ -34,12 +35,17 @@ def setup_periodic_tasks(sender, **kwargs): name="summarise_facility_capacity", ) sender.add_periodic_task( - crontab(hour="*/1", minute=59), + crontab(hour="*/1", minute="59"), summarise_patient.s(), name="summarise_patient", ) sender.add_periodic_task( - crontab(hour="*/1", minute=59), + crontab(hour="*/1", minute="59"), summarise_district_patient.s(), name="summarise_district_patient", ) + sender.add_periodic_task( + crontab(minute="*/30"), + check_asset_status.s(), + name="check_asset_status", + ) diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py new file mode 100644 index 0000000000..82a0bcd0ee --- /dev/null +++ b/care/facility/tasks/asset_monitor.py @@ -0,0 +1,96 @@ +import logging +from datetime import datetime +from typing import Any + +from celery import shared_task +from django.utils import timezone + +from care.facility.models.asset import ( + Asset, + AssetAvailabilityRecord, + AvailabilityStatus, +) +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.assetintegration.base import BaseAssetIntegration + +logger = logging.getLogger(__name__) + + +@shared_task +def check_asset_status(): + logger.info(f"Checking Asset Status: {timezone.now()}") + + assets = Asset.objects.all() + middleware_status_cache = {} + + for asset in assets: + if not asset.asset_class or not asset.meta.get("local_ip_address", None): + continue + try: + hostname = asset.meta.get( + "middleware_hostname", + asset.current_location.facility.middleware_address, + ) + result: Any = {} + + if hostname in middleware_status_cache: + result = middleware_status_cache[hostname] + else: + try: + asset_class: BaseAssetIntegration = AssetClasses[ + asset.asset_class + ].value( + { + **asset.meta, + "middleware_hostname": hostname, + } + ) + result = asset_class.api_get(asset_class.get_url("devices/status")) + middleware_status_cache[hostname] = result + except Exception: + logger.exception("Error in Asset Status Check - Fetching Status") + middleware_status_cache[hostname] = None + continue + + if not result: + continue + + new_status = None + for status_record in result: + if asset.meta.get("local_ip_address") in status_record.get( + "status", {} + ): + new_status = status_record["status"][ + asset.meta.get("local_ip_address") + ] + else: + new_status = "not_monitored" + + last_record = ( + AssetAvailabilityRecord.objects.filter(asset=asset) + .order_by("-timestamp") + .first() + ) + + if new_status == "up": + new_status = AvailabilityStatus.OPERATIONAL + elif new_status == "down": + new_status = AvailabilityStatus.DOWN + elif new_status == "maintenance": + new_status = AvailabilityStatus.UNDER_MAINTENANCE + else: + new_status = AvailabilityStatus.NOT_MONITORED + + if not last_record or ( + datetime.fromisoformat(status_record.get("time")) + > last_record.timestamp + and last_record.status != new_status.value + ): + AssetAvailabilityRecord.objects.create( + asset=asset, + status=new_status.value, + timestamp=status_record.get("time", timezone.now()), + ) + + except Exception: + logger.exception("Error in Asset Status Check") diff --git a/care/facility/tests/test_asset_api.py b/care/facility/tests/test_asset_api.py index dbb8c27d42..36cb493e49 100644 --- a/care/facility/tests/test_asset_api.py +++ b/care/facility/tests/test_asset_api.py @@ -11,17 +11,19 @@ class AssetViewSetTestCase(TestBase, TestClassMixin, APITestCase): asset_id = None - def setUp(self): - self.factory = APIRequestFactory() - state = self.create_state() - district = self.create_district(state=state) - self.user = self.create_user(district=district, username="test user") - facility = self.create_facility(district=district, user=self.user) - self.asset1_location = AssetLocation.objects.create( + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.factory = APIRequestFactory() + state = cls.create_state() + district = cls.create_district(state=state) + cls.user = cls.create_user(district=district, username="test user") + facility = cls.create_facility(district=district, user=cls.user) + cls.asset1_location = AssetLocation.objects.create( name="asset1 location", location_type=1, facility=facility ) - self.asset = Asset.objects.create( - name="Test Asset", current_location=self.asset1_location, asset_type=50 + cls.asset = Asset.objects.create( + name="Test Asset", current_location=cls.asset1_location, asset_type=50 ) def test_list_assets(self): diff --git a/care/facility/tests/test_asset_availability_api.py b/care/facility/tests/test_asset_availability_api.py new file mode 100644 index 0000000000..65b7d36e40 --- /dev/null +++ b/care/facility/tests/test_asset_availability_api.py @@ -0,0 +1,57 @@ +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APIRequestFactory, APITestCase + +from care.facility.api.viewsets.asset import AssetAvailabilityViewSet +from care.facility.models import Asset, AssetAvailabilityRecord, AssetLocation +from care.facility.models.asset import AvailabilityStatus +from care.facility.tests.mixins import TestClassMixin +from care.utils.tests.test_base import TestBase + + +class AssetAvailabilityViewSetTestCase(TestBase, TestClassMixin, APITestCase): + @classmethod + def setUp(cls): + cls.factory = APIRequestFactory() + state = cls.create_state() + district = cls.create_district(state=state) + cls.user = cls.create_user(district=district, username="test user") + facility = cls.create_facility(district=district, user=cls.user) + cls.asset_from_location = AssetLocation.objects.create( + name="asset from location", location_type=1, facility=facility + ) + cls.asset_to_location = AssetLocation.objects.create( + name="asset to location", location_type=1, facility=facility + ) + cls.asset = Asset.objects.create( + name="Test Asset", current_location=cls.asset_from_location, asset_type=50 + ) + + cls.asset_availability = AssetAvailabilityRecord.objects.create( + asset=cls.asset, + status=AvailabilityStatus.OPERATIONAL.value, + timestamp=timezone.now(), + ) + + def test_list_asset_availability(self): + response = self.new_request( + ("/api/v1/asset_availability/",), + {"get": "list"}, + AssetAvailabilityViewSet, + True, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual( + response.data["results"][0]["status"], AvailabilityStatus.OPERATIONAL.value + ) + + def test_retrieve_asset_availability(self): + response = self.new_request( + (f"/api/v1/asset_availability/{self.asset_availability.id}/",), + {"get": "retrieve"}, + AssetAvailabilityViewSet, + True, + {"pk": self.asset_availability.id}, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data["status"], AvailabilityStatus.OPERATIONAL.value) diff --git a/care/facility/tests/test_medibase_api.py b/care/facility/tests/test_medibase_api.py index 34ce13d7d5..ec6b53afad 100644 --- a/care/facility/tests/test_medibase_api.py +++ b/care/facility/tests/test_medibase_api.py @@ -9,17 +9,17 @@ def get_url(self, query=None): def test_search_by_name_exact_word(self): response = self.client.get(self.get_url(query="dolo")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEquals(response.data[0]["name"], "DOLO") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]["name"], "DOLO") def test_search_by_generic_exact_word(self): response = self.client.get(self.get_url(query="pAraCetAmoL")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEquals(response.data[0]["generic"], "paracetamol") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]["generic"], "paracetamol") def test_search_by_name_and_generic_exact_word(self): response = self.client.get(self.get_url(query="panadol paracetamol")) - self.assertEquals(response.status_code, status.HTTP_200_OK) - self.assertEquals(response.data[0]["name"], "PANADOL") - self.assertEquals(response.data[0]["generic"], "paracetamol") - self.assertEquals(response.data[0]["company"], "GSK") + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertEqual(response.data[0]["name"], "PANADOL") + self.assertEqual(response.data[0]["generic"], "paracetamol") + self.assertEqual(response.data[0]["company"], "GSK") diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index 4b675274c6..22733cfadb 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -10,4 +10,4 @@ def get_url(self, external_consultation_id=None): def test_external_consultation_does_not_exists_returns_404(self): sample_uuid = "e4a3d84a-d678-4992-9287-114f029046d8" response = self.client.get(self.get_url(sample_uuid)) - self.assertEquals(response.status_code, status.HTTP_404_NOT_FOUND) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) diff --git a/care/users/tests/test_facility_user_create.py b/care/users/tests/test_facility_user_create.py index 75849c3180..66ae51d4f6 100644 --- a/care/users/tests/test_facility_user_create.py +++ b/care/users/tests/test_facility_user_create.py @@ -55,7 +55,7 @@ def test_create_facility_user__should_fail__when_higher_level(self): response = self.client.post(self.get_url(), data=data, format="json") # Test Creation - self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) def test_create_facility_user__should_fail__when_different_location(self): new_district = self.clone_object(self.district) @@ -64,4 +64,4 @@ def test_create_facility_user__should_fail__when_different_location(self): response = self.client.post(self.get_url(), data=data, format="json") # Test Creation - self.assertEquals(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index f703c277ac..92d318c3a5 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -46,9 +46,9 @@ def api_get(self, url, data=None): headers={"Authorization": (self.auth_header_type + generate_jwt())}, ) try: - response = req.json() if req.status_code >= 400: - raise APIException(response, req.status_code) + raise APIException(req.text, req.status_code) + response = req.json() return response except json.decoder.JSONDecodeError: return {"error": "Invalid Response"} diff --git a/config/api_router.py b/config/api_router.py index 918ae39302..13d85f0da2 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -8,6 +8,7 @@ AmbulanceViewSet, ) from care.facility.api.viewsets.asset import ( + AssetAvailabilityViewSet, AssetLocationViewSet, AssetPublicViewSet, AssetTransactionViewSet, @@ -185,6 +186,7 @@ router.register("asset", AssetViewSet) router.register("asset_transaction", AssetTransactionViewSet) +router.register("asset_availability", AssetAvailabilityViewSet) patient_nested_router = NestedSimpleRouter(router, r"patient", lookup="patient") patient_nested_router.register(r"test_sample", PatientSampleViewSet) From 59a1674c765aed8b01d25e6d2f82d1cf2d91cf70 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 08:12:51 +0530 Subject: [PATCH 142/180] send health id to createHealthIdWithPreVerified --- care/abdm/utils/api_call.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 53ce22b426..6fcc8c244a 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -181,7 +181,7 @@ def verify_mobile_otp(self, data): def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" print("Creating Health ID with data: {}".format(data)) - data.pop("healthId", None) + # data.pop("healthId", None) response = self.api.post(path, data) return response.json() @@ -291,16 +291,15 @@ def get_abha_card_png(self, data): path = "/v1/account/getPngCard" access_token = self.generate_access_token(data) response = self.api.get(path, {}, access_token) - + return b64encode(response.content) - + def get_abha_card_pdf(self, data): path = "/v1/account/getCard" access_token = self.generate_access_token(data) response = self.api.get(path, {}, access_token) - - return b64encode(response.content) + return b64encode(response.content) # /v1/account/qrCode def get_qr_code(self, data, auth): From 87f06b0d682fcf3dbf83ada85df3652fcf7b93f3 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 08:15:33 +0530 Subject: [PATCH 143/180] fixed linting errors --- care/abdm/utils/api_call.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 53ce22b426..aede42a00e 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -291,16 +291,15 @@ def get_abha_card_png(self, data): path = "/v1/account/getPngCard" access_token = self.generate_access_token(data) response = self.api.get(path, {}, access_token) - + return b64encode(response.content) - + def get_abha_card_pdf(self, data): path = "/v1/account/getCard" access_token = self.generate_access_token(data) response = self.api.get(path, {}, access_token) - - return b64encode(response.content) + return b64encode(response.content) # /v1/account/qrCode def get_qr_code(self, data, auth): From e4347bb185acc566b59d5ce7933a690cd2628ea8 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 19 Jul 2023 11:44:45 +0530 Subject: [PATCH 144/180] merge migrations --- care/facility/migrations/0373_merge_20230719_1143.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 care/facility/migrations/0373_merge_20230719_1143.py diff --git a/care/facility/migrations/0373_merge_20230719_1143.py b/care/facility/migrations/0373_merge_20230719_1143.py new file mode 100644 index 0000000000..621d8a72e5 --- /dev/null +++ b/care/facility/migrations/0373_merge_20230719_1143.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.2 on 2023-07-19 06:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0366_merge_20230628_1428"), + ("facility", "0372_assetavailabilityrecord"), + ] + + operations = [] From 7066101c2cfcb0bcd8901a0d7bbd5a94cbbf7971 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 11:46:39 +0530 Subject: [PATCH 145/180] modified link_via_qr to create abha_number --- care/abdm/api/viewsets/healthid.py | 99 +++++++++--------------------- 1 file changed, 28 insertions(+), 71 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 7f29fd85a3..2b4b15eb74 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -11,6 +11,7 @@ from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet +from care.abdm.api.serializers.abhanumber import AbhaNumberSerializer from care.abdm.api.serializers.healthid import ( AadharOtpGenerateRequestPayloadSerializer, AadharOtpResendRequestPayloadSerializer, @@ -25,7 +26,6 @@ from care.abdm.models import AbhaNumber from care.abdm.utils.api_call import AbdmGateway, HealthIdGateway from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models.facility import Facility from care.facility.models.patient import PatientConsultation, PatientRegistration from care.utils.queryset.patient import get_patient_queryset from config.auth_views import CaptchaRequiredException @@ -307,10 +307,9 @@ def get_abha_card(self, request): return Response(response, status=status.HTTP_200_OK) response = HealthIdGateway().get_abha_card_pdf( - {"refreshToken": patient.abha_number.refresh_token} - ) + {"refreshToken": patient.abha_number.refresh_token} + ) return Response(response, status=status.HTTP_200_OK) - @swagger_auto_schema( # /v1/registration/aadhaar/searchByHealthId @@ -334,70 +333,16 @@ def link_via_qr(self, request): dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - if "patientId" not in data or data["patientId"] is None: - patient = PatientRegistration.objects.filter( - abha_number__abha_number=data["hidn"] - ).first() - if patient: - return Response( - { - "message": "A patient is already associated with the provided Abha Number" - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if ( - "facilityId" not in data - or not Facility.objects.filter(external_id=data["facilityId"]).first() - ): - return Response( - {"message": "Enter a valid facilityId"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - if not HealthIdGateway().verify_demographics( - data["phr"] or data["hidn"], - data["name"], - data["gender"], - str(dob.year), - ): - return Response( - {"message": "Please enter valid data"}, - status=status.HTTP_403_FORBIDDEN, - ) - - patient = PatientRegistration.objects.create( - facility=Facility.objects.get(external_id=data["facilityId"]), - name=data["name"], - gender=1 - if data["gender"] == "M" - else 2 - if data["gender"] == "F" - else 3, - is_antenatal=False, - phone_number=data["mobile"], - emergency_phone_number=data["mobile"], - date_of_birth=dob, - blood_group="UNK", - nationality="India", - address=data["address"], - pincode=None, - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, + patient = PatientRegistration.objects.filter( + abha_number__abha_number=data["hidn"] + ).first() + if patient: + return Response( + { + "message": "A patient is already associated with the provided Abha Number" + }, + status=status.HTTP_400_BAD_REQUEST, ) - else: - patient = PatientRegistration.objects.filter( - external_id=data["patientId"] - ).first() - - if not patient: - return Response( - {"message": "Enter a valid patientId"}, - status=status.HTTP_400_BAD_REQUEST, - ) abha_number = AbhaNumber.objects.create( abha_number=data["hidn"], @@ -411,8 +356,6 @@ def link_via_qr(self, request): ) abha_number.save() - patient.abha_number = abha_number - patient.save() AbdmGateway().fetch_modes( { @@ -423,8 +366,22 @@ def link_via_qr(self, request): } ) - patient_serialized = PatientDetailSerializer(patient).data - return Response(patient_serialized, status=status.HTTP_200_OK) + if "patientId" in data or data["patientId"] is not None: + patient = PatientRegistration.objects.filter( + external_id=data["patientId"] + ).first() + + if not patient: + return Response( + {"message": "Enter a valid patientId"}, + status=status.HTTP_400_BAD_REQUEST, + ) + + patient.abha_number = abha_number + patient.save() + + abha_serialized = AbhaNumberSerializer(abha_number).data + return Response(abha_serialized, status=status.HTTP_200_OK) @swagger_auto_schema( operation_id="get_new_linking_token", From ff1c30060b9addf71d162edd12b46caf1d2bffe3 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 12:20:49 +0530 Subject: [PATCH 146/180] return an existing abha number if available in link_via_qr --- care/abdm/api/viewsets/healthid.py | 45 +++++++++++++++++------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 2b4b15eb74..559bfda44a 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -344,29 +344,34 @@ def link_via_qr(self, request): status=status.HTTP_400_BAD_REQUEST, ) - abha_number = AbhaNumber.objects.create( - abha_number=data["hidn"], - health_id=data["phr"], - name=data["name"], - gender=data["gender"], - date_of_birth=str(dob)[0:10], - address=data["address"], - district=data["dist name"], - state=data["state name"], - ) + abha_number = AbhaNumber.objects.filter(abha_number=data["hidn"]).first() + + if not abha_number: + abha_number = AbhaNumber.objects.create( + abha_number=data["hidn"], + health_id=data["phr"], + name=data["name"], + gender=data["gender"], + date_of_birth=str(dob)[0:10], + address=data["address"], + district=data["dist name"], + state=data["state name"], + ) - abha_number.save() + abha_number.save() - AbdmGateway().fetch_modes( - { - "healthId": data["phr"] or data["hidn"], - "name": data["name"], - "gender": data["gender"], - "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[0:10], - } - ) + AbdmGateway().fetch_modes( + { + "healthId": data["phr"] or data["hidn"], + "name": data["name"], + "gender": data["gender"], + "dateOfBirth": str(datetime.strptime(data["dob"], "%d-%m-%Y"))[ + 0:10 + ], + } + ) - if "patientId" in data or data["patientId"] is not None: + if "patientId" in data and data["patientId"] is not None: patient = PatientRegistration.objects.filter( external_id=data["patientId"] ).first() From 869966cb1d0b8e9c0abf0cd585158bd81c443510 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 12:30:35 +0530 Subject: [PATCH 147/180] changed response data in link_via_qr --- care/abdm/api/viewsets/healthid.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 559bfda44a..4278289f16 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -386,7 +386,10 @@ def link_via_qr(self, request): patient.save() abha_serialized = AbhaNumberSerializer(abha_number).data - return Response(abha_serialized, status=status.HTTP_200_OK) + return Response( + {"id": abha_serialized["external_id"], "abha_profile": abha_serialized}, + status=status.HTTP_200_OK, + ) @swagger_auto_schema( operation_id="get_new_linking_token", From e0a1a59a51b2c9d51a5c69257f10f2fdd20220e4 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 12:53:43 +0530 Subject: [PATCH 148/180] changed discover filter logic --- care/abdm/api/viewsets/auth.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7d9305ec01..b8765b726c 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -109,6 +109,7 @@ class DiscoverView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data + print(data) patients = PatientRegistration.objects.all() verified_identifiers = data["patient"]["verifiedIdentifiers"] @@ -139,11 +140,8 @@ def post(self, request, *args, **kwargs): abha_number__health_id=identifier["value"] ) - patient = patients.filter( - abha_number__name=data["patient"]["name"], - abha_number__gender=data["patient"]["gender"], - # TODO: check date also - ).last() + # TODO: also filter by demographics + patient = patients.last() if not patient: return Response( From 3605b8b314a929c68714c879b8b93d9190e3f9d9 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Wed, 19 Jul 2023 13:21:49 +0530 Subject: [PATCH 149/180] fixed searching logic in discover --- care/abdm/api/viewsets/auth.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index b8765b726c..7b94a9b9b0 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -121,11 +121,14 @@ def post(self, request, *args, **kwargs): ) else: for identifier in verified_identifiers: + if identifier["value"] is None: + continue + if identifier["type"] == "MOBILE": matched_by.append(identifier["value"]) + mobile = identifier["value"].replace("+91", "").replace("-", "") patients = patients.filter( - Q(phone_number=f"+91{identifier['value']}") - | Q(phone_number=identifier["value"]) + Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile) ) if identifier["type"] == "NDHM_HEALTH_NUMBER": From 1d97f1503da8f96e01226eebf4cd17a7d875601a Mon Sep 17 00:00:00 2001 From: Mathew Date: Thu, 20 Jul 2023 10:32:30 +0530 Subject: [PATCH 150/180] Update deployment-branch.yaml --- .github/workflows/deployment-branch.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deployment-branch.yaml b/.github/workflows/deployment-branch.yaml index 69e7907486..b376a6f98f 100644 --- a/.github/workflows/deployment-branch.yaml +++ b/.github/workflows/deployment-branch.yaml @@ -5,6 +5,7 @@ on: push: branches: + - abdm - abdm-m2 - hcx-communications paths-ignore: From d9dd79669bc58e2f3a145ed7d1320f26d3a09e1c Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Thu, 20 Jul 2023 13:04:22 +0530 Subject: [PATCH 151/180] Mark asset as down when middleware is down (#1466) * Mark asset as down when middleware is down * refactor code * Update care/facility/tasks/asset_monitor.py --------- Co-authored-by: Aakash Singh --- care/facility/tasks/asset_monitor.py | 37 ++++++++++++++++------------ 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index 82a0bcd0ee..2a30571b05 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -24,19 +24,23 @@ def check_asset_status(): middleware_status_cache = {} for asset in assets: + # Skipping if asset class or local IP address is not present if not asset.asset_class or not asset.meta.get("local_ip_address", None): continue try: + # Fetching middleware hostname hostname = asset.meta.get( "middleware_hostname", asset.current_location.facility.middleware_address, ) - result: Any = {} + result: Any = None + # Checking if middleware status is already cached if hostname in middleware_status_cache: result = middleware_status_cache[hostname] else: try: + # Creating an instance of the asset class asset_class: BaseAssetIntegration = AssetClasses[ asset.asset_class ].value( @@ -45,42 +49,43 @@ def check_asset_status(): "middleware_hostname": hostname, } ) + # Fetching the status of the device result = asset_class.api_get(asset_class.get_url("devices/status")) - middleware_status_cache[hostname] = result except Exception: - logger.exception("Error in Asset Status Check - Fetching Status") - middleware_status_cache[hostname] = None - continue + logger.warn(f"Middleware {hostname} is down", exc_info=True) + # If no status is returned, setting default status as down if not result: - continue + result = [{"time": timezone.now().isoformat(), "status": []}] - new_status = None + middleware_status_cache[hostname] = result + + # Setting new status as down by default + new_status = AvailabilityStatus.DOWN for status_record in result: if asset.meta.get("local_ip_address") in status_record.get( "status", {} ): - new_status = status_record["status"][ + asset_status = status_record["status"][ asset.meta.get("local_ip_address") ] else: - new_status = "not_monitored" + asset_status = "down" + # Fetching the last record of the asset last_record = ( AssetAvailabilityRecord.objects.filter(asset=asset) .order_by("-timestamp") .first() ) - if new_status == "up": + # Setting new status based on the status returned by the device + if asset_status == "up": new_status = AvailabilityStatus.OPERATIONAL - elif new_status == "down": - new_status = AvailabilityStatus.DOWN - elif new_status == "maintenance": + elif asset_status == "maintenance": new_status = AvailabilityStatus.UNDER_MAINTENANCE - else: - new_status = AvailabilityStatus.NOT_MONITORED + # Creating a new record if the status has changed if not last_record or ( datetime.fromisoformat(status_record.get("time")) > last_record.timestamp @@ -93,4 +98,4 @@ def check_asset_status(): ) except Exception: - logger.exception("Error in Asset Status Check") + logger.error("Error in Asset Status Check", exc_info=True) From fdee97a0e338d47f8f470bf548f7ccf845335506 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 13:13:42 +0530 Subject: [PATCH 152/180] removed mobile identifier from discover api --- care/abdm/api/viewsets/auth.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 7b94a9b9b0..020cbde7ab 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -2,7 +2,6 @@ from datetime import datetime, timedelta from django.core.cache import cache -from django.db.models import Q from rest_framework import status from rest_framework.generics import GenericAPIView, get_object_or_404 from rest_framework.permissions import AllowAny @@ -124,12 +123,12 @@ def post(self, request, *args, **kwargs): if identifier["value"] is None: continue - if identifier["type"] == "MOBILE": - matched_by.append(identifier["value"]) - mobile = identifier["value"].replace("+91", "").replace("-", "") - patients = patients.filter( - Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile) - ) + # if identifier["type"] == "MOBILE": + # matched_by.append(identifier["value"]) + # mobile = identifier["value"].replace("+91", "").replace("-", "") + # patients = patients.filter( + # Q(phone_number=f"+91{mobile}") | Q(phone_number=mobile) + # ) if identifier["type"] == "NDHM_HEALTH_NUMBER": matched_by.append(identifier["value"]) From 686eeee8b650fdffd0180c3775ca68884f019b14 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 16:25:08 +0530 Subject: [PATCH 153/180] removed unwanted prints --- care/abdm/api/viewsets/auth.py | 17 ++--------------- care/abdm/api/viewsets/hip.py | 1 - care/abdm/api/viewsets/status.py | 4 +--- care/abdm/utils/api_call.py | 4 ---- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 020cbde7ab..ca240a4098 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -21,8 +21,9 @@ class OnFetchView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print("on-fetch-modes", data) + AbdmGateway().init(data["resp"]["requestId"]) + return Response({}, status=status.HTTP_202_ACCEPTED) @@ -32,7 +33,6 @@ class OnInitView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print("on-init", data) AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) @@ -45,7 +45,6 @@ class OnConfirmView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print(data) if "validity" in data["auth"]: if data["auth"]["validity"]["purpose"] == "LINK": @@ -79,7 +78,6 @@ class AuthNotifyView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print("auth-notify", data) if data["auth"]["status"] != "GRANTED": return @@ -108,7 +106,6 @@ class DiscoverView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print(data) patients = PatientRegistration.objects.all() verified_identifiers = data["patient"]["verifiedIdentifiers"] @@ -232,9 +229,7 @@ class NotifyView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print(data) - # TODO: create a seperate cache and also add a expiration time cache.set(data["notification"]["consentId"], json.dumps(data)) AbdmGateway().on_notify( @@ -252,9 +247,7 @@ class RequestDataView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print(data) - # TODO: uncomment later consent_id = data["hiRequest"]["consent"]["id"] consent = json.loads(cache.get(consent_id)) if consent_id in cache else None if not consent or not consent["notification"]["status"] == "GRANTED": @@ -286,8 +279,6 @@ def post(self, request, *args, **kwargs): data["hiRequest"]["keyMaterial"]["nonce"], ) - print(consent["notification"]["consentDetail"]["careContexts"][:1:-1]) - AbdmGateway().data_transfer( { "transaction_id": data["transactionId"], @@ -335,10 +326,6 @@ def post(self, request, *args, **kwargs): } ) - print("______________________________________________") - print(consent["notification"]["consentDetail"]["careContexts"][:-2:-1]) - print("______________________________________________") - AbdmGateway().data_notify( { "consent_id": data["hiRequest"]["consent"]["id"], diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 4958e23d94..9cae1efdbf 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -26,7 +26,6 @@ def get_linking_token(self, data): @action(detail=False, methods=["POST"]) def share(self, request, *args, **kwargs): data = request.data - print(data) patient_data = data["profile"]["patient"] counter_id = ( diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index 3da724c722..05ef33757b 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -15,7 +15,6 @@ class NotifyView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print("patient_status_notify", data) PatientRegistration.objects.filter( abha_number__health_id=data["notification"]["patient"]["id"] @@ -35,6 +34,5 @@ class SMSOnNotifyView(GenericAPIView): def post(self, request, *args, **kwargs): data = request.data - print("patient_sms_on_notify", data) - + print(data) return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 6fcc8c244a..3ae10d8075 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -437,8 +437,6 @@ def init(self, prev_request_id): data = self.temp_memory[prev_request_id] self.temp_memory[request_id] = data - print("auth-init", data) - payload = { "requestId": request_id, "timestamp": str( @@ -483,8 +481,6 @@ def confirm(self, transaction_id, prev_request_id): }, } - print(payload) - response = self.api.post(path, payload, None, additional_headers) return response From 5d232c75062c45775962fdcaae48ee7bcd6b2765 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 16:36:25 +0530 Subject: [PATCH 154/180] added env to enable and disable abdm apis --- config/api_router.py | 5 +- config/settings/base.py | 1 + config/urls.py | 136 +++++++++++++++++++++------------------- 3 files changed, 74 insertions(+), 68 deletions(-) diff --git a/config/api_router.py b/config/api_router.py index 523841d596..472aa84860 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -227,8 +227,9 @@ def __init__(self): router.register("public/asset", AssetPublicViewSet) # ABDM endpoints -router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") -abdm_router.register("profile", HipViewSet, basename="hip") +if settings.ENABLE_ABDM: + router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") + abdm_router.register("profile", HipViewSet, basename="hip") app_name = "api" urlpatterns = [ diff --git a/config/settings/base.py b/config/settings/base.py index 9ed3266307..611de62d15 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -540,6 +540,7 @@ ) # ABDM +ENABLE_ABDM = env.bool("ENABLE_ABDM", default=False) ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in") diff --git a/config/urls.py b/config/urls.py index 89c5da77f6..e068ad87c1 100644 --- a/config/urls.py +++ b/config/urls.py @@ -78,72 +78,6 @@ name="change_password_view", ), path("api/v1/", include(api_router.urlpatterns)), - path("v1.0/patients/", include(api_router.abdm_urlpatterns)), - path( - "v0.5/users/auth/on-fetch-modes", - OnFetchView.as_view(), - name="abdm_on_fetch_modes_view", - ), - path( - "v0.5/users/auth/on-init", - OnInitView.as_view(), - name="abdm_on_init_view", - ), - path( - "v0.5/users/auth/on-confirm", - OnConfirmView.as_view(), - name="abdm_on_confirm_view", - ), - path( - "v0.5/users/auth/notify", - AuthNotifyView.as_view(), - name="abdm_auth_notify_view", - ), - path( - "v0.5/links/link/on-add-contexts", - OnAddContextsView.as_view(), - name="abdm_on_add_context_view", - ), - path( - "v0.5/care-contexts/discover", - DiscoverView.as_view(), - name="abdm_discover_view", - ), - path( - "v0.5/links/link/init", - LinkInitView.as_view(), - name="abdm_link_init_view", - ), - path( - "v0.5/links/link/confirm", - LinkConfirmView.as_view(), - name="abdm_link_confirm_view", - ), - path( - "v0.5/consents/hip/notify", - NotifyView.as_view(), - name="abdm_notify_view", - ), - path( - "v0.5/health-information/hip/request", - RequestDataView.as_view(), - name="abdm_request_data_view", - ), - path( - "v0.5/patients/status/notify", - PatientStatusNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/patients/sms/on-notify", - SMSOnNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/heartbeat", - HeartbeatView.as_view(), - name="abdm_monitoring_heartbeat_view", - ), # Hcx Listeners path( "coverageeligibility/on_check", @@ -175,6 +109,76 @@ path("health/", include("healthy_django.urls", namespace="healthy_django")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +if settings.ENABLE_ABDM: + urlpatterns += [ + path("v1.0/patients/", include(api_router.abdm_urlpatterns)), + path( + "v0.5/users/auth/on-fetch-modes", + OnFetchView.as_view(), + name="abdm_on_fetch_modes_view", + ), + path( + "v0.5/users/auth/on-init", + OnInitView.as_view(), + name="abdm_on_init_view", + ), + path( + "v0.5/users/auth/on-confirm", + OnConfirmView.as_view(), + name="abdm_on_confirm_view", + ), + path( + "v0.5/users/auth/notify", + AuthNotifyView.as_view(), + name="abdm_auth_notify_view", + ), + path( + "v0.5/links/link/on-add-contexts", + OnAddContextsView.as_view(), + name="abdm_on_add_context_view", + ), + path( + "v0.5/care-contexts/discover", + DiscoverView.as_view(), + name="abdm_discover_view", + ), + path( + "v0.5/links/link/init", + LinkInitView.as_view(), + name="abdm_link_init_view", + ), + path( + "v0.5/links/link/confirm", + LinkConfirmView.as_view(), + name="abdm_link_confirm_view", + ), + path( + "v0.5/consents/hip/notify", + NotifyView.as_view(), + name="abdm_notify_view", + ), + path( + "v0.5/health-information/hip/request", + RequestDataView.as_view(), + name="abdm_request_data_view", + ), + path( + "v0.5/patients/status/notify", + PatientStatusNotifyView.as_view(), + name="abdm_patient_status_notify_view", + ), + path( + "v0.5/patients/sms/on-notify", + SMSOnNotifyView.as_view(), + name="abdm_patient_status_notify_view", + ), + path( + "v0.5/heartbeat", + HeartbeatView.as_view(), + name="abdm_monitoring_heartbeat_view", + ), + ] + if settings.DEBUG: # This allows the error pages to be debugged during development, just visit # these url in browser to see how these error pages look like. From 76717e62238e0cd5816931572c009743a99339da Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 20 Jul 2023 20:50:44 +0530 Subject: [PATCH 155/180] add patient_category ShiftingSerializer (#1464) partial revert from #1280 --- care/facility/api/serializers/shifting.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/care/facility/api/serializers/shifting.py b/care/facility/api/serializers/shifting.py index 17bc352215..21b8b5c458 100644 --- a/care/facility/api/serializers/shifting.py +++ b/care/facility/api/serializers/shifting.py @@ -12,6 +12,7 @@ ) from care.facility.models import ( BREATHLESSNESS_CHOICES, + CATEGORY_CHOICES, FACILITY_TYPES, SHIFTING_STATUS_CHOICES, VEHICLE_CHOICES, @@ -217,6 +218,7 @@ class ShiftingSerializer(serializers.ModelSerializer): last_edited_by_object = UserBaseMinimumSerializer( source="last_edited_by", read_only=True ) + patient_category = ChoiceField(choices=CATEGORY_CHOICES, required=False) ambulance_driver_name = serializers.CharField( required=False, allow_null=True, allow_blank=True ) @@ -327,8 +329,10 @@ def update(self, instance, validated_data): new_instance = super().update(instance, validated_data) patient = new_instance.patient - patient.last_consultation.category = self.initial_data["patient_category"] - patient.last_consultation.save() + patient_category = self.validated_data.pop("patient_category") + if patient.last_consultation and patient_category is not None: + patient.last_consultation.category = patient_category + patient.last_consultation.save(update_fields=["category"]) if ( "status" in validated_data @@ -390,9 +394,10 @@ def create(self, validated_data): patient.allow_transfer = True patient.save() - if patient.last_consultation: - patient.last_consultation.category = self.initial_data["patient_category"] - patient.last_consultation.save() + patient_category = self.validated_data.pop("patient_category") + if patient.last_consultation and patient_category is not None: + patient.last_consultation.category = patient_category + patient.last_consultation.save(update_fields=["category"]) validated_data["origin_facility"] = patient.facility From 667840f315a10b201740aef58afb58ee1edd05dc Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 20 Jul 2023 20:51:21 +0530 Subject: [PATCH 156/180] return proper data type from symptomatic_international_traveller (#1461) --- care/facility/models/patient_icmr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/models/patient_icmr.py b/care/facility/models/patient_icmr.py index 660c6cfcfd..e2f0ac9752 100644 --- a/care/facility/models/patient_icmr.py +++ b/care/facility/models/patient_icmr.py @@ -225,7 +225,7 @@ def is_symptomatic(self): def symptomatic_international_traveller( self, ): - return ( + return bool( self.patient.countries_travelled and len(self.patient.countries_travelled) != 0 and ( From 40cf031f161ffe3b126ba3ff3ab28a1a0e487ea0 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 20 Jul 2023 20:52:35 +0530 Subject: [PATCH 157/180] don't summarize deleted facilities (#1459) --- .../utils/summarisation/facility_capacity.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/care/facility/utils/summarisation/facility_capacity.py b/care/facility/utils/summarisation/facility_capacity.py index 607e4535c6..63548b9c3c 100644 --- a/care/facility/utils/summarisation/facility_capacity.py +++ b/care/facility/utils/summarisation/facility_capacity.py @@ -17,12 +17,7 @@ def facility_capacity_summary(): - capacity_objects = FacilityCapacity.objects.all().select_related( - "facility", - "facility__state", - "facility__district", - "facility__local_body", - ) + capacity_objects = FacilityCapacity.objects.all() capacity_summary = {} current_date = localtime(now()).replace(hour=0, minute=0, second=0, microsecond=0) @@ -106,11 +101,10 @@ def facility_capacity_summary(): capacity_summary[facility_obj.id]["inventory"] = temp_inventory_summary_obj for capacity_object in capacity_objects: - facility_id = capacity_object.facility.id + facility_id = capacity_object.facility_id if facility_id not in capacity_summary: - capacity_summary[facility_id] = FacilitySerializer( - capacity_object.facility - ).data + # This facility is either deleted or not active + continue if "availability" not in capacity_summary[facility_id]: capacity_summary[facility_id]["availability"] = [] capacity_summary[facility_id]["availability"].append( From d58c69b666d1e6c7170984bab1633d000e315855 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 20 Jul 2023 21:29:44 +0530 Subject: [PATCH 158/180] remove unused hba1c field from patient consultation (#1309) * remove unused hba1c field from patient consultation * recreate migrations * update migration Signed-off-by: Aakash Singh --------- Signed-off-by: Aakash Singh Co-authored-by: Vignesh Hari --- .../0373_remove_patientconsultation_hba1c.py | 16 ++++++++++++++++ care/facility/models/patient_consultation.py | 6 ------ data/dummy/facility.json | 1 - 3 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 care/facility/migrations/0373_remove_patientconsultation_hba1c.py diff --git a/care/facility/migrations/0373_remove_patientconsultation_hba1c.py b/care/facility/migrations/0373_remove_patientconsultation_hba1c.py new file mode 100644 index 0000000000..d4b200cce1 --- /dev/null +++ b/care/facility/migrations/0373_remove_patientconsultation_hba1c.py @@ -0,0 +1,16 @@ +# Generated by Django 4.2.2 on 2023-07-20 13:09 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0372_assetavailabilityrecord"), + ] + + operations = [ + migrations.RemoveField( + model_name="patientconsultation", + name="HBA1C", + ), + ] diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index de097a16ce..281401aa26 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -173,12 +173,6 @@ class PatientConsultation(PatientBaseModel, PatientRelatedPermissionMixin): verbose_name="Patient's Weight in KG", validators=[MinValueValidator(0)], ) - HBA1C = models.FloatField( - default=None, - null=True, - verbose_name="HBA1C parameter for reference to current blood sugar levels", - validators=[MinValueValidator(0)], - ) # ICU Information diff --git a/data/dummy/facility.json b/data/dummy/facility.json index e6b997828f..1564622b72 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -206,7 +206,6 @@ "current_bed": null, "height": 0.0, "weight": 0.0, - "HBA1C": null, "operation": null, "special_instruction": "", "intubation_history": [] From 81c838e70ae6e695fd7eaa56292d6b79da827992 Mon Sep 17 00:00:00 2001 From: yaswanth sai vendra <75136628+yaswanthsaivendra@users.noreply.github.com> Date: Thu, 20 Jul 2023 21:36:44 +0530 Subject: [PATCH 159/180] =?UTF-8?q?prefetched=20skills=20for=20userassigne?= =?UTF-8?q?d=20serializer=20in=20patientconsultation=20=E2=80=A6=20(#1370)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * prefetched skills for userassigned serializer in patientconsultation and facilityusers view * added tests for code changes * refactor tests --------- Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari --- care/facility/api/viewsets/facility_users.py | 11 +++- .../api/viewsets/patient_consultation.py | 11 +++- care/facility/tests/test_facilityuser_api.py | 47 +++++++++++++++++ .../tests/test_patient_consultation_api.py | 50 +++++++++++++++++-- care/users/api/serializers/user.py | 6 +-- 5 files changed, 112 insertions(+), 13 deletions(-) create mode 100644 care/facility/tests/test_facilityuser_api.py diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py index 8422991791..a03319ecf8 100644 --- a/care/facility/api/viewsets/facility_users.py +++ b/care/facility/api/viewsets/facility_users.py @@ -1,3 +1,4 @@ +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 mixins @@ -7,7 +8,7 @@ from care.facility.models.facility import Facility from care.users.api.serializers.user import UserAssignedSerializer -from care.users.models import User +from care.users.models import Skill, User class UserFilter(filters.FilterSet): @@ -34,6 +35,12 @@ def get_queryset(self): facility = Facility.objects.get( external_id=self.kwargs.get("facility_external_id") ) - return facility.users.filter(deleted=False).order_by("-last_login") + queryset = facility.users.filter(deleted=False).order_by("-last_login") + queryset = queryset.prefetch_related( + Prefetch( + "skills", queryset=Skill.objects.filter(userskill__deleted=False) + ) + ) + return queryset except Facility.DoesNotExist: raise ValidationError({"Facility": "Facility not found"}) diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index dc18b65851..98b60ba393 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -1,3 +1,4 @@ +from django.db.models import Prefetch from django.db.models.query_utils import Q from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema @@ -24,7 +25,7 @@ email_discharge_summary, generate_and_upload_discharge_summary_task, ) -from care.users.models import User +from care.users.models import Skill, User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities @@ -69,6 +70,14 @@ def get_permissions(self): return super().get_permissions() def get_queryset(self): + if self.serializer_class == PatientConsultationSerializer: + self.queryset = self.queryset.prefetch_related( + "assigned_to", + Prefetch( + "assigned_to__skills", + queryset=Skill.objects.filter(userskill__deleted=False), + ), + ) if self.request.user.is_superuser: return self.queryset elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: diff --git a/care/facility/tests/test_facilityuser_api.py b/care/facility/tests/test_facilityuser_api.py new file mode 100644 index 0000000000..0e9d35edec --- /dev/null +++ b/care/facility/tests/test_facilityuser_api.py @@ -0,0 +1,47 @@ +from django.test import TestCase +from rest_framework import status + +from care.facility.api.viewsets.facility_users import FacilityUserViewSet +from care.facility.models.facility import Facility +from care.facility.tests.mixins import TestClassMixin +from care.users.models import Skill + + +class FacilityUserTest(TestClassMixin, TestCase): + def setUp(self): + super().setUp() + self.creator = self.users[0] + + sample_data = { + "name": "Hospital X", + "ward": self.creator.ward, + "local_body": self.creator.local_body, + "district": self.creator.district, + "state": self.creator.state, + "facility_type": 1, + "address": "Nearby", + "pincode": 390024, + "features": [], + } + self.facility = Facility.objects.create( + external_id="550e8400-e29b-41d4-a716-446655440000", + created_by=self.creator, + **sample_data, + ) + + self.skill1 = Skill.objects.create(name="Skill 1") + self.skill2 = Skill.objects.create(name="Skill 2") + + self.users[0].skills.add(self.skill1, self.skill2) + + def test_get_queryset_with_prefetching(self): + response = self.new_request( + (f"/api/v1/facility/{self.facility.external_id}/get_users/",), + {"get": "list"}, + FacilityUserViewSet, + self.users[0], + {"facility_external_id": self.facility.external_id}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNumQueries(2) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 82e6a914ab..ff55865c27 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -1,18 +1,62 @@ import datetime +from django.test import TestCase from django.utils.timezone import make_aware from rest_framework import status from rest_framework.test import APIRequestFactory, APITestCase +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.patient_consultation import ( CATEGORY_CHOICES, PatientConsultation, ) from care.facility.tests.mixins import TestClassMixin +from care.users.models import Skill from care.utils.tests.test_base import TestBase +class FacilityUserTest(TestClassMixin, TestCase): + def setUp(self): + super().setUp() + self.creator = self.users[0] + + sample_data = { + "name": "Hospital X", + "ward": self.creator.ward, + "local_body": self.creator.local_body, + "district": self.creator.district, + "state": self.creator.state, + "facility_type": 1, + "address": "Nearby", + "pincode": 390024, + "features": [], + } + self.facility = Facility.objects.create( + external_id="550e8400-e29b-41d4-a716-446655440000", + created_by=self.creator, + **sample_data, + ) + + self.skill1 = Skill.objects.create(name="Skill 1") + self.skill2 = Skill.objects.create(name="Skill 2") + + self.users[0].skills.add(self.skill1, self.skill2) + + def test_get_queryset_with_prefetching(self): + response = self.new_request( + (f"/api/v1/facility/{self.facility.external_id}/get_users/",), + {"get": "list"}, + FacilityUserViewSet, + self.users[0], + {"facility_external_id": self.facility.external_id}, + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertNumQueries(2) + + class TestPatientConsultation(TestBase, TestClassMixin, APITestCase): default_data = { "symptoms": [1], @@ -31,11 +75,7 @@ def setUp(self): ) def create_admission_consultation(self, patient=None, **kwargs): - patient = ( - self.create_patient(facility_id=self.facility.id) - if not patient - else patient - ) + patient = patient or self.create_patient(facility_id=self.facility.id) data = self.default_data.copy() kwargs.update( { diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index c1fb589171..61fff8759a 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -353,11 +353,7 @@ class UserAssignedSerializer(serializers.ModelSerializer): home_facility_object = FacilityBareMinimumSerializer( source="home_facility", read_only=True ) - skills = serializers.SerializerMethodField() - - def get_skills(self, obj): - qs = obj.skills.filter(userskill__deleted=False) - return SkillSerializer(qs, many=True).data + skills = SkillSerializer(many=True, read_only=True) class Meta: model = User From 54317daf46015e738ae99ada603f88b2f3dad386 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 22:44:01 +0530 Subject: [PATCH 160/180] use UUIDField for uuids --- care/abdm/api/serializers/healthid.py | 31 +++++---------------------- 1 file changed, 5 insertions(+), 26 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 25b157208e..218a5da8cd 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -1,4 +1,4 @@ -from rest_framework.serializers import CharField, Serializer +from rest_framework.serializers import CharField, Serializer, UUIDField class AadharOtpGenerateRequestPayloadSerializer(Serializer): @@ -61,13 +61,8 @@ class QRContentSerializer(Serializer): required=True, help_text="Name", ) - # {"statelgd":"33","distlgd":"573","address":"C/O Gopalsamy NO 33 A WESTSTREET ODANILAI KASTHURIBAI GRAMAM ARACHALUR Erode","state name":"TAMIL NADU","dist name":"Erode","mobile":"7639899448"} -# { -# "authMethod": "AADHAAR_OTP", -# "healthid": "43-4221-5105-6749" -# } class HealthIdAuthSerializer(Serializer): authMethod = CharField( max_length=64, @@ -83,12 +78,6 @@ class HealthIdAuthSerializer(Serializer): ) -# "gender": "M", -# "mobile": "9545812125", -# "name": "suraj singh karki", -# "yearOfBirth": "1994" - - class ABHASearchRequestSerializer: name = CharField(max_length=64, min_length=1, required=False, help_text="Name") mobile = CharField( @@ -132,9 +121,9 @@ class VerifyOtpRequestPayloadSerializer(Serializer): help_text="Transaction ID", validators=[], ) - patientId = CharField( + patientId = UUIDField( required=False, help_text="Patient ID to be linked", validators=[] - ) # TODO: Add UUID Validation + ) class VerifyDemographicsRequestPayloadSerializer(Serializer): @@ -168,16 +157,6 @@ class VerifyDemographicsRequestPayloadSerializer(Serializer): ) -# { -# "email": "Example@Demo.com", -# "firstName": "manoj", -# "healthId": "deepak.pant", -# "lastName": "singh", -# "middleName": "kishan", -# "password": "India@143", -# "profilePhoto": "/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAkJCQkJCQoLCwoODw0PDhQSERESFB4WFxYXFh4uHSEdHSEdLikxKCUoMSlJOTMzOUlUR0NHVGZbW2aBeoGoqOIBCQkJCQkJCgsLCg4PDQ8OFBIRERIUHhYXFhcWHi4dIR0dIR0uKTEoJSgxKUk5MzM5SVRHQ0dUZltbZoF6gaio4v/CABEIBLAHgAMBIgACEQEDEQH/xAAbAAACAwEBAQAAAAAAAAAAAAACAwABBAUGB//aAAgBAQAAAADwawLpMspcK7qrlE5F0Vtul2bVywMUNeBHUkW/bmxvYELGuNjh2VDvixxo5ViljKjDRMoahCULjs2JCShjhjh2OGxo0Y2MoXHOLszsKLhw7tD99mpZQxj8xceofmLEKFwXLTIyHwY1Ls+iEotjHY0M0pjRYxtGj4VFKLPohQlFQyy4Qipc0XG9pS+CP/2Q==", -# "txnId": "a825f76b-0696-40f3-864c-5a3a5b389a83" -# } class CreateHealthIdSerializer(Serializer): healthId = CharField( max_length=64, @@ -193,6 +172,6 @@ class CreateHealthIdSerializer(Serializer): help_text="PreVerified Transaction ID", validators=[], ) - patientId = CharField( + patientId = UUIDField( required=False, help_text="Patient ID to be linked", validators=[] - ) # TODO: Add UUID Validation + ) From 3bbdb1856aa84fc493fbcef434a30233e7cca7f2 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Thu, 20 Jul 2023 22:47:55 +0530 Subject: [PATCH 161/180] Update care/abdm/api/viewsets/hip.py Co-authored-by: Rithvik Nishad --- care/abdm/api/viewsets/hip.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 9cae1efdbf..10fa8d6773 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -56,11 +56,7 @@ def share(self, request, *args, **kwargs): patient = PatientRegistration.objects.create( facility=Facility.objects.get(external_id=counter_id), name=patient_data["name"], - gender=1 - if patient_data["gender"] == "M" - else 2 - if patient_data["gender"] == "F" - else 3, + gender={ "M": 1, "F": 2 }.get(patient_data["gender"], 3) is_antenatal=False, phone_number=patient_data["mobile"], emergency_phone_number=patient_data["mobile"], From 798b2e3652bdde5de7c7b79747fb608afaa585e5 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 22:53:03 +0530 Subject: [PATCH 162/180] moved constants to settings/base.py --- care/abdm/utils/api_call.py | 6 +++--- config/settings/base.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 3ae10d8075..b703b5e2e2 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -13,10 +13,10 @@ from care.abdm.models import AbhaNumber from care.facility.models.patient_consultation import PatientConsultation -GATEWAY_API_URL = "https://dev.abdm.gov.in/" -HEALTH_SERVICE_API_URL = "https://healthidsbx.abdm.gov.in/api" -ABDM_TOKEN_URL = GATEWAY_API_URL + "gateway/v0.5/sessions" +GATEWAY_API_URL = settings.ABDM_URL +HEALTH_SERVICE_API_URL = settings.HEALTH_SERVICE_API_URL ABDM_GATEWAY_URL = GATEWAY_API_URL + "gateway" +ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" ABDM_TOKEN_CACHE_KEY = "abdm_token" # TODO: Exception handling for all api calls, need to gracefully handle known exceptions diff --git a/config/settings/base.py b/config/settings/base.py index 611de62d15..5b30da365d 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -544,6 +544,9 @@ ABDM_CLIENT_ID = env("ABDM_CLIENT_ID", default="") ABDM_CLIENT_SECRET = env("ABDM_CLIENT_SECRET", default="") ABDM_URL = env("ABDM_URL", default="https://dev.abdm.gov.in") +HEALTH_SERVICE_API_URL = env( + "HEALTH_SERVICE_API_URL", default="https://healthidsbx.abdm.gov.in/api" +) ABDM_USERNAME = env("ABDM_USERNAME", default="abdm_user_internal") X_CM_ID = env("X_CM_ID", default="sbx") FIDELIUS_URL = env("FIDELIUS_URL", default="http://fidelius:8090") From 220fe2021484879efd929ccea0d3324364f3760a Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 22:59:53 +0530 Subject: [PATCH 163/180] removed basic auth --- care/abdm/utils/api_call.py | 2 +- config/authentication.py | 28 +--------------------------- 2 files changed, 2 insertions(+), 28 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index b703b5e2e2..608a70c116 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -15,7 +15,7 @@ GATEWAY_API_URL = settings.ABDM_URL HEALTH_SERVICE_API_URL = settings.HEALTH_SERVICE_API_URL -ABDM_GATEWAY_URL = GATEWAY_API_URL + "gateway" +ABDM_GATEWAY_URL = GATEWAY_API_URL + "/gateway" ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" ABDM_TOKEN_CACHE_KEY = "abdm_token" diff --git a/config/authentication.py b/config/authentication.py index 85a403087d..8d5a3e8281 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -3,12 +3,11 @@ import jwt import requests from django.conf import settings -from django.contrib.auth import authenticate from django.core.exceptions import ValidationError from django.utils.translation import gettext_lazy as _ from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object -from rest_framework import HTTP_HEADER_ENCODING, exceptions, status +from rest_framework import HTTP_HEADER_ENCODING from rest_framework.authentication import BasicAuthentication from rest_framework_simplejwt.authentication import JWTAuthentication from rest_framework_simplejwt.exceptions import AuthenticationFailed, InvalidToken @@ -16,7 +15,6 @@ from care.facility.models import Facility from care.facility.models.asset import Asset from care.users.models import User -from config.ratelimit import ratelimit class CustomJWTAuthentication(JWTAuthentication): @@ -36,30 +34,6 @@ def get_validated_token(self, raw_token): class CustomBasicAuthentication(BasicAuthentication): - def authenticate_credentials(self, userid, password, request=None): - """ - Authenticate the userid and password against username and password - with optional request for context. - """ - from config.auth_views import CaptchaRequiredException - - credentials = {User.USERNAME_FIELD: userid, "password": password} - if ratelimit(request, "login", [userid], increment=False): - raise CaptchaRequiredException( - detail={"status": 429, "detail": "Too Many Requests Provide Captcha"}, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - user = authenticate(request=request, **credentials) - - if user is None: - ratelimit(request, "login", [userid]) - raise exceptions.AuthenticationFailed(_("Invalid username/password.")) - - if not user.is_active: - raise exceptions.AuthenticationFailed(_("User inactive or deleted.")) - - return (user, None) - def authenticate_header(self, request): return "" From ff12327440b5847e240dcf8de460da0af1e863d0 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 23:04:36 +0530 Subject: [PATCH 164/180] typo: missed a comma --- care/abdm/api/viewsets/hip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 10fa8d6773..9c87312f58 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -56,7 +56,7 @@ def share(self, request, *args, **kwargs): patient = PatientRegistration.objects.create( facility=Facility.objects.get(external_id=counter_id), name=patient_data["name"], - gender={ "M": 1, "F": 2 }.get(patient_data["gender"], 3) + gender={"M": 1, "F": 2}.get(patient_data["gender"], 3), is_antenatal=False, phone_number=patient_data["mobile"], emergency_phone_number=patient_data["mobile"], From 30009d7b545b8221a8be55c17c066f2183faf89a Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 20 Jul 2023 23:21:06 +0530 Subject: [PATCH 165/180] squash migrations Signed-off-by: Aakash Singh --- care/abdm/migrations/0001_initial.py | 55 --------------- ...itial_squashed_0007_alter_abhanumber_id.py | 69 +++++++++++++++++++ .../migrations/0002_auto_20221220_2312.py | 32 --------- .../migrations/0003_auto_20221220_2321.py | 21 ------ .../migrations/0004_auto_20221220_2325.py | 52 -------------- .../migrations/0005_auto_20221220_2327.py | 62 ----------------- .../migrations/0006_auto_20230208_0915.py | 47 ------------- .../migrations/0007_alter_abhanumber_id.py | 19 ----- .../migrations/0003_auto_20230614_1048.py | 4 -- .../migrations/0329_auto_20221219_1936.py | 36 ---------- .../migrations/0366_merge_20230628_1428.py | 12 ---- .../migrations/0373_merge_20230719_1143.py | 12 ---- ...tientregistration_abha_number_and_more.py} | 16 +++-- 13 files changed, 81 insertions(+), 356 deletions(-) delete mode 100644 care/abdm/migrations/0001_initial.py create mode 100644 care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py delete mode 100644 care/abdm/migrations/0002_auto_20221220_2312.py delete mode 100644 care/abdm/migrations/0003_auto_20221220_2321.py delete mode 100644 care/abdm/migrations/0004_auto_20221220_2325.py delete mode 100644 care/abdm/migrations/0005_auto_20221220_2327.py delete mode 100644 care/abdm/migrations/0006_auto_20230208_0915.py delete mode 100644 care/abdm/migrations/0007_alter_abhanumber_id.py delete mode 100644 care/facility/migrations/0329_auto_20221219_1936.py delete mode 100644 care/facility/migrations/0366_merge_20230628_1428.py delete mode 100644 care/facility/migrations/0373_merge_20230719_1143.py rename care/facility/migrations/{0330_auto_20221220_2312.py => 0374_historicalpatientregistration_abha_number_and_more.py} (64%) diff --git a/care/abdm/migrations/0001_initial.py b/care/abdm/migrations/0001_initial.py deleted file mode 100644 index bcd8410442..0000000000 --- a/care/abdm/migrations/0001_initial.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-19 14:06 - -import uuid - -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [] - - operations = [ - migrations.CreateModel( - name="AbhaNumber", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("abha_number", models.CharField(max_length=50)), - ("email", models.CharField(max_length=50)), - ("first_name", models.CharField(max_length=50)), - ("health_id", models.CharField(max_length=50)), - ("last_name", models.CharField(max_length=50)), - ("middle_name", models.CharField(max_length=50)), - ("password", models.CharField(max_length=50)), - ("profile_photo", models.CharField(max_length=50)), - ("txn_id", models.CharField(max_length=50)), - ("access_token", models.CharField(max_length=50)), - ("refresh_token", models.CharField(max_length=50)), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py b/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py new file mode 100644 index 0000000000..ad5d70caa0 --- /dev/null +++ b/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py @@ -0,0 +1,69 @@ +# Generated by Django 4.2.2 on 2023-07-20 17:41 + +import uuid + +from django.db import migrations, models + + +class Migration(migrations.Migration): + replaces = [ + ("abdm", "0001_initial"), + ("abdm", "0002_auto_20221220_2312"), + ("abdm", "0003_auto_20221220_2321"), + ("abdm", "0004_auto_20221220_2325"), + ("abdm", "0005_auto_20221220_2327"), + ("abdm", "0006_auto_20230208_0915"), + ("abdm", "0007_alter_abhanumber_id"), + ] + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="AbhaNumber", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("abha_number", models.TextField(blank=True, null=True)), + ("email", models.EmailField(blank=True, max_length=254, null=True)), + ("first_name", models.TextField(blank=True, null=True)), + ("health_id", models.TextField(blank=True, null=True)), + ("last_name", models.TextField(blank=True, null=True)), + ("middle_name", models.TextField(blank=True, null=True)), + ("profile_photo", models.TextField(blank=True, null=True)), + ("txn_id", models.TextField(blank=True, null=True)), + ("access_token", models.TextField(blank=True, null=True)), + ("refresh_token", models.TextField(blank=True, null=True)), + ("address", models.TextField(blank=True, null=True)), + ("date_of_birth", models.TextField(blank=True, null=True)), + ("district", models.TextField(blank=True, null=True)), + ("gender", models.TextField(blank=True, null=True)), + ("name", models.TextField(blank=True, null=True)), + ("pincode", models.TextField(blank=True, null=True)), + ("state", models.TextField(blank=True, null=True)), + ], + options={ + "abstract": False, + }, + ), + ] diff --git a/care/abdm/migrations/0002_auto_20221220_2312.py b/care/abdm/migrations/0002_auto_20221220_2312.py deleted file mode 100644 index 73253c5c75..0000000000 --- a/care/abdm/migrations/0002_auto_20221220_2312.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-20 17:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0001_initial"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="email", - field=models.EmailField(max_length=254), - ), - migrations.AlterField( - model_name="abhanumber", - name="first_name", - field=models.CharField(max_length=512), - ), - migrations.AlterField( - model_name="abhanumber", - name="last_name", - field=models.CharField(max_length=512), - ), - migrations.AlterField( - model_name="abhanumber", - name="middle_name", - field=models.CharField(max_length=512), - ), - ] diff --git a/care/abdm/migrations/0003_auto_20221220_2321.py b/care/abdm/migrations/0003_auto_20221220_2321.py deleted file mode 100644 index aaa8357d2c..0000000000 --- a/care/abdm/migrations/0003_auto_20221220_2321.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-20 17:51 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0002_auto_20221220_2312"), - ] - - operations = [ - migrations.RemoveField( - model_name="abhanumber", - name="password", - ), - migrations.AlterField( - model_name="abhanumber", - name="profile_photo", - field=models.TextField(), - ), - ] diff --git a/care/abdm/migrations/0004_auto_20221220_2325.py b/care/abdm/migrations/0004_auto_20221220_2325.py deleted file mode 100644 index 613d539e15..0000000000 --- a/care/abdm/migrations/0004_auto_20221220_2325.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-20 17:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0003_auto_20221220_2321"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="abha_number", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="access_token", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="first_name", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="health_id", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="last_name", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="middle_name", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="refresh_token", - field=models.TextField(), - ), - migrations.AlterField( - model_name="abhanumber", - name="txn_id", - field=models.TextField(), - ), - ] diff --git a/care/abdm/migrations/0005_auto_20221220_2327.py b/care/abdm/migrations/0005_auto_20221220_2327.py deleted file mode 100644 index 1c0b9bc736..0000000000 --- a/care/abdm/migrations/0005_auto_20221220_2327.py +++ /dev/null @@ -1,62 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-20 17:57 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0004_auto_20221220_2325"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="abha_number", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="access_token", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="email", - field=models.EmailField(blank=True, max_length=254, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="first_name", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="health_id", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="last_name", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="middle_name", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="profile_photo", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="refresh_token", - field=models.TextField(blank=True, null=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="txn_id", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/care/abdm/migrations/0006_auto_20230208_0915.py b/care/abdm/migrations/0006_auto_20230208_0915.py deleted file mode 100644 index 940ed863c8..0000000000 --- a/care/abdm/migrations/0006_auto_20230208_0915.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-08 03:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0005_auto_20221220_2327"), - ] - - operations = [ - migrations.AddField( - model_name="abhanumber", - name="address", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="abhanumber", - name="date_of_birth", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="abhanumber", - name="district", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="abhanumber", - name="gender", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="abhanumber", - name="name", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="abhanumber", - name="pincode", - field=models.TextField(blank=True, null=True), - ), - migrations.AddField( - model_name="abhanumber", - name="state", - field=models.TextField(blank=True, null=True), - ), - ] diff --git a/care/abdm/migrations/0007_alter_abhanumber_id.py b/care/abdm/migrations/0007_alter_abhanumber_id.py deleted file mode 100644 index ef9ec6422c..0000000000 --- a/care/abdm/migrations/0007_alter_abhanumber_id.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-28 09:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0006_auto_20230208_0915"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] diff --git a/care/facility/migrations/0003_auto_20230614_1048.py b/care/facility/migrations/0003_auto_20230614_1048.py index f3d516a7da..ec6a7f9d74 100644 --- a/care/facility/migrations/0003_auto_20230614_1048.py +++ b/care/facility/migrations/0003_auto_20230614_1048.py @@ -8,10 +8,6 @@ class Migration(migrations.Migration): ("facility", "0002_auto_20230613_1657"), ] - replaces = [ - ("facility", "0331_auto_20230130_1652"), - ] - operations = [ migrations.RemoveField( model_name="patientregistration", diff --git a/care/facility/migrations/0329_auto_20221219_1936.py b/care/facility/migrations/0329_auto_20221219_1936.py deleted file mode 100644 index 589981551d..0000000000 --- a/care/facility/migrations/0329_auto_20221219_1936.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 2.2.11 on 2022-12-19 14:06 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0328_merge_20221208_1110"), - ] - - operations = [ - migrations.AlterField( - model_name="fileupload", - name="archived_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="archived_by", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="fileupload", - name="uploaded_by", - field=models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="uploaded_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/care/facility/migrations/0366_merge_20230628_1428.py b/care/facility/migrations/0366_merge_20230628_1428.py deleted file mode 100644 index a8607a065d..0000000000 --- a/care/facility/migrations/0366_merge_20230628_1428.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-28 08:58 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0330_auto_20221220_2312"), - ("facility", "0365_merge_20230626_1834"), - ] - - operations = [] diff --git a/care/facility/migrations/0373_merge_20230719_1143.py b/care/facility/migrations/0373_merge_20230719_1143.py deleted file mode 100644 index 621d8a72e5..0000000000 --- a/care/facility/migrations/0373_merge_20230719_1143.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-07-19 06:13 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0366_merge_20230628_1428"), - ("facility", "0372_assetavailabilityrecord"), - ] - - operations = [] diff --git a/care/facility/migrations/0330_auto_20221220_2312.py b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py similarity index 64% rename from care/facility/migrations/0330_auto_20221220_2312.py rename to care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py index b0808c90ea..304bebb163 100644 --- a/care/facility/migrations/0330_auto_20221220_2312.py +++ b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 2.2.11 on 2022-12-20 17:42 +# Generated by Django 4.2.2 on 2023-07-20 17:45 import django.db.models.deletion from django.db import migrations, models @@ -6,8 +6,16 @@ class Migration(migrations.Migration): dependencies = [ - ("abdm", "0002_auto_20221220_2312"), + ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), + ("facility", "0373_remove_patientconsultation_hba1c"), + ] + + replaces = [ ("facility", "0329_auto_20221219_1936"), + ("facility", "0330_auto_20221220_2312"), + ("facility", "0366_merge_20230628_1428"), + ("facility", "0366_merge_20230628_1428"), + ("facility", "0373_merge_20230719_1143"), ] operations = [ @@ -20,7 +28,7 @@ class Migration(migrations.Migration): null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name="+", - to="abdm.AbhaNumber", + to="abdm.abhanumber", ), ), migrations.AddField( @@ -30,7 +38,7 @@ class Migration(migrations.Migration): blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to="abdm.AbhaNumber", + to="abdm.abhanumber", ), ), ] From 4d23eaeecd1a7becfb4e28d4fb3e4baf22f89aca Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Thu, 20 Jul 2023 23:53:24 +0530 Subject: [PATCH 166/180] minor changes --- care/abdm/api/serializers/healthid.py | 170 +++++--------------------- care/abdm/api/viewsets/abha.py | 5 - 2 files changed, 28 insertions(+), 147 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 218a5da8cd..8c5b98d086 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -2,176 +2,62 @@ class AadharOtpGenerateRequestPayloadSerializer(Serializer): - aadhaar = CharField( - max_length=16, - min_length=12, - required=True, - help_text="Aadhaar Number", - validators=[], - ) + aadhaar = CharField(max_length=16, min_length=12, required=True, validators=[]) class AadharOtpResendRequestPayloadSerializer(Serializer): - txnId = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Transaction ID", - validators=[], - ) + txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) class HealthIdSerializer(Serializer): - healthId = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Health ID", - ) + healthId = CharField(max_length=64, min_length=1, required=True) class QRContentSerializer(Serializer): - hidn = CharField( - max_length=17, - min_length=17, - required=True, - help_text="Health ID Number", - ) - phr = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Health ID", - ) - name = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Name", - ) - gender = CharField( - max_length=1, - min_length=1, - required=True, - help_text="Name", - ) - dob = CharField( - max_length=10, - min_length=8, - required=True, - help_text="Name", - ) + hidn = CharField(max_length=17, min_length=17, required=True) + phr = CharField(max_length=64, min_length=1, required=True) + name = CharField(max_length=64, min_length=1, required=True) + gender = CharField(max_length=1, min_length=1, required=True) + dob = CharField(max_length=10, min_length=8, required=True) class HealthIdAuthSerializer(Serializer): - authMethod = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Auth Method", - ) - healthid = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Health ID", - ) + authMethod = CharField(max_length=64, min_length=1, required=True) + healthid = CharField(max_length=64, min_length=1, required=True) class ABHASearchRequestSerializer: - name = CharField(max_length=64, min_length=1, required=False, help_text="Name") + name = CharField(max_length=64, min_length=1, required=False) mobile = CharField( - max_length=10, min_length=10, required=False, help_text="Mobile Number" - ) - gender = CharField(max_length=1, min_length=1, required=False, help_text="Gender") - yearOfBirth = CharField( - max_length=4, min_length=4, required=False, help_text="Year of Birth" + max_length=10, + min_length=10, + required=False, ) + gender = CharField(max_length=1, min_length=1, required=False) + yearOfBirth = CharField(max_length=4, min_length=4, required=False) class GenerateMobileOtpRequestPayloadSerializer(Serializer): - mobile = CharField( - max_length=10, - min_length=10, - required=True, - help_text="Mobile Number", - validators=[], - ) - txnId = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Transaction ID", - validators=[], - ) + mobile = CharField(max_length=10, min_length=10, required=True, validators=[]) + txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) class VerifyOtpRequestPayloadSerializer(Serializer): otp = CharField( - max_length=6, - min_length=6, - required=True, - help_text="OTP", - validators=[], - ) - txnId = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Transaction ID", - validators=[], - ) - patientId = UUIDField( - required=False, help_text="Patient ID to be linked", validators=[] + max_length=6, min_length=6, required=True, help_text="OTP", validators=[] ) + txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) + patientId = UUIDField(required=False, validators=[]) class VerifyDemographicsRequestPayloadSerializer(Serializer): - gender = CharField( - max_length=10, - min_length=1, - required=True, - help_text="Gender", - validators=[], - ) - name = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Name", - validators=[], - ) - yearOfBirth = CharField( - max_length=4, - min_length=4, - required=True, - help_text="Year Of Birth", - validators=[], - ) - txnId = CharField( - max_length=64, - min_length=1, - required=True, - help_text="Transaction ID", - validators=[], - ) + gender = CharField(max_length=10, min_length=1, required=True, validators=[]) + name = CharField(max_length=64, min_length=1, required=True, validators=[]) + yearOfBirth = CharField(max_length=4, min_length=4, required=True, validators=[]) + txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) class CreateHealthIdSerializer(Serializer): - healthId = CharField( - max_length=64, - min_length=1, - required=False, - help_text="Health ID", - validators=[], - ) - txnId = CharField( - max_length=64, - min_length=1, - required=True, - help_text="PreVerified Transaction ID", - validators=[], - ) - patientId = UUIDField( - required=False, help_text="Patient ID to be linked", validators=[] - ) + healthId = CharField(max_length=64, min_length=1, required=False, validators=[]) + txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) + patientId = UUIDField(required=False, validators=[]) diff --git a/care/abdm/api/viewsets/abha.py b/care/abdm/api/viewsets/abha.py index ac1957a82d..7fcf102850 100644 --- a/care/abdm/api/viewsets/abha.py +++ b/care/abdm/api/viewsets/abha.py @@ -18,9 +18,6 @@ class AbhaViewSet(GenericViewSet): def get_abha_object(self): queryset = get_patient_queryset(self.request.user) - print( - "Finding patient with external_id: ", self.kwargs.get("patient_external_id") - ) patient_obj = get_object_or_404( queryset.filter(external_id=self.kwargs.get("patient_external_id")) ) @@ -30,7 +27,6 @@ def get_abha_object(self): def get_qr_code(self, request, *args, **kwargs): obj = self.get_abha_object() gateway = HealthIdGateway() - # Empty Dict as data, obj.access_token as auth response = gateway.get_qr_code(obj) return Response(response) @@ -38,6 +34,5 @@ def get_qr_code(self, request, *args, **kwargs): def get_profile(self, request, *args, **kwargs): obj = self.get_abha_object() gateway = HealthIdGateway() - # Empty Dict as data, obj.access_token as auth response = gateway.get_profile(obj) return Response(response) From 6034dc7c2938c856bb8db5391217ec4626aa39e2 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 00:11:05 +0530 Subject: [PATCH 167/180] refactors: remove comments and prints --- care/abdm/api/viewsets/healthid.py | 7 ------- care/abdm/api/viewsets/hip.py | 13 ------------- care/abdm/models.py | 4 +--- care/abdm/utils/api_call.py | 1 - 4 files changed, 1 insertion(+), 24 deletions(-) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index 25192814c0..c30818eac0 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -186,13 +186,6 @@ def create_abha(self, abha_profile, token): return abha_object def add_abha_details_to_patient(self, abha_object, patient_object): - # patient = PatientRegistration.objects.filter( - # abha_number__abha_number=abha_object.abha_number - # ).first() - - # if patient or patient_object.abha_number: - # return False - patient_object.abha_number = abha_object patient_object.save() return True diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 9c87312f58..a628d1953d 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -68,11 +68,6 @@ def share(self, request, *args, **kwargs): nationality="India", address=patient_data["address"]["line"], pincode=patient_data["address"]["pincode"], - created_by=None, - state=None, - district=None, - local_body=None, - ward=None, ) abha_number = AbhaNumber.objects.create( @@ -142,11 +137,3 @@ def share(self, request, *args, **kwargs): }, status=status.HTTP_202_ACCEPTED, ) - - return Response( - { - "status": "FAILURE", - "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], - }, - status=status.HTTP_401_UNAUTHORIZED, - ) diff --git a/care/abdm/models.py b/care/abdm/models.py index 18e4c0add7..8b5abbfd65 100644 --- a/care/abdm/models.py +++ b/care/abdm/models.py @@ -25,9 +25,7 @@ class AbhaNumber(BaseModel): pincode = models.TextField(null=True, blank=True) email = models.EmailField(null=True, blank=True) - profile_photo = models.TextField( - null=True, blank=True - ) # What is profile photo? how is it stored as? + profile_photo = models.TextField(null=True, blank=True) txn_id = models.TextField(null=True, blank=True) access_token = models.TextField(null=True, blank=True) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 608a70c116..650d4a7edb 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -61,7 +61,6 @@ def add_user_header(self, headers, user_token): def add_auth_header(self, headers): token = cache.get(ABDM_TOKEN_CACHE_KEY) - print("Using Cached Token") if not token: print("No Token in Cache") data = { From 603c0ee7504457c99ea07cd3f2b26e3cade15d76 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 00:22:05 +0530 Subject: [PATCH 168/180] minor refactors --- care/abdm/utils/api_call.py | 52 ++++++++++++++-------------- care/utils/assetintegration/onvif.py | 6 ---- config/settings/base.py | 3 +- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 650d4a7edb..4bb5353657 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -411,8 +411,8 @@ def fetch_modes(self, data): payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "query": { "id": data["healthId"], @@ -438,8 +438,8 @@ def init(self, prev_request_id): payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "query": { "id": data["healthId"], @@ -466,8 +466,8 @@ def confirm(self, transaction_id, prev_request_id): payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "transactionId": transaction_id, "credential": { @@ -490,8 +490,8 @@ def auth_on_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "acknowledgement": {"status": "OK"}, # "error": {"code": 1000, "message": "string"}, @@ -510,8 +510,8 @@ def add_contexts(self, data): payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "link": { "accessToken": data["access_token"], @@ -538,8 +538,8 @@ def on_discover(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "transactionId": data["transaction_id"], "patient": { @@ -570,8 +570,8 @@ def on_link_init(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "transactionId": data["transaction_id"], "link": { @@ -601,8 +601,8 @@ def on_link_confirm(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "patient": { "referenceNumber": data["patient_id"], @@ -631,8 +631,8 @@ def on_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "acknowledgement": {"status": "OK", "consentId": data["consent_id"]}, # "error": {"code": 1000, "message": "string"}, @@ -649,8 +649,8 @@ def on_data_request(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "hiRequest": { "transactionId": data["transaction_id"], @@ -696,8 +696,8 @@ def data_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "notification": { "consentId": data["consent_id"], @@ -732,8 +732,8 @@ def patient_status_on_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "acknowledgement": {"status": "OK"}, # "error": {"code": 1000, "message": "string"}, @@ -750,8 +750,8 @@ def patient_sms_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + "timestamp": datetime.now(tz=timezone.utc).strftime( + "%Y-%m-%dT%H:%M:%S.000Z" ), "notification": { "phoneNo": f"+91-{data['phone']}", diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index ecdae6d179..8e21c77d43 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -14,8 +14,6 @@ class OnvifActions(enum.Enum): GOTO_PRESET = "goto_preset" ABSOLUTE_MOVE = "absolute_move" RELATIVE_MOVE = "relative_move" - # STEP 1 | Part 1 - # GET_STREAMING_TOKEN = "getStreamingToken" def __init__(self, meta): try: @@ -56,8 +54,4 @@ def handle_action(self, action): if action_type == self.OnvifActions.RELATIVE_MOVE.value: return self.api_post(self.get_url("relativeMove"), request_body) - # STEP 1 | Part 3 - # if action_type == self.OnvifActions.GET_STREAMING_TOKEN.value: - # return self.api_post(self.get_url("getStreamingToken"), request_body) - raise ValidationError({"action": "invalid action type"}) diff --git a/config/settings/base.py b/config/settings/base.py index 5b30da365d..6e1fb60fae 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -33,7 +33,7 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts ALLOWED_HOSTS = env.json("DJANGO_ALLOWED_HOSTS", default=["*"]) # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = env.bool("DJANGO_DEBUG", True) +DEBUG = env.bool("DJANGO_DEBUG", False) # Local time zone. Choices are # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # though not all of them may be available with every OS. @@ -358,6 +358,7 @@ "USER_ID_FIELD": "external_id", } +# Celery (background tasks) # ------------------------------------------------------------------------------ # https://docs.celeryq.dev/en/latest/userguide/configuration.html#std:setting-timezone if USE_TZ: From 61d84bc1fffc73a30e08ce6e449b87fb260fe265 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Fri, 21 Jul 2023 00:23:59 +0530 Subject: [PATCH 169/180] revert requirements changes Signed-off-by: Aakash Singh --- requirements/base.txt | 2 +- requirements/local.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements/base.txt b/requirements/base.txt index 816abf7fc0..448c606713 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -50,6 +50,7 @@ celery==5.3.0 # https://docs.celeryq.dev/en/latest/changelog.html # ------------------------------------------------------------------------------ pywebpush==1.11.0 + # HCX # ------------------------------------------------------------------------------ fhir.resources==6.5.0 @@ -57,4 +58,3 @@ pydantic==1.* jwcrypto==1.5.0 # https://github.com/latchset/jwcrypto/releases pycryptodome==3.16.0 pycryptodomex==3.16.0 -pydantic==1.* diff --git a/requirements/local.txt b/requirements/local.txt index 0af8d4c7a6..ac3be564b5 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -1,6 +1,5 @@ -r ./base.txt -r ./docs.txt - Werkzeug==2.3.6 # https://werkzeug.palletsprojects.com/en/latest/changes/ # Django From 60ba3a7933c01bc1fddd72922e59d2198971dfcc Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 00:48:05 +0530 Subject: [PATCH 170/180] moved abdm endpoints inside abdm app --- care/abdm/urls.py | 99 ++++++++++++++++++++++++++++++++++++++++++++ config/api_router.py | 15 ------- config/urls.py | 85 +------------------------------------ 3 files changed, 101 insertions(+), 98 deletions(-) create mode 100644 care/abdm/urls.py diff --git a/care/abdm/urls.py b/care/abdm/urls.py new file mode 100644 index 0000000000..a6efbe58ea --- /dev/null +++ b/care/abdm/urls.py @@ -0,0 +1,99 @@ +from django.urls import path +from rest_framework.routers import SimpleRouter + +from care.abdm.api.viewsets.auth import ( + AuthNotifyView, + DiscoverView, + LinkConfirmView, + LinkInitView, + NotifyView, + OnAddContextsView, + OnConfirmView, + OnFetchView, + OnInitView, + RequestDataView, +) +from care.abdm.api.viewsets.hip import HipViewSet +from care.abdm.api.viewsets.monitoring import HeartbeatView +from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView +from care.abdm.api.viewsets.status import SMSOnNotifyView + + +class OptionalSlashRouter(SimpleRouter): + def __init__(self): + super().__init__() + self.trailing_slash = "/?" + + +abdm_router = OptionalSlashRouter() + +abdm_router.register("profile/v1.0/patients/", HipViewSet, basename="hip") + +abdm_urlpatterns = [ + *abdm_router.urls, + path( + "v0.5/users/auth/on-fetch-modes", + OnFetchView.as_view(), + name="abdm_on_fetch_modes_view", + ), + path( + "v0.5/users/auth/on-init", + OnInitView.as_view(), + name="abdm_on_init_view", + ), + path( + "v0.5/users/auth/on-confirm", + OnConfirmView.as_view(), + name="abdm_on_confirm_view", + ), + path( + "v0.5/users/auth/notify", + AuthNotifyView.as_view(), + name="abdm_auth_notify_view", + ), + path( + "v0.5/links/link/on-add-contexts", + OnAddContextsView.as_view(), + name="abdm_on_add_context_view", + ), + path( + "v0.5/care-contexts/discover", + DiscoverView.as_view(), + name="abdm_discover_view", + ), + path( + "v0.5/links/link/init", + LinkInitView.as_view(), + name="abdm_link_init_view", + ), + path( + "v0.5/links/link/confirm", + LinkConfirmView.as_view(), + name="abdm_link_confirm_view", + ), + path( + "v0.5/consents/hip/notify", + NotifyView.as_view(), + name="abdm_notify_view", + ), + path( + "v0.5/health-information/hip/request", + RequestDataView.as_view(), + name="abdm_request_data_view", + ), + path( + "v0.5/patients/status/notify", + PatientStatusNotifyView.as_view(), + name="abdm_patient_status_notify_view", + ), + path( + "v0.5/patients/sms/on-notify", + SMSOnNotifyView.as_view(), + name="abdm_patient_status_notify_view", + ), + path( + "v0.5/heartbeat", + HeartbeatView.as_view(), + name="abdm_monitoring_heartbeat_view", + ), +] diff --git a/config/api_router.py b/config/api_router.py index 472aa84860..ff6235afb2 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -5,7 +5,6 @@ from care.abdm.api.viewsets.abha import AbhaViewSet from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet -from care.abdm.api.viewsets.hip import HipViewSet from care.facility.api.viewsets.ambulance import ( AmbulanceCreateViewSet, AmbulanceViewSet, @@ -92,19 +91,10 @@ from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet - -class OptionalSlashRouter(SimpleRouter): - def __init__(self): - super().__init__() - self.trailing_slash = "/?" - - if settings.DEBUG: router = DefaultRouter() - # abdm_router = DefaultRouter() else: router = SimpleRouter() -abdm_router = OptionalSlashRouter() router.register("users", UserViewSet) user_nested_router = NestedSimpleRouter(router, r"users", lookup="users") @@ -229,7 +219,6 @@ def __init__(self): # ABDM endpoints if settings.ENABLE_ABDM: router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") - abdm_router.register("profile", HipViewSet, basename="hip") app_name = "api" urlpatterns = [ @@ -241,7 +230,3 @@ def __init__(self): path("", include(resource_nested_router.urls)), path("", include(shifting_nested_router.urls)), ] - -abdm_urlpatterns = [ - path("", include(abdm_router.urls)), -] diff --git a/config/urls.py b/config/urls.py index e068ad87c1..d2c22c5ce1 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,21 +9,7 @@ SpectacularSwaggerView, ) -from care.abdm.api.viewsets.auth import ( - AuthNotifyView, - DiscoverView, - LinkConfirmView, - LinkInitView, - NotifyView, - OnAddContextsView, - OnConfirmView, - OnFetchView, - OnInitView, - RequestDataView, -) -from care.abdm.api.viewsets.monitoring import HeartbeatView -from care.abdm.api.viewsets.status import NotifyView as PatientStatusNotifyView -from care.abdm.api.viewsets.status import SMSOnNotifyView +from care.abdm.urls import abdm_urlpatterns from care.facility.api.viewsets.open_id import OpenIdConfigView from care.hcx.api.viewsets.listener import ( ClaimOnSubmitView, @@ -110,74 +96,7 @@ ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) if settings.ENABLE_ABDM: - urlpatterns += [ - path("v1.0/patients/", include(api_router.abdm_urlpatterns)), - path( - "v0.5/users/auth/on-fetch-modes", - OnFetchView.as_view(), - name="abdm_on_fetch_modes_view", - ), - path( - "v0.5/users/auth/on-init", - OnInitView.as_view(), - name="abdm_on_init_view", - ), - path( - "v0.5/users/auth/on-confirm", - OnConfirmView.as_view(), - name="abdm_on_confirm_view", - ), - path( - "v0.5/users/auth/notify", - AuthNotifyView.as_view(), - name="abdm_auth_notify_view", - ), - path( - "v0.5/links/link/on-add-contexts", - OnAddContextsView.as_view(), - name="abdm_on_add_context_view", - ), - path( - "v0.5/care-contexts/discover", - DiscoverView.as_view(), - name="abdm_discover_view", - ), - path( - "v0.5/links/link/init", - LinkInitView.as_view(), - name="abdm_link_init_view", - ), - path( - "v0.5/links/link/confirm", - LinkConfirmView.as_view(), - name="abdm_link_confirm_view", - ), - path( - "v0.5/consents/hip/notify", - NotifyView.as_view(), - name="abdm_notify_view", - ), - path( - "v0.5/health-information/hip/request", - RequestDataView.as_view(), - name="abdm_request_data_view", - ), - path( - "v0.5/patients/status/notify", - PatientStatusNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/patients/sms/on-notify", - SMSOnNotifyView.as_view(), - name="abdm_patient_status_notify_view", - ), - path( - "v0.5/heartbeat", - HeartbeatView.as_view(), - name="abdm_monitoring_heartbeat_view", - ), - ] + urlpatterns += abdm_urlpatterns if settings.DEBUG: # This allows the error pages to be debugged during development, just visit From 86003fb2e5db228fcc86d8e1944580c13e26a1dc Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 00:50:57 +0530 Subject: [PATCH 171/180] remove verify=False --- care/abdm/utils/api_call.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 4bb5353657..101fe2ca04 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -24,9 +24,7 @@ def encrypt_with_public_key(a_message): rsa_public_key = RSA.importKey( - requests.get( - HEALTH_SERVICE_API_URL + "/v2/auth/cert", verify=False - ).text.strip() + requests.get(HEALTH_SERVICE_API_URL + "/v2/auth/cert").text.strip() ) rsa_public_key = PKCS1_v1_5.new(rsa_public_key) encrypted_text = rsa_public_key.encrypt(a_message.encode()) @@ -72,10 +70,7 @@ def add_auth_header(self, headers): "Accept": "application/json", } resp = requests.post( - ABDM_TOKEN_URL, - data=json.dumps(data), - headers=auth_headers, - verify=False, + ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers ) print("Token Response Status: {}".format(resp.status_code)) if resp.status_code < 300: @@ -113,7 +108,7 @@ def get(self, path, params=None, auth=None): if auth: headers = self.add_user_header(headers, auth) print("Making GET Request to: {}".format(url)) - response = requests.get(url, headers=headers, params=params, verify=False) + response = requests.get(url, headers=headers, params=params) print("{} Response: {}".format(response.status_code, response.text)) return response @@ -135,7 +130,7 @@ def post(self, path, data=None, auth=None, additional_headers=None): data_json = json.dumps(data) # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) print("Posting Request to: {}".format(url)) - response = requests.post(url, headers=headers, data=data_json, verify=False) + response = requests.post(url, headers=headers, data=data_json) print("{} Response: {}".format(response.status_code, response.text)) return response From e96fc0cfcab3520e9681dd64453dac894858befc Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 01:22:51 +0530 Subject: [PATCH 172/180] replace AllowAny with IsAuthenticated --- care/abdm/api/viewsets/auth.py | 22 +++++++++++----------- care/abdm/api/viewsets/hip.py | 4 ++-- care/abdm/api/viewsets/status.py | 6 +++--- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index ca240a4098..c8c9eb5323 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -4,7 +4,7 @@ from django.core.cache import cache from rest_framework import status from rest_framework.generics import GenericAPIView, get_object_or_404 -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway @@ -16,7 +16,7 @@ class OnFetchView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -28,7 +28,7 @@ def post(self, request, *args, **kwargs): class OnInitView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -40,7 +40,7 @@ def post(self, request, *args, **kwargs): class OnConfirmView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -73,7 +73,7 @@ def post(self, request, *args, **kwargs): class AuthNotifyView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -91,7 +91,7 @@ def post(self, request, *args, **kwargs): class OnAddContextsView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -101,7 +101,7 @@ def post(self, request, *args, **kwargs): class DiscoverView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -170,7 +170,7 @@ def post(self, request, *args, **kwargs): class LinkInitView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -190,7 +190,7 @@ def post(self, request, *args, **kwargs): class LinkConfirmView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -224,7 +224,7 @@ def post(self, request, *args, **kwargs): class NotifyView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -242,7 +242,7 @@ def post(self, request, *args, **kwargs): class RequestDataView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index a628d1953d..ac93ef17de 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -3,7 +3,7 @@ from rest_framework import status from rest_framework.decorators import action -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -16,7 +16,7 @@ class HipViewSet(GenericViewSet): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def get_linking_token(self, data): diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index 05ef33757b..a6e4be550b 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -1,6 +1,6 @@ from rest_framework import status from rest_framework.generics import GenericAPIView -from rest_framework.permissions import AllowAny +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from care.abdm.models import AbhaNumber @@ -10,7 +10,7 @@ class NotifyView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -29,7 +29,7 @@ def post(self, request, *args, **kwargs): class SMSOnNotifyView(GenericAPIView): - permission_classes = (AllowAny,) + permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): From 09ff7b2a0de09121cc67179e38f13903d26ffd74 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 01:29:13 +0530 Subject: [PATCH 173/180] user logger to print --- care/abdm/api/viewsets/auth.py | 2 -- care/abdm/api/viewsets/status.py | 2 -- care/abdm/utils/api_call.py | 39 +++++++++++++++++--------------- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index c8c9eb5323..e72b3975d7 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -95,8 +95,6 @@ class OnAddContextsView(GenericAPIView): authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): - data = request.data - print(data) return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index a6e4be550b..8c126ec7ef 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -33,6 +33,4 @@ class SMSOnNotifyView(GenericAPIView): authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): - data = request.data - print(data) return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 101fe2ca04..4ff2a157ea 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -1,4 +1,5 @@ import json +import logging import uuid from base64 import b64encode from datetime import datetime, timedelta, timezone @@ -21,6 +22,8 @@ # TODO: Exception handling for all api calls, need to gracefully handle known exceptions +logger = logging.getLogger(__name__) + def encrypt_with_public_key(a_message): rsa_public_key = RSA.importKey( @@ -60,7 +63,7 @@ def add_user_header(self, headers, user_token): def add_auth_header(self, headers): token = cache.get(ABDM_TOKEN_CACHE_KEY) if not token: - print("No Token in Cache") + logger.info("No Token in Cache") data = { "clientId": settings.ABDM_CLIENT_ID, "clientSecret": settings.ABDM_CLIENT_SECRET, @@ -72,29 +75,29 @@ def add_auth_header(self, headers): resp = requests.post( ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers ) - print("Token Response Status: {}".format(resp.status_code)) + logger.info("Token Response Status: {}".format(resp.status_code)) if resp.status_code < 300: # Checking if Content-Type is application/json if resp.headers["Content-Type"] != "application/json": - print( + logger.info( "Unsupported Content-Type: {}".format( resp.headers["Content-Type"] ) ) - print("Response: {}".format(resp.text)) + logger.info("Response: {}".format(resp.text)) return None else: data = resp.json() token = data["accessToken"] expires_in = data["expiresIn"] - print("New Token: {}".format(token)) - print("Expires in: {}".format(expires_in)) + logger.info("New Token: {}".format(token)) + logger.info("Expires in: {}".format(expires_in)) cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) else: - print("Bad Response: {}".format(resp.text)) + logger.info("Bad Response: {}".format(resp.text)) return None - # print("Returning Authorization Header: Bearer {}".format(token)) - print("Adding Authorization Header") + # logger.info("Returning Authorization Header: Bearer {}".format(token)) + logger.info("Adding Authorization Header") auth_header = {"Authorization": "Bearer {}".format(token)} return {**headers, **auth_header} @@ -107,9 +110,9 @@ def get(self, path, params=None, auth=None): headers = self.add_auth_header(headers) if auth: headers = self.add_user_header(headers, auth) - print("Making GET Request to: {}".format(url)) + logger.info("Making GET Request to: {}".format(url)) response = requests.get(url, headers=headers, params=params) - print("{} Response: {}".format(response.status_code, response.text)) + logger.info("{} Response: {}".format(response.status_code, response.text)) return response def post(self, path, data=None, auth=None, additional_headers=None): @@ -128,10 +131,10 @@ def post(self, path, data=None, auth=None, additional_headers=None): # ['-H "{}: {}"'.format(k, v) for k, v in headers.items()] # ) data_json = json.dumps(data) - # print("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) - print("Posting Request to: {}".format(url)) + # logger.info("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) + logger.info("Posting Request to: {}".format(url)) response = requests.post(url, headers=headers, data=data_json) - print("{} Response: {}".format(response.status_code, response.text)) + logger.info("{} Response: {}".format(response.status_code, response.text)) return response @@ -142,7 +145,7 @@ def __init__(self): def generate_aadhaar_otp(self, data): path = "/v1/registration/aadhaar/generateOtp" response = self.api.post(path, data) - print("{} Response: {}".format(response.status_code, response.text)) + logger.info("{} Response: {}".format(response.status_code, response.text)) return response.json() def resend_aadhaar_otp(self, data): @@ -174,7 +177,7 @@ def verify_mobile_otp(self, data): # /v1/registration/aadhaar/createHealthIdWithPreVerified def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" - print("Creating Health ID with data: {}".format(data)) + logger.info("Creating Health ID with data: {}".format(data)) # data.pop("healthId", None) response = self.api.post(path, data) return response.json() @@ -299,9 +302,9 @@ def get_abha_card_pdf(self, data): def get_qr_code(self, data, auth): path = "/v1/account/qrCode" access_token = self.generate_access_token(data) - print("Getting QR Code for: {}".format(data)) + logger.info("Getting QR Code for: {}".format(data)) response = self.api.get(path, {}, access_token) - print("QR Code Response: {}".format(response.text)) + logger.info("QR Code Response: {}".format(response.text)) return response.json() From 5275faf07396777deb26d9f8a54ff875cea47a0b Mon Sep 17 00:00:00 2001 From: Bhavik Agarwal <73033511+Bhavik-ag@users.noreply.github.com> Date: Fri, 21 Jul 2023 09:38:24 +0530 Subject: [PATCH 174/180] add created_by_local_user_content field for patients notes (#1445) * replace id with external_id for home_facility in UserBaseMinimumSerializer * lint code base * Used ExternalIdSerializerField for external_id * added created_by_local_user field * updated tests for patient note * added test for user from different facility --------- Co-authored-by: Vignesh Hari Co-authored-by: Aakash Singh --- care/facility/api/serializers/patient.py | 9 +++++++- care/facility/api/viewsets/patient.py | 13 ++++++++++- care/facility/tests/test_patient_api.py | 29 ++++++++++++++++++++---- care/users/api/serializers/user.py | 1 - 4 files changed, 45 insertions(+), 7 deletions(-) diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index e1fe82f098..b886b6b0f8 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -455,6 +455,7 @@ def save(self, **kwargs): class PatientNotesSerializer(serializers.ModelSerializer): facility = FacilityBasicInfoSerializer(read_only=True) created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) + created_by_local_user = serializers.BooleanField(read_only=True) def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -463,5 +464,11 @@ def validate_empty_values(self, data): class Meta: model = PatientNotes - fields = ("note", "facility", "created_by_object", "created_date") + fields = ( + "note", + "facility", + "created_by_object", + "created_by_local_user", + "created_date", + ) read_only_fields = ("created_date",) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 6f458c2902..5e895eab62 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -5,7 +5,7 @@ from django.conf import settings from django.contrib.postgres.search import TrigramSimilarity from django.db import models -from django.db.models import Case, When +from django.db.models import BooleanField, Case, F, Value, When from django.db.models.query_utils import Q from django_filters import rest_framework as filters from djqscsv import render_to_csv_response @@ -595,6 +595,16 @@ class PatientNotesViewSet( queryset = ( PatientNotes.objects.all() .select_related("facility", "patient", "created_by") + .annotate( + created_by_local_user=Case( + When( + created_by__home_facility__external_id=F("facility__external_id"), + then=Value(True), + ), + default=Value(False), + output_field=BooleanField(), + ) + ) .order_by("-created_date") ) serializer_class = PatientNotesSerializer @@ -617,6 +627,7 @@ def get_queryset(self): q_filters |= Q(patient__last_consultation__assigned_to=user) q_filters |= Q(patient__assigned_to=user) queryset = queryset.filter(q_filters) + return queryset def perform_create(self, serializer): diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 86d96ade62..fb5b8bdd18 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -13,6 +13,7 @@ class ExpectedPatientNoteKeys(Enum): FACILITY = "facility" CREATED_BY_OBJECT = "created_by_object" CREATED_DATE = "created_date" + CREATED_BY_LOCAL_USER = "created_by_local_user" class ExpectedFacilityKeys(Enum): @@ -71,7 +72,6 @@ class ExpectedCreatedByObjectKeys(Enum): LAST_NAME = "last_name" USER_TYPE = "user_type" LAST_LOGIN = "last_login" - HOME_FACILITY = "home_facility" class PatientNotesTestCase(TestBase, TestClassMixin, APITestCase): @@ -85,11 +85,23 @@ def setUp(self): # Create users and facility self.user = self.create_user(district=district, username="test user") facility = self.create_facility(district=district, user=self.user) + self.user.home_facility = facility + self.user.save() + + # Create another user from different facility + self.user2 = self.create_user(district=district, username="test user 2") + facility2 = self.create_facility(district=district, user=self.user2) + self.user2.home_facility = facility2 + self.user2.save() self.patient = self.create_patient(district=district.id) self.patient_note = self.create_patient_note( - patient=self.patient, facility=facility + patient=self.patient, facility=facility, created_by=self.user + ) + + self.patient_note2 = self.create_patient_note( + patient=self.patient, facility=facility, created_by=self.user2 ) refresh_token = RefreshToken.for_user(self.user) @@ -103,14 +115,23 @@ def test_patient_notes(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertIsInstance(response.json()["results"], list) - # Ensure only necessary data is being sent and no extra data + # Test created_by_local_user field if user is not from same facility as patient + data2 = response.json()["results"][0] - data = response.json()["results"][0] + created_by_local_user_content2 = data2["created_by_local_user"] + self.assertEqual(created_by_local_user_content2, False) + + # Ensure only necessary data is being sent and no extra data + data = response.json()["results"][1] self.assertCountEqual( data.keys(), [item.value for item in ExpectedPatientNoteKeys] ) + created_by_local_user_content = data["created_by_local_user"] + + self.assertEqual(created_by_local_user_content, True) + facility_content = data["facility"] if facility_content is not None: diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 61fff8759a..b845d3bdc4 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -344,7 +344,6 @@ class Meta: "last_name", "user_type", "last_login", - "home_facility", ) From 80a2ce1e6ecb455d6d50f72aa75aaec108e9da90 Mon Sep 17 00:00:00 2001 From: Vaibhav Sachdeva <72242181+sachdevavaibhav@users.noreply.github.com> Date: Fri, 21 Jul 2023 09:38:59 +0530 Subject: [PATCH 175/180] added referred_to_external field PatientConsultationDischargeSerializer (#1430) * added referred_to_external field PatientConsultationDischargeSerializer * pre-commit formatting * removed redundant referred_to_external field definition * tests for checking the input of referred_to_external field * remove invalid test Signed-off-by: Aakash Singh --------- Signed-off-by: Aakash Singh Co-authored-by: Vignesh Hari Co-authored-by: Aakash Singh --- .../api/serializers/patient_consultation.py | 1 + .../tests/test_patient_consultation_api.py | 43 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 97ebc55220..ede82fda1a 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -419,6 +419,7 @@ class Meta: model = PatientConsultation fields = ( "discharge_reason", + "referred_to_external", "discharge_notes", "discharge_date", "discharge_prescription", diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index ff55865c27..a3993f6c22 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -188,3 +188,46 @@ def test_discharge_as_expired_after_admission(self): discharge_date="2319-04-01T15:30:00Z", ) self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_referred_to_external_null(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="REF", + discharge_date="2023-07-01T12:00:00Z", + discharge_notes="Discharged with null referred_to_external", + referred_to_external=None, + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_referred_to_external_empty_string(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + res = self.discharge( + consultation, + discharge_reason="REF", + discharge_date="2023-07-01T12:00:00Z", + discharge_notes="Discharged with empty referred_to_external", + referred_to_external="", + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + def test_referred_to_external_valid_value(self): + consultation = self.create_admission_consultation( + suggestion="A", + admission_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + referred_to_external = "Test Hospital" + res = self.discharge( + consultation, + discharge_reason="REF", + discharge_date="2023-07-01T12:00:00Z", + discharge_notes="Discharged with valid referred_to_external", + referred_to_external=referred_to_external, + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) From a46ac29fdf15c4ff8b709f29a17595904e8e7820 Mon Sep 17 00:00:00 2001 From: khavinshankar Date: Fri, 21 Jul 2023 11:38:44 +0530 Subject: [PATCH 176/180] removed empty validators --- care/abdm/api/serializers/healthid.py | 30 +++++++++++++-------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py index 8c5b98d086..aa2b7cc1fd 100644 --- a/care/abdm/api/serializers/healthid.py +++ b/care/abdm/api/serializers/healthid.py @@ -2,11 +2,11 @@ class AadharOtpGenerateRequestPayloadSerializer(Serializer): - aadhaar = CharField(max_length=16, min_length=12, required=True, validators=[]) + aadhaar = CharField(max_length=16, min_length=12, required=True) class AadharOtpResendRequestPayloadSerializer(Serializer): - txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) + txnId = CharField(max_length=64, min_length=1, required=True) class HealthIdSerializer(Serializer): @@ -38,26 +38,24 @@ class ABHASearchRequestSerializer: class GenerateMobileOtpRequestPayloadSerializer(Serializer): - mobile = CharField(max_length=10, min_length=10, required=True, validators=[]) - txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) + mobile = CharField(max_length=10, min_length=10, required=True) + txnId = CharField(max_length=64, min_length=1, required=True) class VerifyOtpRequestPayloadSerializer(Serializer): - otp = CharField( - max_length=6, min_length=6, required=True, help_text="OTP", validators=[] - ) - txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) - patientId = UUIDField(required=False, validators=[]) + otp = CharField(max_length=6, min_length=6, required=True, help_text="OTP") + txnId = CharField(max_length=64, min_length=1, required=True) + patientId = UUIDField(required=False) class VerifyDemographicsRequestPayloadSerializer(Serializer): - gender = CharField(max_length=10, min_length=1, required=True, validators=[]) - name = CharField(max_length=64, min_length=1, required=True, validators=[]) - yearOfBirth = CharField(max_length=4, min_length=4, required=True, validators=[]) - txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) + gender = CharField(max_length=10, min_length=1, required=True) + name = CharField(max_length=64, min_length=1, required=True) + yearOfBirth = CharField(max_length=4, min_length=4, required=True) + txnId = CharField(max_length=64, min_length=1, required=True) class CreateHealthIdSerializer(Serializer): - healthId = CharField(max_length=64, min_length=1, required=False, validators=[]) - txnId = CharField(max_length=64, min_length=1, required=True, validators=[]) - patientId = UUIDField(required=False, validators=[]) + healthId = CharField(max_length=64, min_length=1, required=False) + txnId = CharField(max_length=64, min_length=1, required=True) + patientId = UUIDField(required=False) From 9bf6f74bbf24f922ac1f988e6d5097eed82b5f9a Mon Sep 17 00:00:00 2001 From: Mathew Date: Fri, 21 Jul 2023 20:20:15 +0530 Subject: [PATCH 177/180] Update backend.json --- aws/backend.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/aws/backend.json b/aws/backend.json index ec3dc98474..a080073629 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -100,6 +100,26 @@ { "name": "USE_S3", "value": "True" + }, + { + "name": "ENABLE_ABDM", + "value": "True" + }, + { + "name": "ABDM_URL", + "value": "https://dev.abdm.gov.in" + }, + { + "name": "HEALTH_SERVICE_API_URL", + "value": "https://healthidsbx.abdm.gov.in/api" + }, + { + "name": "X_CM_ID", + "value": "sbx" + }, + { + "name": "FIDELIUS_URL", + "value": "https://fidelius.ohc.network" } ], "repositoryCredentials": { @@ -222,6 +242,14 @@ { "valueFrom": "/care/backend/HCX_IG_URL", "name": "HCX_IG_URL" + }, + { + "valueFrom": "/care/backend/ABDM_CLIENT_ID", + "name": "ABDM_CLIENT_ID" + }, + { + "valueFrom": "/care/backend/ABDM_CLIENT_SECRET", + "name": "ABDM_CLIENT_SECRET" } ], "name": "care-backend" From 79356a11e12e3bef57e3a14273e220569d4b0ae7 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sat, 22 Jul 2023 07:54:50 +0000 Subject: [PATCH 178/180] Pop "patient_category" in local `validated_data` instead of `self.validated_data` (#1471) fixes #1470, pop in local `validated_data` --- care/facility/api/serializers/shifting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/shifting.py b/care/facility/api/serializers/shifting.py index 21b8b5c458..8be8fc8863 100644 --- a/care/facility/api/serializers/shifting.py +++ b/care/facility/api/serializers/shifting.py @@ -329,7 +329,7 @@ def update(self, instance, validated_data): new_instance = super().update(instance, validated_data) patient = new_instance.patient - patient_category = self.validated_data.pop("patient_category") + patient_category = validated_data.pop("patient_category") if patient.last_consultation and patient_category is not None: patient.last_consultation.category = patient_category patient.last_consultation.save(update_fields=["category"]) @@ -394,7 +394,7 @@ def create(self, validated_data): patient.allow_transfer = True patient.save() - patient_category = self.validated_data.pop("patient_category") + patient_category = validated_data.pop("patient_category") if patient.last_consultation and patient_category is not None: patient.last_consultation.category = patient_category patient.last_consultation.save(update_fields=["category"]) From 8f307f58cb89e565949624eb813ddfe5defc6a82 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sat, 22 Jul 2023 21:57:28 +0000 Subject: [PATCH 179/180] Adds `REDIS_URL` to `.local.env` (#1468) --- docker/.local.env | 1 + 1 file changed, 1 insertion(+) diff --git a/docker/.local.env b/docker/.local.env index 0de74cbf10..e755fb55b5 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -4,6 +4,7 @@ POSTGRES_HOST=db POSTGRES_DB=care POSTGRES_PORT=5432 DATABASE_URL=postgres://postgres:postgres@db:5432/care +REDIS_URL=redis://redis:6379 CELERY_BROKER_URL=redis://redis:6379/0 DJANGO_DEBUG=False From d4dcbce4dfc003d00690514a2db48240b8add11d Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 25 Jul 2023 11:49:22 +0530 Subject: [PATCH 180/180] handle exception in template date parsing Signed-off-by: Aakash Singh --- care/facility/templatetags/filters.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/care/facility/templatetags/filters.py b/care/facility/templatetags/filters.py index e3e4910300..9a8bc576fa 100644 --- a/care/facility/templatetags/filters.py +++ b/care/facility/templatetags/filters.py @@ -27,4 +27,7 @@ def field_name_to_label(value): @register.filter(expects_localtime=True) def parse_datetime(value): - return datetime.strptime(value, "%Y-%m-%dT%H:%M") + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M") + except ValueError: + return None