From 0b9773326d18c5870a1fefc5ccfd98bdad34aabd Mon Sep 17 00:00:00 2001 From: syamkumar Date: Mon, 14 Oct 2024 09:58:19 +0530 Subject: [PATCH 01/14] ADDITIONAL_PLUGS build arg (#2535) ADDITIONAL_PLUGS build arg --- .github/workflows/deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e2aa44c12f..cccc03b16c 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -102,6 +102,7 @@ jobs: tags: ${{ steps.meta.outputs.tags }} build-args: | APP_VERSION=${{ github.sha }} + ADDITIONAL_PLUGS=${{ secrets.ADDITIONAL_PLUGS }} cache-from: type=local,src=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max From a7fb6ea9e31e574bad795374bd8eda316f14884e Mon Sep 17 00:00:00 2001 From: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> Date: Mon, 14 Oct 2024 12:04:23 +0200 Subject: [PATCH 02/14] Modified the dummy data to support new cypress test (#2536) modified the dummy data --- data/dummy/facility.json | 502 +++++++++++++++++++++++++-------------- data/dummy/users.json | 42 ++++ 2 files changed, 372 insertions(+), 172 deletions(-) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index cea1986085..17d98574ff 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -12,14 +12,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [ - 1, - 2, - 3, - 4, - 5, - 6 - ], + "features": "[\"1\", \"2\", \"3\", \"4\", \"5\", \"6\"]", "longitude": null, "latitude": null, "pincode": 670000, @@ -56,10 +49,7 @@ "verified": false, "facility_type": 1300, "kasp_empanelled": false, - "features": [ - 1, - 6 - ], + "features": "[\"1\", \"6\"]", "longitude": null, "latitude": null, "pincode": 670112, @@ -96,11 +86,7 @@ "verified": false, "facility_type": 1500, "kasp_empanelled": false, - "features": [ - 1, - 4, - 6 - ], + "features": "[\"1\", \"4\", \"6\"]", "longitude": "78.6757364624373000", "latitude": "21.4009146842158660", "pincode": 670000, @@ -137,11 +123,7 @@ "verified": false, "facility_type": 1510, "kasp_empanelled": false, - "features": [ - 1, - 3, - 5 - ], + "features": "[\"1\", \"3\", \"5\"]", "longitude": "75.2139014820876600", "latitude": "18.2774285038890340", "pincode": 670000, @@ -178,7 +160,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -215,7 +197,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -252,7 +234,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -289,7 +271,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -326,7 +308,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -363,7 +345,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -400,7 +382,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -437,7 +419,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -474,7 +456,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -511,7 +493,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -548,7 +530,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -585,7 +567,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -622,7 +604,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -659,7 +641,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -696,7 +678,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -733,7 +715,7 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [], + "features": "[]", "longitude": null, "latitude": null, "pincode": 682001, @@ -1055,6 +1037,15 @@ "created_by": 2 } }, + { + "model": "facility.facilityuser", + "pk": 25, + "fields": { + "facility": 1, + "user": 25, + "created_by": 2 + } + }, { "model": "facility.assetlocation", "pk": 1, @@ -1984,6 +1975,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-01T08:35:00Z", @@ -2011,7 +2003,8 @@ "weight": 0.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2043,6 +2036,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:33:03.700Z", @@ -2070,7 +2064,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2102,6 +2097,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:39:29.394Z", @@ -2129,7 +2125,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2161,6 +2158,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:10.532Z", @@ -2188,7 +2186,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2220,6 +2219,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:33.614Z", @@ -2247,7 +2247,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2279,6 +2280,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:42:56.180Z", @@ -2306,7 +2308,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2338,6 +2341,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:43:18.480Z", @@ -2365,7 +2369,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2397,6 +2402,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:43:41.540Z", @@ -2424,7 +2430,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2456,6 +2463,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:05.398Z", @@ -2483,7 +2491,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2515,6 +2524,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:28.550Z", @@ -2542,7 +2552,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2574,6 +2585,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:44:51.239Z", @@ -2601,7 +2613,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2633,6 +2646,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:45:13.721Z", @@ -2660,7 +2674,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2692,6 +2707,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:45:37.972Z", @@ -2719,7 +2735,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2751,6 +2768,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:00.645Z", @@ -2778,7 +2796,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2810,6 +2829,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:23.492Z", @@ -2837,7 +2857,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2869,6 +2890,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:46:46.028Z", @@ -2896,7 +2918,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2928,6 +2951,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:11.141Z", @@ -2955,7 +2979,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -2987,6 +3012,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:34.395Z", @@ -3014,7 +3040,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3046,6 +3073,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-07T08:47:53.746Z", @@ -3073,7 +3101,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3105,6 +3134,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-15T08:47:53.746Z", @@ -3132,7 +3162,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3140,8 +3171,8 @@ "pk": 21, "fields": { "external_id": "40fa5cc6-6199-48cd-bc2a-dd9e73b920f9", - "created_date": "2024-1-30T08:47:53.746Z", - "modified_date": "2024-1-30T08:47:53.746Z", + "created_date": "2024-01-30T08:47:53.746Z", + "modified_date": "2024-01-30T08:47:53.746Z", "deleted": false, "patient": 18, "patient_no": "IP0010", @@ -3164,9 +3195,10 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, - "encounter_date": "2024-1-30T08:47:53.746Z", + "encounter_date": "2024-01-30T08:47:53.746Z", "icu_admission_date": null, "discharge_date": null, "discharge_reason": null, @@ -3191,7 +3223,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3199,8 +3232,8 @@ "pk": 22, "fields": { "external_id": "40faecc6-6199-48cd-bc2a-6d9e73b920f9", - "created_date": "2024-2-28T08:47:53.746Z", - "modified_date": "2024-2-28T08:47:53.746Z", + "created_date": "2024-02-28T08:47:53.746Z", + "modified_date": "2024-02-28T08:47:53.746Z", "deleted": false, "patient": 18, "patient_no": "IP011", @@ -3223,9 +3256,10 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, - "encounter_date": "2024-2-28T08:47:53.746Z", + "encounter_date": "2024-02-28T08:47:53.746Z", "icu_admission_date": null, "discharge_date": null, "discharge_reason": null, @@ -3250,7 +3284,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3282,6 +3317,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2024-04-01T08:47:53.746Z", @@ -3309,7 +3345,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3341,6 +3378,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2022-05-06T08:47:53.746Z", @@ -3368,7 +3406,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3400,6 +3439,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2022-02-06T08:47:53.746Z", @@ -3427,7 +3467,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3459,6 +3500,7 @@ "referred_from_facility": null, "referred_from_facility_external": "", "referred_by_external": "", + "previous_consultation": null, "is_readmission": false, "admitted": true, "encounter_date": "2023-12-06T08:47:34.395Z", @@ -3486,7 +3528,8 @@ "weight": 170.0, "operation": null, "special_instruction": "", - "intubation_history": [] + "intubation_history": [], + "has_consents": "[]" } }, { @@ -3585,6 +3628,54 @@ "location": 1 } }, + { + "model": "facility.bed", + "pk": 9, + "fields": { + "external_id": "a9ea05d2-4f58-4fc2-bc24-2979a3502dc4", + "created_date": "2024-10-14T07:56:52.641Z", + "modified_date": "2024-10-14T07:56:52.641Z", + "deleted": false, + "name": "Dummy Bed 7", + "description": "", + "bed_type": 2, + "facility": 1, + "meta": {}, + "location": 2 + } + }, + { + "model": "facility.bed", + "pk": 10, + "fields": { + "external_id": "aff5a088-c278-4075-9a4c-64453f50f216", + "created_date": "2024-10-14T07:56:59.896Z", + "modified_date": "2024-10-14T07:56:59.896Z", + "deleted": false, + "name": "Dummy Bed 8", + "description": "", + "bed_type": 6, + "facility": 1, + "meta": {}, + "location": 2 + } + }, + { + "model": "facility.bed", + "pk": 11, + "fields": { + "external_id": "882ce4de-4e2c-4afa-b3e6-29fceec30730", + "created_date": "2024-10-14T07:57:06.701Z", + "modified_date": "2024-10-14T07:57:06.701Z", + "deleted": false, + "name": "Dummy Bed 9", + "description": "", + "bed_type": 2, + "facility": 1, + "meta": {}, + "location": 2 + } + }, { "model": "facility.consultationdiagnosis", "pk": 1, @@ -3985,14 +4076,8 @@ "default_unit": 1, "description": "", "min_quantity": 150.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 1, - 2 - ] + "allowed_units": [1, 2], + "tags": [1, 2] } }, { @@ -4003,13 +4088,8 @@ "default_unit": 1, "description": "", "min_quantity": 2.0, - "allowed_units": [ - 1, - 2 - ], - "tags": [ - 2 - ] + "allowed_units": [1, 2], + "tags": [2] } }, { @@ -4020,12 +4100,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -4036,9 +4112,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4050,9 +4124,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4064,9 +4136,7 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [ - 4 - ], + "allowed_units": [4], "tags": [] } }, @@ -4078,12 +4148,8 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [ - 7 - ], - "tags": [ - 2 - ] + "allowed_units": [7], + "tags": [2] } }, { @@ -4107,8 +4173,10 @@ "pincode": 600115, "date_of_birth": "2005-10-16", "year_of_birth": 2005, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4123,6 +4191,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 5896, "local_body": 95, @@ -4186,8 +4256,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4202,6 +4274,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4265,8 +4339,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4281,6 +4357,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4344,8 +4422,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4360,6 +4440,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4423,8 +4505,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4439,6 +4523,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4502,8 +4588,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4518,6 +4606,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4581,8 +4671,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4597,6 +4689,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4660,8 +4754,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4676,6 +4772,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4739,8 +4837,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4755,6 +4855,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4818,8 +4920,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4834,6 +4938,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4897,8 +5003,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4913,6 +5021,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -4976,8 +5086,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -4992,6 +5104,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5055,8 +5169,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5071,6 +5187,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5134,8 +5252,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5150,6 +5270,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5213,8 +5335,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5229,6 +5353,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5292,8 +5418,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5308,6 +5436,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5371,8 +5501,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5387,6 +5519,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -5450,8 +5584,10 @@ "pincode": 682001, "date_of_birth": "2001-01-01", "year_of_birth": 2001, + "death_datetime": null, "nationality": "India", "passport_no": "", + "ration_card_category": null, "is_medical_worker": false, "blood_group": "O+", "contact_with_confirmed_carrier": false, @@ -5466,6 +5602,8 @@ "ongoing_medication": "", "has_SARI": false, "is_antenatal": false, + "last_menstruation_start_date": null, + "date_of_delivery": null, "ward_old": "", "ward": 15162, "local_body": 6, @@ -7122,6 +7260,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7151,6 +7291,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7180,6 +7322,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7209,6 +7353,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7238,6 +7384,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7267,6 +7415,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7296,6 +7446,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7325,6 +7477,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7354,6 +7508,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7383,6 +7539,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7412,6 +7570,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7441,6 +7601,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7470,6 +7632,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7499,6 +7663,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7528,6 +7694,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7557,6 +7725,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7586,6 +7756,8 @@ "route": null, "base_dosage": "3 mg", "dosage_type": "REGULAR", + "target_dosage": null, + "instruction_on_titration": null, "frequency": "BD", "days": null, "indicator": null, @@ -7877,8 +8049,8 @@ "pk": 9, "fields": { "external_id": "09876543-210e-4567-a890-1234567890ab", - "created_date": "2023-12-06T09:00:00.000Z", - "modified_date": "2023-12-06T09:00:00.000Z", + "created_date": "2023-12-06T09:00:00Z", + "modified_date": "2023-12-06T09:00:00Z", "deleted": false, "origin_facility": 1, "shifting_approving_facility": 2, @@ -8008,40 +8180,6 @@ "last_edited_by": 1 } }, - { - "model": "facility.shiftingrequest", - "pk": 15, - "fields": { - "external_id": "98765432-10e2-40f1-a0b9-876543210abc", - "created_date": "2023-09-05T22:50:10.221Z", - "modified_date": "2023-12-04T14:30:45.501Z", - "deleted": false, - "origin_facility": 1, - "shifting_approving_facility": 2, - "assigned_facility_type": 2, - "assigned_facility": null, - "assigned_facility_external": null, - "patient": 15, - "emergency": true, - "is_up_shift": true, - "reason": "Test", - "vehicle_preference": "", - "preferred_vehicle_choice": 10, - "comments": "", - "refering_facility_contact_name": "Someone at Facility", - "refering_facility_contact_number": "+914455666777", - "is_kasp": false, - "status": 100, - "breathlessness_level": 30, - "is_assigned_to_user": false, - "assigned_to": null, - "ambulance_driver_name": "", - "ambulance_phone_number": "", - "ambulance_number": "", - "created_by": 2, - "last_edited_by": 2 - } - }, { "model": "facility.shiftingrequest", "pk": 14, @@ -8114,6 +8252,10 @@ "model": "facility.resourcerequest", "pk": 1, "fields": { + "external_id": "067ad8fc-2551-4267-8a09-6facaebf0e1f", + "created_date": "2023-09-05T22:50:10.221Z", + "modified_date": "2023-09-05T22:50:10.221Z", + "deleted": false, "origin_facility": 1, "approving_facility": 2, "assigned_facility": 3, @@ -8131,15 +8273,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 1, - "last_edited_by": 1, - "created_date": "2023-09-05T22:50:10.221Z", - "modified_date": "2023-09-05T22:50:10.221Z" + "last_edited_by": 1 } }, { "model": "facility.resourcerequest", "pk": 2, "fields": { + "external_id": "0c77c84a-3b91-419c-807c-65e53f11a74f", + "created_date": "2023-09-06T22:50:10.221Z", + "modified_date": "2023-09-06T22:50:10.221Z", + "deleted": false, "origin_facility": 1, "approving_facility": 5, "assigned_facility": 8, @@ -8157,15 +8301,17 @@ "is_assigned_to_user": true, "assigned_to": 3, "created_by": 2, - "last_edited_by": 2, - "created_date": "2023-09-06T22:50:10.221Z", - "modified_date": "2023-09-06T22:50:10.221Z" + "last_edited_by": 2 } }, { "model": "facility.resourcerequest", "pk": 3, "fields": { + "external_id": "b08312cc-c301-46c0-8347-c521cbae8c54", + "created_date": "2023-09-07T22:50:10.221Z", + "modified_date": "2023-09-07T22:50:10.221Z", + "deleted": false, "origin_facility": 7, "approving_facility": 2, "assigned_facility": 3, @@ -8183,15 +8329,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 3, - "last_edited_by": 3, - "created_date": "2023-09-07T22:50:10.221Z", - "modified_date": "2023-09-07T22:50:10.221Z" + "last_edited_by": 3 } }, { "model": "facility.resourcerequest", "pk": 4, "fields": { + "external_id": "e9664dcf-224f-4e1e-a003-c4ff0d3699d3", + "created_date": "2023-09-08T22:50:10.221Z", + "modified_date": "2023-09-08T22:50:10.221Z", + "deleted": false, "origin_facility": 9, "approving_facility": 1, "assigned_facility": 4, @@ -8209,15 +8357,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 4, - "last_edited_by": 4, - "created_date": "2023-09-08T22:50:10.221Z", - "modified_date": "2023-09-08T22:50:10.221Z" + "last_edited_by": 4 } }, { "model": "facility.resourcerequest", "pk": 5, "fields": { + "external_id": "ce300e4d-fae6-4ee4-8bf9-f70b98abd7be", + "created_date": "2023-09-09T22:50:10.221Z", + "modified_date": "2023-09-09T22:50:10.221Z", + "deleted": false, "origin_facility": 5, "approving_facility": 6, "assigned_facility": 7, @@ -8235,15 +8385,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 5, - "last_edited_by": 5, - "created_date": "2023-09-09T22:50:10.221Z", - "modified_date": "2023-09-09T22:50:10.221Z" + "last_edited_by": 5 } }, { "model": "facility.resourcerequest", "pk": 6, "fields": { + "external_id": "79b90b87-1cac-449b-95d2-656f0c543652", + "created_date": "2023-09-10T22:50:10.221Z", + "modified_date": "2023-09-10T22:50:10.221Z", + "deleted": false, "origin_facility": 12, "approving_facility": 9, "assigned_facility": 1, @@ -8261,15 +8413,17 @@ "is_assigned_to_user": true, "assigned_to": 6, "created_by": 6, - "last_edited_by": 6, - "created_date": "2023-09-10T22:50:10.221Z", - "modified_date": "2023-09-10T22:50:10.221Z" + "last_edited_by": 6 } }, { "model": "facility.resourcerequest", "pk": 7, "fields": { + "external_id": "fb5fcb52-8640-4a34-95f3-8adaa7c34f6d", + "created_date": "2023-09-11T22:50:10.221Z", + "modified_date": "2023-09-11T22:50:10.221Z", + "deleted": false, "origin_facility": 10, "approving_facility": 11, "assigned_facility": 9, @@ -8287,15 +8441,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 7, - "last_edited_by": 7, - "created_date": "2023-09-11T22:50:10.221Z", - "modified_date": "2023-09-11T22:50:10.221Z" + "last_edited_by": 7 } }, { "model": "facility.resourcerequest", "pk": 8, "fields": { + "external_id": "94ac6a65-cf45-4a01-aa5a-11238ae7dca5", + "created_date": "2023-09-12T22:50:10.221Z", + "modified_date": "2023-09-12T22:50:10.221Z", + "deleted": false, "origin_facility": 15, "approving_facility": 7, "assigned_facility": 6, @@ -8313,15 +8469,17 @@ "is_assigned_to_user": true, "assigned_to": 8, "created_by": 8, - "last_edited_by": 8, - "created_date": "2023-09-12T22:50:10.221Z", - "modified_date": "2023-09-12T22:50:10.221Z" + "last_edited_by": 8 } }, { "model": "facility.resourcerequest", "pk": 9, "fields": { + "external_id": "b8137245-82de-48d8-add1-132d8cf4458a", + "created_date": "2023-09-13T22:50:10.221Z", + "modified_date": "2023-09-13T22:50:10.221Z", + "deleted": false, "origin_facility": 3, "approving_facility": 9, "assigned_facility": 1, @@ -8339,15 +8497,17 @@ "is_assigned_to_user": false, "assigned_to": null, "created_by": 9, - "last_edited_by": 9, - "created_date": "2023-09-13T22:50:10.221Z", - "modified_date": "2023-09-13T22:50:10.221Z" + "last_edited_by": 9 } }, { "model": "facility.resourcerequest", "pk": 10, "fields": { + "external_id": "996d4aa0-3694-4b05-bb75-53a194d65158", + "created_date": "2023-09-14T22:50:10.221Z", + "modified_date": "2023-09-14T22:50:10.221Z", + "deleted": false, "origin_facility": 10, "approving_facility": 1, "assigned_facility": 5, @@ -8365,9 +8525,7 @@ "is_assigned_to_user": true, "assigned_to": 10, "created_by": 10, - "last_edited_by": 10, - "created_date": "2023-09-14T22:50:10.221Z", - "modified_date": "2023-09-14T22:50:10.221Z" + "last_edited_by": 10 } } ] diff --git a/data/dummy/users.json b/data/dummy/users.json index fc55f68670..e7b0115614 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -886,5 +886,47 @@ "groups": [], "user_permissions": [] } + }, + { + "model": "users.user", + "pk": 25, + "fields": { + "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$bUNTR1MwejJYNXdXd2VUYjJHMmN5bw$alS6S9Ay3bvIHe9U18luyn7LyVaArgrgHIt+vh4ta48", + "last_login": null, + "is_superuser": false, + "first_name": "Dev", + "last_name": "Doctor Two", + "email": "devdoctor1@test.com", + "is_staff": false, + "is_active": true, + "date_joined": "2024-10-14T07:53:32.400Z", + "external_id": "009c4fc2-f7af-4a02-9383-6fbb4af2fdbb", + "username": "devdoctor1", + "user_type": 15, + "created_by": 2, + "ward": null, + "local_body": null, + "district": 7, + "state": 1, + "phone_number": "+917644536346", + "alt_phone_number": "+917644536346", + "video_connect_link": null, + "gender": 1, + "date_of_birth": "2005-01-01", + "profile_picture_url": null, + "home_facility": null, + "weekly_working_hours": null, + "qualification": "MBBS", + "doctor_experience_commenced_on": "2020-10-14", + "doctor_medical_council_registration": "23532093", + "verified": true, + "deleted": false, + "pf_endpoint": null, + "pf_p256dh": null, + "pf_auth": null, + "asset": null, + "groups": [], + "user_permissions": [] + } } ] From 02c90560eb936eb5610344afe1ccc39fff6064b6 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 14 Oct 2024 19:05:34 +0530 Subject: [PATCH 03/14] load additional plugs on manager startup (#2537) * load additional plugs on manager startup * cleanup --- install_plugins.py | 15 --------------- plugs/manager.py | 13 +++++++++++++ 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/install_plugins.py b/install_plugins.py index b320f0caaf..8324ff795b 100644 --- a/install_plugins.py +++ b/install_plugins.py @@ -1,18 +1,3 @@ -import json -import logging -import os - from plug_config import manager -from plugs.plug import Plug - -logger = logging.getLogger(__name__) - - -if ADDITIONAL_PLUGS := os.getenv("ADDITIONAL_PLUGS"): - try: - for plug in json.loads(ADDITIONAL_PLUGS): - manager.add_plug(Plug(**plug)) - except json.JSONDecodeError: - logger.error("ADDITIONAL_PLUGS is not a valid JSON") manager.install() diff --git a/plugs/manager.py b/plugs/manager.py index dfdac9fa93..2e4516ebb6 100644 --- a/plugs/manager.py +++ b/plugs/manager.py @@ -1,9 +1,14 @@ +import json +import logging +import os import subprocess import sys from collections import defaultdict from plugs.plug import Plug +logger = logging.getLogger(__name__) + class PlugManager: """ @@ -13,6 +18,14 @@ class PlugManager: def __init__(self, plugs: list[Plug]): self.plugs: list[Plug] = plugs + # load additional plugs from environment variable + if additional_plugs := os.getenv("ADDITIONAL_PLUGS"): + try: + for plug in json.loads(additional_plugs): + self.add_plug(Plug(**plug)) + except json.JSONDecodeError: + logger.error("ADDITIONAL_PLUGS is not a valid JSON") + def install(self) -> None: packages: list[str] = [f"{x.package_name}{x.version}" for x in self.plugs] if packages: From e440d463fedac40c042bbd5e7db76b127b995326 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 15 Oct 2024 12:19:40 +0530 Subject: [PATCH 04/14] Adds route to fetch facility hubs (#2520) * Sending hub and spoke data in facility serializer * remove unwanted changes * Added /hubs route to list hubs * Update care/facility/api/viewsets/facility.py Co-authored-by: Aakash Singh --------- Co-authored-by: Aakash Singh --- care/facility/api/viewsets/facility.py | 16 ++++++++++++++++ care/facility/tests/test_facility_api.py | 19 +++++++++++++++++++ config/api_router.py | 2 ++ 3 files changed, 37 insertions(+) diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index dd8221c0cb..1f0ac69442 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -204,3 +204,19 @@ def get_serializer_context(self): context = super().get_serializer_context() context["facility"] = facility return context + + +class FacilityHubsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + queryset = FacilityHubSpoke.objects.all().select_related("spoke", "hub") + serializer_class = FacilitySpokeSerializer + permission_classes = (IsAuthenticated,) + lookup_field = "external_id" + + def get_queryset(self): + return self.queryset.filter(spoke=self.get_facility()) + + def get_facility(self): + facilities = get_facility_queryset(self.request.user) + return get_object_or_404( + facilities.filter(external_id=self.kwargs["facility_external_id"]) + ) diff --git a/care/facility/tests/test_facility_api.py b/care/facility/tests/test_facility_api.py index 800f45fb8e..b86cb0db86 100644 --- a/care/facility/tests/test_facility_api.py +++ b/care/facility/tests/test_facility_api.py @@ -229,6 +229,25 @@ def test_spoke_is_not_ancestor(self): ) self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_hubs_list(self): + facility_a = self.create_facility( + self.super_user, self.district, self.local_body + ) + facility_b = self.create_facility( + self.super_user, self.district, self.local_body + ) + + FacilityHubSpoke.objects.create(hub=facility_a, spoke=facility_b) + + self.client.force_authenticate(user=self.super_user) + response = self.client.get(f"/api/v1/facility/{facility_b.external_id}/hubs/") + self.assertIs(response.status_code, status.HTTP_200_OK) + data = response.json() + self.assertEqual(data["count"], 1) + self.assertEqual( + data["results"][0]["hub_object"]["id"], str(facility_a.external_id) + ) + class FacilityCoverImageTests(TestUtils, APITestCase): @classmethod diff --git a/config/api_router.py b/config/api_router.py index 917b187395..a2e3de78c4 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -37,6 +37,7 @@ ) from care.facility.api.viewsets.facility import ( AllFacilityViewSet, + FacilityHubsViewSet, FacilitySpokesViewSet, FacilityViewSet, ) @@ -218,6 +219,7 @@ facility_nested_router.register( r"spokes", FacilitySpokesViewSet, basename="facility-spokes" ) +facility_nested_router.register(r"hubs", FacilityHubsViewSet, basename="facility-hubs") router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") From 7bdedf48f5c143933dd9268d132021798602f3b4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 15 Oct 2024 20:50:34 +0530 Subject: [PATCH 05/14] disable filtering inactive users by default (#2529) Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/users/admin.py | 7 +++++++ care/users/models.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/care/users/admin.py b/care/users/admin.py index ba40369025..3f0a7f9f5c 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -58,6 +58,13 @@ class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): list_display = ["username", "is_superuser"] search_fields = ["first_name", "last_name"] + def get_queryset(self, request): + # use the base manager to avoid filtering out soft deleted objects + qs = self.model._base_manager.get_queryset() # noqa: SLF001 + if ordering := self.get_ordering(request): + qs = qs.order_by(*ordering) + return qs + @admin.register(State) class StateAdmin(admin.ModelAdmin): diff --git a/care/users/models.py b/care/users/models.py index 0d66392279..5f214871e0 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -130,7 +130,7 @@ def __str__(self): class CustomUserManager(UserManager): def get_queryset(self): qs = super().get_queryset() - return qs.filter(deleted=False, is_active=True).select_related( + return qs.filter(deleted=False).select_related( "local_body", "district", "state" ) From 0fc526031a74aef09c7af2dcc48d348b6c0c187e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Tue, 15 Oct 2024 23:59:38 +0530 Subject: [PATCH 06/14] disable codecov annotations (#2542) --- codecov.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 codecov.yml diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000000..e00ce3d698 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +github_checks: + annotations: false From 14d3ef948146951d4b02ef049165bf5fa8e33ff8 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 18 Oct 2024 18:03:12 +0530 Subject: [PATCH 07/14] Improved asset bed relations for camera preset (#2387) * Adds camera preset model * Migration to backfill and soft delete duplicate asset bed records * Delete assed bed records that has no asset class * rebase migrations * stash * rebase migrations * rebase migrations and fix issues * fix accidentally creating preset in update preset * remove boundary preset support * optimize preset name valdiation check --------- Co-authored-by: Aakash Singh * refactor viewsets * make asset, bed, assetbed get_queryset reusable based on user * prevent accidentally attempting to evaluate queryset early * migration: skip purging data, handle exceptions; add tests --------- Co-authored-by: Aakash Singh Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- care/facility/api/serializers/bed.py | 13 +- .../facility/api/serializers/camera_preset.py | 49 +++++ care/facility/api/viewsets/asset.py | 17 +- care/facility/api/viewsets/bed.py | 48 +---- care/facility/api/viewsets/camera_preset.py | 63 ++++++ .../migrations/0466_camera_presets.py | 169 ++++++++++++++++ care/facility/models/__init__.py | 1 + care/facility/models/bed.py | 13 ++ care/facility/models/camera_preset.py | 33 ++++ care/facility/tests/test_asset_bed_api.py | 183 ++++++++++++++++++ care/utils/queryset/asset_bed.py | 47 +++++ care/utils/tests/test_utils.py | 8 +- config/api_router.py | 18 ++ 13 files changed, 600 insertions(+), 62 deletions(-) create mode 100644 care/facility/api/serializers/camera_preset.py create mode 100644 care/facility/api/viewsets/camera_preset.py create mode 100644 care/facility/migrations/0466_camera_presets.py create mode 100644 care/facility/models/camera_preset.py create mode 100644 care/facility/tests/test_asset_bed_api.py create mode 100644 care/utils/queryset/asset_bed.py diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 41597e186d..508c2f9619 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -123,11 +123,14 @@ def validate(self, attrs): {"asset": "Should be in the same facility as the bed"} ) if ( - asset.asset_class == AssetClasses.HL7MONITOR.name - and AssetBed.objects.filter( - bed=bed, asset__asset_class=asset.asset_class - ).exists() - ): + asset.asset_class + in [ + AssetClasses.HL7MONITOR.name, + AssetClasses.ONVIF.name, + ] + ) and AssetBed.objects.filter( + bed=bed, asset__asset_class=asset.asset_class + ).exists(): raise ValidationError( { "asset": "Bed is already in use by another asset of the same class" diff --git a/care/facility/api/serializers/camera_preset.py b/care/facility/api/serializers/camera_preset.py new file mode 100644 index 0000000000..7157b5245a --- /dev/null +++ b/care/facility/api/serializers/camera_preset.py @@ -0,0 +1,49 @@ +from rest_framework import serializers +from rest_framework.exceptions import ValidationError + +from care.facility.api.serializers.bed import AssetBedSerializer +from care.facility.models import CameraPreset +from care.users.api.serializers.user import UserBaseMinimumSerializer + + +class CameraPresetSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + created_by = UserBaseMinimumSerializer(read_only=True) + updated_by = UserBaseMinimumSerializer(read_only=True) + asset_bed = AssetBedSerializer(read_only=True) + + class Meta: + model = CameraPreset + exclude = ( + "external_id", + "deleted", + ) + read_only_fields = ( + "created_date", + "modified_date", + "is_migrated", + "created_by", + "updated_by", + ) + + def get_asset_bed_obj(self): + return ( + self.instance.asset_bed if self.instance else self.context.get("asset_bed") + ) + + def validate_name(self, value): + if CameraPreset.objects.filter( + asset_bed__bed_id=self.get_asset_bed_obj().bed_id, name=value + ).exists(): + msg = "Name should be unique. Another preset related to this bed already uses the same name." + raise ValidationError(msg) + return value + + def create(self, validated_data): + validated_data["created_by"] = self.context["request"].user + validated_data["asset_bed"] = self.get_asset_bed_obj() + return super().create(validated_data) + + def update(self, instance, validated_data): + validated_data["updated_by"] = self.context["request"].user + return super().update(instance, validated_data) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 15dd00e2aa..fc66eff4bf 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -62,6 +62,7 @@ from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_queryset from care.utils.queryset.asset_location import get_asset_location_queryset from care.utils.queryset.facility import get_facility_queryset from config.authentication import MiddlewareAuthentication @@ -290,21 +291,7 @@ class AssetViewSet( filterset_class = AssetFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(current_location__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - current_location__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - current_location__facility__id__in=allowed_facilities - ) + queryset = get_asset_queryset(user=self.request.user, queryset=self.queryset) return queryset.annotate( latest_status=Subquery( AvailabilityRecord.objects.filter( diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 336b5f83c2..db9dd6652f 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -30,6 +30,7 @@ from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices +from care.utils.queryset.asset_bed import get_asset_bed_queryset, get_bed_queryset inverse_bed_type = inverse_choices(BedTypeChoices) @@ -76,27 +77,14 @@ class BedViewSet( filterset_class = BedFilter def get_queryset(self): - user = self.request.user - queryset = self.queryset - - queryset = queryset.annotate( + queryset = self.queryset.annotate( is_occupied=Exists( ConsultationBed.objects.filter( bed__id=OuterRef("id"), end_date__isnull=True ) ) ) - - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(facility__id__in=allowed_facilities) - return queryset + return get_bed_queryset(user=self.request.user, queryset=queryset) @transaction.atomic def create(self, request, *args, **kwargs): @@ -168,18 +156,7 @@ class AssetBedViewSet( lookup_field = "external_id" def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset + return get_asset_bed_queryset(user=self.request.user, queryset=self.queryset) class PatientAssetBedFilter(filters.FilterSet): @@ -212,20 +189,9 @@ class PatientAssetBedViewSet(ListModelMixin, GenericViewSet): ] def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(bed__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(bed__facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(bed__facility__id__in=allowed_facilities) - return queryset.filter( - bed__facility__external_id=self.kwargs["facility_external_id"] - ) + return get_asset_bed_queryset( + user=self.request.user, queryset=self.queryset + ).filter(bed__facility__external_id=self.kwargs["facility_external_id"]) class ConsultationBedFilter(filters.FilterSet): diff --git a/care/facility/api/viewsets/camera_preset.py b/care/facility/api/viewsets/camera_preset.py new file mode 100644 index 0000000000..bfb168834b --- /dev/null +++ b/care/facility/api/viewsets/camera_preset.py @@ -0,0 +1,63 @@ +from django.shortcuts import get_object_or_404 +from rest_framework.exceptions import NotFound +from rest_framework.mixins import ListModelMixin +from rest_framework.permissions import IsAuthenticated +from rest_framework.viewsets import GenericViewSet, ModelViewSet + +from care.facility.api.serializers.camera_preset import CameraPresetSerializer +from care.facility.models import CameraPreset +from care.utils.queryset.asset_bed import ( + get_asset_bed_queryset, + get_asset_queryset, + get_bed_queryset, +) + + +class AssetBedCameraPresetViewSet(ModelViewSet): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + + def get_asset_bed_obj(self): + queryset = get_asset_bed_queryset(self.request.user).filter( + external_id=self.kwargs["assetbed_external_id"] + ) + return get_object_or_404(queryset) + + def get_queryset(self): + return super().get_queryset().filter(asset_bed=self.get_asset_bed_obj()) + + def get_serializer_context(self): + context = super().get_serializer_context() + context["asset_bed"] = self.get_asset_bed_obj() + return context + + +class CameraPresetViewSet(GenericViewSet, ListModelMixin): + serializer_class = CameraPresetSerializer + queryset = CameraPreset.objects.all().select_related( + "asset_bed", "created_by", "updated_by" + ) + lookup_field = "external_id" + permission_classes = (IsAuthenticated,) + + def get_bed_obj(self, external_id: str): + queryset = get_bed_queryset(self.request.user).filter(external_id=external_id) + return get_object_or_404(queryset) + + def get_asset_obj(self, external_id: str): + queryset = get_asset_queryset(self.request.user).filter(external_id=external_id) + return get_object_or_404(queryset) + + def get_queryset(self): + queryset = super().get_queryset() + if asset_external_id := self.kwargs.get("asset_external_id"): + return queryset.filter( + asset_bed__asset=self.get_asset_obj(asset_external_id) + ) + if bed_external_id := self.kwargs.get("bed_external_id"): + return queryset.filter(asset_bed__bed=self.get_bed_obj(bed_external_id)) + raise NotFound diff --git a/care/facility/migrations/0466_camera_presets.py b/care/facility/migrations/0466_camera_presets.py new file mode 100644 index 0000000000..8ee6942342 --- /dev/null +++ b/care/facility/migrations/0466_camera_presets.py @@ -0,0 +1,169 @@ +# Generated by Django 4.2.8 on 2024-05-30 06:56 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.core.paginator import Paginator +from django.db import migrations, models +from django.db.models import F, Window +from django.db.models.functions import RowNumber + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0465_merge_20240923_1045"), + ] + + def delete_asset_beds_without_asset_class(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + AssetBed.objects.filter(asset__asset_class__isnull=True).delete() + + def backfill_camera_presets(apps, schema_editor): + AssetBed = apps.get_model("facility", "AssetBed") + CameraPreset = apps.get_model("facility", "CameraPreset") + + paginator = Paginator( + AssetBed.objects.annotate( + row_number=Window( + expression=RowNumber(), + partition_by=[F("asset"), F("bed")], + order_by=F("id").asc(), + ) + ) + .filter(deleted=False, asset__asset_class="ONVIF") + .order_by("asset", "bed", "id"), + 1000, + ) + + for page_number in paginator.page_range: + assetbeds_to_delete = [] + presets_to_create = [] + + for asset_bed in paginator.page(page_number).object_list: + name = asset_bed.meta.get("preset_name") + + if position := asset_bed.meta.get("position"): + try: + presets_to_create.append( + CameraPreset( + name=name, + asset_bed=AssetBed.objects.filter( + asset=asset_bed.asset, bed=asset_bed.bed + ).order_by("id")[0], + position={ + "x": float(position["x"]), + "y": float(position["y"]), + "zoom": float(position["zoom"]), + }, + is_migrated=True, + ) + ) + except: + pass + if asset_bed.row_number != 1: + assetbeds_to_delete.append(asset_bed.id) + else: + assetbeds_to_delete.append(asset_bed.id) + + CameraPreset.objects.bulk_create(presets_to_create) + AssetBed.objects.filter(id__in=assetbeds_to_delete).update(deleted=True) + + operations = [ + migrations.CreateModel( + name="CameraPreset", + 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)), + ("name", models.CharField(max_length=255, null=True)), + ( + "position", + models.JSONField( + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": False, + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "zoom": {"type": "number"}, + }, + "required": ["x", "y", "zoom"], + "type": "object", + } + ) + ], + ), + ), + ("is_migrated", models.BooleanField(default=False)), + ( + "asset_bed", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + related_name="camera_presets", + to="facility.assetbed", + ), + ), + ( + "created_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "updated_by", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="+", + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + migrations.RunPython( + delete_asset_beds_without_asset_class, + migrations.RunPython.noop, + ), + migrations.RunPython( + backfill_camera_presets, + migrations.RunPython.noop, + ), + migrations.AddConstraint( + model_name="assetbed", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("asset", "bed"), + name="unique_together_asset_bed", + ), + ), + ] diff --git a/care/facility/models/__init__.py b/care/facility/models/__init__.py index df41476768..d6d63cacca 100644 --- a/care/facility/models/__init__.py +++ b/care/facility/models/__init__.py @@ -4,6 +4,7 @@ from .ambulance import * # noqa from .asset import * # noqa from .bed import * # noqa +from .camera_preset import * # noqa from .daily_round import * # noqa from .encounter_symptom import * # noqa from .events import * # noqa diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index a06db2729c..992f36ac74 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -68,9 +68,22 @@ class AssetBed(BaseModel): bed = models.ForeignKey(Bed, on_delete=models.PROTECT, null=False, blank=False) meta = JSONField(default=dict, blank=True) + class Meta: + constraints = [ + models.UniqueConstraint( + name="unique_together_asset_bed", + fields=("asset", "bed"), + condition=models.Q(deleted=False), + ), + ] + def __str__(self): return f"{self.asset.name} - {self.bed.name}" + def delete(self, *args): + self.camera_presets.update(deleted=True) + return super().delete(*args) + class ConsultationBed(BaseModel): consultation = models.ForeignKey( diff --git a/care/facility/models/camera_preset.py b/care/facility/models/camera_preset.py new file mode 100644 index 0000000000..b1128f8817 --- /dev/null +++ b/care/facility/models/camera_preset.py @@ -0,0 +1,33 @@ +from django.db import models + +from care.utils.models.base import BaseModel +from care.utils.models.validators import JSONFieldSchemaValidator + +CAMERA_PRESET_POSITION_SCHEMA = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "x": {"type": "number"}, + "y": {"type": "number"}, + "zoom": {"type": "number"}, + }, + "required": ["x", "y", "zoom"], + "additionalProperties": False, +} + + +class CameraPreset(BaseModel): + name = models.CharField(max_length=255, null=True) + asset_bed = models.ForeignKey( + "facility.AssetBed", on_delete=models.PROTECT, related_name="camera_presets" + ) + position = models.JSONField( + validators=[JSONFieldSchemaValidator(CAMERA_PRESET_POSITION_SCHEMA)] + ) + created_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + updated_by = models.ForeignKey( + "users.User", null=True, blank=True, on_delete=models.PROTECT, related_name="+" + ) + is_migrated = models.BooleanField(default=False) diff --git a/care/facility/tests/test_asset_bed_api.py b/care/facility/tests/test_asset_bed_api.py new file mode 100644 index 0000000000..d22aae9bfd --- /dev/null +++ b/care/facility/tests/test_asset_bed_api.py @@ -0,0 +1,183 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.users.models import User +from care.utils.assetintegration.asset_classes import AssetClasses +from care.utils.tests.test_utils import TestUtils + + +class AssetBedViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset = cls.create_asset(cls.asset_location) + cls.camera_asset = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + + def test_link_disallowed_asset_class_asset_to_bed(self): + data = { + "asset": self.asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_link_asset_to_bed_and_attempt_duplicate_linking(self): + data = { + "asset": self.camera_asset.external_id, + "bed": self.bed.external_id, + } + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + # Attempt linking same camera to the same bed again. + res = self.client.post("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + # List asset beds filtered by asset and bed ID and check only 1 result exists + res = self.client.get("/api/v1/assetbed/", data) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertEqual(res.data["count"], 1) + + +class AssetBedCameraPresetViewSetTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + User.TYPE_VALUE_MAP["DistrictAdmin"], + cls.district, + home_facility=cls.facility, + ) + cls.asset_location = cls.create_asset_location(cls.facility) + cls.asset1 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.asset2 = cls.create_asset( + cls.asset_location, asset_class=AssetClasses.ONVIF.name + ) + cls.bed = cls.create_bed(cls.facility, cls.asset_location) + cls.asset_bed1 = cls.create_asset_bed(cls.asset1, cls.bed) + cls.asset_bed2 = cls.create_asset_bed(cls.asset2, cls.bed) + + def get_base_url(self, asset_bed_id=None): + return f"/api/v1/assetbed/{asset_bed_id or self.asset_bed1.external_id}/camera_presets/" + + def test_create_camera_preset_without_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset without position", + "position": {}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_missing_required_keys_in_position(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": {"key": "value"}, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_not_number(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "not a number", + "y": 1, + "zoom": 1, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_with_position_values_as_string(self): + res = self.client.post( + self.get_base_url(), + { + "name": "Preset with invalid position", + "position": { + "x": "1", + "y": "1", + "zoom": "1", + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_camera_preset_and_presence_in_various_preset_list_apis(self): + asset_bed = self.asset_bed1 + res = self.client.post( + self.get_base_url(asset_bed.external_id), + { + "name": "Preset with proper position", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + }, + format="json", + ) + self.assertEqual(res.status_code, status.HTTP_201_CREATED) + preset_external_id = res.data["id"] + + # Check if preset in asset-bed preset list + res = self.client.get(self.get_base_url(asset_bed.external_id)) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in asset preset list + res = self.client.get( + f"/api/v1/asset/{asset_bed.asset.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + # Check if preset in bed preset list + res = self.client.get( + f"/api/v1/bed/{asset_bed.bed.external_id}/camera_presets/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertContains(res, preset_external_id) + + def test_create_camera_preset_with_same_name_in_same_bed(self): + data = { + "name": "Duplicate Preset Name", + "position": { + "x": 1.0, + "y": 1.0, + "zoom": 1.0, + }, + } + self.client.post( + self.get_base_url(self.asset_bed1.external_id), data, format="json" + ) + res = self.client.post( + self.get_base_url(self.asset_bed2.external_id), data, format="json" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/utils/queryset/asset_bed.py b/care/utils/queryset/asset_bed.py new file mode 100644 index 0000000000..f9fe8f925e --- /dev/null +++ b/care/utils/queryset/asset_bed.py @@ -0,0 +1,47 @@ +from care.facility.models import Asset, AssetBed, Bed +from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities + + +def get_asset_bed_queryset(user, queryset=None): + queryset = AssetBed.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(bed__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(bed__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(bed__facility__id__in=allowed_facilities) + return queryset + + +def get_bed_queryset(user, queryset=None): + queryset = Bed.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return queryset + + +def get_asset_queryset(user, queryset=None): + queryset = Asset.objects.all() if queryset is None else queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(current_location__facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(current_location__facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter( + current_location__facility__id__in=allowed_facilities + ) + return queryset diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 1f858c7258..91d4ac8d67 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -38,7 +38,7 @@ Ward, ) from care.facility.models.asset import Asset, AssetLocation -from care.facility.models.bed import Bed, ConsultationBed +from care.facility.models.bed import AssetBed, Bed, ConsultationBed from care.facility.models.facility import FacilityUser from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, @@ -446,6 +446,12 @@ def create_bed(cls, facility: Facility, location: AssetLocation, **kwargs): data.update(kwargs) return Bed.objects.create(**data) + @classmethod + def create_asset_bed(cls, asset: Asset, bed: Bed, **kwargs): + data = {"asset": asset, "bed": bed} + data.update(kwargs) + return AssetBed.objects.create(**data) + @classmethod def create_consultation_bed( cls, diff --git a/config/api_router.py b/config/api_router.py index a2e3de78c4..b4a3aa0f4a 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -26,6 +26,10 @@ ConsultationBedViewSet, PatientAssetBedViewSet, ) +from care.facility.api.viewsets.camera_preset import ( + AssetBedCameraPresetViewSet, + CameraPresetViewSet, +) from care.facility.api.viewsets.consultation_diagnosis import ( ConsultationDiagnosisViewSet, ) @@ -223,6 +227,9 @@ router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") +asset_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="asset-camera-presets" +) asset_nested_router.register( r"availability", AvailabilityViewSet, basename="asset-availability" ) @@ -234,8 +241,17 @@ router.register("asset_transaction", AssetTransactionViewSet) router.register("bed", BedViewSet, basename="bed") +bed_nested_router = NestedSimpleRouter(router, r"bed", lookup="bed") +bed_nested_router.register( + r"camera_presets", CameraPresetViewSet, basename="bed-camera-presets" +) + router.register("assetbed", AssetBedViewSet, basename="asset-bed") router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") +assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") +assetbed_nested_router.register( + r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" +) router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") @@ -329,6 +345,8 @@ path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), path("", include(asset_nested_router.urls)), + path("", include(bed_nested_router.urls)), + path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), path("", include(consultation_nested_router.urls)), From 0677656a93ad530d16bfc42b3660f7a04c34e0b5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:12:34 +0530 Subject: [PATCH 08/14] Convert ABDM into Plug - Part 2/3 (#2313) Convert ABDM into Plug - Part 2/3 (#2313) --------- Co-authored-by: Aakash Singh Co-authored-by: Khavin Shankar --- care/abdm/migrations/0014_replace_0013.py | 30 +++++++++++++++++++ .../0013_abhanumber_patient.py | 4 +-- ...atientregistration_abha_number_and_more.py | 12 ++------ ...atientregistration_abha_number_and_more.py | 2 +- 4 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 care/abdm/migrations/0014_replace_0013.py rename care/abdm/{migrations => migrations_old}/0013_abhanumber_patient.py (89%) diff --git a/care/abdm/migrations/0014_replace_0013.py b/care/abdm/migrations/0014_replace_0013.py new file mode 100644 index 0000000000..1c30a4e0a7 --- /dev/null +++ b/care/abdm/migrations/0014_replace_0013.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.10 on 2024-04-21 17:40 + +# This is a replacement migration for abdm.0013 that omits the RunPython operation (reverse_patient_abhanumber_relation) and facility migration dependency. + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("abdm", "0012_consentrequest_status"), + ] + + replaces = [ + ("abdm", "0013_abhanumber_patient"), + ] + + operations = [ + migrations.AddField( + model_name="abhanumber", + name="patient", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + related_name="abha_number", + to="facility.patientregistration", + ), + ), + ] diff --git a/care/abdm/migrations/0013_abhanumber_patient.py b/care/abdm/migrations_old/0013_abhanumber_patient.py similarity index 89% rename from care/abdm/migrations/0013_abhanumber_patient.py rename to care/abdm/migrations_old/0013_abhanumber_patient.py index 41d3854797..433dea8576 100644 --- a/care/abdm/migrations/0013_abhanumber_patient.py +++ b/care/abdm/migrations_old/0013_abhanumber_patient.py @@ -11,9 +11,7 @@ def reverse_patient_abhanumber_relation(apps, schema_editor): AbhaNumber = apps.get_model("abdm", "AbhaNumber") patients = ( - Patient.objects.annotate(removed_field=RawSQL("abha_number_id", ())) - .filter(abha_number__isnull=False) - .select_related("abha_number") + Patient.objects.filter(abha_number__isnull=False) ) abha_numbers_to_update = [] diff --git a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py index 304bebb163..194458e274 100644 --- a/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py +++ b/care/facility/migrations/0374_historicalpatientregistration_abha_number_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ - ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), + # ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), ("facility", "0373_remove_patientconsultation_hba1c"), ] @@ -22,23 +22,17 @@ class Migration(migrations.Migration): migrations.AddField( model_name="historicalpatientregistration", name="abha_number", - field=models.ForeignKey( + field=models.IntegerField( 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( + field=models.IntegerField( blank=True, null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="abdm.abhanumber", ), ), ] diff --git a/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py b/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py index 4dc7fd5054..9e10ce90d1 100644 --- a/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py +++ b/care/facility/migrations/0454_remove_historicalpatientregistration_abha_number_and_more.py @@ -6,7 +6,7 @@ class Migration(migrations.Migration): dependencies = [ ("facility", "0453_merge_20240824_2040"), - ("abdm", "0013_abhanumber_patient"), + # ("abdm", "0013_abhanumber_patient"), ] operations = [ From e9303a077989060ad0e87371d3120e94c021b0cb Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:13:12 +0530 Subject: [PATCH 09/14] Convert ABDM into Plug - Part 3/3 (#2312) Convert ABDM into Plug - Part 3/3 (#2312) --------- Co-authored-by: Aakash Singh Co-authored-by: Khavin Shankar Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .env.example | 2 - aws/backend.json | 16 +- aws/celery.json | 42 +- care/abdm/__init__.py | 0 care/abdm/admin.py | 1 - care/abdm/api/__init__.py | 0 care/abdm/api/serializers/abha_number.py | 20 - care/abdm/api/serializers/auth.py | 24 - care/abdm/api/serializers/consent.py | 33 - care/abdm/api/serializers/health_facility.py | 12 - care/abdm/api/serializers/healthid.py | 66 - care/abdm/api/serializers/hip.py | 33 - care/abdm/api/viewsets/abha_number.py | 50 - care/abdm/api/viewsets/auth.py | 341 ----- care/abdm/api/viewsets/consent.py | 260 ---- care/abdm/api/viewsets/health_facility.py | 117 -- care/abdm/api/viewsets/health_information.py | 144 -- care/abdm/api/viewsets/healthid.py | 765 ----------- care/abdm/api/viewsets/hip.py | 146 -- care/abdm/api/viewsets/monitoring.py | 22 - care/abdm/api/viewsets/patients.py | 77 -- care/abdm/api/viewsets/status.py | 33 - care/abdm/apps.py | 7 - ...itial_squashed_0007_alter_abhanumber_id.py | 69 - care/abdm/migrations/0008_abhanumber_new.py | 17 - care/abdm/migrations/0009_healthfacility.py | 55 - .../0010_healthfacility_registered.py | 17 - ...1_alter_abhanumber_abha_number_and_more.py | 429 ------ .../migrations/0012_consentrequest_status.py | 27 - care/abdm/migrations/__init__.py | 0 care/abdm/models/__init__.py | 3 - care/abdm/models/abha_number.py | 41 - care/abdm/models/base.py | 43 - care/abdm/models/consent.py | 160 --- care/abdm/models/health_facility.py | 15 - care/abdm/models/json_schema.py | 15 - .../models/permissions/health_facility.py | 29 - care/abdm/receivers/consultation.py | 27 - care/abdm/service/gateway.py | 215 --- care/abdm/service/request.py | 106 -- care/abdm/tests.py | 3 - care/abdm/urls.py | 141 -- care/abdm/utils/api_call.py | 812 ----------- care/abdm/utils/cipher.py | 95 -- care/abdm/utils/fhir.py | 1220 ----------------- care/abdm/views.py | 1 - .../management/commands/load_dummy_data.py | 25 + config/api_router.py | 23 - config/authentication.py | 53 - config/settings/base.py | 16 - config/urls.py | 6 +- docker-compose.yaml | 6 - docker/.local.env | 8 + docker/dev.Dockerfile | 2 +- plug_config.py | 9 +- 55 files changed, 60 insertions(+), 5839 deletions(-) delete mode 100644 care/abdm/__init__.py delete mode 100644 care/abdm/admin.py delete mode 100644 care/abdm/api/__init__.py delete mode 100644 care/abdm/api/serializers/abha_number.py delete mode 100644 care/abdm/api/serializers/auth.py delete mode 100644 care/abdm/api/serializers/consent.py delete mode 100644 care/abdm/api/serializers/health_facility.py delete mode 100644 care/abdm/api/serializers/healthid.py delete mode 100644 care/abdm/api/serializers/hip.py delete mode 100644 care/abdm/api/viewsets/abha_number.py delete mode 100644 care/abdm/api/viewsets/auth.py delete mode 100644 care/abdm/api/viewsets/consent.py delete mode 100644 care/abdm/api/viewsets/health_facility.py delete mode 100644 care/abdm/api/viewsets/health_information.py delete mode 100644 care/abdm/api/viewsets/healthid.py delete mode 100644 care/abdm/api/viewsets/hip.py delete mode 100644 care/abdm/api/viewsets/monitoring.py delete mode 100644 care/abdm/api/viewsets/patients.py delete mode 100644 care/abdm/api/viewsets/status.py delete mode 100644 care/abdm/apps.py delete mode 100644 care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py delete mode 100644 care/abdm/migrations/0008_abhanumber_new.py delete mode 100644 care/abdm/migrations/0009_healthfacility.py delete mode 100644 care/abdm/migrations/0010_healthfacility_registered.py delete mode 100644 care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py delete mode 100644 care/abdm/migrations/0012_consentrequest_status.py delete mode 100644 care/abdm/migrations/__init__.py delete mode 100644 care/abdm/models/__init__.py delete mode 100644 care/abdm/models/abha_number.py delete mode 100644 care/abdm/models/base.py delete mode 100644 care/abdm/models/consent.py delete mode 100644 care/abdm/models/health_facility.py delete mode 100644 care/abdm/models/json_schema.py delete mode 100644 care/abdm/models/permissions/health_facility.py delete mode 100644 care/abdm/receivers/consultation.py delete mode 100644 care/abdm/service/gateway.py delete mode 100644 care/abdm/service/request.py delete mode 100644 care/abdm/tests.py delete mode 100644 care/abdm/urls.py delete mode 100644 care/abdm/utils/api_call.py delete mode 100644 care/abdm/utils/cipher.py delete mode 100644 care/abdm/utils/fhir.py delete mode 100644 care/abdm/views.py diff --git a/.env.example b/.env.example index 091bea02fd..057f853951 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,6 @@ DATABASE_URL=postgres://postgres:postgres@localhost:5433/care REDIS_URL=redis://localhost:6380 CELERY_BROKER_URL=redis://localhost:6380/0 -FIDELIUS_URL=http://localhost:8092 - DJANGO_DEBUG=False BUCKET_REGION=ap-south-1 diff --git a/aws/backend.json b/aws/backend.json index fcacb36194..145b36061b 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -114,29 +114,21 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { "name": "ABDM_FACILITY_URL", "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "X_CM_ID", + "name": "ABDM_CM_ID", "value": "sbx" }, - { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" - }, { "name": "SENTRY_TRACES_SAMPLE_RATE", "value": "1.0" diff --git a/aws/celery.json b/aws/celery.json index efb6182134..197c6d7346 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -11,9 +11,7 @@ } }, "portMappings": [], - "command": [ - "/app/celery_beat-ecs.sh" - ], + "command": ["/app/celery_beat-ecs.sh"], "cpu": 128, "environment": [ { @@ -97,29 +95,21 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { "name": "ABDM_FACILITY_URL", "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "X_CM_ID", + "name": "ABDM_CM_ID", "value": "sbx" }, - { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" - }, { "name": "SENTRY_TRACES_SAMPLE_RATE", "value": "1.0" @@ -296,9 +286,7 @@ "awslogs-stream-prefix": "ecs" } }, - "command": [ - "/app/celery_worker-ecs.sh" - ], + "command": ["/app/celery_worker-ecs.sh"], "cpu": 384, "memory": 1536, "memoryReservation": 1536, @@ -384,24 +372,20 @@ "value": "True" }, { - "name": "ENABLE_ABDM", - "value": "True" - }, - { - "name": "ABDM_URL", + "name": "ABDM_GATEWAY_URL", "value": "https://dev.abdm.gov.in" }, { - "name": "HEALTH_SERVICE_API_URL", - "value": "https://healthidsbx.abdm.gov.in/api" + "name": "ABDM_ABHA_URL", + "value": "https://abhasbx.abdm.gov.in" }, { - "name": "X_CM_ID", - "value": "sbx" + "name": "ABDM_FACILITY_URL", + "value": "https://facilitysbx.abdm.gov.in" }, { - "name": "FIDELIUS_URL", - "value": "https://fidelius.ohc.network" + "name": "ABDM_CM_ID", + "value": "sbx" }, { "name": "SENTRY_TRACES_SAMPLE_RATE", diff --git a/care/abdm/__init__.py b/care/abdm/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/abdm/admin.py b/care/abdm/admin.py deleted file mode 100644 index 846f6b4061..0000000000 --- a/care/abdm/admin.py +++ /dev/null @@ -1 +0,0 @@ -# Register your models here. diff --git a/care/abdm/api/__init__.py b/care/abdm/api/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/abdm/api/serializers/abha_number.py b/care/abdm/api/serializers/abha_number.py deleted file mode 100644 index c166d57228..0000000000 --- a/care/abdm/api/serializers/abha_number.py +++ /dev/null @@ -1,20 +0,0 @@ -# ModelSerializer -from rest_framework import serializers - -from care.abdm.models import AbhaNumber -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models import PatientRegistration -from care.utils.serializers.fields import ExternalIdSerializerField - - -class AbhaNumberSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - patient = ExternalIdSerializerField( - queryset=PatientRegistration.objects.all(), required=False, allow_null=True - ) - patient_object = PatientDetailSerializer(source="patient", read_only=True) - new = serializers.BooleanField(read_only=True) - - class Meta: - model = AbhaNumber - exclude = ("deleted", "access_token", "refresh_token", "txn_id") diff --git a/care/abdm/api/serializers/auth.py b/care/abdm/api/serializers/auth.py deleted file mode 100644 index 9b533d0c9b..0000000000 --- a/care/abdm/api/serializers/auth.py +++ /dev/null @@ -1,24 +0,0 @@ -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() - - -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/consent.py b/care/abdm/api/serializers/consent.py deleted file mode 100644 index e2f3d6cb93..0000000000 --- a/care/abdm/api/serializers/consent.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework import serializers - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.users.api.serializers.user import UserBaseMinimumSerializer - - -class ConsentArtefactSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - - class Meta: - model = ConsentArtefact - exclude = ( - "deleted", - "external_id", - "key_material_private_key", - "key_material_public_key", - "key_material_nonce", - "key_material_algorithm", - "key_material_curve", - "signature", - ) - - -class ConsentRequestSerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - patient_abha_object = AbhaNumberSerializer(source="patient_abha", read_only=True) - requester = UserBaseMinimumSerializer(read_only=True) - consent_artefacts = ConsentArtefactSerializer(many=True, read_only=True) - - class Meta: - model = ConsentRequest - exclude = ("deleted", "external_id") diff --git a/care/abdm/api/serializers/health_facility.py b/care/abdm/api/serializers/health_facility.py deleted file mode 100644 index 336f348584..0000000000 --- a/care/abdm/api/serializers/health_facility.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework import serializers - -from care.abdm.models import HealthFacility - - -class HealthFacilitySerializer(serializers.ModelSerializer): - id = serializers.CharField(source="external_id", read_only=True) - registered = serializers.BooleanField(read_only=True) - - class Meta: - model = HealthFacility - exclude = ("deleted",) diff --git a/care/abdm/api/serializers/healthid.py b/care/abdm/api/serializers/healthid.py deleted file mode 100644 index 2c1910b823..0000000000 --- a/care/abdm/api/serializers/healthid.py +++ /dev/null @@ -1,66 +0,0 @@ -from rest_framework.serializers import CharField, Serializer, UUIDField - - -class AadharOtpGenerateRequestPayloadSerializer(Serializer): - aadhaar = CharField(max_length=16, min_length=12, required=True) - - -class AadharOtpResendRequestPayloadSerializer(Serializer): - txnId = CharField(max_length=64, min_length=1, required=True) - - -class HealthIdSerializer(Serializer): - healthId = CharField(max_length=64, min_length=1, required=True) - - -class QRContentSerializer(Serializer): - 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) - healthid = CharField(max_length=64, min_length=1, required=True) - - -class ABHASearchRequestSerializer: - name = CharField(max_length=64, min_length=1, required=False) - mobile = CharField( - 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) - 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") - 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) - 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) - txnId = CharField(max_length=64, min_length=1, required=True) - patientId = UUIDField(required=False) - - -class LinkPatientSerializer(Serializer): - abha_number = UUIDField(required=True) - patient = UUIDField(required=True) diff --git a/care/abdm/api/serializers/hip.py b/care/abdm/api/serializers/hip.py deleted file mode 100644 index 4e3bb0f9ab..0000000000 --- a/care/abdm/api/serializers/hip.py +++ /dev/null @@ -1,33 +0,0 @@ -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/abha_number.py b/care/abdm/api/viewsets/abha_number.py deleted file mode 100644 index 2e94f2aae6..0000000000 --- a/care/abdm/api/viewsets/abha_number.py +++ /dev/null @@ -1,50 +0,0 @@ -from django.db.models import Q -from django.http import Http404 -from rest_framework.decorators import action -from rest_framework.mixins import RetrieveModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.models import AbhaNumber -from care.abdm.utils.api_call import HealthIdGateway -from care.utils.queryset.patient import get_patient_queryset - - -class AbhaNumberViewSet( - GenericViewSet, - RetrieveModelMixin, -): - serializer_class = AbhaNumberSerializer - model = AbhaNumber - queryset = AbhaNumber.objects.all() - - def get_object(self): - id = self.kwargs.get("pk") - - instance = self.queryset.filter( - Q(abha_number=id) | Q(health_id=id) | Q(patient__external_id=id) - ).first() - - if not instance or not get_patient_queryset(self.request.user).contains( - instance.patient - ): - raise Http404 - - self.check_object_permissions(self.request, instance) - - return instance - - @action(detail=True, methods=["GET"]) - def qr_code(self, request, *args, **kwargs): - obj = self.get_object() - serializer = self.get_serializer(obj) - response = HealthIdGateway().get_qr_code(serializer.data) - return Response(response) - - @action(detail=True, methods=["GET"]) - def profile(self, request, *args, **kwargs): - obj = self.get_object() - serializer = self.get_serializer(obj) - response = HealthIdGateway().get_profile(serializer.data) - return Response(response) diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py deleted file mode 100644 index b63b484583..0000000000 --- a/care/abdm/api/viewsets/auth.py +++ /dev/null @@ -1,341 +0,0 @@ -import json -import logging -from datetime import datetime, timedelta - -from django.core.cache import cache -from rest_framework import status -from rest_framework.generics import GenericAPIView, get_object_or_404 -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 Fhir -from care.facility.models.patient import PatientRegistration -from care.facility.models.patient_consultation import PatientConsultation -from config.authentication import ABDMAuthentication - -logger = logging.getLogger(__name__) - - -class OnFetchView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - AbdmGateway().init(data["resp"]["requestId"]) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class OnInitView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - AbdmGateway().confirm(data["auth"]["transactionId"], data["resp"]["requestId"]) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class OnConfirmView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - 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) - - -class AuthNotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.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): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class DiscoverView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - 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["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"] == "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"] - ) - - # TODO: also filter by demographics - patient = patients.last() - - if not patient: - 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(patient.external_id), - "patient_name": patient.name, - "care_contexts": list( - map( - lambda consultation: { - "id": str(consultation.external_id), - "name": f"Encounter: {consultation.created_date.date()!s}", - }, - PatientConsultation.objects.filter(patient=patient), - ) - ), - "matched_by": matched_by, - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class LinkInitView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - 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): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - # TODO: verify otp - - patient = get_object_or_404( - PatientRegistration.objects.filter( - 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: {consultation.created_date.date()!s}", - }, - PatientConsultation.objects.filter(patient=patient), - ) - ), - } - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class NotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - cache.set(data["notification"]["consentId"], json.dumps(data)) - - AbdmGateway().on_notify( - { - "request_id": data["requestId"], - "consent_id": data["notification"]["consentId"], - } - ) - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class RequestDataView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.data - - consent_id = data["hiRequest"]["consent"]["id"] - consent = json.loads(cache.get(consent_id)) if consent_id in cache else None - if not consent or 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( - # 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) - - on_data_request_response = AbdmGateway().on_data_request( - {"request_id": data["requestId"], "transaction_id": data["transactionId"]} - ) - - if 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 - ) - - cipher = Cipher( - data["hiRequest"]["keyMaterial"]["dhPublicKey"]["keyValue"], - data["hiRequest"]["keyMaterial"]["nonce"], - ) - - data_transfer_response = AbdmGateway().data_transfer( - { - "transaction_id": data["transactionId"], - "data_push_url": data["hiRequest"]["dataPushUrl"], - "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.filter( - external_id=context[ - "careContextReference" - ] - ).first() - ).create_record(record) - )["data"], - }, - consent["notification"]["consentDetail"]["hiTypes"], - ) - ), - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) - ), - [], - ), - "key_material": { - "cryptoAlg": "ECDH", - "curve": "Curve25519", - "dhPublicKey": { - "expiry": (datetime.now() + timedelta(days=2)).isoformat(), - "parameters": "Curve25519/32byte random key", - "keyValue": cipher.key_to_share, - }, - "nonce": cipher.internal_nonce, - }, - } - ) - - AbdmGateway().data_notify( - { - "health_id": consent["notification"]["consentDetail"]["patient"]["id"], - "consent_id": data["hiRequest"]["consent"]["id"], - "transaction_id": data["transactionId"], - "session_status": ( - "TRANSFERRED" - if data_transfer_response - and data_transfer_response.status_code == 202 - else "FAILED" - ), - "care_contexts": list( - map( - lambda context: {"id": context["careContextReference"]}, - consent["notification"]["consentDetail"]["careContexts"][ - :-2:-1 - ], - ) - ), - } - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/consent.py b/care/abdm/api/viewsets/consent.py deleted file mode 100644 index da6fc0ac4f..0000000000 --- a/care/abdm/api/viewsets/consent.py +++ /dev/null @@ -1,260 +0,0 @@ -import logging - -from django_filters import rest_framework as filters -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.consent import ConsentRequestSerializer -from care.abdm.api.viewsets.health_information import HealthInformationViewSet -from care.abdm.models.base import Status -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.abdm.service.gateway import Gateway -from care.utils.queryset.facility import get_facility_queryset -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -class ConsentRequestFilter(filters.FilterSet): - patient = filters.UUIDFilter(field_name="patient_abha__patient__external_id") - health_id = filters.CharFilter(field_name="patient_abha__health_id") - ordering = filters.OrderingFilter( - fields=( - "created_date", - "updated_date", - ) - ) - facility = filters.UUIDFilter( - field_name="patient_abha__patient__facility__external_id" - ) - - class Meta: - model = ConsentRequest - fields = ["patient", "health_id", "purpose"] - - -class ConsentViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): - serializer_class = ConsentRequestSerializer - model = ConsentRequest - queryset = ConsentRequest.objects.all() - filter_backends = (filters.DjangoFilterBackend,) - filterset_class = ConsentRequestFilter - - def get_queryset(self): - queryset = self.queryset - facilities = get_facility_queryset(self.request.user) - return queryset.filter(requester__facility__in=facilities).distinct() - - def create(self, request): - serializer = self.get_serializer(data=request.data) - serializer.is_valid(raise_exception=True) - - if ratelimit( - request, "consent__create", [serializer.validated_data["patient_abha"]] - ): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = ConsentRequest(**serializer.validated_data, requester=request.user) - - response = Gateway().consent_requests__init(consent) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - consent.save() - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_201_CREATED - ) - - @action(detail=True, methods=["GET"]) - def status(self, request, pk): - if ratelimit(request, "consent__status", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = self.queryset.filter(external_id=pk).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - response = Gateway().consent_requests__status(str(consent.consent_id)) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK - ) - - @action(detail=True, methods=["GET"]) - def fetch(self, request, pk): - if ratelimit(request, "consent__fetch", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consent = self.queryset.filter(external_id=pk).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - for artefact in consent.consent_artefacts.all(): - response = Gateway().consents__fetch(str(artefact.artefact_id)) - - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response( - ConsentRequestSerializer(consent).data, status=status.HTTP_200_OK - ) - - -class ConsentCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def consent_request__on_init(self, request): - data = request.data - consent = ConsentRequest.objects.filter( - external_id=data["resp"]["requestId"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - consent.consent_id = data["consentRequest"]["id"] - consent.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def consent_request__on_status(self, request): - data = request.data - consent = ConsentRequest.objects.filter( - consent_id=data["consentRequest"]["id"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - if "notification" not in data: - return Response(status=status.HTTP_202_ACCEPTED) - - if data["notification"]["status"] != Status.DENIED: - consent_artefacts = data["notification"]["consentArtefacts"] or [] - for artefact in consent_artefacts: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - if not consent_artefact: - consent_artefact = ConsentArtefact( - external_id=artefact["id"], - consent_request=consent, - **consent.consent_details_dict(), - ) - - consent_artefact.status = data["notification"]["status"] - consent_artefact.save() - consent.status = data["notification"]["status"] - consent.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def consents__hiu__notify(self, request): - data = request.data - - if not data["notification"]["consentRequestId"]: - for artefact in data["notification"]["consentArtefacts"]: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - - consent_artefact.status = Status.REVOKED - consent_artefact.save() - return Response(status=status.HTTP_202_ACCEPTED) - - consent = ConsentRequest.objects.filter( - consent_id=data["notification"]["consentRequestId"] - ).first() - - if not consent: - return Response(status=status.HTTP_404_NOT_FOUND) - - if data["notification"]["status"] != Status.DENIED: - consent_artefacts = data["notification"]["consentArtefacts"] or [] - for artefact in consent_artefacts: - consent_artefact = ConsentArtefact.objects.filter( - external_id=artefact["id"] - ).first() - if not consent_artefact: - consent_artefact = ConsentArtefact( - external_id=artefact["id"], - consent_request=consent, - **consent.consent_details_dict(), - ) - - consent_artefact.status = data["notification"]["status"] - consent_artefact.save() - consent.status = data["notification"]["status"] - consent.save() - - Gateway().consents__hiu__on_notify(consent, data["requestId"]) - - if data["notification"]["status"] == Status.GRANTED: - ConsentViewSet().fetch(request, consent.external_id) - - return Response(status=status.HTTP_202_ACCEPTED) - - def consents__on_fetch(self, request): - data = request.data["consent"] - artefact = ConsentArtefact.objects.filter( - external_id=data["consentDetail"]["consentId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - artefact.hip = data["consentDetail"]["hip"]["id"] - artefact.hiu = data["consentDetail"]["hiu"]["id"] - artefact.cm = data["consentDetail"]["consentManager"]["id"] - - artefact.care_contexts = data["consentDetail"]["careContexts"] - artefact.hi_types = data["consentDetail"]["hiTypes"] - - artefact.access_mode = data["consentDetail"]["permission"]["accessMode"] - artefact.from_time = data["consentDetail"]["permission"]["dateRange"]["from"] - artefact.to_time = data["consentDetail"]["permission"]["dateRange"]["to"] - artefact.expiry = data["consentDetail"]["permission"]["dataEraseAt"] - - artefact.frequency_unit = data["consentDetail"]["permission"]["frequency"][ - "unit" - ] - artefact.frequency_value = data["consentDetail"]["permission"]["frequency"][ - "value" - ] - artefact.frequency_repeats = data["consentDetail"]["permission"]["frequency"][ - "repeats" - ] - - artefact.signature = data["signature"] - artefact.save() - - HealthInformationViewSet().request(request, artefact.external_id) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/health_facility.py b/care/abdm/api/viewsets/health_facility.py deleted file mode 100644 index 8b1acbeab0..0000000000 --- a/care/abdm/api/viewsets/health_facility.py +++ /dev/null @@ -1,117 +0,0 @@ -import re - -from celery import shared_task -from django.conf import settings -from dry_rest_permissions.generics import DRYPermissions -from rest_framework.decorators import action -from rest_framework.mixins import ( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.abdm.api.serializers.health_facility import HealthFacilitySerializer -from care.abdm.models import HealthFacility -from care.abdm.utils.api_call import Facility -from care.utils.queryset.facility import get_facility_queryset - - -@shared_task -def register_health_facility_as_service(facility_external_id): - if settings.ENABLE_ABDM: - return [False, "ABDM Services are currently disabled"] - - health_facility = HealthFacility.objects.filter( - facility__external_id=facility_external_id - ).first() - - if not health_facility: - return [False, "Health Facility Not Found"] - - if health_facility.registered: - return [True, None] - - clean_facility_name = re.sub(r"[^A-Za-z0-9 ]+", " ", health_facility.facility.name) - clean_facility_name = re.sub(r"\s+", " ", clean_facility_name).strip() - hip_name = settings.HIP_NAME_PREFIX + clean_facility_name + settings.HIP_NAME_SUFFIX - response = Facility().add_update_service( - { - "facilityId": health_facility.hf_id, - "facilityName": hip_name, - "HRP": [ - { - "bridgeId": settings.ABDM_CLIENT_ID, - "hipName": hip_name, - "type": "HIP", - "active": True, - "alias": ["CARE_HIP"], - } - ], - } - ) - - if response.status_code == 200: - data = response.json()[0] - - if "error" in data: - if ( - data["error"].get("code") == "2500" - and settings.ABDM_CLIENT_ID in data["error"].get("message") - and "already associated" in data["error"].get("message") - ): - health_facility.registered = True - health_facility.save() - return [True, None] - - return [ - False, - data["error"].get("message", "Error while registering HIP as service"), - ] - - if "servicesLinked" in data: - health_facility.registered = True - health_facility.save() - return [True, None] - - return [False, None] - - -class HealthFacilityViewSet( - GenericViewSet, - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -): - serializer_class = HealthFacilitySerializer - model = HealthFacility - queryset = HealthFacility.objects.all() - permission_classes = (IsAuthenticated, DRYPermissions) - lookup_field = "facility__external_id" - - def get_queryset(self): - queryset = self.queryset - facilities = get_facility_queryset(self.request.user) - return queryset.filter(facility__in=facilities) - - @action(detail=True, methods=["POST"]) - def register_service(self, request, facility__external_id): - [registered, error] = register_health_facility_as_service(facility__external_id) - - if error: - return Response({"detail": error}, status=400) - - return Response({"registered": registered}) - - def perform_create(self, serializer): - instance = serializer.save() - register_health_facility_as_service.delay(instance.facility.external_id) - - def perform_update(self, serializer): - serializer.validated_data["registered"] = False - instance = serializer.save() - register_health_facility_as_service.delay(instance.facility.external_id) diff --git a/care/abdm/api/viewsets/health_information.py b/care/abdm/api/viewsets/health_information.py deleted file mode 100644 index 98a2825276..0000000000 --- a/care/abdm/api/viewsets/health_information.py +++ /dev/null @@ -1,144 +0,0 @@ -import json -import logging - -from django.db.models import Q -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.models.consent import ConsentArtefact -from care.abdm.service.gateway import Gateway -from care.abdm.utils.cipher import Cipher -from care.facility.models.file_upload import FileUpload -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -class HealthInformationViewSet(GenericViewSet): - - def retrieve(self, request, pk): - files = FileUpload.objects.filter( - Q(internal_name=f"{pk}.json") | Q(associating_id=pk), - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - ) - - if files.count() == 0 or all([not file.upload_completed for file in files]): - return Response( - {"detail": "No Health Information found with the given id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if files.count() == 1: - file = files.first() - - if file.is_archived: - return Response( - { - "is_archived": True, - "archived_reason": file.archive_reason, - "archived_time": file.archived_datetime, - "detail": f"This file has been archived as {file.archive_reason} at {file.archived_datetime}", - }, - status=status.HTTP_404_NOT_FOUND, - ) - - contents = [] - for file in files: - if file.upload_completed: - content_type, content = file.file_contents() - contents.extend(content) - - return Response({"data": json.loads(content)}, status=status.HTTP_200_OK) - - @action(detail=True, methods=["POST"]) - def request(self, request, pk): - if ratelimit(request, "health_information__request", [pk]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - artefact = ConsentArtefact.objects.filter(external_id=pk).first() - - if not artefact: - return Response( - {"detail": "No Consent artefact found with the given id"}, - status=status.HTTP_404_NOT_FOUND, - ) - - response = Gateway().health_information__cm__request(artefact) - if response.status_code != 202: - return Response(response.json(), status=response.status_code) - - return Response(status=status.HTTP_200_OK) - - -class HealthInformationCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def health_information__hiu__on_request(self, request): - data = request.data - - artefact = ConsentArtefact.objects.filter( - consent_id=data["resp"]["requestId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - if "hiRequest" in data: - artefact.consent_id = data["hiRequest"]["transactionId"] - artefact.save() - - return Response(status=status.HTTP_202_ACCEPTED) - - def health_information__transfer(self, request): - data = request.data - - artefact = ConsentArtefact.objects.filter( - consent_id=data["transactionId"] - ).first() - - if not artefact: - return Response(status=status.HTTP_404_NOT_FOUND) - - cipher = Cipher( - data["keyMaterial"]["dhPublicKey"]["keyValue"], - data["keyMaterial"]["nonce"], - artefact.key_material_private_key, - artefact.key_material_public_key, - artefact.key_material_nonce, - ) - entries = [] - for entry in data["entries"]: - if "content" in entry: - entries.append( - { - "content": cipher.decrypt(entry["content"]), - "care_context_reference": entry["careContextReference"], - } - ) - - if "link" in entry: - # TODO: handle link - pass - - file = FileUpload( - internal_name=f"{artefact.external_id}.json", - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - associating_id=artefact.consent_request.external_id, - ) - file.put_object(json.dumps(entries), ContentType="application/json") - file.upload_completed = True - file.save() - - Gateway().health_information__notify(artefact) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py deleted file mode 100644 index 347f1a01b2..0000000000 --- a/care/abdm/api/viewsets/healthid.py +++ /dev/null @@ -1,765 +0,0 @@ -# ABDM HealthID APIs - -import logging -from datetime import datetime - -from drf_spectacular.utils import extend_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 - -from care.abdm.api.serializers.abha_number import AbhaNumberSerializer -from care.abdm.api.serializers.healthid import ( - AadharOtpGenerateRequestPayloadSerializer, - AadharOtpResendRequestPayloadSerializer, - CreateHealthIdSerializer, - GenerateMobileOtpRequestPayloadSerializer, - HealthIdAuthSerializer, - HealthIdSerializer, - LinkPatientSerializer, - QRContentSerializer, - VerifyDemographicsRequestPayloadSerializer, - VerifyOtpRequestPayloadSerializer, -) -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.patient import PatientConsultation, PatientRegistration -from care.utils.queryset.patient import get_patient_queryset -from config.auth_views import CaptchaRequiredException -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - -logger = logging.getLogger(__name__) - - -# API for Generating OTP for HealthID -class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): - base_name = "healthid" - model = AbhaNumber - - @extend_schema( - operation_id="generate_aadhaar_otp", - request=AadharOtpGenerateRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = AadharOtpGenerateRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/resendAadhaarOtp - operation_id="resend_aadhaar_otp", - request=AadharOtpResendRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = AadharOtpResendRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().resend_aadhaar_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/verifyAadhaarOtp - operation_id="verify_aadhaar_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_aadhaar_otp( - data - ) # HealthIdGatewayV2().verify_document_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/generateMobileOTP - operation_id="generate_mobile_otp", - request=GenerateMobileOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def generate_mobile_otp(self, request): - data = request.data - - if ratelimit(request, "generate_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = GenerateMobileOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().generate_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - @extend_schema( - # /v1/registration/aadhaar/verifyMobileOTP - operation_id="verify_mobile_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().verify_mobile_otp(data) - return Response(response, status=status.HTTP_200_OK) - - def create_abha(self, abha_profile, token): - abha_object = AbhaNumber.objects.filter( - abha_number=abha_profile["healthIdNumber"] - ).first() - - if abha_object: - return abha_object - - 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"] if "address" in abha_profile else "", - district=abha_profile["districtName"], - state=abha_profile["stateName"], - pincode=abha_profile["pincode"], - email=abha_profile["email"], - profile_photo=abha_profile["profilePhoto"], - new=abha_profile["new"], - txn_id=token["txn_id"], - access_token=token["access_token"], - refresh_token=token["refresh_token"], - ) - abha_object.save() - - return abha_object - - def add_abha_details_to_patient(self, abha_object, patient_object): - if abha_object.patient is not None: - raise ValidationError(detail="Abha Number is already linked to a patient") - - if getattr(patient_object, "abha_number", None) is not None: - raise ValidationError(detail="Patient already has an Abha Number linked") - - abha_object.patient = patient_object - abha_object.save() - - @extend_schema( - # /v1/registration/aadhaar/createHealthId - operation_id="create_health_id", - request=CreateHealthIdSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = CreateHealthIdSerializer(data=data) - serializer.is_valid(raise_exception=True) - abha_profile = HealthIdGateway().create_health_id(data) - - if "token" not in abha_profile: - raise ValidationError( - detail="\n\n".join( - detail.get("message", "") - for detail in abha_profile.get("details", []) - ) - or abha_profile.get("message", "Error while fetching abha profile") - ) - - # have a serializer to verify data of abha_profile - abha_object = self.create_abha( - abha_profile, - { - "txn_id": data["txnId"], - "access_token": abha_profile["token"], - "refresh_token": abha_profile["refreshToken"], - }, - ) - - 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(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - # APIs to Find & Link Existing HealthID - # searchByHealthId - @extend_schema( - # /v1/registration/aadhaar/searchByHealthId - operation_id="search_by_health_id", - request=HealthIdSerializer, - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def search_by_health_id(self, request): - data = request.data - - if ratelimit( - request, "search_by_health_id", [data["healthId"]], increment=False - ): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - 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) - - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - 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(detail="Patient not found") - - if getattr(patient, "abha_number", None) is None: - raise ValidationError(detail="Patient hasn't linked thier abha") - - 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) - - @extend_schema( - # /v1/registration/aadhaar/searchByHealthId - operation_id="link_via_qr", - request=HealthIdSerializer, - responses={"200": "{'status': 'boolean'}"}, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def link_via_qr(self, request): - data = request.data - - if ratelimit(request, "link_via_qr", [data["hidn"]], increment=False): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = QRContentSerializer(data=data) - serializer.is_valid(raise_exception=True) - - dob = datetime.strptime(data["dob"], "%d-%m-%Y").date() - - 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, - ) - - 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"], - ) - - 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 - ], - } - ) - - abha_number.save() - - if "patientId" in data and 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, - ) - - abha_number.patient = patient - abha_number.save() - - abha_serialized = AbhaNumberSerializer(abha_number).data - return Response( - {"id": abha_serialized["external_id"], "abha_profile": abha_serialized}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="search_by_health_id", - request=LinkPatientSerializer, - tags=["ABDM HealthID"], - ) - @action(detail=False, methods=["post"]) - def link_patient(self, request): - data = request.data - - serializer = LinkPatientSerializer(data=data) - serializer.is_valid(raise_exception=True) - - patient_queryset = get_patient_queryset(request.user) - patient = patient_queryset.filter(external_id=data.get("patient")).first() - - if not patient: - return Response( - { - "detail": "Patient not found or you do not have permission to access the patient", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if hasattr(patient, "abha_number"): - return Response( - { - "detail": "Patient already linked to an ABHA Number", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number = AbhaNumber.objects.filter( - external_id=data.get("abha_number") - ).first() - - if not abha_number: - return Response( - { - "detail": "ABHA Number not found", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - if abha_number.patient is not None: - return Response( - { - "detail": "ABHA Number already linked to a patient", - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - return Response( - AbhaNumberSerializer(abha_number).data, - status=status.HTTP_200_OK, - ) - - @extend_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 - - if ratelimit(request, "get_new_linking_token", [data["patient"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - 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) - - @action(detail=False, methods=["POST"]) - def add_care_context(self, request, *args, **kwargs): - consultation_id = request.data["consultation"] - - if ratelimit(request, "add_care_context", [consultation_id]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - consultation = PatientConsultation.objects.get(external_id=consultation_id) - - if not consultation: - raise ValidationError(detail="Consultation not found") - - if getattr(consultation.patient, "abha_number", None) is None: - raise ValidationError(detail="Patient hasn't linked thier abha") - - AbdmGateway().fetch_modes( - { - "healthId": consultation.patient.abha_number.health_id, - "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", - "purpose": "LINK", - } - ) - - 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]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - patient = PatientRegistration.objects.filter(external_id=patient_id).first() - - if not patient: - return Response( - {"patient": "No matching records found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - if getattr(patient, "abha_number", None) is None: - return Response( - {"abha": "Patient hasn't linked thier abha"}, - status=status.HTTP_400_BAD_REQUEST, - ) - - response = AbdmGateway().patient_sms_notify( - { - "phone": patient.phone_number, - "healthId": patient.abha_number.health_id, - } - ) - - return Response(response, status=status.HTTP_202_ACCEPTED) - - # auth/init - @extend_schema( - # /v1/auth/init - operation_id="auth_init", - request=HealthIdAuthSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - 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 - @extend_schema( - operation_id="confirm_with_aadhaar_otp", - request=VerifyOtpRequestPayloadSerializer, - responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_aadhaar_otp(data) - abha_profile = HealthIdGateway().get_profile(response) - - # have a serializer to verify data of 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") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - # /v1/auth/confirmWithMobileOtp - @extend_schema( - operation_id="confirm_with_mobile_otp", - request=VerifyOtpRequestPayloadSerializer, - # responses={"200": "{'txnId': 'string'}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyOtpRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_mobile_otp(data) - abha_profile = HealthIdGateway().get_profile(response) - - # have a serializer to verify data of 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") - allowed_patients = get_patient_queryset(request.user) - patient_obj = allowed_patients.filter(external_id=patient_id).first() - if not patient_obj: - raise ValidationError(detail="Patient not found") - - self.add_abha_details_to_patient(abha_object, patient_obj) - - return Response( - {"id": abha_object.external_id, "abha_profile": abha_profile}, - status=status.HTTP_200_OK, - ) - - @extend_schema( - operation_id="confirm_with_demographics", - request=VerifyDemographicsRequestPayloadSerializer, - responses={"200": "{'status': true}"}, - tags=["ABDM HealthID"], - ) - @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": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - serializer = VerifyDemographicsRequestPayloadSerializer(data=data) - serializer.is_valid(raise_exception=True) - response = HealthIdGateway().confirm_with_demographics(data) - return Response(response, status=status.HTTP_200_OK) - - ############################################################################################################ - # HealthID V2 APIs - @extend_schema( - # /v2/registration/aadhaar/checkAndGenerateMobileOTP - operation_id="check_and_generate_mobile_otp", - request=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 - - if ratelimit(request, "check_and_generate_mobile_otp", [data["txnId"]]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - 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) - return Response(response, status=status.HTTP_200_OK) diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py deleted file mode 100644 index aa6abb5b1e..0000000000 --- a/care/abdm/api/viewsets/hip.py +++ /dev/null @@ -1,146 +0,0 @@ -import uuid -from datetime import UTC, 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 AbdmGateway, HealthIdGateway -from care.facility.models.facility import Facility -from care.facility.models.patient import PatientRegistration -from config.authentication import ABDMAuthentication - - -class HipViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def get_linking_token(self, data): - AbdmGateway().fetch_modes(data) - return True - - @action(detail=False, methods=["POST"]) - def share(self, request, *args, **kwargs): - data = request.data - - patient_data = data["profile"]["patient"] - counter_id = ( - data["profile"]["hipCode"] - if len(data["profile"]["hipCode"]) == 36 - else Facility.objects.first().external_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) - - 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"] - ).first() - - if not patient: - 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), - 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"], - ) - - 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"], - ) - - try: - self.get_linking_token( - { - "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], - } - ) - except Exception: - return Response( - { - "status": "FAILED", - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - abha_number.patient = patient - abha_number.save() - - payload = { - "requestId": str(uuid.uuid4()), - "timestamp": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "acknowledgement": { - "status": "SUCCESS", - "healthId": patient_data["healthId"] - or patient_data["healthIdNumber"], - "tokenNumber": "100", - }, - "error": None, - "resp": { - "requestId": data["requestId"], - }, - } - - on_share_response = AbdmGateway().on_share(payload) - if on_share_response.status_code == 202: - return Response( - on_share_response.request.body, - status=status.HTTP_202_ACCEPTED, - ) - - return Response( - { - "status": "ACCEPTED", - "healthId": patient_data["healthId"] or patient_data["healthIdNumber"], - }, - status=status.HTTP_202_ACCEPTED, - ) diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py deleted file mode 100644 index b1ee830398..0000000000 --- a/care/abdm/api/viewsets/monitoring.py +++ /dev/null @@ -1,22 +0,0 @@ -from datetime import UTC, datetime - -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.response import Response - - -class HeartbeatView(GenericAPIView): - permission_classes = () - authentication_classes = () - - def get(self, request, *args, **kwargs): - return Response( - { - "timestamp": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "status": "UP", - "error": None, - }, - status=status.HTTP_200_OK, - ) diff --git a/care/abdm/api/viewsets/patients.py b/care/abdm/api/viewsets/patients.py deleted file mode 100644 index e29a72487f..0000000000 --- a/care/abdm/api/viewsets/patients.py +++ /dev/null @@ -1,77 +0,0 @@ -import json - -from django.core.cache import cache -from django.db.models import Q -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.models.abha_number import AbhaNumber -from care.abdm.service.gateway import Gateway -from care.utils.notification_handler import send_webpush -from config.auth_views import CaptchaRequiredException -from config.authentication import ABDMAuthentication -from config.ratelimit import USER_READABLE_RATE_LIMIT_TIME, ratelimit - - -class PatientsViewSet(GenericViewSet): - - @action(detail=False, methods=["POST"]) - def find(self, request): - identifier = request.data["id"] - - if ratelimit(request, "patients__find", [identifier]): - raise CaptchaRequiredException( - detail={ - "status": 429, - "detail": f"Request limit reached. Try after {USER_READABLE_RATE_LIMIT_TIME}", - }, - code=status.HTTP_429_TOO_MANY_REQUESTS, - ) - - abha_object = AbhaNumber.objects.filter( - Q(abha_number=identifier) | Q(health_id=identifier) - ).first() - - if not abha_object: - return Response( - {"error": "Patient with given id not found"}, - status=status.HTTP_404_NOT_FOUND, - ) - - response = Gateway().patients__find(abha_object) - if response.status_code != 202: - return Response(response.text, status=status.HTTP_400_BAD_REQUEST) - - cache.set( - f"abdm__patients__find__{json.loads(response.request.body)['requestId']}", - request.user.username, - timeout=60 * 60, - ) - return Response( - {"detail": "Requested ABDM for patient details"}, status=status.HTTP_200_OK - ) - - -class PatientsCallbackViewSet(GenericViewSet): - authentication_classes = [ABDMAuthentication] - - def patients__on_find(self, request): - username = cache.get( - f"abdm__patients__find__{request.data['resp']['requestId']}" - ) - - if username: - send_webpush( - username=username, - message=json.dumps( - { - "type": "MESSAGE", - "from": "patients/on_find", - "message": request.data, - } - ), - ) - - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py deleted file mode 100644 index 72913c847a..0000000000 --- a/care/abdm/api/viewsets/status.py +++ /dev/null @@ -1,33 +0,0 @@ -from rest_framework import status -from rest_framework.generics import GenericAPIView -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 -from config.authentication import ABDMAuthentication - - -class NotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - data = request.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) - - -class SMSOnNotifyView(GenericAPIView): - authentication_classes = [ABDMAuthentication] - - def post(self, request, *args, **kwargs): - return Response(status=status.HTTP_202_ACCEPTED) diff --git a/care/abdm/apps.py b/care/abdm/apps.py deleted file mode 100644 index 54e278d631..0000000000 --- a/care/abdm/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class AbdmConfig(AppConfig): - name = "care.abdm" - verbose_name = _("ABDM Integration") 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 deleted file mode 100644 index ad5d70caa0..0000000000 --- a/care/abdm/migrations/0001_initial_squashed_0007_alter_abhanumber_id.py +++ /dev/null @@ -1,69 +0,0 @@ -# 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/0008_abhanumber_new.py b/care/abdm/migrations/0008_abhanumber_new.py deleted file mode 100644 index 74fdc32e78..0000000000 --- a/care/abdm/migrations/0008_abhanumber_new.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.2 on 2023-08-07 07:33 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0001_initial_squashed_0007_alter_abhanumber_id"), - ] - - operations = [ - migrations.AddField( - model_name="abhanumber", - name="new", - field=models.BooleanField(default=False), - ), - ] diff --git a/care/abdm/migrations/0009_healthfacility.py b/care/abdm/migrations/0009_healthfacility.py deleted file mode 100644 index 0480e91d9a..0000000000 --- a/care/abdm/migrations/0009_healthfacility.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 4.2.2 on 2023-08-21 09:53 - -import uuid - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0378_consultationbedasset_consultationbed_assets"), - ("abdm", "0008_abhanumber_new"), - ] - - operations = [ - migrations.CreateModel( - name="HealthFacility", - 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)), - ("hf_id", models.CharField(max_length=50, unique=True)), - ( - "facility", - models.OneToOneField( - on_delete=django.db.models.deletion.PROTECT, - to="facility.facility", - to_field="external_id", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0010_healthfacility_registered.py b/care/abdm/migrations/0010_healthfacility_registered.py deleted file mode 100644 index 5a5d753925..0000000000 --- a/care/abdm/migrations/0010_healthfacility_registered.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 4.2.2 on 2023-09-05 06:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0009_healthfacility"), - ] - - operations = [ - migrations.AddField( - model_name="healthfacility", - name="registered", - field=models.BooleanField(default=False), - ), - ] diff --git a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py b/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py deleted file mode 100644 index 905cd09719..0000000000 --- a/care/abdm/migrations/0011_alter_abhanumber_abha_number_and_more.py +++ /dev/null @@ -1,429 +0,0 @@ -# Generated by Django 4.2.2 on 2023-10-01 16:44 - -import uuid - -import django.contrib.postgres.fields -import django.core.validators -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.abdm.models.consent -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("abdm", "0010_healthfacility_registered"), - ] - - operations = [ - migrations.AlterField( - model_name="abhanumber", - name="abha_number", - field=models.TextField(blank=True, null=True, unique=True), - ), - migrations.AlterField( - model_name="abhanumber", - name="health_id", - field=models.TextField(blank=True, null=True, unique=True), - ), - migrations.CreateModel( - name="ConsentRequest", - 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)), - ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), - ( - "care_contexts", - models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "careContextReference": { - "type": "string" - }, - "patientReference": {"type": "string"}, - }, - "required": [ - "patientReference", - "careContextReference", - ], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "purpose", - models.CharField( - choices=[ - ("CAREMGT", "Care Management"), - ("BTG", "Break The Glass"), - ("PUBHLTH", "Public Health"), - ("HPAYMT", "Healthcare Payment"), - ("DSRCH", "Disease Specific Healthcare Research"), - ("PATRQT", "Self Requested"), - ], - default="CAREMGT", - max_length=20, - ), - ), - ( - "hi_types", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[ - ("Prescription", "Prescription"), - ("DiagnosticReport", "Diagnostic Report"), - ("OPConsultation", "Op Consultation"), - ("DischargeSummary", "Discharge Summary"), - ("ImmunizationRecord", "Immunization Record"), - ("HealthDocumentRecord", "Record Artifact"), - ("WellnessRecord", "Wellness Record"), - ], - max_length=20, - ), - default=list, - size=None, - ), - ), - ("hip", models.CharField(blank=True, max_length=50, null=True)), - ("hiu", models.CharField(blank=True, max_length=50, null=True)), - ( - "access_mode", - models.CharField( - choices=[ - ("VIEW", "View"), - ("STORE", "Store"), - ("QUERY", "Query"), - ("STREAM", "Stream"), - ], - default="VIEW", - max_length=20, - ), - ), - ( - "from_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_from_time, - null=True, - ), - ), - ( - "to_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_to_time, - null=True, - ), - ), - ( - "expiry", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_expiry, - null=True, - ), - ), - ( - "frequency_unit", - models.CharField( - choices=[ - ("HOUR", "Hour"), - ("DAY", "Day"), - ("WEEK", "Week"), - ("MONTH", "Month"), - ("YEAR", "Year"), - ], - default="HOUR", - max_length=20, - ), - ), - ( - "frequency_value", - models.PositiveSmallIntegerField( - default=1, - validators=[django.core.validators.MinValueValidator(1)], - ), - ), - ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), - ( - "patient_abha", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="abdm.abhanumber", - to_field="health_id", - ), - ), - ( - "requester", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="ConsentArtefact", - 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)), - ("consent_id", models.UUIDField(blank=True, null=True, unique=True)), - ( - "care_contexts", - models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "careContextReference": { - "type": "string" - }, - "patientReference": {"type": "string"}, - }, - "required": [ - "patientReference", - "careContextReference", - ], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "purpose", - models.CharField( - choices=[ - ("CAREMGT", "Care Management"), - ("BTG", "Break The Glass"), - ("PUBHLTH", "Public Health"), - ("HPAYMT", "Healthcare Payment"), - ("DSRCH", "Disease Specific Healthcare Research"), - ("PATRQT", "Self Requested"), - ], - default="CAREMGT", - max_length=20, - ), - ), - ( - "hi_types", - django.contrib.postgres.fields.ArrayField( - base_field=models.CharField( - choices=[ - ("Prescription", "Prescription"), - ("DiagnosticReport", "Diagnostic Report"), - ("OPConsultation", "Op Consultation"), - ("DischargeSummary", "Discharge Summary"), - ("ImmunizationRecord", "Immunization Record"), - ("HealthDocumentRecord", "Record Artifact"), - ("WellnessRecord", "Wellness Record"), - ], - max_length=20, - ), - default=list, - size=None, - ), - ), - ("hip", models.CharField(blank=True, max_length=50, null=True)), - ("hiu", models.CharField(blank=True, max_length=50, null=True)), - ( - "access_mode", - models.CharField( - choices=[ - ("VIEW", "View"), - ("STORE", "Store"), - ("QUERY", "Query"), - ("STREAM", "Stream"), - ], - default="VIEW", - max_length=20, - ), - ), - ( - "from_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_from_time, - null=True, - ), - ), - ( - "to_time", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_to_time, - null=True, - ), - ), - ( - "expiry", - models.DateTimeField( - blank=True, - default=care.abdm.models.consent.Consent.default_expiry, - null=True, - ), - ), - ( - "frequency_unit", - models.CharField( - choices=[ - ("HOUR", "Hour"), - ("DAY", "Day"), - ("WEEK", "Week"), - ("MONTH", "Month"), - ("YEAR", "Year"), - ], - default="HOUR", - max_length=20, - ), - ), - ( - "frequency_value", - models.PositiveSmallIntegerField( - default=1, - validators=[django.core.validators.MinValueValidator(1)], - ), - ), - ("frequency_repeats", models.PositiveSmallIntegerField(default=0)), - ( - "status", - models.CharField( - choices=[ - ("REQUESTED", "Requested"), - ("GRANTED", "Granted"), - ("DENIED", "Denied"), - ("EXPIRED", "Expired"), - ("REVOKED", "Revoked"), - ], - default="REQUESTED", - max_length=20, - ), - ), - ("cm", models.CharField(blank=True, max_length=50, null=True)), - ( - "key_material_algorithm", - models.CharField( - blank=True, default="ECDH", max_length=20, null=True - ), - ), - ( - "key_material_curve", - models.CharField( - blank=True, default="Curve25519", max_length=20, null=True - ), - ), - ( - "key_material_public_key", - models.CharField(blank=True, max_length=100, null=True), - ), - ( - "key_material_private_key", - models.CharField(blank=True, max_length=200, null=True), - ), - ( - "key_material_nonce", - models.CharField(blank=True, max_length=100, null=True), - ), - ("signature", models.TextField(blank=True, null=True)), - ( - "consent_request", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.PROTECT, - related_name="consent_artefacts", - to="abdm.consentrequest", - to_field="consent_id", - ), - ), - ( - "patient_abha", - models.ForeignKey( - on_delete=django.db.models.deletion.PROTECT, - to="abdm.abhanumber", - to_field="health_id", - ), - ), - ( - "requester", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/abdm/migrations/0012_consentrequest_status.py b/care/abdm/migrations/0012_consentrequest_status.py deleted file mode 100644 index 43bf1ecb62..0000000000 --- a/care/abdm/migrations/0012_consentrequest_status.py +++ /dev/null @@ -1,27 +0,0 @@ -# Generated by Django 4.2.2 on 2023-12-02 04:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("abdm", "0011_alter_abhanumber_abha_number_and_more"), - ] - - operations = [ - migrations.AddField( - model_name="consentrequest", - name="status", - field=models.CharField( - choices=[ - ("REQUESTED", "Requested"), - ("GRANTED", "Granted"), - ("DENIED", "Denied"), - ("EXPIRED", "Expired"), - ("REVOKED", "Revoked"), - ], - default="REQUESTED", - max_length=20, - ), - ), - ] diff --git a/care/abdm/migrations/__init__.py b/care/abdm/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/abdm/models/__init__.py b/care/abdm/models/__init__.py deleted file mode 100644 index 5b7edbb6fb..0000000000 --- a/care/abdm/models/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .abha_number import * # noqa -from .consent import * # noqa -from .health_facility import * # noqa diff --git a/care/abdm/models/abha_number.py b/care/abdm/models/abha_number.py deleted file mode 100644 index f5d67d2132..0000000000 --- a/care/abdm/models/abha_number.py +++ /dev/null @@ -1,41 +0,0 @@ -from django.db import models - -from care.utils.models.base import BaseModel - - -class AbhaNumber(BaseModel): - abha_number = models.TextField(null=True, blank=True, unique=True) - health_id = models.TextField(null=True, blank=True, unique=True) - - patient = models.OneToOneField( - "facility.PatientRegistration", - related_name="abha_number", - on_delete=models.PROTECT, - 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) - 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) - - new = models.BooleanField(default=False) - - 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): - return f"{self.pk} {self.abha_number}" diff --git a/care/abdm/models/base.py b/care/abdm/models/base.py deleted file mode 100644 index 82ff16ca85..0000000000 --- a/care/abdm/models/base.py +++ /dev/null @@ -1,43 +0,0 @@ -from django.db import models - - -class Status(models.TextChoices): - REQUESTED = "REQUESTED" - GRANTED = "GRANTED" - DENIED = "DENIED" - EXPIRED = "EXPIRED" - REVOKED = "REVOKED" - - -class Purpose(models.TextChoices): - CARE_MANAGEMENT = "CAREMGT" - BREAK_THE_GLASS = "BTG" - PUBLIC_HEALTH = "PUBHLTH" - HEALTHCARE_PAYMENT = "HPAYMT" - DISEASE_SPECIFIC_HEALTHCARE_RESEARCH = "DSRCH" - SELF_REQUESTED = "PATRQT" - - -class HealthInformationTypes(models.TextChoices): - PRESCRIPTION = "Prescription" - DIAGNOSTIC_REPORT = "DiagnosticReport" - OP_CONSULTATION = "OPConsultation" - DISCHARGE_SUMMARY = "DischargeSummary" - IMMUNIZATION_RECORD = "ImmunizationRecord" - RECORD_ARTIFACT = "HealthDocumentRecord" - WELLNESS_RECORD = "WellnessRecord" - - -class AccessMode(models.TextChoices): - VIEW = "VIEW" - STORE = "STORE" - QUERY = "QUERY" - STREAM = "STREAM" - - -class FrequencyUnit(models.TextChoices): - HOUR = "HOUR" - DAY = "DAY" - WEEK = "WEEK" - MONTH = "MONTH" - YEAR = "YEAR" diff --git a/care/abdm/models/consent.py b/care/abdm/models/consent.py deleted file mode 100644 index ee4b0f8264..0000000000 --- a/care/abdm/models/consent.py +++ /dev/null @@ -1,160 +0,0 @@ -from django.contrib.postgres.fields import ArrayField -from django.core.validators import MinValueValidator -from django.db import models -from django.utils import timezone - -from care.abdm.models import AbhaNumber -from care.abdm.models.base import ( - AccessMode, - FrequencyUnit, - HealthInformationTypes, - Purpose, - Status, -) -from care.abdm.models.json_schema import CARE_CONTEXTS -from care.abdm.utils.cipher import Cipher -from care.facility.models.file_upload import FileUpload -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Consent(BaseModel): - class Meta: - abstract = True - - def default_expiry(): - return timezone.now() + timezone.timedelta(days=30) - - def default_from_time(): - return timezone.now() - timezone.timedelta(days=30) - - def default_to_time(): - return timezone.now() - - consent_id = models.UUIDField(null=True, blank=True, unique=True) - - patient_abha = models.ForeignKey( - AbhaNumber, on_delete=models.PROTECT, to_field="health_id" - ) - - care_contexts = models.JSONField( - default=list, validators=[JSONFieldSchemaValidator(CARE_CONTEXTS)] - ) - - status = models.CharField(choices=Status, max_length=20, default=Status.REQUESTED) - purpose = models.CharField( - choices=Purpose, max_length=20, default=Purpose.CARE_MANAGEMENT - ) - hi_types = ArrayField( - models.CharField(choices=HealthInformationTypes, max_length=20), - default=list, - ) - - hip = models.CharField(max_length=50, null=True, blank=True) - hiu = models.CharField(max_length=50, null=True, blank=True) - - requester = models.ForeignKey( - User, on_delete=models.SET_NULL, null=True, blank=True - ) - - access_mode = models.CharField( - choices=AccessMode, max_length=20, default=AccessMode.VIEW - ) - from_time = models.DateTimeField(null=True, blank=True, default=default_from_time) - to_time = models.DateTimeField(null=True, blank=True, default=default_to_time) - expiry = models.DateTimeField(null=True, blank=True, default=default_expiry) - - frequency_unit = models.CharField( - choices=FrequencyUnit, max_length=20, default=FrequencyUnit.HOUR - ) - frequency_value = models.PositiveSmallIntegerField( - default=1, validators=[MinValueValidator(1)] - ) - frequency_repeats = models.PositiveSmallIntegerField(default=0) - - def consent_details_dict(self): - return { - "patient_abha": self.patient_abha, - "care_contexts": self.care_contexts, - "status": self.status, - "purpose": self.purpose, - "hi_types": self.hi_types, - "hip": self.hip, - "hiu": self.hiu, - "requester": self.requester, - "access_mode": self.access_mode, - "from_time": self.from_time, - "to_time": self.to_time, - "expiry": self.expiry, - "frequency_unit": self.frequency_unit, - "frequency_value": self.frequency_value, - "frequency_repeats": self.frequency_repeats, - } - - -class ConsentRequest(Consent): - @property - def request_id(self): - return self.consent_id - - -class ConsentArtefact(Consent): - @property - def artefact_id(self): - return self.external_id - - @property - def transaction_id(self): - return self.consent_id - - def save(self, *args, **kwargs): - if self.key_material_private_key is None: - cipher = Cipher("", "") - key_material = cipher.generate_key_pair() - - self.key_material_algorithm = "ECDH" - self.key_material_curve = "Curve25519" - self.key_material_public_key = key_material["publicKey"] - self.key_material_private_key = key_material["privateKey"] - self.key_material_nonce = key_material["nonce"] - - if self.status in [Status.REVOKED.value, Status.EXPIRED.value]: - file = FileUpload.objects.filter( - internal_name=f"{self.external_id}.json", - file_type=FileUpload.FileType.ABDM_HEALTH_INFORMATION.value, - ).first() - - if file: - file.is_archived = True - file.archived_datetime = timezone.now() - file.archive_reason = self.status - file.save() - - return super().save(*args, **kwargs) - - consent_request = models.ForeignKey( - ConsentRequest, - on_delete=models.PROTECT, - to_field="consent_id", - null=True, - blank=True, - related_name="consent_artefacts", - ) - - cm = models.CharField(max_length=50, null=True, blank=True) - - key_material_algorithm = models.CharField( - max_length=20, - null=True, - blank=True, - default="ECDH", - ) - key_material_curve = models.CharField( - max_length=20, null=True, blank=True, default="Curve25519" - ) - key_material_public_key = models.CharField(max_length=100, null=True, blank=True) - key_material_private_key = models.CharField(max_length=200, null=True, blank=True) - key_material_nonce = models.CharField(max_length=100, null=True, blank=True) - - signature = models.TextField(null=True, blank=True) diff --git a/care/abdm/models/health_facility.py b/care/abdm/models/health_facility.py deleted file mode 100644 index a38bf1367c..0000000000 --- a/care/abdm/models/health_facility.py +++ /dev/null @@ -1,15 +0,0 @@ -from django.db import models - -from care.abdm.models.permissions.health_facility import HealthFacilityPermissions -from care.utils.models.base import BaseModel - - -class HealthFacility(BaseModel, HealthFacilityPermissions): - hf_id = models.CharField(max_length=50, unique=True) - registered = models.BooleanField(default=False) - facility = models.OneToOneField( - "facility.Facility", on_delete=models.PROTECT, to_field="external_id" - ) - - def __str__(self): - return f"{self.hf_id} {self.facility}" diff --git a/care/abdm/models/json_schema.py b/care/abdm/models/json_schema.py deleted file mode 100644 index 081b65cc7c..0000000000 --- a/care/abdm/models/json_schema.py +++ /dev/null @@ -1,15 +0,0 @@ -CARE_CONTEXTS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "content": [ - { - "type": "object", - "properties": { - "patientReference": {"type": "string"}, - "careContextReference": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["patientReference", "careContextReference"], - } - ], -} diff --git a/care/abdm/models/permissions/health_facility.py b/care/abdm/models/permissions/health_facility.py deleted file mode 100644 index f1ccd0045e..0000000000 --- a/care/abdm/models/permissions/health_facility.py +++ /dev/null @@ -1,29 +0,0 @@ -from care.facility.models.mixins.permissions.base import BasePermissionMixin -from care.users.models import User - - -class HealthFacilityPermissions(BasePermissionMixin): - """ - Permissions for HealthFacilityViewSet - """ - - def has_object_read_permission(self, request): - return self.facility.has_object_read_permission(request) - - def has_object_write_permission(self, request): - allowed_user_types = [ - User.TYPE_VALUE_MAP["WardAdmin"], - User.TYPE_VALUE_MAP["LocalBodyAdmin"], - User.TYPE_VALUE_MAP["DistrictAdmin"], - User.TYPE_VALUE_MAP["StateAdmin"], - ] - return request.user.is_superuser or ( - request.user.user_type in allowed_user_types - and self.facility.has_object_write_permission(request) - ) - - def has_object_update_permission(self, request): - return self.has_object_write_permission(request) - - def has_object_destroy_permission(self, request): - return self.has_object_write_permission(request) diff --git a/care/abdm/receivers/consultation.py b/care/abdm/receivers/consultation.py deleted file mode 100644 index 82c9c3c847..0000000000 --- a/care/abdm/receivers/consultation.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from care.abdm.utils.api_call import AbdmGateway -from care.facility.models import PatientConsultation - - -@receiver(post_save, sender=PatientConsultation) -def create_care_context(sender, instance, created, **kwargs): - patient = instance.patient - - if created and getattr(patient, "abha_number", None) is not None: - abha_number = patient.abha_number - - try: - AbdmGateway().fetch_modes( - { - "healthId": abha_number.abha_number, - "name": abha_number.name, - "gender": abha_number.gender, - "dateOfBirth": str(abha_number.date_of_birth), - "consultationId": instance.external_id, - "purpose": "LINK", - } - ) - except Exception: - pass diff --git a/care/abdm/service/gateway.py b/care/abdm/service/gateway.py deleted file mode 100644 index 45f0c781e0..0000000000 --- a/care/abdm/service/gateway.py +++ /dev/null @@ -1,215 +0,0 @@ -import uuid -from datetime import UTC, datetime - -from django.conf import settings -from django.db.models import Q -from rest_framework.exceptions import ValidationError - -from care.abdm.models.abha_number import AbhaNumber -from care.abdm.models.base import Purpose -from care.abdm.models.consent import ConsentArtefact, ConsentRequest -from care.abdm.service.request import Request - - -class Gateway: - def __init__(self): - self.request = Request(settings.ABDM_URL + "/gateway") - - def consent_requests__init(self, consent: ConsentRequest): - data = { - "requestId": str(consent.external_id), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consent": { - "purpose": { - "text": Purpose(consent.purpose).label, - "code": Purpose(consent.purpose).value, - }, - "patient": {"id": consent.patient_abha.health_id}, - "hiu": { - "id": self.get_hf_id_by_health_id(consent.patient_abha.health_id) - }, - "requester": { - "name": f"{consent.requester.REVERSE_TYPE_MAP[consent.requester.user_type]}, {consent.requester.first_name} {consent.requester.last_name}", - "identifier": { - "type": "Care Username", - "value": consent.requester.username, - "system": settings.CURRENT_DOMAIN, - }, - }, - "hiTypes": consent.hi_types, - "permission": { - "accessMode": consent.access_mode, - "dateRange": { - "from": consent.from_time.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "to": consent.to_time.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - }, - "dataEraseAt": consent.expiry.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "frequency": { - "unit": consent.frequency_unit, - "value": consent.frequency_value, - "repeats": consent.frequency_repeats, - }, - }, - }, - } - - path = "/v0.5/consent-requests/init" - return self.request.post(path, data, headers={"X-CM-ID": settings.X_CM_ID}) - - def consent_requests__status(self, consent_request_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consentRequestId": consent_request_id, - } - - return self.request.post( - "/v0.5/consent-requests/status", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def consents__hiu__on_notify(self, consent: ConsentRequest, request_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "resp": {"requestId": request_id}, - } - - if len(consent.consent_artefacts.all()): - data["acknowledgement"] = [] - - for aretefact in consent.consent_artefacts.all(): - data["acknowledgement"].append( - { - "consentId": str(aretefact.artefact_id), - "status": "OK", - } - ) - - return self.request.post( - "/v0.5/consents/hiu/on-notify", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def consents__fetch(self, consent_artefact_id: str): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "consentId": consent_artefact_id, - } - - return self.request.post( - "/v0.5/consents/fetch", data, headers={"X-CM-ID": settings.X_CM_ID} - ) - - def health_information__cm__request(self, artefact: ConsentArtefact): - request_id = str(uuid.uuid4()) - artefact.consent_id = request_id - artefact.save() - - data = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "hiRequest": { - "consent": {"id": str(artefact.artefact_id)}, - "dataPushUrl": settings.BACKEND_DOMAIN - + "/v0.5/health-information/transfer", - "keyMaterial": { - "cryptoAlg": artefact.key_material_algorithm, - "curve": artefact.key_material_curve, - "dhPublicKey": { - "expiry": artefact.expiry.astimezone(UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "parameters": f"{artefact.key_material_curve}/{artefact.key_material_algorithm}", - "keyValue": artefact.key_material_public_key, - }, - "nonce": artefact.key_material_nonce, - }, - "dateRange": { - "from": artefact.from_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - "to": artefact.to_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"), - }, - }, - } - - return self.request.post( - "/v0.5/health-information/cm/request", - data, - headers={"X-CM-ID": settings.X_CM_ID}, - ) - - def get_hf_id_by_health_id(self, health_id): - abha_number = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - if not abha_number: - raise ValidationError(detail="No ABHA Number found") - - patient_facility = abha_number.patient.last_consultation.facility - if not hasattr(patient_facility, "healthfacility"): - raise ValidationError(detail="Health Facility not linked") - - return patient_facility.healthfacility.hf_id - - def health_information__notify(self, artefact: ConsentArtefact): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "consentId": str(artefact.artefact_id), - "transactionId": str(artefact.transaction_id), - "doneAt": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notifier": { - "type": "HIU", - "id": self.get_hf_id_by_health_id(artefact.patient_abha.health_id), - }, - "statusNotification": { - "sessionStatus": "TRANSFERRED", - "hipId": artefact.hip, - }, - }, - } - - return self.request.post( - "/v0.5/health-information/notify", - data, - headers={"X-CM-ID": settings.X_CM_ID}, - ) - - def patients__find(self, abha_number: AbhaNumber): - data = { - "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "patient": {"id": abha_number.health_id}, - "requester": { - "type": "HIU", - "id": self.get_hf_id_by_health_id(abha_number.health_id), - }, - }, - } - - return self.request.post( - "/v0.5/patients/find", data, headers={"X-CM-ID": settings.X_CM_ID} - ) diff --git a/care/abdm/service/request.py b/care/abdm/service/request.py deleted file mode 100644 index 55b9f12127..0000000000 --- a/care/abdm/service/request.py +++ /dev/null @@ -1,106 +0,0 @@ -import json -import logging - -import requests -from django.conf import settings -from django.core.cache import cache - -ABDM_GATEWAY_URL = settings.ABDM_URL + "/gateway" -ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" -ABDM_TOKEN_CACHE_KEY = "abdm_token" - -logger = logging.getLogger(__name__) - - -class Request: - def __init__(self, base_url): - self.url = base_url - - def user_header(self, user_token): - if not user_token: - return {} - return {"X-Token": "Bearer " + user_token} - - def auth_header(self): - token = cache.get(ABDM_TOKEN_CACHE_KEY) - if not token: - data = json.dumps( - { - "clientId": settings.ABDM_CLIENT_ID, - "clientSecret": settings.ABDM_CLIENT_SECRET, - } - ) - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - - logger.info("No Token in Cache") - response = requests.post(ABDM_TOKEN_URL, data=data, headers=headers) - - if response.status_code < 300: - if response.headers["Content-Type"] != "application/json": - logger.info( - f"Unsupported Content-Type: {response.headers['Content-Type']}" - ) - logger.info(f"Response: {response.text}") - - return None - else: - data = response.json() - token = data["accessToken"] - expires_in = data["expiresIn"] - - logger.info(f"New Token: {token}") - logger.info(f"Expires in: {expires_in}") - - cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) - else: - logger.info(f"Bad Response: {response.text}") - return None - - return {"Authorization": f"Bearer {token}"} - - def headers(self, additional_headers=None, auth=None): - return { - "Content-Type": "application/json", - "Accept": "*/*", - **(additional_headers or {}), - **(self.user_header(auth) or {}), - **(self.auth_header() or {}), - } - - def get(self, path, params=None, headers=None, auth=None): - url = self.url + path - headers = self.headers(headers, auth) - - logger.info(f"GET: {url}") - response = requests.get(url, headers=headers, params=params) - logger.info(f"{response.status_code} Response: {response.text}") - - return self._handle_response(response) - - def post(self, path, data=None, headers=None, auth=None): - url = self.url + path - payload = json.dumps(data) - headers = self.headers(headers, auth) - - logger.info(f"POST: {url}, {headers}, {data}") - response = requests.post(url, data=payload, headers=headers) - logger.info(f"{response.status_code} Response: {response.text}") - - return self._handle_response(response) - - def _handle_response(self, response: requests.Response): - def custom_json(): - try: - return response.json() - except json.JSONDecodeError as json_err: - logger.error(f"JSON Decode error: {json_err}") - return {"error": response.text} - except Exception as err: - logger.error(f"Unknown error while decoding json: {err}") - return {} - - response.json = custom_json - return response diff --git a/care/abdm/tests.py b/care/abdm/tests.py deleted file mode 100644 index a79ca8be56..0000000000 --- a/care/abdm/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/care/abdm/urls.py b/care/abdm/urls.py deleted file mode 100644 index bf4eb8a5d9..0000000000 --- a/care/abdm/urls.py +++ /dev/null @@ -1,141 +0,0 @@ -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.consent import ConsentCallbackViewSet -from care.abdm.api.viewsets.health_information import HealthInformationCallbackViewSet -from care.abdm.api.viewsets.hip import HipViewSet -from care.abdm.api.viewsets.monitoring import HeartbeatView -from care.abdm.api.viewsets.patients import PatientsCallbackViewSet -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/consent-requests/on-init", - ConsentCallbackViewSet.as_view({"post": "consent_request__on_init"}), - name="abdm__consent_request__on_init", - ), - path( - "v0.5/consent-requests/on-status", - ConsentCallbackViewSet.as_view({"post": "consent_request__on_status"}), - name="abdm__consent_request__on_status", - ), - path( - "v0.5/consents/hiu/notify", - ConsentCallbackViewSet.as_view({"post": "consents__hiu__notify"}), - name="abdm__consents__hiu__notify", - ), - path( - "v0.5/consents/on-fetch", - ConsentCallbackViewSet.as_view({"post": "consents__on_fetch"}), - name="abdm__consents__on_fetch", - ), - path( - "v0.5/health-information/hiu/on-request", - HealthInformationCallbackViewSet.as_view( - {"post": "health_information__hiu__on_request"} - ), - name="abdm__health_information__hiu__on_request", - ), - path( - "v0.5/health-information/transfer", - HealthInformationCallbackViewSet.as_view( - {"post": "health_information__transfer"} - ), - name="abdm__health_information__transfer", - ), - path( - "v0.5/patients/on-find", - PatientsCallbackViewSet.as_view({"post": "patients__on_find"}), - name="abdm__patients__on_find", - ), - 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/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py deleted file mode 100644 index e357a6f781..0000000000 --- a/care/abdm/utils/api_call.py +++ /dev/null @@ -1,812 +0,0 @@ -import json -import logging -import uuid -from base64 import b64encode -from datetime import UTC, datetime, timedelta - -import requests -from Crypto.Cipher import PKCS1_v1_5 -from Crypto.PublicKey import RSA -from django.conf import settings -from django.core.cache import cache -from django.db.models import Q -from rest_framework.exceptions import ValidationError - -from care.abdm.models import AbhaNumber -from care.abdm.service.request import Request -from care.facility.models.patient_consultation import PatientConsultation - -GATEWAY_API_URL = settings.ABDM_URL -HEALTH_SERVICE_API_URL = settings.HEALTH_SERVICE_API_URL -ABDM_DEVSERVICE_URL = GATEWAY_API_URL + "/devservice" -ABDM_GATEWAY_URL = GATEWAY_API_URL + "/gateway" -ABDM_TOKEN_URL = ABDM_GATEWAY_URL + "/v0.5/sessions" -ABDM_TOKEN_CACHE_KEY = "abdm_token" -ABDM_FACILITY_URL = settings.ABDM_FACILITY_URL - -# 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( - 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()) - return b64encode(encrypted_text).decode() - - -class APIGateway: - def __init__(self, gateway, token): - if gateway == "health": - self.url = HEALTH_SERVICE_API_URL - elif gateway == "abdm": - self.url = GATEWAY_API_URL - elif gateway == "abdm_gateway": - self.url = ABDM_GATEWAY_URL - elif gateway == "abdm_devservice": - self.url = ABDM_DEVSERVICE_URL - elif gateway == "facility": - self.url = ABDM_FACILITY_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_user_header(self, headers, user_token): - headers.update( - { - "X-Token": "Bearer " + user_token, - } - ) - return headers - - def add_auth_header(self, headers): - token = cache.get(ABDM_TOKEN_CACHE_KEY) - if not token: - logger.info("No Token in Cache") - 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 - ) - logger.info(f"Token Response Status: {resp.status_code}") - if resp.status_code < 300: - # Checking if Content-Type is application/json - if resp.headers["Content-Type"] != "application/json": - logger.info( - "Unsupported Content-Type: {}".format( - resp.headers["Content-Type"] - ) - ) - logger.info(f"Response: {resp.text}") - return None - else: - data = resp.json() - token = data["accessToken"] - expires_in = data["expiresIn"] - logger.info(f"New Token: {token}") - logger.info(f"Expires in: {expires_in}") - cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) - else: - logger.info(f"Bad Response: {resp.text}") - return None - # logger.info("Returning Authorization Header: Bearer {}".format(token)) - logger.info("Adding Authorization Header") - auth_header = {"Authorization": f"Bearer {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 = {} - headers = self.add_auth_header(headers) - if auth: - headers = self.add_user_header(headers, auth) - logger.info(f"Making GET Request to: {url}") - response = requests.get(url, headers=headers, params=params) - logger.info(f"{response.status_code} Response: {response.text}") - return response - - def post(self, path, data=None, auth=None, additional_headers=None, method="POST"): - url = self.url + path - headers = { - "Content-Type": "application/json", - "accept": "*/*", - "Accept-Language": "en-US", - } - 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()] - # ) - data_json = json.dumps(data) - # logger.info("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) - logger.info(f"Posting Request to: {url}") - response = requests.request(method, url, headers=headers, data=data_json) - logger.info(f"{response.status_code} Response: {response.text}") - 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) - logger.info(f"{response.status_code} Response: {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 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 = "/v2/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" - logger.info(f"Creating Health ID with data: {data}") - # data.pop("healthId", None) - 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/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/confirmWithDemographics - def confirm_with_demographics(self, data): - path = "/v1/auth/confirmWithDemographics" - 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: - 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" - response = self.api.post(path, {"refreshToken": refreshToken}) - return response.json()["accessToken"] - - # 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/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 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): - path = "/v1/account/qrCode" - access_token = self.generate_access_token(data) - logger.info(f"Getting QR Code for: {data}") - response = self.api.get(path, {}, access_token) - logger.info(f"QR Code Response: {response.text}") - return response.json() - - -class HealthIdGatewayV2: - def __init__(self): - self.api = APIGateway("health", None) - - # V2 APIs - 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_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() - - -class AbdmGateway: - # TODO: replace this with in-memory db (redis) - temp_memory = {} - - def __init__(self): - self.api = APIGateway("abdm_gateway", None) - - def get_hip_id_by_health_id(self, health_id): - abha_number = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - if not abha_number: - raise ValidationError(detail="No ABHA Number found") - - patient_facility = abha_number.patient.last_consultation.facility - if not getattr(patient_facility, "healthfacility", None): - raise ValidationError(detail="Health Facility not linked") - - return patient_facility.healthfacility.hf_id - - def add_care_context(self, access_token, request_id): - if request_id not in self.temp_memory: - return None - - 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: {consultation.created_date.date()!s}", - } - ) - - return response - - return False - - def save_linking_token(self, patient, access_token, request_id): - if request_id not in self.temp_memory: - return None - - data = self.temp_memory[request_id] - health_id = patient and patient["id"] or data["healthId"] - - abha_object = AbhaNumber.objects.filter( - Q(abha_number=health_id) | Q(health_id=health_id) - ).first() - - 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): - path = "/v0.5/users/auth/fetch-modes" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - request_id = str(uuid.uuid4()) - - self.temp_memory[request_id] = data - if "authMode" in data and data["authMode"] == "DIRECT": - self.init(request_id) - return None - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "id": data["healthId"], - "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "requester": { - "type": "HIP", - "id": self.get_hip_id_by_health_id(data["healthId"]), - }, - }, - } - response = self.api.post(path, payload, None, additional_headers) - return response - - # "/v0.5/users/auth/init" - def init(self, prev_request_id): - if prev_request_id not in self.temp_memory: - return None - - path = "/v0.5/users/auth/init" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - data = self.temp_memory[prev_request_id] - self.temp_memory[request_id] = data - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "query": { - "id": data["healthId"], - "purpose": data["purpose"] if "purpose" in data else "KYC_AND_LINK", - "authMode": data["authMode"] if "authMode" in data else "DEMOGRAPHICS", - "requester": { - "type": "HIP", - "id": self.get_hip_id_by_health_id(data["healthId"]), - }, - }, - } - response = self.api.post(path, payload, None, additional_headers) - return response - - # "/v0.5/users/auth/confirm" - def confirm(self, transaction_id, prev_request_id): - if prev_request_id not in self.temp_memory: - return None - - path = "/v0.5/users/auth/confirm" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - data = self.temp_memory[prev_request_id] - self.temp_memory[request_id] = data - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "transactionId": transaction_id, - "credential": { - "demographic": { - "name": data["name"], - "gender": data["gender"], - "dateOfBirth": data["dateOfBirth"], - }, - "authCode": "", - }, - } - - 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": datetime.now(tz=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" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - - request_id = str(uuid.uuid4()) - - payload = { - "requestId": request_id, - "timestamp": datetime.now(tz=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": datetime.now(tz=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": datetime.now(tz=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": datetime.now(tz=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 - - 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": datetime.now(tz=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 - - 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": datetime.now(tz=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): - auth_header = Request("").auth_header() - - if not auth_header: - return None - - headers = { - "Content-Type": "application/json", - **auth_header, - } - - payload = { - "pageNumber": 1, - "pageCount": 1, - "transactionId": data["transaction_id"], - "entries": list( - map( - lambda context: { - "content": context["data"], - "media": "application/fhir+json", - "checksum": "string", - "careContextReference": context["consultation_id"], - }, - data["care_contexts"], - ) - ), - "keyMaterial": data["key_material"], - } - - response = requests.post( - data["data_push_url"], data=json.dumps(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": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "consentId": data["consent_id"], - "transactionId": data["transaction_id"], - "doneAt": str( - datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") - ), - "statusNotification": { - "sessionStatus": data["session_status"], - "hipId": self.get_hip_id_by_health_id(data["health_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 - - 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": datetime.now(tz=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 - - 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": datetime.now(tz=UTC).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ), - "notification": { - "phoneNo": f"+91-{data['phone']}", - "hip": {"id": self.get_hip_id_by_health_id(data["healthId"])}, - }, - } - - 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" - additional_headers = {"X-CM-ID": settings.X_CM_ID} - response = self.api.post(path, data, None, additional_headers) - return response - - -class Bridge: - def __init__(self): - self.api = APIGateway("abdm_devservice", None) - - def add_update_service(self, data): - path = "/v1/bridges/addUpdateServices" - response = self.api.post(path, data, method="PUT") - return response - - -class Facility: - def __init__(self) -> None: - self.api = APIGateway("facility", None) - - def add_update_service(self, data): - path = "/v1/bridges/MutipleHRPAddUpdateServices" - response = self.api.post(path, data, method="POST") - return response diff --git a/care/abdm/utils/cipher.py b/care/abdm/utils/cipher.py deleted file mode 100644 index 5a48430956..0000000000 --- a/care/abdm/utils/cipher.py +++ /dev/null @@ -1,95 +0,0 @@ -import json - -import requests -from django.conf import settings - - -class Cipher: - server_url = settings.FIDELIUS_URL - - def __init__( - self, - external_public_key, - external_nonce, - internal_private_key=None, - internal_public_key=None, - internal_nonce=None, - ): - self.external_public_key = external_public_key - self.external_nonce = external_nonce - - self.internal_private_key = internal_private_key - self.internal_public_key = internal_public_key - self.internal_nonce = internal_nonce - - 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.internal_private_key = key_material["privateKey"] - self.internal_public_key = key_material["publicKey"] - self.internal_nonce = key_material["nonce"] - - return key_material - - return None - - def encrypt(self, paylaod): - if not self.internal_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.external_public_key, - "receiverNonce": self.external_nonce, - "senderPrivateKey": self.internal_private_key, - "senderPublicKey": self.internal_public_key, - "senderNonce": self.internal_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.internal_nonce, - } - - return None - - def decrypt(self, paylaod): - response = requests.post( - f"{self.server_url}/decrypt", - headers={"Content-Type": "application/json"}, - data=json.dumps( - { - "receiverPrivateKey": self.internal_private_key, - "receiverNonce": self.internal_nonce, - "senderPublicKey": self.external_public_key, - "senderNonce": self.external_nonce, - "encryptedData": paylaod, - } - ), - ) - - if response.status_code == 200: - data = response.json() - - return data["decryptedData"] - - return None diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py deleted file mode 100644 index eb9db58f2e..0000000000 --- a/care/abdm/utils/fhir.py +++ /dev/null @@ -1,1220 +0,0 @@ -import base64 -from datetime import UTC, datetime -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.diagnosticreport import DiagnosticReport -from fhir.resources.documentreference import DocumentReference, DocumentReferenceContent -from fhir.resources.dosage import Dosage -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 -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.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.models.icd11_diagnosis import REVERSE_CONDITION_VERIFICATION_STATUSES -from care.facility.models.patient_investigation import InvestigationValue -from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_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._careplan_profile = None - self._diagnostic_report_profile = None - self._immunization_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: - return "" - - return f"{resource.resource_type}/{resource.id}" - - 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.treating_physician - and f"{self.consultation.treating_physician.first_name} {self.consultation.treating_physician.last_name}" - ) - or self.consultation.deprecated_verified_by - or f"{self.consultation.created_by.first_name} {self.consultation.created_by.last_name}" - ) - self._practitioner_profile = Practitioner( - 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) - 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(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 _condition(self, diagnosis_id, verification_status): - diagnosis = get_icd11_diagnosis_object_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=verification_status, - display=REVERSE_CONDITION_VERIFICATION_STATUSES[ - verification_status - ], - ) - ] - ), - 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.encounter_date.isoformat(), - end=( - self.consultation.discharge_date.isoformat() - if self.consultation.discharge_date - else None - ), - ), - note=[ - Annotation(text=self.consultation.treatment_plan), - Annotation(text=self.consultation.consultation_notes), - Annotation(text=self.consultation.special_instruction), - ], - subject=self._reference(self._patient()), - ) - - 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 (isinstance(value, dict) and not value["value"]): - return None - - return Observation( - 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), - valueQuantity=( - Quantity(value=str(value["value"]), unit=value["unit"]) - if isinstance(value, dict) - else None - ), - valueString=value if isinstance(value, str) else None, - component=( - list( - map( - lambda component: ObservationComponent( - code=CodeableConcept(text=component["title"]), - valueQuantity=( - Quantity( - value=component["value"], unit=component["unit"] - ) - if isinstance(component, dict) - else None - ), - valueString=( - component if isinstance(component, str) else None - ), - ), - value, - ) - ) - if isinstance(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.ventilator_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, include_diagnosis=False): - 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.encounter_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), - "diagnosis": ( - list( - map( - lambda consultation_diagnosis: EncounterDiagnosis( - condition=self._reference( - self._condition( - consultation_diagnosis.diagnosis_id, - consultation_diagnosis.verification_status, - ), - ) - ), - self.consultation.diagnoses.all(), - ) - ) - if include_diagnosis - else None - ), - } - ) - - return self._encounter_profile - - def _immunization(self): - if self._immunization_profile: - return self._immunization_profile - - if not self.consultation.patient.is_vaccinated: - return None - - self._immunization_profile = 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 _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)) - - self._medication_profiles.append(medication_profile) - return medication_profile - - def _medication_request(self, medicine): - id = str(uuid()) - prescription_date = ( - self.consultation.encounter_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']} 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 _prescription_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="440545006", - display="Prescription record", - ) - ] - ), - title="Prescription", - date=datetime.now(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())], - ) - - 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(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( - 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(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 _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(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())] - if self._immunization() - else [] - ) - ], - emptyReason=( - None - if self._immunization() - else CodeableConcept( - coding=[Coding(code="notasked", display="Not Asked")] - ) - ), - ), - ], - subject=self._reference(self._patient()), - encounter=self._reference(self._encounter()), - 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(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( - 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(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 _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(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) - - def create_prescription_record(self): - id = str(uuid()) - now = datetime.now(UTC).isoformat() - return Bundle( - id=id, - identifier=Identifier(value=id), - type="document", - meta=Meta(lastUpdated=now), - timestamp=now, - entry=[ - self._bundle_entry(self._prescription_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._medication_profiles, - ) - ), - *list( - map( - lambda resource: self._bundle_entry(resource), - self._medication_request_profiles, - ) - ), - ], - ).json() - - def create_wellness_record(self): - id = str(uuid()) - now = datetime.now(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() - - def create_immunization_record(self): - id = str(uuid()) - now = datetime.now(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() - - def create_diagnostic_report_record(self): - id = str(uuid()) - now = datetime.now(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(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() - - def create_discharge_summary_record(self): - id = str(uuid()) - now = datetime.now(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() - - def create_op_consultation_record(self): - id = str(uuid()) - now = datetime.now(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() - if record_type == "WellnessRecord": - return self.create_wellness_record() - if record_type == "ImmunizationRecord": - return self.create_immunization_record() - if record_type == "HealthDocumentRecord": - return self.create_health_document_record() - if record_type == "DiagnosticReport": - return self.create_diagnostic_report_record() - if record_type == "DischargeSummary": - return self.create_discharge_summary_record() - if record_type == "OPConsultation": - return self.create_op_consultation_record() - return self.create_discharge_summary_record() diff --git a/care/abdm/views.py b/care/abdm/views.py deleted file mode 100644 index 60f00ef0ef..0000000000 --- a/care/abdm/views.py +++ /dev/null @@ -1 +0,0 @@ -# Create your views here. diff --git a/care/facility/management/commands/load_dummy_data.py b/care/facility/management/commands/load_dummy_data.py index 4a0633801c..ea57bf17a6 100644 --- a/care/facility/management/commands/load_dummy_data.py +++ b/care/facility/management/commands/load_dummy_data.py @@ -2,6 +2,13 @@ from django.core import management from django.core.management import BaseCommand, CommandError +from django.db.models.signals import ( + m2m_changed, + post_delete, + post_save, + pre_delete, + pre_save, +) class Command(BaseCommand): @@ -20,6 +27,20 @@ def handle(self, *args, **options): msg = "This command is not intended to be run in production environment." raise CommandError(msg) + # Disconnecting signals temporarily to avoid conflicts + signals_to_disconnect = [ + post_save, + post_delete, + pre_save, + pre_delete, + m2m_changed, + ] + original_receivers = {} + + for signal in signals_to_disconnect: + original_receivers[signal] = signal.receivers + signal.receivers = [] + try: management.call_command("loaddata", self.BASE_URL + "states.json") management.call_command("load_skill_data") @@ -32,3 +53,7 @@ def handle(self, *args, **options): management.call_command("populate_investigations") except Exception as e: raise CommandError(e) from e + finally: + # Reconnect original signals + for signal in signals_to_disconnect: + signal.receivers = original_receivers[signal] diff --git a/config/api_router.py b/config/api_router.py index b4a3aa0f4a..7e00754a00 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -3,12 +3,6 @@ from rest_framework.routers import DefaultRouter, SimpleRouter from rest_framework_nested.routers import NestedSimpleRouter -from care.abdm.api.viewsets.abha_number import AbhaNumberViewSet -from care.abdm.api.viewsets.consent import ConsentViewSet -from care.abdm.api.viewsets.health_facility import HealthFacilityViewSet -from care.abdm.api.viewsets.health_information import HealthInformationViewSet -from care.abdm.api.viewsets.healthid import ABDMHealthIDViewSet -from care.abdm.api.viewsets.patients import PatientsViewSet from care.facility.api.viewsets.ambulance import AmbulanceViewSet from care.facility.api.viewsets.asset import ( AssetLocationViewSet, @@ -321,23 +315,6 @@ router.register("public/asset", AssetPublicViewSet, basename="public-asset") router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") -# ABDM endpoints -if settings.ENABLE_ABDM: - router.register("abdm/healthid", ABDMHealthIDViewSet, basename="abdm-healthid") - router.register("abdm/consent", ConsentViewSet, basename="abdm-consent") - router.register( - "abdm/health_information", - HealthInformationViewSet, - basename="abdm-healthinformation", - ) - router.register("abdm/patients", PatientsViewSet, basename="abdm-patients") - router.register("abdm/abha_numbers", AbhaNumberViewSet, basename="abdm-abhanumber") - -router.register( - "abdm/health_facility", HealthFacilityViewSet, basename="abdm-healthfacility" -) - - app_name = "api" urlpatterns = [ path("", include(router.urls)), diff --git a/config/authentication.py b/config/authentication.py index 086348b1bc..619ae9ab22 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,9 +1,7 @@ -import json import logging import jwt import requests -from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.core.cache import cache from django.core.exceptions import ValidationError @@ -199,57 +197,6 @@ def get_user(self, validated_token, facility): return asset_user -class ABDMAuthentication(JWTAuthentication): - def open_id_authenticate(self, url, token): - public_key = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) - 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 = self.get_header(request) - 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: - logger.info(e, "Token: ", token) - raise InvalidToken({"detail": "Invalid Authorization token"}) from 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@ohc.network", - password=f"{password}xyz", - gender=3, - phone_number="917777777777", - user_type=User.TYPE_VALUE_MAP["Volunteer"], - verified=True, - date_of_birth=timezone.now().date(), - ) - user.save() - return user - - class CustomJWTAuthenticationScheme(OpenApiAuthenticationExtension): target_class = "config.authentication.CustomJWTAuthentication" name = "jwtAuth" diff --git a/config/settings/base.py b/config/settings/base.py index 5bf8ddd3b6..df20ebb0cf 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -125,7 +125,6 @@ ] LOCAL_APPS = [ "care.facility", - "care.abdm", "care.users", "care.audit_log", ] @@ -633,21 +632,6 @@ APP_VERSION = env("APP_VERSION", default="unknown") -# 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") -HEALTH_SERVICE_API_URL = env( - "HEALTH_SERVICE_API_URL", default="https://healthidsbx.abdm.gov.in/api" -) -ABDM_FACILITY_URL = env("ABDM_FACILITY_URL", default="https://facilitysbx.abdm.gov.in") -HIP_NAME_PREFIX = env("HIP_NAME_PREFIX", default="") -HIP_NAME_SUFFIX = env("HIP_NAME_SUFFIX", default="") -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") - IS_PRODUCTION = False PLAUSIBLE_HOST = env("PLAUSIBLE_HOST", default="") diff --git a/config/urls.py b/config/urls.py index c90bd2adc2..904af4b3f3 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,7 +9,6 @@ SpectacularSwaggerView, ) -from care.abdm.urls import abdm_urlpatterns from care.facility.api.viewsets.open_id import PublicJWKsView from care.facility.api.viewsets.patient_consultation import ( dev_preview_discharge_summary, @@ -34,7 +33,7 @@ path("ping/", ping, name="ping"), path("app_version/", app_version, name="app_version"), # Django Admin, use {% url 'admin:index' %} - path(f"{settings.ADMIN_URL.rstrip("/")}/", admin.site.urls), + path(f"{settings.ADMIN_URL.rstrip('/')}/", admin.site.urls), # Rest API path("api/v1/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path( @@ -75,9 +74,6 @@ *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), ] -if settings.ENABLE_ABDM: - urlpatterns += abdm_urlpatterns - 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. diff --git a/docker-compose.yaml b/docker-compose.yaml index 166297a383..b361cbdd8b 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -37,12 +37,6 @@ services: ports: - "4566:4566" - fidelius: - image: khavinshankar/fidelius:latest - restart: unless-stopped - ports: - - "8092:8090" - volumes: postgres-data: redis-data: diff --git a/docker/.local.env b/docker/.local.env index b00327fc9b..83b5dadb33 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -26,3 +26,11 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem + +# ABDM envs: added to avoid test failures +ABDM_CLIENT_ID=SBX_001 +ABDM_CLIENT_SECRET=xxxx +ABDM_GATEWAY_URL=https://dev.abdm.gov.in +ABDM_ABHA_URL=https://abhasbx.abdm.gov.in +ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in +ABDM_CM_ID=sbx diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index f83f059d2a..0405816f59 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -5,7 +5,7 @@ ARG TYPST_VERSION=0.11.0 ENV PATH=/venv/bin:$PATH RUN apt-get update && apt-get install --no-install-recommends -y \ - build-essential libjpeg-dev zlib1g-dev \ + build-essential libjpeg-dev zlib1g-dev libgmp-dev \ libpq-dev gettext wget curl gnupg git \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* diff --git a/plug_config.py b/plug_config.py index c177c62cde..27be9de162 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,6 +1,13 @@ from plugs.manager import PlugManager from plugs.plug import Plug +abdm_plugin = Plug( + name="abdm", + package_name="git+https://github.com/ohcnetwork/care_abdm.git", + version="@main", + configs={}, +) + hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -8,6 +15,6 @@ configs={}, ) -plugs = [hcx_plugin] +plugs = [hcx_plugin, abdm_plugin] manager = PlugManager(plugs) From 7471c65360d7b352d152b2d57e3cb4149bbaaab5 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:33:51 +0530 Subject: [PATCH 10/14] added missing mandatory env to .local.env and .prebuild.env --- docker/.local.env | 2 ++ docker/.prebuilt.env | 10 ++++++++++ 2 files changed, 12 insertions(+) diff --git a/docker/.local.env b/docker/.local.env index 83b5dadb33..cfbd0d888a 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -34,3 +34,5 @@ ABDM_GATEWAY_URL=https://dev.abdm.gov.in ABDM_ABHA_URL=https://abhasbx.abdm.gov.in ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in ABDM_CM_ID=sbx +CURRENT_DOMAIN=https://care.ohc.network +BACKEND_DOMAIN=https://careapi.ohc.network diff --git a/docker/.prebuilt.env b/docker/.prebuilt.env index 8bcc36312e..18a9422988 100644 --- a/docker/.prebuilt.env +++ b/docker/.prebuilt.env @@ -39,3 +39,13 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem + +# ABDM envs: added to avoid test failures +ABDM_CLIENT_ID=SBX_001 +ABDM_CLIENT_SECRET=xxxx +ABDM_GATEWAY_URL=https://dev.abdm.gov.in +ABDM_ABHA_URL=https://abhasbx.abdm.gov.in +ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in +ABDM_CM_ID=sbx +CURRENT_DOMAIN=https://care.ohc.network +BACKEND_DOMAIN=https://careapi.ohc.network From f1c2f772f5efc922f2e2ae9831fd2658c8abeb0e Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 01:55:01 +0530 Subject: [PATCH 11/14] removed abdm config from plug_config --- docker/.local.env | 10 ---------- docker/.prebuilt.env | 10 ---------- plug_config.py | 9 +-------- 3 files changed, 1 insertion(+), 28 deletions(-) diff --git a/docker/.local.env b/docker/.local.env index cfbd0d888a..b00327fc9b 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -26,13 +26,3 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem - -# ABDM envs: added to avoid test failures -ABDM_CLIENT_ID=SBX_001 -ABDM_CLIENT_SECRET=xxxx -ABDM_GATEWAY_URL=https://dev.abdm.gov.in -ABDM_ABHA_URL=https://abhasbx.abdm.gov.in -ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in -ABDM_CM_ID=sbx -CURRENT_DOMAIN=https://care.ohc.network -BACKEND_DOMAIN=https://careapi.ohc.network diff --git a/docker/.prebuilt.env b/docker/.prebuilt.env index 18a9422988..8bcc36312e 100644 --- a/docker/.prebuilt.env +++ b/docker/.prebuilt.env @@ -39,13 +39,3 @@ HCX_PASSWORD=Opensaber@123 HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 HCX_USERNAME=qwertyreboot@gmail.com HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem - -# ABDM envs: added to avoid test failures -ABDM_CLIENT_ID=SBX_001 -ABDM_CLIENT_SECRET=xxxx -ABDM_GATEWAY_URL=https://dev.abdm.gov.in -ABDM_ABHA_URL=https://abhasbx.abdm.gov.in -ABDM_FACILITY_URL=https://facilitysbx.abdm.gov.in -ABDM_CM_ID=sbx -CURRENT_DOMAIN=https://care.ohc.network -BACKEND_DOMAIN=https://careapi.ohc.network diff --git a/plug_config.py b/plug_config.py index 27be9de162..c177c62cde 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,13 +1,6 @@ from plugs.manager import PlugManager from plugs.plug import Plug -abdm_plugin = Plug( - name="abdm", - package_name="git+https://github.com/ohcnetwork/care_abdm.git", - version="@main", - configs={}, -) - hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -15,6 +8,6 @@ configs={}, ) -plugs = [hcx_plugin, abdm_plugin] +plugs = [hcx_plugin] manager = PlugManager(plugs) From ee5caf9bcad844d3943761b1660d2702ff97353f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sat, 19 Oct 2024 02:01:50 +0530 Subject: [PATCH 12/14] Discard changes to plug_config.py --- plug_config.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plug_config.py b/plug_config.py index c177c62cde..27be9de162 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,6 +1,13 @@ from plugs.manager import PlugManager from plugs.plug import Plug +abdm_plugin = Plug( + name="abdm", + package_name="git+https://github.com/ohcnetwork/care_abdm.git", + version="@main", + configs={}, +) + hcx_plugin = Plug( name="hcx", package_name="git+https://github.com/ohcnetwork/care_hcx.git", @@ -8,6 +15,6 @@ configs={}, ) -plugs = [hcx_plugin] +plugs = [hcx_plugin, abdm_plugin] manager = PlugManager(plugs) From 2363119498da7b9663f58ae2707b0730deebadc9 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Sat, 19 Oct 2024 11:47:20 +0530 Subject: [PATCH 13/14] Fixed prod deploy: install libgmp-dev in prod (#2548) * fixed prod deploy: install libgmp-dev in prod * added libgmp-dev to runtime step in prod dockerfile --- docker/prod.Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 877478c3a9..067b0498d6 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -20,7 +20,7 @@ WORKDIR $APP_HOME FROM base AS builder RUN apt-get update && apt-get install --no-install-recommends -y \ - build-essential libjpeg-dev zlib1g-dev libpq-dev git wget \ + build-essential libjpeg-dev zlib1g-dev libgmp-dev libpq-dev git wget \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* @@ -54,7 +54,7 @@ RUN python3 $APP_HOME/install_plugins.py FROM base AS runtime RUN apt-get update && apt-get install --no-install-recommends -y \ - libpq-dev gettext wget curl gnupg \ + libpq-dev libgmp-dev gettext wget curl gnupg \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* From 00cd5f9a3d9860d55fed44fcc78bc62ef1047186 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sat, 19 Oct 2024 12:46:31 +0530 Subject: [PATCH 14/14] Move code owners to backend admins --- .github/CODEOWNERS | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 04cb82a8da..e6d222c56c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ -* @ohcnetwork/care-developers -*.yml @tomahawk-pilot +* @ohcnetwork/care-backend-admins