From 16b7cbbe6b47e4e834c66622f23db265434dfdac Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:11:03 +0530 Subject: [PATCH 01/32] Fix bed assignment validation for admission --- care/facility/api/serializers/daily_round.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 72d6b23893..3f14980056 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -179,8 +179,11 @@ def create(self, validated_data): ) # Authorisation Checks End - # Patient needs to have a bed assigned - if not consultation.current_bed: + # Patient needs to have a bed assigned for admission + if ( + not consultation.current_bed + and consultation.suggestion == SuggestionChoices.A + ): raise ValidationError( { "bed": "Patient does not have a bed assigned. Please assign a bed first" From 89f9749a386ab8f932c897d44afc7b29f9bdcbfe Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:11:27 +0530 Subject: [PATCH 02/32] add tests --- .../tests/test_patient_daily_rounds_api.py | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index ceb221e32f..037701a31d 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -4,6 +4,7 @@ from rest_framework.test import APITestCase from care.facility.models import PatientRegistration +from care.facility.models.patient_consultation import PatientConsultation from care.utils.tests.test_utils import TestUtils @@ -19,8 +20,15 @@ def setUpTestData(cls) -> None: cls.patient = cls.create_patient(district=cls.district, facility=cls.facility) cls.asset_location = cls.create_asset_location(cls.facility) cls.bed = cls.create_bed(facility=cls.facility, location=cls.asset_location) - cls.consultation_without_bed = cls.create_consultation( - facility=cls.facility, patient=cls.patient + cls.admission_consultation_no_bed = cls.create_consultation( + facility=cls.facility, + patient=cls.patient, + suggestion=PatientConsultation.SUGGESTION_CHOICES[1][0], + ) + cls.domiciliary_consultation_no_bed = cls.create_consultation( + facility=cls.facility, + patient=cls.patient, + suggestion=PatientConsultation.SUGGESTION_CHOICES[4][0], ) cls.consultation_with_bed = cls.create_consultation( facility=cls.facility, patient=cls.patient @@ -65,11 +73,11 @@ def test_action_in_log_update( patient.action, PatientRegistration.ActionEnum.DISCHARGE_RECOMMENDED.value ) - def test_log_update_without_bed( + def test_log_update_without_bed_for_admission( self, ): response = self.client.post( - f"/api/v1/consultation/{self.consultation_without_bed.external_id}/daily_rounds/", + f"/api/v1/consultation/{self.admission_consultation_no_bed.external_id}/daily_rounds/", data=self.log_update, format="json", ) @@ -78,3 +86,13 @@ def test_log_update_without_bed( response.data["bed"], "Patient does not have a bed assigned. Please assign a bed first", ) + + def test_log_update_without_bed_for_domiciliary( + self, + ): + response = self.client.post( + f"/api/v1/consultation/{self.domiciliary_consultation_no_bed.external_id}/daily_rounds/", + data=self.log_update, + format="json", + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) From d37985abf2f404bc4429a5d664737d3ac87e0770 Mon Sep 17 00:00:00 2001 From: Ashesh3 <3626859+Ashesh3@users.noreply.github.com> Date: Tue, 5 Mar 2024 18:56:07 +0530 Subject: [PATCH 03/32] add comments for suggestion choices --- care/facility/tests/test_patient_consultation_api.py | 4 +++- care/facility/tests/test_patient_daily_rounds_api.py | 4 ++-- care/utils/tests/test_utils.py | 4 +++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 9b74338d1a..2e3e007a06 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -39,7 +39,9 @@ def get_default_data(self): "examination_details": "examination_details", "history_of_present_illness": "history_of_present_illness", "treatment_plan": "treatment_plan", - "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], + "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][ + 0 + ], # HOME ISOLATION "treating_physician": self.doctor.id, "create_diagnoses": [ { diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index 037701a31d..07ccb87df6 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -23,12 +23,12 @@ def setUpTestData(cls) -> None: cls.admission_consultation_no_bed = cls.create_consultation( facility=cls.facility, patient=cls.patient, - suggestion=PatientConsultation.SUGGESTION_CHOICES[1][0], + suggestion=PatientConsultation.SUGGESTION_CHOICES[1][0], # ADMISSION ) cls.domiciliary_consultation_no_bed = cls.create_consultation( facility=cls.facility, patient=cls.patient, - suggestion=PatientConsultation.SUGGESTION_CHOICES[4][0], + suggestion=PatientConsultation.SUGGESTION_CHOICES[4][0], # DOMICILIARY CARE ) cls.consultation_with_bed = cls.create_consultation( facility=cls.facility, patient=cls.patient diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index c71622b2cf..5135e360b1 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -312,7 +312,9 @@ def get_consultation_data(cls): "examination_details": "examination_details", "history_of_present_illness": "history_of_present_illness", "treatment_plan": "treatment_plan", - "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][0], + "suggestion": PatientConsultation.SUGGESTION_CHOICES[0][ + 0 + ], # HOME ISOLATION "referred_to": None, "encounter_date": make_aware(datetime(2020, 4, 7, 15, 30)), "discharge_date": None, From 9ecf566a7f93a960d92245005fc5242dd81de2d2 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 13 Mar 2024 12:58:17 +0530 Subject: [PATCH 04/32] add builds for staging branches --- .github/workflows/deployment.yaml | 136 +++++++++--------------------- 1 file changed, 39 insertions(+), 97 deletions(-) diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index 762f965284..db925187e1 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -3,9 +3,12 @@ name: Deploy Care on: workflow_dispatch: push: + tags: + - 'v*' branches: - master - - production + - develop + - staging paths-ignore: - "docs/**" @@ -33,32 +36,37 @@ jobs: test: uses: ./.github/workflows/test-base.yml - build-staging: + build: needs: test - name: Build & Push Staging to container registries - if: github.ref == 'refs/heads/master' + name: Build & Push to container registries runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Docker meta + - name: Generate docker tags id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | ghcr.io/${{ github.repository }} ${{ secrets.DOCKER_HUB_USERNAME }}/${{ github.event.repository.name }} tags: | - type=raw,value=latest-${{ github.run_number }} + type=raw,value=production-latest,enable=${{ github.ref == 'refs/heads/v*' }} + type=raw,value=production-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}},enable=${{ github.ref == 'refs/heads/v*' }} + type=raw,value=staging-latest,enable=${{ github.ref == 'refs/heads/staging' }} + type=raw,value=staging-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}},enable=${{ github.ref == 'refs/heads/staging' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + type=raw,value=latest-${{ github.run_number }},enable=${{ github.ref == 'refs/heads/master' }} + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/develop' }} + type=raw,value=latest-${{ github.run_number }},enable=${{ github.ref == 'refs/heads/develop' }} type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} flavor: | - latest=true + latest=false - - name: Set up QEMU + - name: Setup QEMU uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx + - name: Setup Docker Buildx uses: docker/setup-buildx-action@v3 - name: Login to DockerHub @@ -75,14 +83,14 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Cache Docker layers - uses: actions/cache@v3 + uses: actions/cache@v4 with: path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ hashFiles('Pipfile.lock', 'docker/prod.Dockerfile') }} + key: ${{ runner.os }}-buildx-build-${{ hashFiles('Pipfile.lock', 'docker/prod.Dockerfile') }} restore-keys: | - ${{ runner.os }}-buildx- + ${{ runner.os }}-buildx-build- - - name: Build image + - name: Build and push image uses: docker/build-push-action@v5 with: context: . @@ -110,86 +118,19 @@ jobs: rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache - build-production: - needs: test - name: Build & Push Production to container registries - if: github.ref == 'refs/heads/production' + notify-release: + needs: build + if: github.ref == 'refs/tags/v*' + name: Notify release runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - - name: Docker meta - id: meta - uses: docker/metadata-action@v4 - with: - images: | - ghcr.io/${{ github.repository }} - ${{ secrets.DOCKER_HUB_USERNAME }}/${{ github.event.repository.name }} - tags: | - type=raw,value=production-latest,enable=${{ github.ref == 'refs/heads/production' }} - type=raw,value=production-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}} - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - flavor: | - latest=false - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Cache Docker layers - uses: actions/cache@v3 - with: - path: /tmp/.buildx-cache - key: ${{ runner.os }}-buildx-${{ hashFiles('Pipfile.lock', 'docker/prod.Dockerfile') }} - restore-keys: | - ${{ runner.os }}-buildx- - - - name: Build image - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod.Dockerfile - push: true - provenance: false - platforms: linux/amd64,linux/arm64 - tags: ${{ steps.meta.outputs.tags }} - build-args: | - APP_VERSION=${{ github.sha }} - cache-from: type=local,src=/tmp/.buildx-cache - cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max - - - name: Create Sentry release - uses: getsentry/action-release@v1 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: ${{ secrets.SENTRY_ORG }} - SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }} - with: - version: ${{ github.sha }} - - - name: Move cache + - name: Notify release run: | - rm -rf /tmp/.buildx-cache - mv /tmp/.buildx-cache-new /tmp/.buildx-cache + echo "Release ${{ github.sha }} is ready to be deployed to production" deploy-staging-egov: - needs: build-staging + needs: build + if: github.ref == 'refs/heads/master' name: Deploy to ECS API Egov runs-on: ubuntu-latest environment: @@ -253,7 +194,8 @@ jobs: wait-for-service-stability: true deploy-staging-gcp: - needs: build-staging + needs: build + if: github.ref == 'refs/heads/staging' name: Deploy to staging GCP cluster runs-on: ubuntu-latest environment: @@ -299,7 +241,7 @@ jobs: kubectl apply -f care-celery-worker.yaml deploy-production-manipur: - needs: build-production + needs: notify-release name: Deploy to GKE Manipur runs-on: ubuntu-latest environment: @@ -345,7 +287,7 @@ jobs: kubectl apply -f care-celery-worker.yaml deploy-production-karnataka: - needs: build-production + needs: notify-release name: Deploy to GKE Karnataka runs-on: ubuntu-latest environment: @@ -391,7 +333,7 @@ jobs: kubectl apply -f care-celery-worker.yaml deploy-production-assam: - needs: build-production + needs: notify-release name: Deploy to GKE Assam runs-on: ubuntu-latest environment: @@ -437,7 +379,7 @@ jobs: kubectl apply -f care-celery-worker.yaml deploy-production-sikkim: - needs: build-production + needs: notify-release name: Deploy to GKE Sikkim runs-on: ubuntu-latest environment: @@ -483,7 +425,7 @@ jobs: kubectl apply -f care-celery-worker.yaml deploy-production-nagaland: - needs: build-production + needs: notify-release name: Deploy to GKE Nagaland runs-on: ubuntu-latest environment: @@ -529,7 +471,7 @@ jobs: kubectl apply -f care-celery-worker.yaml deploy-production-meghalaya: - needs: build-production + needs: notify-release name: Deploy to GKE Meghalaya runs-on: ubuntu-latest environment: From 140f0bc187fe40a78cad8cb06b6405eb6927b404 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 13 Mar 2024 13:04:12 +0530 Subject: [PATCH 05/32] update branch names --- .github/workflows/deployment.yaml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml index db925187e1..38d6537ef5 100644 --- a/.github/workflows/deployment.yaml +++ b/.github/workflows/deployment.yaml @@ -6,7 +6,6 @@ on: tags: - 'v*' branches: - - master - develop - staging paths-ignore: @@ -55,8 +54,6 @@ jobs: type=raw,value=production-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}},enable=${{ github.ref == 'refs/heads/v*' }} type=raw,value=staging-latest,enable=${{ github.ref == 'refs/heads/staging' }} type=raw,value=staging-latest-${{ github.run_number }}-{{date 'YYYYMMDD'}}-{{sha}},enable=${{ github.ref == 'refs/heads/staging' }} - type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} - type=raw,value=latest-${{ github.run_number }},enable=${{ github.ref == 'refs/heads/master' }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/develop' }} type=raw,value=latest-${{ github.run_number }},enable=${{ github.ref == 'refs/heads/develop' }} type=semver,pattern={{version}} @@ -130,7 +127,7 @@ jobs: deploy-staging-egov: needs: build - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/develop' name: Deploy to ECS API Egov runs-on: ubuntu-latest environment: From 8616f5c956f0ee8f252ea5f0581bb1fd2dff986f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 14 Mar 2024 16:17:27 +0530 Subject: [PATCH 06/32] Fix assigned_facility_type migration (#1972) add missing migrate shifting assigned_facility_type --- .../0420_migrate_shifting_facility_type.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 care/facility/migrations/0420_migrate_shifting_facility_type.py diff --git a/care/facility/migrations/0420_migrate_shifting_facility_type.py b/care/facility/migrations/0420_migrate_shifting_facility_type.py new file mode 100644 index 0000000000..2f3e9de30e --- /dev/null +++ b/care/facility/migrations/0420_migrate_shifting_facility_type.py @@ -0,0 +1,32 @@ +# Generated by Django 4.2.10 on 2024-03-14 10:18 + +from django.db import migrations + + +def update_facility_types(apps, schema_editor): + Facility = apps.get_model("facility", "ShiftingRequest") + facilities_to_update = { + 801: 800, # 24x7 Public Health Centres to Primary Health Centres + 820: 800, # Urban Primary Health Center to Primary Health Centres + 831: 830, # Taluk Headquarters Hospitals to Taluk Hospitals + 850: 860, # General hospitals to District Hospitals + 900: 910, # Co-operative hospitals to Autonomous healthcare facility + 950: 870, # Corona Testing Labs to Govt. Labs + 1000: 3, # Corona Care Centre to Other + 8: 870, # Govt Hospital to Govt Medical College Hospitals + } + + for old_id, new_id in facilities_to_update.items(): + Facility.objects.filter(assigned_facility_type=old_id).update( + assigned_facility_type=new_id + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0419_alter_patientconsultation_patient_no"), + ] + + operations = [ + migrations.RunPython(update_facility_types, migrations.RunPython.noop), + ] From 379cf51b5bd97e2ec6b317ea9280b0e333dcfc46 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 15 Mar 2024 10:50:01 +0530 Subject: [PATCH 07/32] fixes https://github.com/coronasafe/care_fe/issues/7410 --- .../api/serializers/patient_consultation.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index f3df403581..74cfdc4ab2 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -518,17 +518,16 @@ def validate(self, attrs): validated = super().validate(attrs) # TODO Add Bed Authorisation Validation - if ( - not self.instance - and "suggestion" in validated - and validated["suggestion"] == SuggestionChoices.A - ): + if not self.instance and "suggestion" in validated: + suggestion = validated["suggestion"] patient_no = validated.get("patient_no") - if not patient_no: + if suggestion == SuggestionChoices.A and not patient_no: raise ValidationError( - {"ip_no": ["This field is required for admission."]} + {"patient_no": "This field is required for admission."} ) - if PatientConsultation.objects.filter( + if ( + suggestion == SuggestionChoices.A or suggestion == SuggestionChoices.OP + ) and PatientConsultation.objects.filter( patient_no=patient_no, facility=( self.instance.facility @@ -537,7 +536,9 @@ def validate(self, attrs): ), ).exists(): raise ValidationError( - "Patient number must be unique within the facility." + { + "patient_no": "Consultation with this IP/OP number already exists within the facility." + } ) if ( From 15abfe1a9cdd47f604767b9aab01768a082f450a Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 15 Mar 2024 11:05:59 +0530 Subject: [PATCH 08/32] update tests --- care/facility/tests/test_patient_consultation_api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 1fa6f056af..758163f404 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -546,6 +546,7 @@ def test_create_consultations_with_duplicate_patient_no_within_facility(self): ) res = self.client.post(self.get_url(), data, format="json") self.assertEqual(res.status_code, status.HTTP_201_CREATED) + data.update( { "patient_no": "IP1234", @@ -557,6 +558,10 @@ def test_create_consultations_with_duplicate_patient_no_within_facility(self): res = self.client.post(self.get_url(), data, format="json") self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + data.update({"suggestion": SuggestionChoices.A}) + res = self.client.post(self.get_url(), data, format="json") + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + def test_create_consultations_with_same_patient_no_in_different_facilities(self): facility2 = self.create_facility( self.super_user, self.district, self.local_body, name="bar" From 55b7bff1b85868f29776bb5efe27123a46d87809 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 15 Mar 2024 14:20:28 +0530 Subject: [PATCH 09/32] treat all non admission consultations as OP consultations --- care/facility/api/serializers/patient_consultation.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 74cfdc4ab2..c1b495e762 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -518,15 +518,19 @@ def validate(self, attrs): validated = super().validate(attrs) # TODO Add Bed Authorisation Validation - if not self.instance and "suggestion" in validated: + if ( + not self.instance or validated.get("patient_no") != self.instance.patient_no + ) and "suggestion" in validated: suggestion = validated["suggestion"] patient_no = validated.get("patient_no") + if suggestion == SuggestionChoices.A and not patient_no: raise ValidationError( {"patient_no": "This field is required for admission."} ) + if ( - suggestion == SuggestionChoices.A or suggestion == SuggestionChoices.OP + suggestion == SuggestionChoices.A or patient_no is not None ) and PatientConsultation.objects.filter( patient_no=patient_no, facility=( From 51ce81e9ca6897e4f0283f967bb242b60022f2bd Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Fri, 15 Mar 2024 15:08:17 +0530 Subject: [PATCH 10/32] minor fix --- care/facility/api/serializers/patient_consultation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index c1b495e762..6b992b414c 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -530,7 +530,7 @@ def validate(self, attrs): ) if ( - suggestion == SuggestionChoices.A or patient_no is not None + suggestion == SuggestionChoices.A or patient_no ) and PatientConsultation.objects.filter( patient_no=patient_no, facility=( From 93b85edd01b6f77e45c82233df1f28d96b24a7aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Mar 2024 11:05:48 +0530 Subject: [PATCH 11/32] Bump jwcrypto from 1.5.1 to 1.5.6 (#1969) Bumps [jwcrypto](https://github.com/latchset/jwcrypto) from 1.5.1 to 1.5.6. - [Release notes](https://github.com/latchset/jwcrypto/releases) - [Commits](https://github.com/latchset/jwcrypto/compare/v1.5.1...v1.5.6) --- updated-dependencies: - dependency-name: jwcrypto dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- Pipfile.lock | 81 ++++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/Pipfile b/Pipfile index 590a56effd..f801d13301 100644 --- a/Pipfile +++ b/Pipfile @@ -31,7 +31,7 @@ drf-spectacular = "==0.26.4" gunicorn = "==21.2.0" healthy-django = "==0.1.0" jsonschema = "==4.20.0" -jwcrypto = "==1.5.1" +jwcrypto = "==1.5.6" newrelic = "==9.3.0" pillow = "==10.2.0" psycopg = "==3.1.14" diff --git a/Pipfile.lock b/Pipfile.lock index 6cdb09ff6a..751cbd5a4a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "e46c7dd4bb313bf57fc2b800581aab91c31c2fdf7599dafebac09b66bb22208c" + "sha256": "4c4ba65d42c7c8a69efbc3a7985748950559c05f9e957e3c8a796b9a82600ab9" }, "pipfile-spec": 6, "requires": { @@ -313,41 +313,41 @@ }, "cryptography": { "hashes": [ - "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380", - "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589", - "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea", - "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65", - "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a", - "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3", - "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008", - "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1", - "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2", - "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635", - "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2", - "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90", - "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee", - "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a", - "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242", - "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12", - "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2", - "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d", - "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be", - "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee", - "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6", - "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529", - "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929", - "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1", - "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6", - "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a", - "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446", - "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9", - "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888", - "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4", - "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33", - "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f" + "sha256:0270572b8bd2c833c3981724b8ee9747b3ec96f699a9665470018594301439ee", + "sha256:111a0d8553afcf8eb02a4fea6ca4f59d48ddb34497aa8706a6cf536f1a5ec576", + "sha256:16a48c23a62a2f4a285699dba2e4ff2d1cff3115b9df052cdd976a18856d8e3d", + "sha256:1b95b98b0d2af784078fa69f637135e3c317091b615cd0905f8b8a087e86fa30", + "sha256:1f71c10d1e88467126f0efd484bd44bca5e14c664ec2ede64c32f20875c0d413", + "sha256:2424ff4c4ac7f6b8177b53c17ed5d8fa74ae5955656867f5a8affaca36a27abb", + "sha256:2bce03af1ce5a5567ab89bd90d11e7bbdff56b8af3acbbec1faded8f44cb06da", + "sha256:329906dcc7b20ff3cad13c069a78124ed8247adcac44b10bea1130e36caae0b4", + "sha256:37dd623507659e08be98eec89323469e8c7b4c1407c85112634ae3dbdb926fdd", + "sha256:3eaafe47ec0d0ffcc9349e1708be2aaea4c6dd4978d76bf6eb0cb2c13636c6fc", + "sha256:5e6275c09d2badf57aea3afa80d975444f4be8d3bc58f7f80d2a484c6f9485c8", + "sha256:6fe07eec95dfd477eb9530aef5bead34fec819b3aaf6c5bd6d20565da607bfe1", + "sha256:7367d7b2eca6513681127ebad53b2582911d1736dc2ffc19f2c3ae49997496bc", + "sha256:7cde5f38e614f55e28d831754e8a3bacf9ace5d1566235e39d91b35502d6936e", + "sha256:9481ffe3cf013b71b2428b905c4f7a9a4f76ec03065b05ff499bb5682a8d9ad8", + "sha256:98d8dc6d012b82287f2c3d26ce1d2dd130ec200c8679b6213b3c73c08b2b7940", + "sha256:a011a644f6d7d03736214d38832e030d8268bcff4a41f728e6030325fea3e400", + "sha256:a2913c5375154b6ef2e91c10b5720ea6e21007412f6437504ffea2109b5a33d7", + "sha256:a30596bae9403a342c978fb47d9b0ee277699fa53bbafad14706af51fe543d16", + "sha256:b03c2ae5d2f0fc05f9a2c0c997e1bc18c8229f392234e8a0194f202169ccd278", + "sha256:b6cd2203306b63e41acdf39aa93b86fb566049aeb6dc489b70e34bcd07adca74", + "sha256:b7ffe927ee6531c78f81aa17e684e2ff617daeba7f189f911065b2ea2d526dec", + "sha256:b8cac287fafc4ad485b8a9b67d0ee80c66bf3574f655d3b97ef2e1082360faf1", + "sha256:ba334e6e4b1d92442b75ddacc615c5476d4ad55cc29b15d590cc6b86efa487e2", + "sha256:ba3e4a42397c25b7ff88cdec6e2a16c2be18720f317506ee25210f6d31925f9c", + "sha256:c41fb5e6a5fe9ebcd58ca3abfeb51dffb5d83d6775405305bfa8715b76521922", + "sha256:cd2030f6650c089aeb304cf093f3244d34745ce0cfcc39f20c6fbfe030102e2a", + "sha256:cd65d75953847815962c84a4654a84850b2bb4aed3f26fadcc1c13892e1e29f6", + "sha256:e4985a790f921508f36f81831817cbc03b102d643b5fcb81cd33df3fa291a1a1", + "sha256:e807b3188f9eb0eaa7bbb579b462c5ace579f1cedb28107ce8b48a9f7ad3679e", + "sha256:f12764b8fffc7a123f641d7d049d382b73f96a34117e0b637b80643169cec8ac", + "sha256:f8837fe1d6ac4a8052a9a8ddab256bc006242696f03368a4009be7ee3075cdb7" ], "markers": "python_version >= '3.7'", - "version": "==42.0.2" + "version": "==42.0.5" }, "deprecated": { "hashes": [ @@ -723,11 +723,12 @@ }, "jwcrypto": { "hashes": [ - "sha256:48bb9bf433777136253579e52b75ffe0f9a4a721d133d01f45a0b91ed5f4f1ae" + "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", + "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039" ], "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==1.5.1" + "markers": "python_version >= '3.8'", + "version": "==1.5.6" }, "kombu": { "hashes": [ @@ -1283,11 +1284,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.10.0" }, "tzdata": { "hashes": [ From 5545aa4200b5e55c430dae0611d73f65d3b780bc Mon Sep 17 00:00:00 2001 From: Gokulram A Date: Sun, 17 Mar 2024 11:08:50 +0530 Subject: [PATCH 12/32] Prescription: Titrated drug dose (#1692) * adds titrated prescription * fix linting * optimize prn copy query in migration * fix reverse migration * added tests and implemented suggested changes * fix lint errors * Add titration dosage information to patient discharge summary * Refactor prescription serializer validation * Merge two facility migrations * Fix validation for titrated prescriptions in MedicineAdministrationSerializer * recreated migrations * Refactor prescription serializers and models * fix migrations * update tests and redo migration * fix: imports that are incorrectly sorted and/or formatted * rebase migrations * Rename dosage field to base_dosage and add dosage_type field in dummy data * update migrations * Rebase migrations and adds missing custom migration for setting dosage_type for existing prn prescriptions --------- Co-authored-by: Ashesh <3626859+Ashesh3@users.noreply.github.com> Co-authored-by: Aakash Singh Co-authored-by: Rithvik Nishad Co-authored-by: rithviknishad --- .../api/serializers/patient_consultation.py | 31 +++-- care/facility/api/serializers/prescription.py | 121 +++++++++++------- care/facility/api/viewsets/prescription.py | 7 +- ...osage_prescription_base_dosage_and_more.py | 88 +++++++++++++ care/facility/models/prescription.py | 26 +++- care/facility/tests/test_medicine_api.py | 104 ++++++++++++--- care/facility/tests/test_prescriptions_api.py | 48 ++++++- .../utils/reports/discharge_summary.py | 12 +- .../patient_discharge_summary_pdf.html | 14 +- data/dummy/facility.json | 68 +++++----- 10 files changed, 391 insertions(+), 128 deletions(-) create mode 100644 care/facility/migrations/0415_rename_dosage_prescription_base_dosage_and_more.py diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 6b992b414c..f67b1cc49e 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -24,6 +24,7 @@ Facility, PatientRegistration, Prescription, + PrescriptionDosageType, PrescriptionType, ) from care.facility.models.asset import AssetLocation @@ -151,17 +152,20 @@ class PatientConsultationSerializer(serializers.ModelSerializer): medico_legal_case = serializers.BooleanField(default=False, required=False) def get_discharge_prescription(self, consultation): - return Prescription.objects.filter( - consultation=consultation, - prescription_type=PrescriptionType.DISCHARGE.value, - is_prn=False, - ).values() + return ( + Prescription.objects.filter( + consultation=consultation, + prescription_type=PrescriptionType.DISCHARGE.value, + ) + .exclude(dosage_type=PrescriptionDosageType.PRN.value) + .values() + ) def get_discharge_prn_prescription(self, consultation): return Prescription.objects.filter( consultation=consultation, prescription_type=PrescriptionType.DISCHARGE.value, - is_prn=True, + dosage_type=PrescriptionDosageType.PRN.value, ).values() class Meta: @@ -641,17 +645,20 @@ class PatientConsultationDischargeSerializer(serializers.ModelSerializer): ) def get_discharge_prescription(self, consultation): - return Prescription.objects.filter( - consultation=consultation, - prescription_type=PrescriptionType.DISCHARGE.value, - is_prn=False, - ).values() + return ( + Prescription.objects.filter( + consultation=consultation, + prescription_type=PrescriptionType.DISCHARGE.value, + ) + .exclude(dosage_type=PrescriptionDosageType.PRN.value) + .values() + ) def get_discharge_prn_prescription(self, consultation): return Prescription.objects.filter( consultation=consultation, prescription_type=PrescriptionType.DISCHARGE.value, - is_prn=True, + dosage_type=PrescriptionDosageType.PRN.value, ).values() class Meta: diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index 49ad3d9913..ed68e772a7 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -2,7 +2,12 @@ from django.utils import timezone from rest_framework import serializers -from care.facility.models import MedibaseMedicine, MedicineAdministration, Prescription +from care.facility.models import ( + MedibaseMedicine, + MedicineAdministration, + Prescription, + PrescriptionDosageType, +) from care.users.api.serializers.user import UserBaseMinimumSerializer @@ -19,23 +24,60 @@ class Meta: ) +class MedicineAdministrationSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + + administered_by = UserBaseMinimumSerializer(read_only=True) + archived_by = UserBaseMinimumSerializer(read_only=True) + + def validate_administered_date(self, value): + if value > timezone.now(): + raise serializers.ValidationError( + "Administered Date cannot be in the future." + ) + if self.context["prescription"].created_date > value: + raise serializers.ValidationError( + "Administered Date cannot be before Prescription Date." + ) + return value + + def validate(self, attrs): + if ( + not attrs.get("dosage") + and self.context["prescription"].dosage_type + == PrescriptionDosageType.TITRATED + ): + raise serializers.ValidationError( + {"dosage": "Dosage is required for titrated prescriptions."} + ) + elif ( + self.context["prescription"].dosage_type != PrescriptionDosageType.TITRATED + ): + attrs.pop("dosage", None) + + return super().validate(attrs) + + class Meta: + model = MedicineAdministration + exclude = ("deleted",) + read_only_fields = ( + "external_id", + "administered_by", + "archived_by", + "archived_on", + "created_date", + "modified_date", + "prescription", + ) + + class PrescriptionSerializer(serializers.ModelSerializer): id = serializers.UUIDField(source="external_id", read_only=True) prescribed_by = UserBaseMinimumSerializer(read_only=True) - last_administered_on = serializers.SerializerMethodField() + last_administration = MedicineAdministrationSerializer(read_only=True) medicine_object = MedibaseMedicineSerializer(read_only=True, source="medicine") medicine = serializers.UUIDField(write_only=True) - def get_last_administered_on(self, obj): - last_administration = ( - MedicineAdministration.objects.filter(prescription=obj) - .order_by("-administered_date") - .first() - ) - if last_administration: - return last_administration.administered_date - return None - class Meta: model = Prescription exclude = ( @@ -75,47 +117,36 @@ def validate(self, attrs): } ) - if attrs.get("is_prn"): + if not attrs.get("base_dosage"): + raise serializers.ValidationError( + {"base_dosage": "Base dosage is required."} + ) + + if attrs.get("dosage_type") == PrescriptionDosageType.PRN: if not attrs.get("indicator"): raise serializers.ValidationError( {"indicator": "Indicator should be set for PRN prescriptions."} ) + attrs.pop("frequency", None) + attrs.pop("days", None) else: if not attrs.get("frequency"): raise serializers.ValidationError( {"frequency": "Frequency should be set for prescriptions."} ) + attrs.pop("indicator", None) + attrs.pop("max_dosage", None) + attrs.pop("min_hours_between_doses", None) + + if attrs.get("dosage_type") == PrescriptionDosageType.TITRATED: + if not attrs.get("target_dosage"): + raise serializers.ValidationError( + { + "target_dosage": "Target dosage should be set for titrated prescriptions." + } + ) + else: + attrs.pop("target_dosage", None) + return super().validate(attrs) # TODO: Ensure that this medicine is not already prescribed to the same patient and is currently active. - - -class MedicineAdministrationSerializer(serializers.ModelSerializer): - id = serializers.UUIDField(source="external_id", read_only=True) - - administered_by = UserBaseMinimumSerializer(read_only=True) - prescription = PrescriptionSerializer(read_only=True) - archived_by = UserBaseMinimumSerializer(read_only=True) - - def validate_administered_date(self, value): - if value > timezone.now(): - raise serializers.ValidationError( - "Administered Date cannot be in the future." - ) - if self.context["prescription"].created_date > value: - raise serializers.ValidationError( - "Administered Date cannot be before Prescription Date." - ) - return value - - class Meta: - model = MedicineAdministration - exclude = ("deleted",) - read_only_fields = ( - "external_id", - "administered_by", - "archived_by", - "archived_on", - "created_date", - "modified_date", - "prescription", - ) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index 779287ef7d..537faae98c 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -17,11 +17,13 @@ from care.facility.models import ( MedicineAdministration, Prescription, + PrescriptionDosageType, PrescriptionType, generate_choices, ) from care.facility.static_data.medibase import MedibaseMedicine from care.utils.filters.choicefilter import CareChoiceFilter +from care.utils.filters.multiselect import MultiSelectFilter from care.utils.queryset.consultation import get_consultation_queryset from care.utils.static_data.helpers import query_builder, token_escaper @@ -34,6 +36,9 @@ def inverse_choices(choices): inverse_prescription_type = inverse_choices(generate_choices(PrescriptionType)) +inverse_prescription_dosage_type = inverse_choices( + generate_choices(PrescriptionDosageType) +) class MedicineAdminstrationFilter(filters.FilterSet): @@ -83,7 +88,7 @@ def archive(self, request, *args, **kwargs): class ConsultationPrescriptionFilter(filters.FilterSet): - is_prn = filters.BooleanFilter() + dosage_type = MultiSelectFilter() prescription_type = CareChoiceFilter(choice_dict=inverse_prescription_type) discontinued = filters.BooleanFilter() diff --git a/care/facility/migrations/0415_rename_dosage_prescription_base_dosage_and_more.py b/care/facility/migrations/0415_rename_dosage_prescription_base_dosage_and_more.py new file mode 100644 index 0000000000..1c9638643b --- /dev/null +++ b/care/facility/migrations/0415_rename_dosage_prescription_base_dosage_and_more.py @@ -0,0 +1,88 @@ +# Generated by Django 4.2.8 on 2024-02-10 10:05 + +from django.db import migrations, models + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0414_remove_bed_old_name"), + ] + + def set_prn_prescriptions_dosage_type(apps, schema_editor): + Prescription = apps.get_model("facility", "Prescription") + Prescription.objects.filter(is_prn=True).update(dosage_type="PRN") + + def reverse_set_prn_prescriptions_dosage_type(apps, schema_editor): + Prescription = apps.get_model("facility", "Prescription") + Prescription.objects.filter(dosage_type="PRN").update(is_prn=True) + + operations = [ + migrations.RenameField( + model_name="prescription", + old_name="dosage", + new_name="base_dosage", + ), + migrations.AddField( + model_name="prescription", + name="dosage_type", + field=models.CharField( + choices=[ + ("REGULAR", "REGULAR"), + ("TITRATED", "TITRATED"), + ("PRN", "PRN"), + ], + default="REGULAR", + max_length=100, + ), + ), + migrations.RunPython( + set_prn_prescriptions_dosage_type, reverse_set_prn_prescriptions_dosage_type + ), + migrations.RemoveField( + model_name="prescription", + name="is_prn", + ), + migrations.AddField( + model_name="medicineadministration", + name="dosage", + field=models.CharField( + blank=True, + max_length=100, + null=True, + validators=[ + care.utils.models.validators.DenominationValidator( + allow_floats=True, + max_amount=5000, + min_amount=0.0001, + precision=4, + units={"mg", "ml", "drop(s)", "ampule(s)", "g", "tsp"}, + ) + ], + ), + ), + migrations.AddField( + model_name="prescription", + name="instruction_on_titration", + field=models.TextField(blank=True, null=True), + ), + migrations.AddField( + model_name="prescription", + name="target_dosage", + field=models.CharField( + blank=True, + max_length=100, + null=True, + validators=[ + care.utils.models.validators.DenominationValidator( + allow_floats=True, + max_amount=5000, + min_amount=0.0001, + precision=4, + units={"mg", "ml", "drop(s)", "ampule(s)", "g", "tsp"}, + ) + ], + ), + ), + ] diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index 5c34ab0e18..3c9bdedba2 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -42,6 +42,12 @@ class PrescriptionType(enum.Enum): REGULAR = "REGULAR" +class PrescriptionDosageType(models.TextChoices): + REGULAR = "REGULAR", "REGULAR" + TITRATED = "TITRATED", "TITRATED" + PRN = "PRN", "PRN" + + def generate_choices(enum_class): return [(tag.name, tag.value) for tag in enum_class] @@ -101,11 +107,20 @@ class Prescription(BaseModel, ConsultationRelatedPermissionMixin): blank=True, null=True, ) - dosage = models.CharField( + base_dosage = models.CharField( max_length=100, blank=True, null=True, validators=[dosage_validator] ) + dosage_type = models.CharField( + max_length=100, + choices=PrescriptionDosageType.choices, + default=PrescriptionDosageType.REGULAR.value, + ) - is_prn = models.BooleanField(default=False) + # titrated fields + target_dosage = models.CharField( + max_length=100, blank=True, null=True, validators=[dosage_validator] + ) + instruction_on_titration = models.TextField(blank=True, null=True) # non prn fields frequency = models.CharField( @@ -151,6 +166,10 @@ def save(self, *args, **kwargs) -> None: def medicine_name(self): return str(self.medicine) if self.medicine else self.medicine_old + @property + def last_administration(self): + return self.administrations.order_by("-administered_date").first() + def has_object_write_permission(self, request): return ConsultationRelatedPermissionMixin.has_write_permission(request) @@ -164,6 +183,9 @@ class MedicineAdministration(BaseModel, ConsultationRelatedPermissionMixin): on_delete=models.PROTECT, related_name="administrations", ) + dosage = models.CharField( + max_length=100, blank=True, null=True, validators=[dosage_validator] + ) notes = models.TextField(default="", blank=True) administered_by = models.ForeignKey( "users.User", diff --git a/care/facility/tests/test_medicine_api.py b/care/facility/tests/test_medicine_api.py index a34e73c43b..b683a6c22e 100644 --- a/care/facility/tests/test_medicine_api.py +++ b/care/facility/tests/test_medicine_api.py @@ -2,7 +2,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import MedibaseMedicine, Prescription +from care.facility.models import MedibaseMedicine, Prescription, PrescriptionDosageType from care.utils.tests.test_utils import TestUtils @@ -29,94 +29,96 @@ def prescription_data(self, **kwargs): data = { "medicine": self.medicine1, "prescription_type": "REGULAR", - "dosage": "1 mg", + "base_dosage": "1 mg", "frequency": "OD", - "is_prn": False, + "dosage_type": kwargs.get( + "dosage_type", PrescriptionDosageType.REGULAR.value + ), } return {**data, **kwargs} def test_invalid_dosage(self): - data = self.prescription_data(dosage="abc") + data = self.prescription_data(base_dosage="abc") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - res.json()["dosage"][0], + res.json()["base_dosage"][0], "Invalid Input, must be in the format: ", ) def test_dosage_out_of_range(self): - data = self.prescription_data(dosage="10000 mg") + data = self.prescription_data(base_dosage="10000 mg") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - res.json()["dosage"][0], + res.json()["base_dosage"][0], "Input amount must be between 0.0001 and 5000", ) - data = self.prescription_data(dosage="-1 mg") + data = self.prescription_data(base_dosage="-1 mg") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - res.json()["dosage"][0], + res.json()["base_dosage"][0], "Input amount must be between 0.0001 and 5000", ) def test_dosage_precision(self): - data = self.prescription_data(dosage="0.300003 mg") + data = self.prescription_data(base_dosage="0.300003 mg") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - res.json()["dosage"][0], + res.json()["base_dosage"][0], "Input amount must have at most 4 decimal places", ) def test_dosage_unit_invalid(self): - data = self.prescription_data(dosage="1 abc") + data = self.prescription_data(base_dosage="1 abc") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - self.assertTrue(res.json()["dosage"][0].startswith("Unit must be one of")) + self.assertTrue(res.json()["base_dosage"][0].startswith("Unit must be one of")) def test_dosage_leading_zero(self): - data = self.prescription_data(dosage="01 mg") + data = self.prescription_data(base_dosage="01 mg") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - res.json()["dosage"][0], + res.json()["base_dosage"][0], "Input amount must be a valid number without leading or trailing zeroes", ) def test_dosage_trailing_zero(self): - data = self.prescription_data(dosage="1.0 mg") + data = self.prescription_data(base_dosage="1.0 mg") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( - res.json()["dosage"][0], + res.json()["base_dosage"][0], "Input amount must be a valid number without leading or trailing zeroes", ) def test_dosage_validator_clean(self): - data = self.prescription_data(dosage=" 1 mg ") + data = self.prescription_data(base_dosage=" 1 mg ") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, @@ -124,13 +126,54 @@ def test_dosage_validator_clean(self): self.assertEqual(res.status_code, status.HTTP_201_CREATED) def test_valid_dosage(self): - data = self.prescription_data(dosage="1 mg") + data = self.prescription_data(base_dosage="1 mg") res = self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", data, ) self.assertEqual(res.status_code, status.HTTP_201_CREATED) + def test_create_titrated_prescription(self): + titrated_prescription_data = self.prescription_data( + dosage_type=PrescriptionDosageType.TITRATED.value, + target_dosage="2 mg", + instruction_on_titration="Test Instruction", + ) + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + titrated_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + titrated_prescription_data = self.prescription_data( + dosage_type=PrescriptionDosageType.TITRATED.value, + ) + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + titrated_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_prn_prescription(self): + prn_prescription_data = self.prescription_data( + dosage_type=PrescriptionDosageType.PRN.value, + indicator="Test Indicator", + ) + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + prn_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + prn_prescription_data = self.prescription_data( + dosage_type=PrescriptionDosageType.PRN.value, + ) + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + prn_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + class MedicineAdministrationsApiTestCase(TestUtils, APITestCase): @classmethod @@ -154,9 +197,11 @@ def create_prescription(self, **kwargs): "consultation": self.create_consultation(self.patient, self.facility), "medicine": MedibaseMedicine.objects.first(), "prescription_type": "REGULAR", - "dosage": "1 mg", + "base_dosage": "1 mg", "frequency": "OD", - "is_prn": False, + "dosage_type": kwargs.get( + "dosage_type", PrescriptionDosageType.REGULAR.value + ), } return Prescription.objects.create( **{**data, **kwargs, "prescribed_by": self.user} @@ -234,3 +279,20 @@ def test_administer_discontinued(self): {"notes": "Test Notes"}, ) self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_titrated_dosage(self): + prescription = self.create_prescription( + dosage_type=PrescriptionDosageType.TITRATED.value, target_dosage="10 mg" + ) + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes", "dosage": "1 mg"}, + ) + + self.assertEqual(res.status_code, status.HTTP_201_CREATED) diff --git a/care/facility/tests/test_prescriptions_api.py b/care/facility/tests/test_prescriptions_api.py index 35c2a198da..87d5fa3c1d 100644 --- a/care/facility/tests/test_prescriptions_api.py +++ b/care/facility/tests/test_prescriptions_api.py @@ -24,9 +24,9 @@ def setUp(self) -> None: self.normal_prescription_data = { "medicine": self.medicine.external_id, "prescription_type": "REGULAR", - "dosage": "1 mg", + "base_dosage": "1 mg", "frequency": "OD", - "is_prn": False, + "dosage_type": "REGULAR", } def test_create_normal_prescription(self): @@ -69,3 +69,47 @@ def test_prescribe_duplicate_active_medicine_and_discontinue(self): self.normal_prescription_data, ) self.assertEqual(res.status_code, status.HTTP_201_CREATED) + + def test_create_titrated_prescription(self): + titrated_prescription_data = { + **self.normal_prescription_data, + "dosage_type": "TITRATED", + "target_dosage": "2 mg", + } + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + titrated_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + titrated_prescription_data = { + **self.normal_prescription_data, + "dosage_type": "TITRATED", + } + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + titrated_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_prn_prescription(self): + prn_prescription_data = { + **self.normal_prescription_data, + "dosage_type": "PRN", + "indicator": "Test Indicator", + } + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + prn_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + prn_prescription_data = { + **self.normal_prescription_data, + "dosage_type": "PRN", + } + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + prn_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index 48af9e6c66..9c7db4db17 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -18,6 +18,7 @@ PatientConsultation, PatientSample, Prescription, + PrescriptionDosageType, PrescriptionType, ) from care.facility.models.file_upload import FileUpload @@ -105,22 +106,21 @@ def get_discharge_summary_data(consultation: PatientConsultation): prescriptions = Prescription.objects.filter( consultation=consultation, prescription_type=PrescriptionType.REGULAR.value, - is_prn=False, - ) + ).exclude(dosage_type=PrescriptionDosageType.PRN.value) prn_prescriptions = Prescription.objects.filter( consultation=consultation, prescription_type=PrescriptionType.REGULAR.value, - is_prn=True, + dosage_type=PrescriptionDosageType.PRN.value, ) discharge_prescriptions = Prescription.objects.filter( consultation=consultation, prescription_type=PrescriptionType.DISCHARGE.value, - is_prn=False, - ) + ).exclude(dosage_type=PrescriptionDosageType.PRN.value) + discharge_prn_prescriptions = Prescription.objects.filter( consultation=consultation, prescription_type=PrescriptionType.DISCHARGE.value, - is_prn=True, + dosage_type=PrescriptionDosageType.PRN.value, ) files = FileUpload.objects.filter( associating_id=consultation.id, diff --git a/care/templates/reports/patient_discharge_summary_pdf.html b/care/templates/reports/patient_discharge_summary_pdf.html index 1fbc572e26..8884e1d43a 100644 --- a/care/templates/reports/patient_discharge_summary_pdf.html +++ b/care/templates/reports/patient_discharge_summary_pdf.html @@ -480,8 +480,12 @@

{{ prescription.medicine_name }} - - {{ prescription.dosage }} + + {{ prescription.base_dosage }} + {% if prescription.dosage_type == 'TITRATED' %} + to {{ prescription.target_dosage }} +

Instruction on titration: {{ prescription.instruction_on_titration }}

+ {% endif %} {{ prescription.route }} @@ -538,7 +542,7 @@

{{ prescription.medicine_name }} - {{ prescription.dosage }} + {{ prescription.base_dosage }} {{ prescription.max_dosage }} @@ -885,7 +889,7 @@

{{ prescription.medicine_name }} - {{ prescription.dosage }} + {{ prescription.base_dosage }} {{ prescription.route }} @@ -942,7 +946,7 @@

{{ prescription.medicine_name }} - {{ prescription.dosage }} + {{ prescription.base_dosage }} {{ prescription.max_dosage }} diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 39b690712b..16e4c2fab2 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -6864,8 +6864,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -6893,8 +6893,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -6922,8 +6922,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -6951,8 +6951,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -6980,8 +6980,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7009,8 +7009,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7038,8 +7038,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7067,8 +7067,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7096,8 +7096,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7125,8 +7125,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7154,8 +7154,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7183,8 +7183,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7212,8 +7212,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7241,8 +7241,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7270,8 +7270,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7299,8 +7299,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, @@ -7328,8 +7328,8 @@ "medicine": 2, "medicine_old": null, "route": null, - "dosage": "3 mg", - "is_prn": false, + "base_dosage": "3 mg", + "dosage_type": "REGULAR", "frequency": "BD", "days": null, "indicator": null, From 257abe61bf496a3e4fc0ba2cb50a6a83acdfe96c Mon Sep 17 00:00:00 2001 From: Abhiuday Gupta <77210185+aeswibon@users.noreply.github.com> Date: Sun, 17 Mar 2024 11:10:09 +0530 Subject: [PATCH 13/32] fix: Custom Migration to clean no input values as null in Daily Rounds table (#1839) * fix(daily_rounds): migrated all fields that has zero values to null * fix(daily_rounds): migrated blood pressure field * fix: resolved comments * fix: updated migrations * fix(daily_round): resolved comments --- ...dailyround_consciousness_level_and_more.py | 225 ++++++++++++++++++ care/facility/models/daily_round.py | 60 +++-- 2 files changed, 260 insertions(+), 25 deletions(-) create mode 100644 care/facility/migrations/0415_alter_dailyround_consciousness_level_and_more.py diff --git a/care/facility/migrations/0415_alter_dailyround_consciousness_level_and_more.py b/care/facility/migrations/0415_alter_dailyround_consciousness_level_and_more.py new file mode 100644 index 0000000000..8b2f523587 --- /dev/null +++ b/care/facility/migrations/0415_alter_dailyround_consciousness_level_and_more.py @@ -0,0 +1,225 @@ +# Generated by Django 4.2.8 on 2024-01-20 10:02 + +from django.db import migrations, models +from django.db.models import Q + + +def forwards_func(apps, schema_editor): + DailyRound = apps.get_model("facility", "DailyRound") + DailyRound.objects.filter(consciousness_level=0).update(consciousness_level=None) + DailyRound.objects.filter(left_pupil_light_reaction=0).update( + left_pupil_light_reaction=None + ) + DailyRound.objects.filter(right_pupil_light_reaction=0).update( + right_pupil_light_reaction=None + ) + DailyRound.objects.filter(limb_response_upper_extremity_left=0).update( + limb_response_upper_extremity_left=None + ) + DailyRound.objects.filter(limb_response_lower_extremity_right=0).update( + limb_response_lower_extremity_right=None + ) + DailyRound.objects.filter(rhythm=0).update(rhythm=None) + DailyRound.objects.filter(ventilator_mode=0).update(ventilator_mode=None) + DailyRound.objects.filter(ventilator_interface=0).update(ventilator_interface=None) + DailyRound.objects.filter(ventilator_oxygen_modality=0).update( + ventilator_oxygen_modality=None + ) + DailyRound.objects.filter(insulin_intake_frequency=0).update( + insulin_intake_frequency=None + ) + DailyRound.objects.filter(Q(bp__systolic__lt=0) | Q(bp__diastolic__lt=0)).update( + bp={"systolic": None, "diastolic": None} + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0414_remove_bed_old_name"), + ] + + operations = [ + migrations.AlterField( + model_name="dailyround", + name="consciousness_level", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "ALERT"), + (10, "RESPONDS_TO_VOICE"), + (15, "RESPONDS_TO_PAIN"), + (20, "UNRESPONSIVE"), + (25, "AGITATED_OR_CONFUSED"), + (30, "ONSET_OF_AGITATION_AND_CONFUSION"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="left_pupil_light_reaction", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "BRISK"), + (10, "SLUGGISH"), + (15, "FIXED"), + (20, "CANNOT_BE_ASSESSED"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="limb_response_lower_extremity_left", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "STRONG"), + (10, "MODERATE"), + (15, "WEAK"), + (20, "FLEXION"), + (25, "EXTENSION"), + (30, "NONE"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="limb_response_lower_extremity_right", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "STRONG"), + (10, "MODERATE"), + (15, "WEAK"), + (20, "FLEXION"), + (25, "EXTENSION"), + (30, "NONE"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="limb_response_upper_extremity_left", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "STRONG"), + (10, "MODERATE"), + (15, "WEAK"), + (20, "FLEXION"), + (25, "EXTENSION"), + (30, "NONE"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="limb_response_upper_extremity_right", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "STRONG"), + (10, "MODERATE"), + (15, "WEAK"), + (20, "FLEXION"), + (25, "EXTENSION"), + (30, "NONE"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="rhythm", + field=models.IntegerField( + choices=[(0, "UNKNOWN"), (5, "REGULAR"), (10, "IRREGULAR")], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="right_pupil_light_reaction", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "BRISK"), + (10, "SLUGGISH"), + (15, "FIXED"), + (20, "CANNOT_BE_ASSESSED"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="ventilator_interface", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "INVASIVE"), + (10, "NON_INVASIVE"), + (15, "OXYGEN_SUPPORT"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="ventilator_mode", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "VCV"), + (10, "PCV"), + (15, "PRVC"), + (20, "APRV"), + (25, "VC_SIMV"), + (30, "PC_SIMV"), + (40, "PRVC_SIMV"), + (45, "ASV"), + (50, "PSV"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="ventilator_oxygen_modality", + field=models.IntegerField( + choices=[ + (0, "UNKNOWN"), + (5, "NASAL_PRONGS"), + (10, "SIMPLE_FACE_MASK"), + (15, "NON_REBREATHING_MASK"), + (20, "HIGH_FLOW_NASAL_CANNULA"), + ], + default=None, + null=True, + ), + ), + migrations.AlterField( + model_name="dailyround", + name="insulin_intake_frequency", + field=models.IntegerField( + choices=[(0, "UNKNOWN"), (5, "OD"), (10, "BD"), (15, "TD")], + default=None, + null=True, + ), + ), + migrations.RunPython(forwards_func, migrations.RunPython.noop), + ] diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 7140623e37..b9ae08eedf 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -191,7 +191,7 @@ class InsulinIntakeFrequencyType(enum.Enum): # Critical Care Attributes consciousness_level = models.IntegerField( - choices=ConsciousnessChoice, default=ConsciousnessType.UNKNOWN.value + choices=ConsciousnessChoice, default=None, null=True ) consciousness_level_detail = models.TextField(default=None, null=True, blank=True) @@ -205,7 +205,7 @@ class InsulinIntakeFrequencyType(enum.Enum): ) left_pupil_size_detail = models.TextField(default=None, null=True, blank=True) left_pupil_light_reaction = models.IntegerField( - choices=PupilReactionChoice, default=PupilReactionType.UNKNOWN.value + choices=PupilReactionChoice, default=None, null=True ) left_pupil_light_reaction_detail = models.TextField( default=None, null=True, blank=True @@ -218,7 +218,7 @@ class InsulinIntakeFrequencyType(enum.Enum): ) right_pupil_size_detail = models.TextField(default=None, null=True, blank=True) right_pupil_light_reaction = models.IntegerField( - choices=PupilReactionChoice, default=PupilReactionType.UNKNOWN.value + choices=PupilReactionChoice, default=None, null=True ) right_pupil_light_reaction_detail = models.TextField( default=None, null=True, blank=True @@ -244,16 +244,16 @@ class InsulinIntakeFrequencyType(enum.Enum): validators=[MinValueValidator(3), MaxValueValidator(15)], ) limb_response_upper_extremity_right = models.IntegerField( - choices=LimbResponseChoice, default=LimbResponseType.UNKNOWN.value + choices=LimbResponseChoice, default=None, null=True ) limb_response_upper_extremity_left = models.IntegerField( - choices=LimbResponseChoice, default=LimbResponseType.UNKNOWN.value + choices=LimbResponseChoice, default=None, null=True ) limb_response_lower_extremity_left = models.IntegerField( - choices=LimbResponseChoice, default=LimbResponseType.UNKNOWN.value + choices=LimbResponseChoice, default=None, null=True ) limb_response_lower_extremity_right = models.IntegerField( - choices=LimbResponseChoice, default=LimbResponseType.UNKNOWN.value + choices=LimbResponseChoice, default=None, null=True ) bp = JSONField(default=dict, validators=[JSONFieldSchemaValidator(BLOOD_PRESSURE)]) pulse = models.IntegerField( @@ -266,14 +266,15 @@ class InsulinIntakeFrequencyType(enum.Enum): null=True, validators=[MinValueValidator(0), MaxValueValidator(150)], ) - rhythm = models.IntegerField(choices=RythmnChoice, default=RythmnType.UNKNOWN.value) + rhythm = models.IntegerField(choices=RythmnChoice, default=None, null=True) rhythm_detail = models.TextField(default=None, null=True, blank=True) ventilator_interface = models.IntegerField( choices=VentilatorInterfaceChoice, - default=VentilatorInterfaceType.UNKNOWN.value, + default=None, + null=True, ) ventilator_mode = models.IntegerField( - choices=VentilatorModeChoice, default=VentilatorModeType.UNKNOWN.value + choices=VentilatorModeChoice, default=None, null=True ) ventilator_peep = models.DecimalField( decimal_places=2, @@ -309,8 +310,7 @@ class InsulinIntakeFrequencyType(enum.Enum): validators=[MinValueValidator(0), MaxValueValidator(1000)], ) ventilator_oxygen_modality = models.IntegerField( - choices=VentilatorOxygenModalityChoice, - default=VentilatorOxygenModalityType.UNKNOWN.value, + choices=VentilatorOxygenModalityChoice, default=None, null=True ) ventilator_oxygen_modality_oxygen_rate = models.IntegerField( default=None, @@ -417,7 +417,8 @@ class InsulinIntakeFrequencyType(enum.Enum): ) insulin_intake_frequency = models.IntegerField( choices=InsulinIntakeFrequencyChoice, - default=InsulinIntakeFrequencyType.UNKNOWN.value, + default=None, + null=True, ) infusions = JSONField( default=list, validators=[JSONFieldSchemaValidator(INFUSIONS)] @@ -504,18 +505,27 @@ def set_push_score(item): def save(self, *args, **kwargs): # Calculate all automated columns and populate them - self.glasgow_total_calculated = ( - self.cztn(self.glasgow_eye_open) - + self.cztn(self.glasgow_motor_response) - + self.cztn(self.glasgow_verbal_response) - ) - self.total_intake_calculated = sum([x["quantity"] for x in self.infusions]) - self.total_intake_calculated += sum([x["quantity"] for x in self.iv_fluids]) - self.total_intake_calculated += sum([x["quantity"] for x in self.feeds]) - - self.total_output_calculated = sum([x["quantity"] for x in self.output]) - - # self.pressure_sore = self.update_pressure_sore() + if ( + self.glasgow_eye_open is not None + and self.glasgow_motor_response is not None + and self.glasgow_verbal_response is not None + ): + self.glasgow_total_calculated = ( + self.cztn(self.glasgow_eye_open) + + self.cztn(self.glasgow_motor_response) + + self.cztn(self.glasgow_verbal_response) + ) + if ( + self.infusions is not None + and self.iv_fluids is not None + and self.feeds is not None + ): + self.total_intake_calculated = sum([x["quantity"] for x in self.infusions]) + self.total_intake_calculated += sum([x["quantity"] for x in self.iv_fluids]) + self.total_intake_calculated += sum([x["quantity"] for x in self.feeds]) + + if self.output is not None: + self.total_output_calculated = sum([x["quantity"] for x in self.output]) super(DailyRound, self).save(*args, **kwargs) From 114c643252f68456551ee9cfcc731b9de307a3f4 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sun, 17 Mar 2024 11:11:50 +0530 Subject: [PATCH 14/32] ExternalIDSerializerField to be UUID Field instead of Field (#1958) --- care/utils/serializer/external_id_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/utils/serializer/external_id_field.py b/care/utils/serializer/external_id_field.py index b71718c524..88d711731c 100644 --- a/care/utils/serializer/external_id_field.py +++ b/care/utils/serializer/external_id_field.py @@ -13,7 +13,7 @@ def __call__(self, value): raise serializers.ValidationError("invalid uuid") -class ExternalIdSerializerField(serializers.Field): +class ExternalIdSerializerField(serializers.UUIDField): def __init__(self, queryset=None, *args, **kwargs): super().__init__(*args, **kwargs) self.queryset = queryset From 7f3bb9379edfee43cd282792ea987fe8ee2abca5 Mon Sep 17 00:00:00 2001 From: Rashmik <146672184+rash-27@users.noreply.github.com> Date: Sun, 17 Mar 2024 11:13:46 +0530 Subject: [PATCH 15/32] Add validation checks in ResourceRequestSerializer (#1928) * Add validation checks in ResourceRequestSerializer * add test for resource * Fix lint errors in Contributing.md * Discard changes to CONTRIBUTING.md * lint fix --------- Co-authored-by: Aakash Singh --- care/facility/api/serializers/resources.py | 56 ++++----------------- care/facility/models/tests/test_resource.py | 36 +++++++++++++ 2 files changed, 45 insertions(+), 47 deletions(-) create mode 100644 care/facility/models/tests/test_resource.py diff --git a/care/facility/api/serializers/resources.py b/care/facility/api/serializers/resources.py index e94cc8f05f..efc5df3294 100644 --- a/care/facility/api/serializers/resources.py +++ b/care/facility/api/serializers/resources.py @@ -13,6 +13,7 @@ ) from care.facility.models.resources import RESOURCE_SUB_CATEGORY_CHOICES from care.users.api.serializers.user import UserBaseMinimumSerializer +from care.utils.serializer.external_id_field import ExternalIdSerializerField from config.serializers import ChoiceField @@ -61,14 +62,16 @@ class ResourceRequestSerializer(serializers.ModelSerializer): category = ChoiceField(choices=RESOURCE_CATEGORY_CHOICES) sub_category = ChoiceField(choices=RESOURCE_SUB_CATEGORY_CHOICES) - origin_facility = serializers.UUIDField( - source="origin_facility.external_id", allow_null=False, required=True + origin_facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=True ) - approving_facility = serializers.UUIDField( - source="approving_facility.external_id", allow_null=False, required=True + + approving_facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=True ) - assigned_facility = serializers.UUIDField( - source="assigned_facility.external_id", allow_null=True, required=False + + assigned_facility = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=False ) assigned_to_object = UserBaseMinimumSerializer(source="assigned_to", read_only=True) @@ -115,24 +118,6 @@ def update(self, instance, validated_data): if "origin_facility" in validated_data: validated_data.pop("origin_facility") - if "approving_facility" in validated_data: - approving_facility_external_id = validated_data.pop("approving_facility")[ - "external_id" - ] - if approving_facility_external_id: - validated_data["approving_facility_id"] = Facility.objects.get( - external_id=approving_facility_external_id - ).id - - if "assigned_facility" in validated_data: - assigned_facility_external_id = validated_data.pop("assigned_facility")[ - "external_id" - ] - if assigned_facility_external_id: - validated_data["assigned_facility_id"] = Facility.objects.get( - external_id=assigned_facility_external_id - ).id - instance.last_edited_by = self.context["request"].user new_instance = super().update(instance, validated_data) @@ -144,29 +129,6 @@ def create(self, validated_data): if "status" in validated_data: validated_data.pop("status") - origin_facility_external_id = validated_data.pop("origin_facility")[ - "external_id" - ] - validated_data["origin_facility_id"] = Facility.objects.get( - external_id=origin_facility_external_id - ).id - - request_approving_facility_external_id = validated_data.pop( - "approving_facility" - )["external_id"] - validated_data["approving_facility_id"] = Facility.objects.get( - external_id=request_approving_facility_external_id - ).id - - if "assigned_facility" in validated_data: - assigned_facility_external_id = validated_data.pop("assigned_facility")[ - "external_id" - ] - if assigned_facility_external_id: - validated_data["assigned_facility_id"] = Facility.objects.get( - external_id=assigned_facility_external_id - ).id - validated_data["created_by"] = self.context["request"].user validated_data["last_edited_by"] = self.context["request"].user diff --git a/care/facility/models/tests/test_resource.py b/care/facility/models/tests/test_resource.py new file mode 100644 index 0000000000..51e349a75b --- /dev/null +++ b/care/facility/models/tests/test_resource.py @@ -0,0 +1,36 @@ +from rest_framework import status +from rest_framework.test import APITestCase + +from care.utils.tests.test_utils import TestUtils + + +class ResourceTransferTest(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("staff1", cls.district, home_facility=cls.facility) + cls.patient = cls.create_patient(cls.district, cls.facility) + + def test_with_invalid_facilityid_input(self): + dist_admin = self.create_user("dist_admin", self.district, user_type=30) + sample_data = { + "approving_facility": self.facility.external_id, + "category": "OXYGEN", + "emergency": "false", + "origin_facility": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", # invalid facility id + "reason": "adadasa", + "refering_facility_contact_name": "rash", + "refering_facility_contact_number": "+918888888889", + "requested_quantity": "10", + "status": "PENDING", + "sub_category": 110, + "title": "a", + } + self.client.force_authenticate(user=dist_admin) + response = self.client.post("/api/v1/resource/", sample_data, format="json") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertIn("origin_facility", response.data) From c0e66fd0df437e338bf517c0b99b810536729dfc Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Sun, 17 Mar 2024 11:14:39 +0530 Subject: [PATCH 16/32] Add assigned_clinicians field to PatientConsultation model (#1900) * Add assigned_clinicians field to PatientConsultation model * Make related_name more intuitive Co-authored-by: Aakash Singh * Fix lint and migration * use a through model --------- Co-authored-by: Aakash Singh --- .../0415_consultationclinician_and_more.py | 52 +++++++++++++++++++ care/facility/models/patient_consultation.py | 17 ++++++ 2 files changed, 69 insertions(+) create mode 100644 care/facility/migrations/0415_consultationclinician_and_more.py diff --git a/care/facility/migrations/0415_consultationclinician_and_more.py b/care/facility/migrations/0415_consultationclinician_and_more.py new file mode 100644 index 0000000000..0165cef66b --- /dev/null +++ b/care/facility/migrations/0415_consultationclinician_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.8 on 2024-02-23 06:38 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("facility", "0414_remove_bed_old_name"), + ] + + operations = [ + migrations.CreateModel( + name="ConsultationClinician", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "clinician", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "consultation", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="facility.patientconsultation", + ), + ), + ], + ), + migrations.AddField( + model_name="patientconsultation", + name="assigned_clinicians", + field=models.ManyToManyField( + related_name="patient_assigned_clinician", + through="facility.ConsultationClinician", + to=settings.AUTH_USER_MODEL, + ), + ), + ] diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 5c9d8802f3..91e18dd351 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -168,6 +168,12 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): related_name="patient_assigned_to", ) + assigned_clinicians = models.ManyToManyField( + User, + related_name="patient_assigned_clinician", + through="ConsultationClinician", + ) + medico_legal_case = models.BooleanField(default=False) deprecated_verified_by = models.TextField( @@ -343,3 +349,14 @@ def has_object_email_discharge_summary_permission(self, request): def has_object_generate_discharge_summary_permission(self, request): return self.has_object_read_permission(request) + + +class ConsultationClinician(models.Model): + consultation = models.ForeignKey( + PatientConsultation, + on_delete=models.PROTECT, + ) + clinician = models.ForeignKey( + User, + on_delete=models.PROTECT, + ) From 29cf6d8c7e8a66d875f6cf3007c5080af00335cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 17 Mar 2024 11:16:47 +0530 Subject: [PATCH 17/32] Bump the boto group with 2 updates (#1979) Bumps the boto group with 2 updates: [boto3](https://github.com/boto/boto3) and [boto3-stubs](https://github.com/youtype/mypy_boto3_builder). Updates `boto3` from 1.34.27 to 1.34.64 - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.34.27...1.34.64) Updates `boto3-stubs` from 1.34.27 to 1.34.64 - [Release notes](https://github.com/youtype/mypy_boto3_builder/releases) - [Commits](https://github.com/youtype/mypy_boto3_builder/commits) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: boto - dependency-name: boto3-stubs dependency-type: direct:development update-type: version-update:semver-patch dependency-group: boto ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 4 ++-- Pipfile.lock | 57 ++++++++++++++++++++++++++-------------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/Pipfile b/Pipfile index f801d13301..3372325244 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.2.1" -boto3 = "==1.34.27" +boto3 = "==1.34.64" celery = "==5.3.6" django = "==4.2.10" django-environ = "==0.11.2" @@ -48,7 +48,7 @@ redis-om = "==0.2.1" [dev-packages] black = "==23.9.1" -boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.27"} +boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.64"} coverage = "==7.4.0" debugpy = "==1.7.0" django-coverage-plugin = "==3.1.0" diff --git a/Pipfile.lock b/Pipfile.lock index 751cbd5a4a..4cf91f7cec 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "4c4ba65d42c7c8a69efbc3a7985748950559c05f9e957e3c8a796b9a82600ab9" + "sha256": "bb40e429be9e7ada4cc778753e43986f12d1fbf5c776fa81cdfd8bf95e0b1864" }, "pipfile-spec": 6, "requires": { @@ -94,20 +94,20 @@ }, "boto3": { "hashes": [ - "sha256:3626db4ba9fbb1b58c8fe923da5ed670873b3d881a102956ea19d3b69cd097cc", - "sha256:ebdd938019f3df2e7b50585353963d4553faf3fbb7b2085c440107fa6caa233b" + "sha256:8c6fbd3d45399a4e4685010117fb2dc52fc6afdab5a9460957d463ae0c2cc55d", + "sha256:e5d681f443645e6953ed0727bf756bf16d85efefcb69cf051d04a070ce65e545" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.27" + "version": "==1.34.64" }, "botocore": { "hashes": [ - "sha256:9f00bd5e4698bcdd37ce6e224a896baf58d209678ed92834944b767de9061cc5", - "sha256:e175360445424b83b0e28ae20d301b99cf44ff2c9d5ab1d8670899bec05a9753" + "sha256:084f8c45216d62dc1add2350e236a2d5283526aacd0681e9818b37a6a5e5438b", + "sha256:0ab760908749fe82325698591c49755a5bb20307d85a419aca9cc74e783b9407" ], "markers": "python_version >= '3.8'", - "version": "==1.34.39" + "version": "==1.34.64" }, "celery": { "hashes": [ @@ -984,11 +984,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "python-fsutil": { "hashes": [ @@ -1229,11 +1229,11 @@ }, "s3transfer": { "hashes": [ - "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", - "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" + "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", + "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" ], "markers": "python_version >= '3.8'", - "version": "==0.10.0" + "version": "==0.10.1" }, "sentry-sdk": { "hashes": [ @@ -1314,11 +1314,11 @@ }, "urllib3": { "hashes": [ - "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", - "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], "markers": "python_version >= '3.6'", - "version": "==2.0.7" + "version": "==2.2.1" }, "vine": { "hashes": [ @@ -1504,11 +1504,12 @@ "s3" ], "hashes": [ - "sha256:a7e264794673b4fdecaed6d09575b94af5f23f1bf1bc6865366b59a5cd102b00", - "sha256:fd84678e3b1237103410690e68bc7a630641138ee26adeeecfd679894514d869" + "sha256:87f7d93b1e73e3a7a743ee79dfe6945c2fca918690f382717e327cb444af96ad", + "sha256:ad33aaf98de67c780094f3a4dd6c89b117d46765847355ca23b627c728ec7947" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.27" + "version": "==1.34.64" }, "botocore": { "hashes": [ @@ -1520,11 +1521,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:2caf0a0d547ded49306408ea959d94aba39dd960b7de119d906c2829d149ece6", - "sha256:f1d689abbaaf4ba50aa5534417ee0be3513acab785b8f8fbd1107639ae2c0ae5" + "sha256:15ca91eaa28bd8014bd316a85edf09d164e9379a128cba6da472e834d72de2ed", + "sha256:2a918c8109e4817d16d80a4f8c92894e1925f2402e6919f8e9cedc7cf18e1685" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.39" + "version": "==1.34.64" }, "certifi": { "hashes": [ @@ -2301,11 +2302,11 @@ }, "types-awscrt": { "hashes": [ - "sha256:06a859189a329ca8e66d56ceeef2391488e39b878fbd2141f115eab4d416fe22", - "sha256:f61a120d3e98ee1387bc5ca4b93437f258cc5c2af1f55f8634ec4cee5729f178" + "sha256:61811bbf4de95248939f9276a434be93d2b95f6ccfe8aa94e56999e9778cfcc2", + "sha256:79d5bfb01f64701b6cf442e89a37d9c4dc6dbb79a46f2f611739b2418d30ecfd" ], "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.20.3" + "version": "==0.20.5" }, "types-pytz": { "hashes": [ @@ -2340,11 +2341,11 @@ }, "typing-extensions": { "hashes": [ - "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783", - "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd" + "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", + "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], "markers": "python_version >= '3.8'", - "version": "==4.9.0" + "version": "==4.10.0" }, "urllib3": { "hashes": [ From 99ab4e0e938c0e528c42b57fa48203b50290c91e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 18 Mar 2024 14:51:40 +0530 Subject: [PATCH 18/32] merge migrations (#1980) --- .../migrations/0421_merge_20240318_1434.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 care/facility/migrations/0421_merge_20240318_1434.py diff --git a/care/facility/migrations/0421_merge_20240318_1434.py b/care/facility/migrations/0421_merge_20240318_1434.py new file mode 100644 index 0000000000..454921ccb3 --- /dev/null +++ b/care/facility/migrations/0421_merge_20240318_1434.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.10 on 2024-03-18 09:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0415_alter_dailyround_consciousness_level_and_more"), + ("facility", "0415_consultationclinician_and_more"), + ("facility", "0415_rename_dosage_prescription_base_dosage_and_more"), + ("facility", "0420_migrate_shifting_facility_type"), + ] + + operations = [] From 3e3b65d04c8da4e9f3bb57131f23cf75d05b1bb8 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 18 Mar 2024 15:02:01 +0530 Subject: [PATCH 19/32] update branch names in docs and workflows (#1981) --- .github/workflows/docs.yml | 6 +++--- .github/workflows/linter.yml | 6 ++++-- .pre-commit-config.yaml | 2 +- .vscode/settings.json | 2 +- CONTRIBUTING.md | 2 +- README.md | 5 +++-- docs/github-repo/configuration.rst | 4 +++- 7 files changed, 16 insertions(+), 11 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a6d012e3f0..dfa5fb20de 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -3,12 +3,12 @@ name: Docs on: push: branches: - - master + - develop paths: - "docs/**" pull_request: branches: - - master + - develop paths: - "docs/**" workflow_dispatch: @@ -46,7 +46,7 @@ jobs: retention-days: 30 deploy-docs: - if: github.repository == 'coronasafe/care' && github.ref == 'refs/heads/master' + if: github.repository == 'coronasafe/care' && github.ref == 'refs/heads/develop' name: Deploy docs runs-on: ubuntu-latest needs: build-docs diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 3c3b3136da..d0d5cc2f00 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -2,7 +2,9 @@ name: Lint Code Base on: pull_request: - branches: [master] + branches: + - develop + - staging merge_group: jobs: @@ -24,7 +26,7 @@ jobs: - name: Lint Code Base uses: github/super-linter/slim@v5 env: - DEFAULT_BRANCH: master + DEFAULT_BRANCH: develop GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} VALIDATE_ALL_CODEBASE: false VALIDATE_PYTHON_BLACK: true diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4561b2650..a8fe2cd73c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,7 +5,7 @@ repos: rev: v4.4.0 hooks: - id: no-commit-to-branch - args: [--branch, master, --branch, production] + args: [--branch, develop, --branch, staging, --branch, production] - id: check-merge-conflict - id: check-builtin-literals - id: mixed-line-ending diff --git a/.vscode/settings.json b/.vscode/settings.json index c2df4bc2a8..88b18e9481 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,7 +18,7 @@ }, "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, - "githubPullRequests.ignoredPullRequestBranches": ["master"], + "githubPullRequests.ignoredPullRequestBranches": ["develop", "staging"], "python.formatting.blackPath": "${workspaceFolder}/.venv/bin/black", "python.formatting.provider": "black", "python.languageServer": "Pylance", diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cab4ef6ccc..cd950287fc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,7 +23,7 @@ pre-commit install to run pre-commit on your branch: ```bash -pre-commit run --files $(git diff --name-only master...HEAD) +pre-commit run --files $(git diff --name-only develop...HEAD) ``` #### Using Docker diff --git a/README.md b/README.md index b1950e85ac..ce601d095b 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,11 @@ You can find the docs at https://care-be-docs.coronasafe.network ### Staging Deployments -Staging instances for testing are automatically deployed on every commit to the `master` branch. The staging instances -are available at: +Dev and staging instances for testing are automatically deployed on every commit to the `develop` and `staging` branches. +The staging instances are available at: - https://careapi.ohc.network +- https://careapi-staging.ohc.network ### Self hosting diff --git a/docs/github-repo/configuration.rst b/docs/github-repo/configuration.rst index bf29224cc9..6729d8a268 100644 --- a/docs/github-repo/configuration.rst +++ b/docs/github-repo/configuration.rst @@ -3,7 +3,9 @@ GitHub Repository The Github Repo available here_ contains the source code for the care project, Apart from the secrets configured at runtime, the exact copy is deployed in production. -The :code:`master` branch auto deploys to the Development instance and is regarded as the Beta version of the application. +The :code:`develop` branch auto deploys to the Development instance and is regarded as the Beta version of the application. + +The :code:`staging` branch auto deploys to the Staging instance and is regarded as the Release Candidate version of the application. The :code:`production` branch auto deploys to Production instance and is regarded as the Stable version of the application. From 02804ec3f1848244a98aaf59ed32f745f72f18f1 Mon Sep 17 00:00:00 2001 From: Ashraf Mohammed <98876115+AshrafMd-1@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:42:20 +0530 Subject: [PATCH 20/32] Add nurse logins for cypress testing (#1985) --- data/dummy/users.json | 70 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/data/dummy/users.json b/data/dummy/users.json index 70fdf67fe0..cf28ec50c4 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -768,5 +768,75 @@ "groups": [], "user_permissions": [] } + }, + { + "model": "users.user", + "pk": 23, + "fields": { + "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$WDE0YXVzR1dIV09zNWJZalF4QzdXSw$ZTpeDj5sbB3TKfu0DEJ5EkQnL3EmM2vGgvdx+Qt2sAw", + "last_login": "2024-03-18 17:04:08.753272+00", + "is_superuser": false, + "first_name": "Dummy", + "last_name": "Nurse", + "email": "dummynurse1@test.com", + "is_staff": false, + "is_active": true, + "date_joined": "2024-03-08 06:31:23.199154+00", + "username": "dummynurse1", + "user_type": 14, + "created_by": 1, + "ward": null, + "local_body": null, + "district": 7, + "state": 1, + "phone_number": "+918878825662", + "alt_phone_number": "+918878825662", + "gender": 2, + "age": 21, + "home_facility": 1, + "verified": true, + "deleted": false, + "pf_endpoint": null, + "pf_p256dh": null, + "pf_auth": null, + "asset": null, + "groups": [], + "user_permissions": [] + } + }, + { + "model": "users.user", + "pk": 24, + "fields": { + "password": "argon2$argon2id$v=19$m=102400,t=2,p=8$WlVSNkdVTEd4Vkgzako3SHUxT0xJag$b5A8WE7JdwlysGuiJPwoQneBQHtzjWTs3Br0VnX3XRc", + "last_login": "2024-03-12 16:34:18.859463+00", + "is_superuser": false, + "first_name": "Dummy", + "last_name": "Nurse", + "email": "dummynurse2@test.com", + "is_staff": false, + "is_active": true, + "date_joined": "2024-03-08 06:32:12.270135+00", + "username": "dummynurse2", + "user_type": 14, + "created_by": 1, + "ward": null, + "local_body": null, + "district": 7, + "state": 1, + "phone_number": "+918744587566", + "alt_phone_number": "+918744587566", + "gender": 1, + "age": 35, + "home_facility": 1, + "verified": true, + "deleted": false, + "pf_endpoint": null, + "pf_p256dh": null, + "pf_auth": null, + "asset": null, + "groups": [], + "user_permissions": [] + } } ] From 0ff895a257b7e083e017be265674cfc33578ab1e Mon Sep 17 00:00:00 2001 From: Karolina Krassowska <39529572+krassowska@users.noreply.github.com> Date: Wed, 20 Mar 2024 06:28:31 +0000 Subject: [PATCH 21/32] Duplicated .pdf in the discharge summary file names (#1978) * Add test for discharge summary file name * Remove .pdf from discharge summary file name --------- Co-authored-by: Aakash Singh --- care/facility/models/file_upload.py | 6 ++-- .../tests/test_patient_consultation_api.py | 28 +++++++++++++++++++ .../utils/reports/discharge_summary.py | 2 +- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 584cda5e05..4aec7366ca 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -38,8 +38,10 @@ class FileCategory(enum.Enum): FileTypeChoices = [(e.value, e.name) for e in FileType] FileCategoryChoices = [(e.value, e.name) for e in FileCategory] - name = models.CharField(max_length=2000) - internal_name = models.CharField(max_length=2000) + name = models.CharField(max_length=2000) # name should not contain file extension + internal_name = models.CharField( + max_length=2000 + ) # internal_name should include file extension associating_id = models.CharField(max_length=100, blank=False, null=False) upload_completed = models.BooleanField(default=False) is_archived = models.BooleanField(default=False) diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 758163f404..5a4104b1ec 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -1,10 +1,12 @@ import datetime +from unittest.mock import patch from django.utils.timezone import make_aware from rest_framework import status from rest_framework.test import APITestCase from care.facility.api.serializers.patient_consultation import MIN_ENCOUNTER_DATE +from care.facility.models.file_upload import FileUpload from care.facility.models.icd11_diagnosis import ( ConditionVerificationStatus, ICD11Diagnosis, @@ -224,6 +226,32 @@ def test_discharge_as_recovered_with_expired_fields(self): self.assertIsNone(consultation.death_datetime) self.assertIsNot(consultation.death_confirmed_doctor, "Dr. Test") + def discharge_summary(self, consultation, **kwargs): + return self.client.post( + f"{self.get_url(consultation)}generate_discharge_summary/", kwargs, "json" + ) + + def test_discharge_summary(self): + consultation = self.create_admission_consultation( + suggestion=SuggestionChoices.A, + encounter_date=make_aware(datetime.datetime(2020, 4, 1, 15, 30, 00)), + ) + with patch.object(FileUpload, "put_object"): + self.discharge_summary( + consultation, + new_discharge_reason=NewDischargeReasonEnum.RECOVERED, + discharge_date="2020-04-02T15:30:00Z", + discharge_notes="Discharge as recovered after admission before future", + ) + + file_res = FileUpload.objects.filter( + associating_id=consultation.external_id, + upload_completed=True, + is_archived=False, + ) + uploaded_file = file_res[0] + self.assertFalse(uploaded_file.name.endswith(".pdf")) + def test_referred_to_external_null(self): consultation = self.create_admission_consultation( suggestion=SuggestionChoices.A, diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index 9c7db4db17..ac3afd1665 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -178,7 +178,7 @@ def generate_and_upload_discharge_summary(consultation: PatientConsultation): try: current_date = timezone.now() summary_file = FileUpload( - name=f"discharge_summary-{consultation.patient.name}-{current_date}.pdf", + name=f"discharge_summary-{consultation.patient.name}-{current_date}", internal_name=f"{uuid4()}.pdf", file_type=FileUpload.FileType.DISCHARGE_SUMMARY.value, associating_id=consultation.external_id, From 9aa1bbbe62f444eb6b9048ef6f8d6fab7485a0fb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 11:59:25 +0530 Subject: [PATCH 22/32] Bump the boto group with 2 updates (#1986) Bumps the boto group with 2 updates: [boto3](https://github.com/boto/boto3) and [boto3-stubs](https://github.com/youtype/mypy_boto3_builder). Updates `boto3` from 1.34.64 to 1.34.65 - [Release notes](https://github.com/boto/boto3/releases) - [Changelog](https://github.com/boto/boto3/blob/develop/CHANGELOG.rst) - [Commits](https://github.com/boto/boto3/compare/1.34.64...1.34.65) Updates `boto3-stubs` from 1.34.64 to 1.34.65 - [Release notes](https://github.com/youtype/mypy_boto3_builder/releases) - [Commits](https://github.com/youtype/mypy_boto3_builder/commits) --- updated-dependencies: - dependency-name: boto3 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: boto - dependency-name: boto3-stubs dependency-type: direct:development update-type: version-update:semver-patch dependency-group: boto ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vignesh Hari --- Pipfile | 4 ++-- Pipfile.lock | 28 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Pipfile b/Pipfile index 3372325244..d6db95b8c0 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.2.1" -boto3 = "==1.34.64" +boto3 = "==1.34.65" celery = "==5.3.6" django = "==4.2.10" django-environ = "==0.11.2" @@ -48,7 +48,7 @@ redis-om = "==0.2.1" [dev-packages] black = "==23.9.1" -boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.64"} +boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.65"} coverage = "==7.4.0" debugpy = "==1.7.0" django-coverage-plugin = "==3.1.0" diff --git a/Pipfile.lock b/Pipfile.lock index 4cf91f7cec..30dda76c82 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bb40e429be9e7ada4cc778753e43986f12d1fbf5c776fa81cdfd8bf95e0b1864" + "sha256": "f65912c79b71aecee17c7fc9f400d477f87e6c4b92f26c9bd2368abcb8a8193e" }, "pipfile-spec": 6, "requires": { @@ -94,20 +94,20 @@ }, "boto3": { "hashes": [ - "sha256:8c6fbd3d45399a4e4685010117fb2dc52fc6afdab5a9460957d463ae0c2cc55d", - "sha256:e5d681f443645e6953ed0727bf756bf16d85efefcb69cf051d04a070ce65e545" + "sha256:b611de58ab28940a36c77d7ef9823427ebf25d5ee8277b802f9979b14e780534", + "sha256:db97f9c29f1806cf9020679be0dd5ffa2aff2670e28e0e2046f98b979be498a4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.64" + "version": "==1.34.65" }, "botocore": { "hashes": [ - "sha256:084f8c45216d62dc1add2350e236a2d5283526aacd0681e9818b37a6a5e5438b", - "sha256:0ab760908749fe82325698591c49755a5bb20307d85a419aca9cc74e783b9407" + "sha256:399a1b1937f7957f0ee2e0df351462b86d44986b795ced980c11eb768b0e61c5", + "sha256:3b0012d7293880c0a4883883047e93f2888d7317b5e9e8a982a991b90d951f3e" ], "markers": "python_version >= '3.8'", - "version": "==1.34.64" + "version": "==1.34.65" }, "celery": { "hashes": [ @@ -1504,12 +1504,12 @@ "s3" ], "hashes": [ - "sha256:87f7d93b1e73e3a7a743ee79dfe6945c2fca918690f382717e327cb444af96ad", - "sha256:ad33aaf98de67c780094f3a4dd6c89b117d46765847355ca23b627c728ec7947" + "sha256:105da4a04dcb5e4ddc90f21ab8b24a3423ecfacb4775b8ccd3879574e5dce358", + "sha256:2afd696c8bb4daf8890ecd75a720e1733cd8b8556eaecc92c36f9b56fc6013bd" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.64" + "version": "==1.34.65" }, "botocore": { "hashes": [ @@ -1521,11 +1521,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:15ca91eaa28bd8014bd316a85edf09d164e9379a128cba6da472e834d72de2ed", - "sha256:2a918c8109e4817d16d80a4f8c92894e1925f2402e6919f8e9cedc7cf18e1685" + "sha256:2f0bae0f8d934cb3446ecec9813aa365407bd7b98586280feb8cfc75a40b4513", + "sha256:7c510adb2c6ffee2e2a5f198c1030bb8edff2136ee56e46729c02ea37b2424a5" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.64" + "version": "==1.34.65" }, "certifi": { "hashes": [ @@ -2344,7 +2344,7 @@ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], - "markers": "python_version >= '3.8'", + "markers": "python_version < '3.12'", "version": "==4.10.0" }, "urllib3": { From 0ecbad0ed9d5fdd0b5c3d3eff6f25614639f9506 Mon Sep 17 00:00:00 2001 From: Vignesh Hari Date: Wed, 20 Mar 2024 12:30:43 +0530 Subject: [PATCH 23/32] Resolve Dependencies (#1992) Refresh Lockfile with upgraded dependencies --- Pipfile | 8 +- Pipfile.lock | 536 +++++++++++++++++++++------------------------------ 2 files changed, 226 insertions(+), 318 deletions(-) diff --git a/Pipfile b/Pipfile index d6db95b8c0..61b01767b0 100644 --- a/Pipfile +++ b/Pipfile @@ -23,7 +23,7 @@ django-rest-passwordreset = "==1.3.0" django-simple-history = "==3.3.0" djangoql = "==0.17.1" djangorestframework = "==3.14.0" -djangorestframework-simplejwt = "==5.3.0" +djangorestframework-simplejwt = "==5.3.1" dry-rest-permissions = "==0.1.10" drf-nested-routers = "==0.93.4" drf-spectacular = "==0.26.4" @@ -34,7 +34,7 @@ jsonschema = "==4.20.0" jwcrypto = "==1.5.6" newrelic = "==9.3.0" pillow = "==10.2.0" -psycopg = "==3.1.14" +psycopg = "==3.1.18" pycryptodome = "==3.20.0" pydantic = "==1.10.12" # fix for fhir.resources < 7.0.2 pyjwt = "==2.8.0" @@ -50,7 +50,7 @@ redis-om = "==0.2.1" black = "==23.9.1" boto3-stubs = {extras = ["s3", "boto3"], version = "==1.34.65"} coverage = "==7.4.0" -debugpy = "==1.7.0" +debugpy = "==1.8.1" django-coverage-plugin = "==3.1.0" django-debug-toolbar = "==4.2.0" django-extensions = "==3.2.3" @@ -58,7 +58,7 @@ django-silk = "==5.0.3" django-stubs = "==4.2.4" djangorestframework-stubs = "==3.14.2" factory-boy = "==3.3.0" -flake8 = "==6.1.0" +flake8 = "==7.0.0" freezegun = "==1.2.2" ipython = "==8.15.0" isort = "==5.12.0" diff --git a/Pipfile.lock b/Pipfile.lock index 30dda76c82..90f6080148 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "f65912c79b71aecee17c7fc9f400d477f87e6c4b92f26c9bd2368abcb8a8193e" + "sha256": "ea4fe23094588fde2796a4107473d14dc5a47a9795a08582da5182dac2b5c4fb" }, "pipfile-spec": 6, "requires": { @@ -103,11 +103,11 @@ }, "botocore": { "hashes": [ - "sha256:399a1b1937f7957f0ee2e0df351462b86d44986b795ced980c11eb768b0e61c5", - "sha256:3b0012d7293880c0a4883883047e93f2888d7317b5e9e8a982a991b90d951f3e" + "sha256:92560f8fbdaa9dd221212a3d3a7609219ba0bbf308c13571674c0cda9d8f39e1", + "sha256:fd7d8742007c220f897cb126b8916ca0cf3724a739d4d716aa5385d7f9d8aeb1" ], "markers": "python_version >= '3.8'", - "version": "==1.34.65" + "version": "==1.34.66" }, "celery": { "hashes": [ @@ -349,14 +349,6 @@ "markers": "python_version >= '3.7'", "version": "==42.0.5" }, - "deprecated": { - "hashes": [ - "sha256:6fac8b097794a90302bdbb17b9b815e732d3c4720583ff1b198499d78470466c", - "sha256:e5323eb936458dccc2582dc6f9c322c852a775a27065ff2b0c4970b9d53d01b3" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.2.14" - }, "django": { "hashes": [ "sha256:a2d4c4d4ea0b6f0895acde632071aff6400bfc331228fc978b05452a0ff3e9f1", @@ -486,20 +478,20 @@ }, "djangorestframework-simplejwt": { "hashes": [ - "sha256:631d7ae2ed4365d7196a35d3cc0f6d382f7bd3361fb24c894f8f92b4da5db27d", - "sha256:8e4c5dfca8d11c0b8a66dfd8a4e3fc1c6aa7ea188d10907ff91c942f4b52ed66" + "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220", + "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==5.3.0" + "markers": "python_version >= '3.8'", + "version": "==5.3.1" }, "dnspython": { "hashes": [ - "sha256:6facdf76b73c742ccf2d07add296f178e629da60be23ce4b0a9c927b1e02c3a6", - "sha256:a0034815a59ba9ae888946be7ccca8f7c157b286f8455b379c692efb51022a15" + "sha256:5ef3b9680161f6fa89daf8ad451b5f1a33b18ae8a1c6778cdf4b43f08c0a6e50", + "sha256:e8f0f9c23a7b7cb99ded64e6c3a6f3e701d78f50c55e002b839dea7225cff7cc" ], "markers": "python_version >= '3.8'", - "version": "==2.5.0" + "version": "==2.6.1" }, "drf-nested-routers": { "hashes": [ @@ -529,10 +521,10 @@ }, "email-validator": { "hashes": [ - "sha256:a4b0bd1cf55f073b924258d19321b1f3aa74b4b5a71a42c305575dba920e1a44", - "sha256:c973053efbeddfef924dc0bd93f6e77a1ea7ee0fce935aea7103c7a3d6d2d637" + "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84", + "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05" ], - "version": "==2.1.0.post1" + "version": "==2.1.1" }, "fhir.resources": { "hashes": [ @@ -770,11 +762,11 @@ }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pillow": { "hashes": [ @@ -868,12 +860,12 @@ }, "psycopg": { "hashes": [ - "sha256:7a63249f52e9c312d2d3978df5f170d21a0defd3a0c950d7859d226b7cfbfad5", - "sha256:f5bce37d357578b230ede15fb461e2c601122986f6dd590e94283aaca8958b14" + "sha256:31144d3fb4c17d78094d9e579826f047d4af1da6a10427d91dfcfb6ecdf6f12b", + "sha256:4d5a0a5a8590906daa58ebd5f3cfc34091377354a1acced269dd10faf55da60e" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==3.1.14" + "version": "==3.1.18" }, "py-vapid": { "hashes": [ @@ -992,10 +984,10 @@ }, "python-fsutil": { "hashes": [ - "sha256:302b0a1739b85151df6e6fddb1593a8fb9648c655fad0fac6c66c21bce900c13", - "sha256:b4ad4c57c243ba46ee5aacebe58ce7a6f62ddeff20ea53e52afc420884b5e544" + "sha256:0d45e623f0f4403f674bdd8ae7aa7d24a4b3132ea45c65416bd2865e6b20b035", + "sha256:8fb204fa8059f37bdeee8a1dc0fff010170202ea47c4225ee71bb3c26f3997be" ], - "version": "==0.13.1" + "version": "==0.14.1" }, "python-slugify": { "hashes": [ @@ -1107,11 +1099,11 @@ }, "referencing": { "hashes": [ - "sha256:39240f2ecc770258f28b642dd47fd74bc8b02484de54e1882b74b35ebd779bd5", - "sha256:c775fedf74bc0f9189c2a3be1c12fd03e8c23f4d371dce795df44e06c5b412f7" + "sha256:5773bd84ef41799a5a8ca72dc34590c041eb01bf9aa02632b4a973fb0181a844", + "sha256:d53ae300ceddd3169f1ffa9caf2cb7b769e92657e4fafb23d34b93679116dfd4" ], "markers": "python_version >= '3.8'", - "version": "==0.33.0" + "version": "==0.34.0" }, "requests": { "hashes": [ @@ -1124,108 +1116,108 @@ }, "rpds-py": { "hashes": [ - "sha256:01f58a7306b64e0a4fe042047dd2b7d411ee82e54240284bab63e325762c1147", - "sha256:0210b2668f24c078307260bf88bdac9d6f1093635df5123789bfee4d8d7fc8e7", - "sha256:02866e060219514940342a1f84303a1ef7a1dad0ac311792fbbe19b521b489d2", - "sha256:0387ce69ba06e43df54e43968090f3626e231e4bc9150e4c3246947567695f68", - "sha256:060f412230d5f19fc8c8b75f315931b408d8ebf56aec33ef4168d1b9e54200b1", - "sha256:071bc28c589b86bc6351a339114fb7a029f5cddbaca34103aa573eba7b482382", - "sha256:0bfb09bf41fe7c51413f563373e5f537eaa653d7adc4830399d4e9bdc199959d", - "sha256:10162fe3f5f47c37ebf6d8ff5a2368508fe22007e3077bf25b9c7d803454d921", - "sha256:149c5cd24f729e3567b56e1795f74577aa3126c14c11e457bec1b1c90d212e38", - "sha256:1701fc54460ae2e5efc1dd6350eafd7a760f516df8dbe51d4a1c79d69472fbd4", - "sha256:1957a2ab607f9added64478a6982742eb29f109d89d065fa44e01691a20fc20a", - "sha256:1a746a6d49665058a5896000e8d9d2f1a6acba8a03b389c1e4c06e11e0b7f40d", - "sha256:1bfcad3109c1e5ba3cbe2f421614e70439f72897515a96c462ea657261b96518", - "sha256:1d36b2b59e8cc6e576f8f7b671e32f2ff43153f0ad6d0201250a7c07f25d570e", - "sha256:1db228102ab9d1ff4c64148c96320d0be7044fa28bd865a9ce628ce98da5973d", - "sha256:1dc29db3900cb1bb40353772417800f29c3d078dbc8024fd64655a04ee3c4bdf", - "sha256:1e626b365293a2142a62b9a614e1f8e331b28f3ca57b9f05ebbf4cf2a0f0bdc5", - "sha256:1f3c3461ebb4c4f1bbc70b15d20b565759f97a5aaf13af811fcefc892e9197ba", - "sha256:20de7b7179e2031a04042e85dc463a93a82bc177eeba5ddd13ff746325558aa6", - "sha256:24e4900a6643f87058a27320f81336d527ccfe503984528edde4bb660c8c8d59", - "sha256:2528ff96d09f12e638695f3a2e0c609c7b84c6df7c5ae9bfeb9252b6fa686253", - "sha256:25f071737dae674ca8937a73d0f43f5a52e92c2d178330b4c0bb6ab05586ffa6", - "sha256:270987bc22e7e5a962b1094953ae901395e8c1e1e83ad016c5cfcfff75a15a3f", - "sha256:292f7344a3301802e7c25c53792fae7d1593cb0e50964e7bcdcc5cf533d634e3", - "sha256:2953937f83820376b5979318840f3ee47477d94c17b940fe31d9458d79ae7eea", - "sha256:2a792b2e1d3038daa83fa474d559acfd6dc1e3650ee93b2662ddc17dbff20ad1", - "sha256:2a7b2f2f56a16a6d62e55354dd329d929560442bd92e87397b7a9586a32e3e76", - "sha256:2f4eb548daf4836e3b2c662033bfbfc551db58d30fd8fe660314f86bf8510b93", - "sha256:3664d126d3388a887db44c2e293f87d500c4184ec43d5d14d2d2babdb4c64cad", - "sha256:3677fcca7fb728c86a78660c7fb1b07b69b281964673f486ae72860e13f512ad", - "sha256:380e0df2e9d5d5d339803cfc6d183a5442ad7ab3c63c2a0982e8c824566c5ccc", - "sha256:3ac732390d529d8469b831949c78085b034bff67f584559340008d0f6041a049", - "sha256:4128980a14ed805e1b91a7ed551250282a8ddf8201a4e9f8f5b7e6225f54170d", - "sha256:4341bd7579611cf50e7b20bb8c2e23512a3dc79de987a1f411cb458ab670eb90", - "sha256:436474f17733c7dca0fbf096d36ae65277e8645039df12a0fa52445ca494729d", - "sha256:4dc889a9d8a34758d0fcc9ac86adb97bab3fb7f0c4d29794357eb147536483fd", - "sha256:4e21b76075c01d65d0f0f34302b5a7457d95721d5e0667aea65e5bb3ab415c25", - "sha256:516fb8c77805159e97a689e2f1c80655c7658f5af601c34ffdb916605598cda2", - "sha256:5576ee2f3a309d2bb403ec292d5958ce03953b0e57a11d224c1f134feaf8c40f", - "sha256:5a024fa96d541fd7edaa0e9d904601c6445e95a729a2900c5aec6555fe921ed6", - "sha256:5d0e8a6434a3fbf77d11448c9c25b2f25244226cfbec1a5159947cac5b8c5fa4", - "sha256:5e7d63ec01fe7c76c2dbb7e972fece45acbb8836e72682bde138e7e039906e2c", - "sha256:60e820ee1004327609b28db8307acc27f5f2e9a0b185b2064c5f23e815f248f8", - "sha256:637b802f3f069a64436d432117a7e58fab414b4e27a7e81049817ae94de45d8d", - "sha256:65dcf105c1943cba45d19207ef51b8bc46d232a381e94dd38719d52d3980015b", - "sha256:698ea95a60c8b16b58be9d854c9f993c639f5c214cf9ba782eca53a8789d6b19", - "sha256:70fcc6c2906cfa5c6a552ba7ae2ce64b6c32f437d8f3f8eea49925b278a61453", - "sha256:720215373a280f78a1814becb1312d4e4d1077b1202a56d2b0815e95ccb99ce9", - "sha256:7450dbd659fed6dd41d1a7d47ed767e893ba402af8ae664c157c255ec6067fde", - "sha256:7b7d9ca34542099b4e185b3c2a2b2eda2e318a7dbde0b0d83357a6d4421b5296", - "sha256:7fbd70cb8b54fe745301921b0816c08b6d917593429dfc437fd024b5ba713c58", - "sha256:81038ff87a4e04c22e1d81f947c6ac46f122e0c80460b9006e6517c4d842a6ec", - "sha256:810685321f4a304b2b55577c915bece4c4a06dfe38f6e62d9cc1d6ca8ee86b99", - "sha256:82ada4a8ed9e82e443fcef87e22a3eed3654dd3adf6e3b3a0deb70f03e86142a", - "sha256:841320e1841bb53fada91c9725e766bb25009cfd4144e92298db296fb6c894fb", - "sha256:8587fd64c2a91c33cdc39d0cebdaf30e79491cc029a37fcd458ba863f8815383", - "sha256:8ffe53e1d8ef2520ebcf0c9fec15bb721da59e8ef283b6ff3079613b1e30513d", - "sha256:9051e3d2af8f55b42061603e29e744724cb5f65b128a491446cc029b3e2ea896", - "sha256:91e5a8200e65aaac342a791272c564dffcf1281abd635d304d6c4e6b495f29dc", - "sha256:93432e747fb07fa567ad9cc7aaadd6e29710e515aabf939dfbed8046041346c6", - "sha256:938eab7323a736533f015e6069a7d53ef2dcc841e4e533b782c2bfb9fb12d84b", - "sha256:9584f8f52010295a4a417221861df9bea4c72d9632562b6e59b3c7b87a1522b7", - "sha256:9737bdaa0ad33d34c0efc718741abaafce62fadae72c8b251df9b0c823c63b22", - "sha256:99da0a4686ada4ed0f778120a0ea8d066de1a0a92ab0d13ae68492a437db78bf", - "sha256:99f567dae93e10be2daaa896e07513dd4bf9c2ecf0576e0533ac36ba3b1d5394", - "sha256:9bdf1303df671179eaf2cb41e8515a07fc78d9d00f111eadbe3e14262f59c3d0", - "sha256:9f0e4dc0f17dcea4ab9d13ac5c666b6b5337042b4d8f27e01b70fae41dd65c57", - "sha256:a000133a90eea274a6f28adc3084643263b1e7c1a5a66eb0a0a7a36aa757ed74", - "sha256:a3264e3e858de4fc601741498215835ff324ff2482fd4e4af61b46512dd7fc83", - "sha256:a71169d505af63bb4d20d23a8fbd4c6ce272e7bce6cc31f617152aa784436f29", - "sha256:a967dd6afda7715d911c25a6ba1517975acd8d1092b2f326718725461a3d33f9", - "sha256:aa5bfb13f1e89151ade0eb812f7b0d7a4d643406caaad65ce1cbabe0a66d695f", - "sha256:ae35e8e6801c5ab071b992cb2da958eee76340e6926ec693b5ff7d6381441745", - "sha256:b686f25377f9c006acbac63f61614416a6317133ab7fafe5de5f7dc8a06d42eb", - "sha256:b760a56e080a826c2e5af09002c1a037382ed21d03134eb6294812dda268c811", - "sha256:b86b21b348f7e5485fae740d845c65a880f5d1eda1e063bc59bef92d1f7d0c55", - "sha256:b9412abdf0ba70faa6e2ee6c0cc62a8defb772e78860cef419865917d86c7342", - "sha256:bd345a13ce06e94c753dab52f8e71e5252aec1e4f8022d24d56decd31e1b9b23", - "sha256:be22ae34d68544df293152b7e50895ba70d2a833ad9566932d750d3625918b82", - "sha256:bf046179d011e6114daf12a534d874958b039342b347348a78b7cdf0dd9d6041", - "sha256:c3d2010656999b63e628a3c694f23020322b4178c450dc478558a2b6ef3cb9bb", - "sha256:c64602e8be701c6cfe42064b71c84ce62ce66ddc6422c15463fd8127db3d8066", - "sha256:d65e6b4f1443048eb7e833c2accb4fa7ee67cc7d54f31b4f0555b474758bee55", - "sha256:d8bbd8e56f3ba25a7d0cf980fc42b34028848a53a0e36c9918550e0280b9d0b6", - "sha256:da1ead63368c04a9bded7904757dfcae01eba0e0f9bc41d3d7f57ebf1c04015a", - "sha256:dbbb95e6fc91ea3102505d111b327004d1c4ce98d56a4a02e82cd451f9f57140", - "sha256:dbc56680ecf585a384fbd93cd42bc82668b77cb525343170a2d86dafaed2a84b", - "sha256:df3b6f45ba4515632c5064e35ca7f31d51d13d1479673185ba8f9fefbbed58b9", - "sha256:dfe07308b311a8293a0d5ef4e61411c5c20f682db6b5e73de6c7c8824272c256", - "sha256:e796051f2070f47230c745d0a77a91088fbee2cc0502e9b796b9c6471983718c", - "sha256:efa767c220d94aa4ac3a6dd3aeb986e9f229eaf5bce92d8b1b3018d06bed3772", - "sha256:f0b8bf5b8db49d8fd40f54772a1dcf262e8be0ad2ab0206b5a2ec109c176c0a4", - "sha256:f175e95a197f6a4059b50757a3dca33b32b61691bdbd22c29e8a8d21d3914cae", - "sha256:f2f3b28b40fddcb6c1f1f6c88c6f3769cd933fa493ceb79da45968a21dccc920", - "sha256:f6c43b6f97209e370124baf2bf40bb1e8edc25311a158867eb1c3a5d449ebc7a", - "sha256:f7f4cb1f173385e8a39c29510dd11a78bf44e360fb75610594973f5ea141028b", - "sha256:fad059a4bd14c45776600d223ec194e77db6c20255578bb5bcdd7c18fd169361", - "sha256:ff1dcb8e8bc2261a088821b2595ef031c91d499a0c1b031c152d43fe0a6ecec8", - "sha256:ffee088ea9b593cc6160518ba9bd319b5475e5f3e578e4552d63818773c6f56a" + "sha256:01e36a39af54a30f28b73096dd39b6802eddd04c90dbe161c1b8dbe22353189f", + "sha256:044a3e61a7c2dafacae99d1e722cc2d4c05280790ec5a05031b3876809d89a5c", + "sha256:08231ac30a842bd04daabc4d71fddd7e6d26189406d5a69535638e4dcb88fe76", + "sha256:08f9ad53c3f31dfb4baa00da22f1e862900f45908383c062c27628754af2e88e", + "sha256:0ab39c1ba9023914297dd88ec3b3b3c3f33671baeb6acf82ad7ce883f6e8e157", + "sha256:0af039631b6de0397ab2ba16eaf2872e9f8fca391b44d3d8cac317860a700a3f", + "sha256:0b8612cd233543a3781bc659c731b9d607de65890085098986dfd573fc2befe5", + "sha256:11a8c85ef4a07a7638180bf04fe189d12757c696eb41f310d2426895356dcf05", + "sha256:1374f4129f9bcca53a1bba0bb86bf78325a0374577cf7e9e4cd046b1e6f20e24", + "sha256:1d4acf42190d449d5e89654d5c1ed3a4f17925eec71f05e2a41414689cda02d1", + "sha256:1d9a5be316c15ffb2b3c405c4ff14448c36b4435be062a7f578ccd8b01f0c4d8", + "sha256:1df3659d26f539ac74fb3b0c481cdf9d725386e3552c6fa2974f4d33d78e544b", + "sha256:22806714311a69fd0af9b35b7be97c18a0fc2826e6827dbb3a8c94eac6cf7eeb", + "sha256:2644e47de560eb7bd55c20fc59f6daa04682655c58d08185a9b95c1970fa1e07", + "sha256:2e6d75ab12b0bbab7215e5d40f1e5b738aa539598db27ef83b2ec46747df90e1", + "sha256:30f43887bbae0d49113cbaab729a112251a940e9b274536613097ab8b4899cf6", + "sha256:34b18ba135c687f4dac449aa5157d36e2cbb7c03cbea4ddbd88604e076aa836e", + "sha256:36b3ee798c58ace201289024b52788161e1ea133e4ac93fba7d49da5fec0ef9e", + "sha256:39514da80f971362f9267c600b6d459bfbbc549cffc2cef8e47474fddc9b45b1", + "sha256:39f5441553f1c2aed4de4377178ad8ff8f9d733723d6c66d983d75341de265ab", + "sha256:3a96e0c6a41dcdba3a0a581bbf6c44bb863f27c541547fb4b9711fd8cf0ffad4", + "sha256:3f26b5bd1079acdb0c7a5645e350fe54d16b17bfc5e71f371c449383d3342e17", + "sha256:41ef53e7c58aa4ef281da975f62c258950f54b76ec8e45941e93a3d1d8580594", + "sha256:42821446ee7a76f5d9f71f9e33a4fb2ffd724bb3e7f93386150b61a43115788d", + "sha256:43fbac5f22e25bee1d482c97474f930a353542855f05c1161fd804c9dc74a09d", + "sha256:4457a94da0d5c53dc4b3e4de1158bdab077db23c53232f37a3cb7afdb053a4e3", + "sha256:465a3eb5659338cf2a9243e50ad9b2296fa15061736d6e26240e713522b6235c", + "sha256:482103aed1dfe2f3b71a58eff35ba105289b8d862551ea576bd15479aba01f66", + "sha256:4832d7d380477521a8c1644bbab6588dfedea5e30a7d967b5fb75977c45fd77f", + "sha256:4901165d170a5fde6f589acb90a6b33629ad1ec976d4529e769c6f3d885e3e80", + "sha256:5307def11a35f5ae4581a0b658b0af8178c65c530e94893345bebf41cc139d33", + "sha256:5417558f6887e9b6b65b4527232553c139b57ec42c64570569b155262ac0754f", + "sha256:56a737287efecafc16f6d067c2ea0117abadcd078d58721f967952db329a3e5c", + "sha256:586f8204935b9ec884500498ccc91aa869fc652c40c093bd9e1471fbcc25c022", + "sha256:5b4e7d8d6c9b2e8ee2d55c90b59c707ca59bc30058269b3db7b1f8df5763557e", + "sha256:5ddcba87675b6d509139d1b521e0c8250e967e63b5909a7e8f8944d0f90ff36f", + "sha256:618a3d6cae6ef8ec88bb76dd80b83cfe415ad4f1d942ca2a903bf6b6ff97a2da", + "sha256:635dc434ff724b178cb192c70016cc0ad25a275228f749ee0daf0eddbc8183b1", + "sha256:661d25cbffaf8cc42e971dd570d87cb29a665f49f4abe1f9e76be9a5182c4688", + "sha256:66e6a3af5a75363d2c9a48b07cb27c4ea542938b1a2e93b15a503cdfa8490795", + "sha256:67071a6171e92b6da534b8ae326505f7c18022c6f19072a81dcf40db2638767c", + "sha256:685537e07897f173abcf67258bee3c05c374fa6fff89d4c7e42fb391b0605e98", + "sha256:69e64831e22a6b377772e7fb337533c365085b31619005802a79242fee620bc1", + "sha256:6b0817e34942b2ca527b0e9298373e7cc75f429e8da2055607f4931fded23e20", + "sha256:6c81e5f372cd0dc5dc4809553d34f832f60a46034a5f187756d9b90586c2c307", + "sha256:6d7faa6f14017c0b1e69f5e2c357b998731ea75a442ab3841c0dbbbfe902d2c4", + "sha256:6ef0befbb5d79cf32d0266f5cff01545602344eda89480e1dd88aca964260b18", + "sha256:6ef687afab047554a2d366e112dd187b62d261d49eb79b77e386f94644363294", + "sha256:7223a2a5fe0d217e60a60cdae28d6949140dde9c3bcc714063c5b463065e3d66", + "sha256:77f195baa60a54ef9d2de16fbbfd3ff8b04edc0c0140a761b56c267ac11aa467", + "sha256:793968759cd0d96cac1e367afd70c235867831983f876a53389ad869b043c948", + "sha256:7bd339195d84439cbe5771546fe8a4e8a7a045417d8f9de9a368c434e42a721e", + "sha256:7cd863afe7336c62ec78d7d1349a2f34c007a3cc6c2369d667c65aeec412a5b1", + "sha256:7f2facbd386dd60cbbf1a794181e6aa0bd429bd78bfdf775436020172e2a23f0", + "sha256:84ffab12db93b5f6bad84c712c92060a2d321b35c3c9960b43d08d0f639d60d7", + "sha256:8c8370641f1a7f0e0669ddccca22f1da893cef7628396431eb445d46d893e5cd", + "sha256:8db715ebe3bb7d86d77ac1826f7d67ec11a70dbd2376b7cc214199360517b641", + "sha256:8e8916ae4c720529e18afa0b879473049e95949bf97042e938530e072fde061d", + "sha256:8f03bccbd8586e9dd37219bce4d4e0d3ab492e6b3b533e973fa08a112cb2ffc9", + "sha256:8f2fc11e8fe034ee3c34d316d0ad8808f45bc3b9ce5857ff29d513f3ff2923a1", + "sha256:923d39efa3cfb7279a0327e337a7958bff00cc447fd07a25cddb0a1cc9a6d2da", + "sha256:93df1de2f7f7239dc9cc5a4a12408ee1598725036bd2dedadc14d94525192fc3", + "sha256:998e33ad22dc7ec7e030b3df701c43630b5bc0d8fbc2267653577e3fec279afa", + "sha256:99f70b740dc04d09e6b2699b675874367885217a2e9f782bdf5395632ac663b7", + "sha256:9a00312dea9310d4cb7dbd7787e722d2e86a95c2db92fbd7d0155f97127bcb40", + "sha256:9d54553c1136b50fd12cc17e5b11ad07374c316df307e4cfd6441bea5fb68496", + "sha256:9dbbeb27f4e70bfd9eec1be5477517365afe05a9b2c441a0b21929ee61048124", + "sha256:a1ce3ba137ed54f83e56fb983a5859a27d43a40188ba798993812fed73c70836", + "sha256:a34d557a42aa28bd5c48a023c570219ba2593bcbbb8dc1b98d8cf5d529ab1434", + "sha256:a5f446dd5055667aabaee78487f2b5ab72e244f9bc0b2ffebfeec79051679984", + "sha256:ad36cfb355e24f1bd37cac88c112cd7730873f20fb0bdaf8ba59eedf8216079f", + "sha256:aec493917dd45e3c69d00a8874e7cbed844efd935595ef78a0f25f14312e33c6", + "sha256:b316144e85316da2723f9d8dc75bada12fa58489a527091fa1d5a612643d1a0e", + "sha256:b34ae4636dfc4e76a438ab826a0d1eed2589ca7d9a1b2d5bb546978ac6485461", + "sha256:b34b7aa8b261c1dbf7720b5d6f01f38243e9b9daf7e6b8bc1fd4657000062f2c", + "sha256:bc362ee4e314870a70f4ae88772d72d877246537d9f8cb8f7eacf10884862432", + "sha256:bed88b9a458e354014d662d47e7a5baafd7ff81c780fd91584a10d6ec842cb73", + "sha256:c0013fe6b46aa496a6749c77e00a3eb07952832ad6166bd481c74bda0dcb6d58", + "sha256:c0b5dcf9193625afd8ecc92312d6ed78781c46ecbf39af9ad4681fc9f464af88", + "sha256:c4325ff0442a12113a6379af66978c3fe562f846763287ef66bdc1d57925d337", + "sha256:c463ed05f9dfb9baebef68048aed8dcdc94411e4bf3d33a39ba97e271624f8f7", + "sha256:c8362467a0fdeccd47935f22c256bec5e6abe543bf0d66e3d3d57a8fb5731863", + "sha256:cd5bf1af8efe569654bbef5a3e0a56eca45f87cfcffab31dd8dde70da5982475", + "sha256:cf1ea2e34868f6fbf070e1af291c8180480310173de0b0c43fc38a02929fc0e3", + "sha256:d62dec4976954a23d7f91f2f4530852b0c7608116c257833922a896101336c51", + "sha256:d68c93e381010662ab873fea609bf6c0f428b6d0bb00f2c6939782e0818d37bf", + "sha256:d7c36232a90d4755b720fbd76739d8891732b18cf240a9c645d75f00639a9024", + "sha256:dd18772815d5f008fa03d2b9a681ae38d5ae9f0e599f7dda233c439fcaa00d40", + "sha256:ddc2f4dfd396c7bfa18e6ce371cba60e4cf9d2e5cdb71376aa2da264605b60b9", + "sha256:e003b002ec72c8d5a3e3da2989c7d6065b47d9eaa70cd8808b5384fbb970f4ec", + "sha256:e32a92116d4f2a80b629778280103d2a510a5b3f6314ceccd6e38006b5e92dcb", + "sha256:e4461d0f003a0aa9be2bdd1b798a041f177189c1a0f7619fe8c95ad08d9a45d7", + "sha256:e541ec6f2ec456934fd279a3120f856cd0aedd209fc3852eca563f81738f6861", + "sha256:e546e768d08ad55b20b11dbb78a745151acbd938f8f00d0cfbabe8b0199b9880", + "sha256:ea7d4a99f3b38c37eac212dbd6ec42b7a5ec51e2c74b5d3223e43c811609e65f", + "sha256:ed4eb745efbff0a8e9587d22a84be94a5eb7d2d99c02dacf7bd0911713ed14dd", + "sha256:f8a2f084546cc59ea99fda8e070be2fd140c3092dc11524a71aa8f0f3d5a55ca", + "sha256:fcb25daa9219b4cf3a0ab24b0eb9a5cc8949ed4dc72acb8fa16b7e1681aa3c58", + "sha256:fdea4952db2793c4ad0bdccd27c1d8fdd1423a92f04598bc39425bcc2b8ee46e" ], "markers": "python_version >= '3.8'", - "version": "==0.17.1" + "version": "==0.18.0" }, "s3transfer": { "hashes": [ @@ -1268,19 +1260,19 @@ }, "types-pyopenssl": { "hashes": [ - "sha256:24a255458b5b8a7fca8139cf56f2a8ad5a4f1a5f711b73a5bb9cb50dc688fab5", - "sha256:c812e5c1c35249f75ef5935708b2a997d62abf9745be222e5f94b9595472ab25" + "sha256:6e8e8bfad34924067333232c93f7fc4b369856d8bea0d5c9d1808cb290ab1972", + "sha256:7bca00cfc4e7ef9c5d2663c6a1c068c35798e59670595439f6296e7ba3d58083" ], "markers": "python_version >= '3.8'", - "version": "==24.0.0.20240130" + "version": "==24.0.0.20240311" }, "types-redis": { "hashes": [ - "sha256:2b2fa3a78f84559616242d23f86de5f4130dfd6c3b83fb2d8ce3329e503f756e", - "sha256:912de6507b631934bd225cdac310b04a58def94391003ba83939e5a10e99568d" + "sha256:6b9d68a29aba1ee400c823d8e5fe88675282eb69d7211e72fe65dbe54b33daca", + "sha256:e049bbdff0e0a1f8e701b64636811291d21bff79bf1e7850850a44055224a85f" ], "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240106" + "version": "==4.6.0.20240311" }, "typing-extensions": { "hashes": [ @@ -1317,7 +1309,7 @@ "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", + "markers": "python_version >= '3.8'", "version": "==2.2.1" }, "vine": { @@ -1343,93 +1335,9 @@ "index": "pypi", "markers": "python_version >= '3.7'", "version": "==6.5.0" - }, - "wrapt": { - "hashes": [ - "sha256:0d2691979e93d06a95a26257adb7bfd0c93818e89b1406f5a28f36e0d8c1e1fc", - "sha256:14d7dc606219cdd7405133c713f2c218d4252f2a469003f8c46bb92d5d095d81", - "sha256:1a5db485fe2de4403f13fafdc231b0dbae5eca4359232d2efc79025527375b09", - "sha256:1acd723ee2a8826f3d53910255643e33673e1d11db84ce5880675954183ec47e", - "sha256:1ca9b6085e4f866bd584fb135a041bfc32cab916e69f714a7d1d397f8c4891ca", - "sha256:1dd50a2696ff89f57bd8847647a1c363b687d3d796dc30d4dd4a9d1689a706f0", - "sha256:2076fad65c6736184e77d7d4729b63a6d1ae0b70da4868adeec40989858eb3fb", - "sha256:2a88e6010048489cda82b1326889ec075a8c856c2e6a256072b28eaee3ccf487", - "sha256:3ebf019be5c09d400cf7b024aa52b1f3aeebeff51550d007e92c3c1c4afc2a40", - "sha256:418abb18146475c310d7a6dc71143d6f7adec5b004ac9ce08dc7a34e2babdc5c", - "sha256:43aa59eadec7890d9958748db829df269f0368521ba6dc68cc172d5d03ed8060", - "sha256:44a2754372e32ab315734c6c73b24351d06e77ffff6ae27d2ecf14cf3d229202", - "sha256:490b0ee15c1a55be9c1bd8609b8cecd60e325f0575fc98f50058eae366e01f41", - "sha256:49aac49dc4782cb04f58986e81ea0b4768e4ff197b57324dcbd7699c5dfb40b9", - "sha256:5eb404d89131ec9b4f748fa5cfb5346802e5ee8836f57d516576e61f304f3b7b", - "sha256:5f15814a33e42b04e3de432e573aa557f9f0f56458745c2074952f564c50e664", - "sha256:5f370f952971e7d17c7d1ead40e49f32345a7f7a5373571ef44d800d06b1899d", - "sha256:66027d667efe95cc4fa945af59f92c5a02c6f5bb6012bff9e60542c74c75c362", - "sha256:66dfbaa7cfa3eb707bbfcd46dab2bc6207b005cbc9caa2199bcbc81d95071a00", - "sha256:685f568fa5e627e93f3b52fda002c7ed2fa1800b50ce51f6ed1d572d8ab3e7fc", - "sha256:6906c4100a8fcbf2fa735f6059214bb13b97f75b1a61777fcf6432121ef12ef1", - "sha256:6a42cd0cfa8ffc1915aef79cb4284f6383d8a3e9dcca70c445dcfdd639d51267", - "sha256:6dcfcffe73710be01d90cae08c3e548d90932d37b39ef83969ae135d36ef3956", - "sha256:6f6eac2360f2d543cc875a0e5efd413b6cbd483cb3ad7ebf888884a6e0d2e966", - "sha256:72554a23c78a8e7aa02abbd699d129eead8b147a23c56e08d08dfc29cfdddca1", - "sha256:73870c364c11f03ed072dda68ff7aea6d2a3a5c3fe250d917a429c7432e15228", - "sha256:73aa7d98215d39b8455f103de64391cb79dfcad601701a3aa0dddacf74911d72", - "sha256:75ea7d0ee2a15733684badb16de6794894ed9c55aa5e9903260922f0482e687d", - "sha256:7bd2d7ff69a2cac767fbf7a2b206add2e9a210e57947dd7ce03e25d03d2de292", - "sha256:807cc8543a477ab7422f1120a217054f958a66ef7314f76dd9e77d3f02cdccd0", - "sha256:8e9723528b9f787dc59168369e42ae1c3b0d3fadb2f1a71de14531d321ee05b0", - "sha256:9090c9e676d5236a6948330e83cb89969f433b1943a558968f659ead07cb3b36", - "sha256:9153ed35fc5e4fa3b2fe97bddaa7cbec0ed22412b85bcdaf54aeba92ea37428c", - "sha256:9159485323798c8dc530a224bd3ffcf76659319ccc7bbd52e01e73bd0241a0c5", - "sha256:941988b89b4fd6b41c3f0bfb20e92bd23746579736b7343283297c4c8cbae68f", - "sha256:94265b00870aa407bd0cbcfd536f17ecde43b94fb8d228560a1e9d3041462d73", - "sha256:98b5e1f498a8ca1858a1cdbffb023bfd954da4e3fa2c0cb5853d40014557248b", - "sha256:9b201ae332c3637a42f02d1045e1d0cccfdc41f1f2f801dafbaa7e9b4797bfc2", - "sha256:a0ea261ce52b5952bf669684a251a66df239ec6d441ccb59ec7afa882265d593", - "sha256:a33a747400b94b6d6b8a165e4480264a64a78c8a4c734b62136062e9a248dd39", - "sha256:a452f9ca3e3267cd4d0fcf2edd0d035b1934ac2bd7e0e57ac91ad6b95c0c6389", - "sha256:a86373cf37cd7764f2201b76496aba58a52e76dedfaa698ef9e9688bfd9e41cf", - "sha256:ac83a914ebaf589b69f7d0a1277602ff494e21f4c2f743313414378f8f50a4cf", - "sha256:aefbc4cb0a54f91af643660a0a150ce2c090d3652cf4052a5397fb2de549cd89", - "sha256:b3646eefa23daeba62643a58aac816945cadc0afaf21800a1421eeba5f6cfb9c", - "sha256:b47cfad9e9bbbed2339081f4e346c93ecd7ab504299403320bf85f7f85c7d46c", - "sha256:b935ae30c6e7400022b50f8d359c03ed233d45b725cfdd299462f41ee5ffba6f", - "sha256:bb2dee3874a500de01c93d5c71415fcaef1d858370d405824783e7a8ef5db440", - "sha256:bc57efac2da352a51cc4658878a68d2b1b67dbe9d33c36cb826ca449d80a8465", - "sha256:bf5703fdeb350e36885f2875d853ce13172ae281c56e509f4e6eca049bdfb136", - "sha256:c31f72b1b6624c9d863fc095da460802f43a7c6868c5dda140f51da24fd47d7b", - "sha256:c5cd603b575ebceca7da5a3a251e69561bec509e0b46e4993e1cac402b7247b8", - "sha256:d2efee35b4b0a347e0d99d28e884dfd82797852d62fcd7ebdeee26f3ceb72cf3", - "sha256:d462f28826f4657968ae51d2181a074dfe03c200d6131690b7d65d55b0f360f8", - "sha256:d5e49454f19ef621089e204f862388d29e6e8d8b162efce05208913dde5b9ad6", - "sha256:da4813f751142436b075ed7aa012a8778aa43a99f7b36afe9b742d3ed8bdc95e", - "sha256:db2e408d983b0e61e238cf579c09ef7020560441906ca990fe8412153e3b291f", - "sha256:db98ad84a55eb09b3c32a96c576476777e87c520a34e2519d3e59c44710c002c", - "sha256:dbed418ba5c3dce92619656802cc5355cb679e58d0d89b50f116e4a9d5a9603e", - "sha256:dcdba5c86e368442528f7060039eda390cc4091bfd1dca41e8046af7c910dda8", - "sha256:decbfa2f618fa8ed81c95ee18a387ff973143c656ef800c9f24fb7e9c16054e2", - "sha256:e4fdb9275308292e880dcbeb12546df7f3e0f96c6b41197e0cf37d2826359020", - "sha256:eb1b046be06b0fce7249f1d025cd359b4b80fc1c3e24ad9eca33e0dcdb2e4a35", - "sha256:eb6e651000a19c96f452c85132811d25e9264d836951022d6e81df2fff38337d", - "sha256:ed867c42c268f876097248e05b6117a65bcd1e63b779e916fe2e33cd6fd0d3c3", - "sha256:edfad1d29c73f9b863ebe7082ae9321374ccb10879eeabc84ba3b69f2579d537", - "sha256:f2058f813d4f2b5e3a9eb2eb3faf8f1d99b81c3e51aeda4b168406443e8ba809", - "sha256:f6b2d0c6703c988d334f297aa5df18c45e97b0af3679bb75059e0e0bd8b1069d", - "sha256:f8212564d49c50eb4565e502814f694e240c55551a5f1bc841d4fcaabb0a9b8a", - "sha256:ffa565331890b90056c01db69c0fe634a776f8019c143a5ae265f9c6bc4bd6d4" - ], - "markers": "python_version >= '3.6'", - "version": "==1.16.0" } }, "develop": { - "appnope": { - "hashes": [ - "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", - "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c" - ], - "markers": "sys_platform == 'darwin'", - "version": "==0.1.4" - }, "asgiref": { "hashes": [ "sha256:89b2ef2247e3b562a16eef663bc0e2e703ec6468e2fa8a5cd61cd449786d4f6e", @@ -1447,11 +1355,11 @@ }, "autopep8": { "hashes": [ - "sha256:067959ca4a07b24dbd5345efa8325f5f58da4298dab0dde0443d5ed765de80cb", - "sha256:2913064abd97b3419d1cc83ea71f042cb821f87e45b9c88cad5ad3c4ea87fe0c" + "sha256:1fa8964e4618929488f4ec36795c7ff12924a68b8bf01366c094fc52f770b6e7", + "sha256:2bb76888c5edbcafe6aabab3c47ba534f5a2c2d245c2eddced4a30c4b4946357" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.4" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "backcall": { "hashes": [ @@ -1491,12 +1399,12 @@ }, "boto3": { "hashes": [ - "sha256:3626db4ba9fbb1b58c8fe923da5ed670873b3d881a102956ea19d3b69cd097cc", - "sha256:ebdd938019f3df2e7b50585353963d4553faf3fbb7b2085c440107fa6caa233b" + "sha256:b611de58ab28940a36c77d7ef9823427ebf25d5ee8277b802f9979b14e780534", + "sha256:db97f9c29f1806cf9020679be0dd5ffa2aff2670e28e0e2046f98b979be498a4" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.34.27" + "version": "==1.34.65" }, "boto3-stubs": { "extras": [ @@ -1507,25 +1415,24 @@ "sha256:105da4a04dcb5e4ddc90f21ab8b24a3423ecfacb4775b8ccd3879574e5dce358", "sha256:2afd696c8bb4daf8890ecd75a720e1733cd8b8556eaecc92c36f9b56fc6013bd" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==1.34.65" }, "botocore": { "hashes": [ - "sha256:9f00bd5e4698bcdd37ce6e224a896baf58d209678ed92834944b767de9061cc5", - "sha256:e175360445424b83b0e28ae20d301b99cf44ff2c9d5ab1d8670899bec05a9753" + "sha256:92560f8fbdaa9dd221212a3d3a7609219ba0bbf308c13571674c0cda9d8f39e1", + "sha256:fd7d8742007c220f897cb126b8916ca0cf3724a739d4d716aa5385d7f9d8aeb1" ], "markers": "python_version >= '3.8'", - "version": "==1.34.39" + "version": "==1.34.66" }, "botocore-stubs": { "hashes": [ - "sha256:2f0bae0f8d934cb3446ecec9813aa365407bd7b98586280feb8cfc75a40b4513", - "sha256:7c510adb2c6ffee2e2a5f198c1030bb8edff2136ee56e46729c02ea37b2424a5" + "sha256:530ea7d66022ec6aa0ba0c5200a2aede5d30b839c632d00962f0cf4f806c6a51", + "sha256:a5aa1240c3c8ccc62d43916395943896afa81399dc5d4203127cc0ffba20f999" ], "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.34.65" + "version": "==1.34.66" }, "certifi": { "hashes": [ @@ -1708,32 +1615,32 @@ }, "debugpy": { "hashes": [ - "sha256:0e90314a078d4e3f009520c8387aba8f74c3034645daa7a332a3d1bb81335756", - "sha256:1285920a3f9a75f5d1acf59ab1b9da9ae6eb9a05884cd7674f95170c9cafa4de", - "sha256:1565fd904f9571c430adca597771255cff4f92171486fced6f765dcbdfc8ec8d", - "sha256:17ad9a681aca1704c55b9a5edcb495fa8f599e4655c9872b7f9cf3dc25890d48", - "sha256:18a69f8e142a716310dd0af6d7db08992aed99e2606108732efde101e7c65e2a", - "sha256:18bca8429d6632e2d3435055416d2d88f0309cc39709f4f6355c8d412cc61f24", - "sha256:2b0e489613bc066051439df04c56777ec184b957d6810cb65f235083aef7a0dc", - "sha256:538765a41198aa88cc089295b39c7322dd598f9ef1d52eaae12145c63bf9430a", - "sha256:6516f36a2e95b3be27f171f12b641e443863f4ad5255d0fdcea6ae0be29bb912", - "sha256:676911c710e85567b17172db934a71319ed9d995104610ce23fd74a07f66e6f6", - "sha256:7515a5ba5ee9bfe956685909c5f28734c1cecd4ee813523363acfe3ca824883a", - "sha256:7bf0b4bbd841b2397b6a8de15da9227f1164f6d43ceee971c50194eaed930a9d", - "sha256:9e9571d831ad3c75b5fb6f3efcb71c471cf2a74ba84af6ac1c79ce00683bed4b", - "sha256:a5036e918c6ba8fc4c4f1fd0207d81db634431a02f0dc2ba51b12fd793c8c9de", - "sha256:a6f43a681c5025db1f1c0568069d1d1bad306a02e7c36144912b26d9c90e4724", - "sha256:ad22e1095b9977af432465c1e09132ba176e18df3834b1efcab1a449346b350b", - "sha256:bc8da67ade39d9e75608cdb8601d07e63a4e85966e0572c981f14e2cf42bcdef", - "sha256:c7e8cf91f8f3f9b5fad844dd88427b85d398bda1e2a0cd65d5a21312fcbc0c6f", - "sha256:d5be95b3946a4d7b388e45068c7b75036ac5a610f41014aee6cafcd5506423ad", - "sha256:dc8a12ac8b97ef3d6973c6679a093138c7c9b03eb685f0e253269a195f651559", - "sha256:f625e427f21423e5874139db529e18cb2966bdfcc1cb87a195538c5b34d163d1", - "sha256:f6de2e6f24f62969e0f0ef682d78c98161c4dca29e9fb05df4d2989005005502" + "sha256:016a9fcfc2c6b57f939673c874310d8581d51a0fe0858e7fac4e240c5eb743cb", + "sha256:0de56aba8249c28a300bdb0672a9b94785074eb82eb672db66c8144fff673146", + "sha256:1a9fe0829c2b854757b4fd0a338d93bc17249a3bf69ecf765c61d4c522bb92a8", + "sha256:28acbe2241222b87e255260c76741e1fbf04fdc3b6d094fcf57b6c6f75ce1242", + "sha256:3a79c6f62adef994b2dbe9fc2cc9cc3864a23575b6e387339ab739873bea53d0", + "sha256:3bda0f1e943d386cc7a0e71bfa59f4137909e2ed947fb3946c506e113000f741", + "sha256:3ebb70ba1a6524d19fa7bb122f44b74170c447d5746a503e36adc244a20ac539", + "sha256:58911e8521ca0c785ac7a0539f1e77e0ce2df753f786188f382229278b4cdf23", + "sha256:6df9aa9599eb05ca179fb0b810282255202a66835c6efb1d112d21ecb830ddd3", + "sha256:7a3afa222f6fd3d9dfecd52729bc2e12c93e22a7491405a0ecbf9e1d32d45b39", + "sha256:7eb7bd2b56ea3bedb009616d9e2f64aab8fc7000d481faec3cd26c98a964bcdd", + "sha256:92116039b5500633cc8d44ecc187abe2dfa9b90f7a82bbf81d079fcdd506bae9", + "sha256:a2e658a9630f27534e63922ebf655a6ab60c370f4d2fc5c02a5b19baf4410ace", + "sha256:bfb20cb57486c8e4793d41996652e5a6a885b4d9175dd369045dad59eaacea42", + "sha256:caad2846e21188797a1f17fc09c31b84c7c3c23baf2516fed5b40b378515bbf0", + "sha256:d915a18f0597ef685e88bb35e5d7ab968964b7befefe1aaea1eb5b2640b586c7", + "sha256:dda73bf69ea479c8577a0448f8c707691152e6c4de7f0c4dec5a4bc11dee516e", + "sha256:e38beb7992b5afd9d5244e96ad5fa9135e94993b0c551ceebf3fe1a5d9beb234", + "sha256:edcc9f58ec0fd121a25bc950d4578df47428d72e1a0d66c07403b04eb93bcf98", + "sha256:efd3fdd3f67a7e576dd869c184c5dd71d9aaa36ded271939da352880c012e703", + "sha256:f696d6be15be87aef621917585f9bb94b1dc9e8aced570db1b8a6fc14e8f9b42", + "sha256:fd97ed11a4c7f6d042d320ce03d83b20c3fb40da892f994bc041bbc415d7a099" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==1.7.0" + "markers": "python_version >= '3.8'", + "version": "==1.8.1" }, "decorator": { "hashes": [ @@ -1839,11 +1746,11 @@ }, "faker": { "hashes": [ - "sha256:60e89e5c0b584e285a7db05eceba35011a241954afdab2853cb246c8a56700a2", - "sha256:b7f76bb1b2ac4cdc54442d955e36e477c387000f31ce46887fb9722a041be60b" + "sha256:5fb5aa9749d09971e04a41281ae3ceda9414f683d4810a694f8a8eebb8f9edec", + "sha256:9978025e765ba79f8bf6154c9630a9c2b7f9c9b0f175d4ad5e04b19a82a8d8d6" ], "markers": "python_version >= '3.8'", - "version": "==23.1.0" + "version": "==24.3.0" }, "filelock": { "hashes": [ @@ -1855,12 +1762,12 @@ }, "flake8": { "hashes": [ - "sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23", - "sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5" + "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132", + "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3" ], "index": "pypi", "markers": "python_full_version >= '3.8.1'", - "version": "==6.1.0" + "version": "==7.0.0" }, "freezegun": { "hashes": [ @@ -1881,11 +1788,11 @@ }, "identify": { "hashes": [ - "sha256:a4316013779e433d08b96e5eabb7f641e6c7942e4ab5d4c509ebd2e7a8994aed", - "sha256:ee17bc9d499899bc9eaec1ac7bf2dc9eedd480db9d88b96d123d3b64a9d34f5d" + "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791", + "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e" ], "markers": "python_version >= '3.8'", - "version": "==2.5.34" + "version": "==2.5.35" }, "idna": { "hashes": [ @@ -2047,10 +1954,10 @@ }, "mypy-boto3-s3": { "hashes": [ - "sha256:71c39ab0623cdb442d225b71c1783f6a513cff4c4a13505a2efbb2e3aff2e965", - "sha256:f9669ecd182d5bf3532f5f2dcc5e5237776afe157ad5a0b37b26d6bec5fcc432" + "sha256:2aecfbe1c00654bc21f839068218d60123366954bf43a708baa50f9543e3f205", + "sha256:2fcdf412ce2924b2f0b34db59abf06a9c0bbe4cd3361f14f0d2c1e211c0f7ddd" ], - "version": "==1.34.14" + "version": "==1.34.65" }, "mypy-extensions": { "hashes": [ @@ -2070,11 +1977,11 @@ }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "parso": { "hashes": [ @@ -2156,11 +2063,11 @@ }, "pyflakes": { "hashes": [ - "sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774", - "sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc" + "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", + "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" ], "markers": "python_version >= '3.8'", - "version": "==3.1.0" + "version": "==3.2.0" }, "pygments": { "hashes": [ @@ -2172,11 +2079,11 @@ }, "python-dateutil": { "hashes": [ - "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", - "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", + "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.8.2" + "version": "==2.9.0.post0" }, "pyyaml": { "hashes": [ @@ -2246,19 +2153,19 @@ }, "s3transfer": { "hashes": [ - "sha256:3cdb40f5cfa6966e812209d0994f2a4709b561c88e90cf00c2696d2df4e56b2e", - "sha256:d0c8bbf672d5eebbe4e57945e23b972d963f07d82f661cabf678a5c88831595b" + "sha256:5683916b4c724f799e600f41dd9e10a9ff19871bf87623cc8f491cb4f5fa0a19", + "sha256:ceb252b11bcf87080fb7850a224fb6e05c8a776bab8f2b64b7f25b969464839d" ], "markers": "python_version >= '3.8'", - "version": "==0.10.0" + "version": "==0.10.1" }, "setuptools": { "hashes": [ - "sha256:850894c4195f09c4ed30dba56213bf7c3f21d86ed6bdaafb5df5972593bfc401", - "sha256:c054629b81b946d63a9c6e732bc8b2513a7c3ea645f11d0139a2191d735c60c6" + "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e", + "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c" ], "markers": "python_version >= '3.8'", - "version": "==69.1.0" + "version": "==69.2.0" }, "six": { "hashes": [ @@ -2294,11 +2201,11 @@ }, "traitlets": { "hashes": [ - "sha256:2e5a030e6eff91737c643231bfcf04a65b0132078dad75e4936700b213652e74", - "sha256:8585105b371a04b8316a43d5ce29c098575c2e477850b62b848b964f1444527e" + "sha256:8cdd83c040dab7d1dee822678e5f5d100b514f7b72b01615b26fc5718916fdf9", + "sha256:fcdf85684a772ddeba87db2f398ce00b40ff550d1528c03c14dbf6a02003cd80" ], "markers": "python_version >= '3.8'", - "version": "==5.14.1" + "version": "==5.14.2" }, "types-awscrt": { "hashes": [ @@ -2318,18 +2225,19 @@ }, "types-pyyaml": { "hashes": [ - "sha256:334373d392fde0fdf95af5c3f1661885fa10c52167b14593eb856289e1855062", - "sha256:c05bc6c158facb0676674b7f11fe3960db4f389718e19e62bd2b84d6205cfd24" + "sha256:a9e0f0f88dc835739b0c1ca51ee90d04ca2a897a71af79de9aec5f38cb0a5342", + "sha256:b845b06a1c7e54b8e5b4c683043de0d9caf205e7434b3edc678ff2411979b8f6" ], - "version": "==6.0.12.12" + "markers": "python_version >= '3.8'", + "version": "==6.0.12.20240311" }, "types-requests": { "hashes": [ - "sha256:03a28ce1d7cd54199148e043b2079cdded22d6795d19a2c2a6791a4b2b5e2eb5", - "sha256:9592a9a4cb92d6d75d9b491a41477272b710e021011a2a3061157e2fb1f1a5d1" + "sha256:47872893d65a38e282ee9f277a4ee50d1b28bd592040df7d1fdaffdf3779937d", + "sha256:b1c1b66abfb7fa79aae09097a811c4aa97130eb8831c60e47aee4ca344731ca5" ], "markers": "python_version >= '3.8'", - "version": "==2.31.0.20240125" + "version": "==2.31.0.20240311" }, "types-s3transfer": { "hashes": [ @@ -2344,24 +2252,24 @@ "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb" ], - "markers": "python_version < '3.12'", + "markers": "python_version >= '3.8'", "version": "==4.10.0" }, "urllib3": { "hashes": [ - "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", - "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.7" + "markers": "python_version >= '3.8'", + "version": "==2.2.1" }, "virtualenv": { "hashes": [ - "sha256:4238949c5ffe6876362d9c0180fc6c3a824a7b12b80604eeb8085f2ed7460de3", - "sha256:bf51c0d9c7dd63ea8e44086fa1e4fb1093a31e963b86959257378aef020e1f1b" + "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a", + "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197" ], "markers": "python_version >= '3.7'", - "version": "==20.25.0" + "version": "==20.25.1" }, "watchdog": { "hashes": [ @@ -2652,11 +2560,11 @@ }, "packaging": { "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9" ], "markers": "python_version >= '3.7'", - "version": "==23.2" + "version": "==24.0" }, "pygments": { "hashes": [ @@ -2757,11 +2665,11 @@ }, "urllib3": { "hashes": [ - "sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84", - "sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e" + "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d", + "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19" ], - "markers": "python_version >= '3.6'", - "version": "==2.0.7" + "markers": "python_version >= '3.8'", + "version": "==2.2.1" } } } From 5e750a45789da56e21a4fd7ea36689a7596209ad Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Fri, 22 Mar 2024 18:01:45 +0530 Subject: [PATCH 24/32] fixes issues with patient annotate + distinct not implemented for csv export (#2002) --- care/facility/api/viewsets/patient.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 631928d5f4..0ce204bdf9 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -406,6 +406,14 @@ def get_serializer_class(self): else: return self.serializer_class + def filter_queryset(self, queryset: QuerySet) -> QuerySet: + if self.action == "list" and settings.CSV_REQUEST_PARAMETER in self.request.GET: + for backend in (PatientDRYFilter, filters.DjangoFilterBackend): + queryset = backend().filter_queryset(self.request, queryset, self) + return queryset.filter(last_consultation__discharge_date__isnull=True) + + return super().filter_queryset(queryset) + def list(self, request, *args, **kwargs): """ Patient List From 0f321b014fd67b25f074d29c5ae9414cb6515c68 Mon Sep 17 00:00:00 2001 From: Soumyaranjan Pradhan <72642534+itxsoumya@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:20:13 +0530 Subject: [PATCH 25/32] reset password schema for API docs swagger updated #1434 (#1934) * reset password schema for API docs swagger updated * removed unused libs * fixed parameters * lint * use serializers to correctly generate schema * fix test cases --------- Co-authored-by: Aakash Singh --- care/users/reset_password_views.py | 44 ++++++++++++++++++++++-------- care/users/tests/test_auth.py | 4 +-- 2 files changed, 35 insertions(+), 13 deletions(-) diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index 6cec930e4c..28ab1cfe12 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -15,7 +15,7 @@ get_password_reset_lookup_field, get_password_reset_token_expiry_time, ) -from django_rest_passwordreset.serializers import PasswordTokenSerializer +from django_rest_passwordreset.serializers import PasswordValidateMixin from django_rest_passwordreset.signals import ( post_password_reset, pre_password_reset, @@ -38,8 +38,24 @@ ) -class ResetPasswordUserSerializer(serializers.Serializer): - username = serializers.CharField() +class ResetPasswordCheckSerializer(serializers.Serializer): + token = serializers.CharField( + write_only=True, help_text="The token that was sent to the user's email address" + ) + status = serializers.CharField(read_only=True, help_text="Request status") + + +class ResetPasswordConfirmSerializer(PasswordValidateMixin, serializers.Serializer): + token = serializers.CharField( + write_only=True, help_text="The token that was sent to the user's email address" + ) + password = serializers.CharField(write_only=True, help_text="The new password") + status = serializers.CharField(read_only=True, help_text="Request status") + + +class ResetPasswordRequestTokenSerializer(serializers.Serializer): + username = serializers.CharField(write_only=True) + status = serializers.CharField(read_only=True, help_text="Request status") class ResetPasswordCheck(GenericAPIView): @@ -47,11 +63,15 @@ class ResetPasswordCheck(GenericAPIView): An Api View which provides a method to check if a password reset token is valid """ + authentication_classes = () permission_classes = () + serializer_class = ResetPasswordCheckSerializer - @extend_schema(tags=["auth", "users"]) + @extend_schema(tags=["auth"]) def post(self, request, *args, **kwargs): - token = request.data.get("token", None) + serializer = self.serializer_class(data=request.data) + serializer.is_valid(raise_exception=True) + token = serializer.validated_data["token"] if ratelimit(request, "reset", [token], "20/h"): return Response( @@ -92,10 +112,11 @@ class ResetPasswordConfirm(GenericAPIView): An Api View which provides a method to reset a password based on a unique token """ + authentication_classes = () permission_classes = () - serializer_class = PasswordTokenSerializer + serializer_class = ResetPasswordConfirmSerializer - @extend_schema(tags=["auth", "users"]) + @extend_schema(tags=["auth"]) def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) @@ -171,10 +192,11 @@ class ResetPasswordRequestToken(GenericAPIView): """ throttle_classes = () + authentication_classes = () permission_classes = () - serializer_class = ResetPasswordUserSerializer + serializer_class = ResetPasswordRequestTokenSerializer - @extend_schema(tags=["auth", "users"]) + @extend_schema(tags=["auth"]) def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) @@ -218,9 +240,9 @@ def post(self, request, *args, **kwargs): ): raise exceptions.ValidationError( { - "email": [ + "username": [ _( - "There is no active user associated with this e-mail address or the password can not be changed" + "There is no active user associated with this username or the password can not be changed" ) ], } diff --git a/care/users/tests/test_auth.py b/care/users/tests/test_auth.py index 29e5f4168c..c03665f0b6 100644 --- a/care/users/tests/test_auth.py +++ b/care/users/tests/test_auth.py @@ -162,8 +162,8 @@ def test_verify_password_reset_token(self): def test_verify_password_reset_token_with_missing_fields(self): response = self.client.post("/api/v1/password_reset/check/") - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - self.assertEqual(response.data["detail"], "The password reset link is invalid") + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual(response.json()["token"], ["This field is required."]) def test_verify_password_reset_token_with_invalid_token(self): response = self.client.post( From f4824be4913a5a32701a8382d7fcd4b9d9d2c59c Mon Sep 17 00:00:00 2001 From: Rashmik <146672184+rash-27@users.noreply.github.com> Date: Sun, 24 Mar 2024 21:59:34 +0530 Subject: [PATCH 26/32] Replace age field with DOB in users model (#1935) * Replace age field with DOB in users model * Add custom migrations * Add imports * squash migrations and add dummy data * Add reverse function * fix lint errors * Update dummy data and DOB field * update tests --------- Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari --- care/facility/api/serializers/patient.py | 4 +- care/users/admin.py | 1 - care/users/api/serializers/user.py | 26 ++++++++-- .../migrations/0015_age_to_dateofbirth.py | 50 +++++++++++++++++++ care/users/models.py | 5 +- care/users/tests/test_api.py | 26 ++++++---- care/users/tests/test_facility_user_create.py | 6 ++- care/users/tests/test_models.py | 4 +- care/utils/tests/test_utils.py | 4 +- data/dummy/users.json | 48 +++++++++--------- data/medibase.json | 2 +- 11 files changed, 125 insertions(+), 51 deletions(-) create mode 100644 care/users/migrations/0015_age_to_dateofbirth.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 721df68fbe..28993af4ca 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -361,8 +361,8 @@ def update(self, instance, validated_data): if meta_info: if patient.meta_info is None: - meta_info_obj = PatientMetaInfo.objects.create(**meta_info) - patient.meta_info = meta_info_obj + meta_info_obj = PatientMetaInfo.objects.create(**meta_info) + patient.meta_info = meta_info_obj else: for key, value in meta_info.items(): setattr(patient.meta_info, key, value) diff --git a/care/users/admin.py b/care/users/admin.py index 4b02b5e568..32b64980dd 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -40,7 +40,6 @@ class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): "phone_number", "alt_phone_number", "gender", - "age", "verified", ) }, diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index f4df1a426e..3530d695bd 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -1,7 +1,6 @@ -from datetime import date - from django.contrib.auth.hashers import make_password from django.db import transaction +from django.utils.timezone import now from rest_framework import exceptions, serializers from care.facility.api.serializers.facility import FacilityBareMinimumSerializer @@ -43,7 +42,7 @@ class Meta: "phone_number", "alt_phone_number", "gender", - "age", + "date_of_birth", ) def create(self, validated_data): @@ -67,7 +66,7 @@ def validate(self, attrs): }, ) - if attrs["doctor_experience_commenced_on"] > date.today(): + if attrs["doctor_experience_commenced_on"] > now().date(): raise serializers.ValidationError( { "doctor_experience_commenced_on": "Experience cannot be in the future", @@ -113,6 +112,14 @@ class Meta: "created_by", ) + date_of_birth = serializers.DateField(required=True) + + def validate_date_of_birth(self, value): + if value and now().year - value.year < 16: + raise serializers.ValidationError("Age must be greater than 15 years") + + return value + def validate_facilities(self, facility_ids): if facility_ids: if ( @@ -275,6 +282,8 @@ class UserSerializer(SignUpSerializer): home_facility = ExternalIdSerializerField(queryset=Facility.objects.all()) + date_of_birth = serializers.DateField(required=True) + class Meta: model = User fields = ( @@ -297,7 +306,7 @@ class Meta: "phone_number", "alt_phone_number", "gender", - "age", + "date_of_birth", "is_superuser", "verified", "home_facility_object", @@ -323,6 +332,12 @@ class Meta: extra_kwargs = {"url": {"lookup_field": "username"}} + def validate_date_of_birth(self, value): + if value and now().year - value.year < 16: + raise serializers.ValidationError("Age must be greater than 15 years") + + return value + def validate(self, attrs): validated = super().validate(attrs) if "home_facility" in validated: @@ -401,6 +416,7 @@ class Meta: "first_name", "last_name", "username", + "date_of_birth", "local_body_object", "district_object", "state_object", diff --git a/care/users/migrations/0015_age_to_dateofbirth.py b/care/users/migrations/0015_age_to_dateofbirth.py new file mode 100644 index 0000000000..f58f5ef180 --- /dev/null +++ b/care/users/migrations/0015_age_to_dateofbirth.py @@ -0,0 +1,50 @@ +from django.db import migrations, models +from django.utils.timezone import now + + +def age_to_date_of_birth(apps, schema_editor): + User = apps.get_model("users", "User") + users_to_update = [] + for user in User.objects.all(): + if user.age: + user.date_of_birth = now().replace(year=now().year - user.age) + users_to_update.append(user) + + User.objects.bulk_update(users_to_update, ["date_of_birth"]) + + +def date_of_birth_to_age(apps, schema_editor): + User = apps.get_model("users", "User") + users_to_update = [] + for user in User.objects.all(): + if user.date_of_birth: + user.age = ( + now().year + - user.date_of_birth.year + - ( + (now().month, now().day) + < (user.date_of_birth.month, user.date_of_birth.day) + ) + ) + users_to_update.append(user) + + User.objects.bulk_update(users_to_update, ["age"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0014_alter_user_username"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="date_of_birth", + field=models.DateField(blank=True, null=True), + ), + migrations.RunPython(age_to_date_of_birth, date_of_birth_to_age), + migrations.RemoveField( + model_name="user", + name="age", + ), + ] diff --git a/care/users/models.py b/care/users/models.py index ed18a887b6..c3099816b3 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -132,7 +132,6 @@ def get_entire_queryset(self): def create_superuser(self, username, email, password, **extra_fields): district = District.objects.all()[0] extra_fields["district"] = district - extra_fields["age"] = 20 extra_fields["phone_number"] = "+919696969696" extra_fields["gender"] = 3 extra_fields["user_type"] = 40 @@ -235,7 +234,7 @@ class User(AbstractUser): video_connect_link = models.URLField(blank=True, null=True) gender = models.IntegerField(choices=GENDER_CHOICES, blank=False) - age = models.IntegerField(validators=[MinValueValidator(1), MaxValueValidator(100)]) + date_of_birth = models.DateField(null=True, blank=True) skills = models.ManyToManyField("Skill", through=UserSkill) home_facility = models.ForeignKey( "facility.Facility", on_delete=models.PROTECT, null=True, blank=True @@ -290,7 +289,7 @@ class User(AbstractUser): "last_name": "Last Name", "phone_number": "Phone Number", "gender": "Gender", - "age": "Age", + "date_of_birth": "Date of Birth", "verified": "verified", "local_body__name": "Local Body", "district__name": "District", diff --git a/care/users/tests/test_api.py b/care/users/tests/test_api.py index 950486b499..18512436db 100644 --- a/care/users/tests/test_api.py +++ b/care/users/tests/test_api.py @@ -1,3 +1,5 @@ +from datetime import date + from rest_framework import status from rest_framework.test import APITestCase @@ -29,7 +31,7 @@ def get_detail_representation(self, obj=None) -> dict: "created_by": obj.created_by, "phone_number": obj.phone_number, "alt_phone_number": obj.alt_phone_number, - "age": obj.age, + "date_of_birth": str(obj.date_of_birth), "gender": GENDER_CHOICES[obj.gender - 1][1], "home_facility": None, "home_facility_object": None, @@ -56,8 +58,8 @@ def test_superuser_can_view(self): response = self.client.get(f"/api/v1/users/{self.user.username}/") res_data_json = response.json() res_data_json.pop("id") - data = self.user_data.copy() + data["date_of_birth"] = str(data["date_of_birth"]) data.pop("password") self.assertDictEqual( res_data_json, @@ -74,14 +76,16 @@ def test_superuser_can_modify(self): response = self.client.patch( f"/api/v1/users/{username}/", - {"age": 31}, + {"date_of_birth": date(1992, 4, 1)}, ) self.assertEqual(response.status_code, status.HTTP_200_OK) # test the value from api - self.assertEqual(response.json()["age"], 31) + self.assertEqual(response.json()["date_of_birth"], "1992-04-01") # test value at the backend - self.assertEqual(User.objects.get(username=username).age, 31) + self.assertEqual( + User.objects.get(username=username).date_of_birth, date(1992, 4, 1) + ) def test_superuser_can_delete(self): """Test superuser can delete other users""" @@ -108,7 +112,7 @@ def get_detail_representation(self, obj=None) -> dict: "phone_number": obj.phone_number, "first_name": obj.first_name, "last_name": obj.last_name, - "age": obj.age, + "date_of_birth": str(obj.date_of_birth), **self.get_local_body_district_state_representation(obj), } @@ -150,15 +154,17 @@ def test_user_can_modify_themselves(self): response = self.client.patch( f"/api/v1/users/{username}/", { - "age": 31, + "date_of_birth": date(2005, 4, 1), "password": password, }, ) self.assertEqual(response.status_code, status.HTTP_200_OK) # test the value from api - self.assertEqual(response.json()["age"], 31) + self.assertEqual(response.json()["date_of_birth"], "2005-04-01") # test value at the backend - self.assertEqual(User.objects.get(username=username).age, 31) + self.assertEqual( + User.objects.get(username=username).date_of_birth, date(2005, 4, 1) + ) def test_user_cannot_read_others(self): """Test 1 user can read the attributes of the other user""" @@ -173,7 +179,7 @@ def test_user_cannot_modify_others(self): response = self.client.patch( f"/api/v1/users/{username}/", { - "age": 31, + "date_of_birth": date(2005, 4, 1), "password": password, }, ) diff --git a/care/users/tests/test_facility_user_create.py b/care/users/tests/test_facility_user_create.py index e1ee56a64d..54d7edea3b 100644 --- a/care/users/tests/test_facility_user_create.py +++ b/care/users/tests/test_facility_user_create.py @@ -1,3 +1,5 @@ +from datetime import date + from rest_framework import status from rest_framework.test import APITestCase @@ -31,7 +33,7 @@ def get_detail_representation(self, obj: User = None) -> dict: "last_name": obj.last_name, "email": obj.email, "phone_number": obj.phone_number, - "age": obj.age, + "date_of_birth": str(obj.date_of_birth), "local_body": getattr(obj.local_body, "id", None), "district": getattr(obj.district, "id", None), "state": getattr(obj.state, "id", None), @@ -50,7 +52,7 @@ def get_new_user_data(self): "user_type": "Staff", "phone_number": "+917795937091", "gender": "Male", - "age": 28, + "date_of_birth": date(2005, 1, 1), "first_name": "Roopak", "last_name": "A N", "email": "anroopak@gmail.com", diff --git a/care/users/tests/test_models.py b/care/users/tests/test_models.py index a559df17e4..e2f45d650b 100644 --- a/care/users/tests/test_models.py +++ b/care/users/tests/test_models.py @@ -1,3 +1,5 @@ +from datetime import date + from django.test import TestCase from care.users.models import District, LocalBody, Skill, State, User @@ -110,7 +112,7 @@ def setUpTestData(cls): district=district, phone_number=8_888_888_888, gender=1, - age=21, + date_of_birth=date(2005, 1, 1), ) def test_max_length_phone_number(self): diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index ccc6b17628..446249e949 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -132,7 +132,7 @@ def get_user_data(cls, district: District, user_type: str = None): "state": district.state, "phone_number": "8887776665", "gender": 2, - "age": 30, + "date_of_birth": date(1992, 4, 1), "email": "foo@foobar.com", "username": "user", "password": "bar", @@ -156,7 +156,7 @@ def create_user( data = { "email": f"{username}@somedomain.com", "phone_number": "5554446667", - "age": 30, + "date_of_birth": date(1992, 4, 1), "gender": 2, "verified": True, "username": username, diff --git a/data/dummy/users.json b/data/dummy/users.json index cf28ec50c4..e952cb5a44 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -22,7 +22,7 @@ "phone_number": "+919696969696", "alt_phone_number": null, "gender": 3, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": false, "deleted": false, @@ -57,7 +57,7 @@ "phone_number": "+911234567891", "alt_phone_number": "+911234567891", "gender": 3, - "age": 11, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -92,7 +92,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -127,7 +127,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -162,7 +162,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -197,7 +197,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -232,7 +232,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -267,7 +267,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -302,7 +302,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -337,7 +337,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -372,7 +372,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -407,7 +407,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -442,7 +442,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -477,7 +477,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -512,7 +512,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -547,7 +547,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -582,7 +582,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -617,7 +617,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -652,7 +652,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -687,7 +687,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, "deleted": false, @@ -722,7 +722,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, "deleted": false, @@ -757,7 +757,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, - "age": 20, + "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, "deleted": false, @@ -792,7 +792,7 @@ "phone_number": "+918878825662", "alt_phone_number": "+918878825662", "gender": 2, - "age": 21, + "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, "deleted": false, @@ -827,7 +827,7 @@ "phone_number": "+918744587566", "alt_phone_number": "+918744587566", "gender": 1, - "age": 35, + "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, "deleted": false, diff --git a/data/medibase.json b/data/medibase.json index 5b9b0cf92f..372b0b43d9 100644 --- a/data/medibase.json +++ b/data/medibase.json @@ -1144305,4 +1144305,4 @@ "cims_class_link": "/india/drug/search?q=drugs+for+erectile+dysfunction&mtype=brand&code=10d", "cims_class": "Drugs for Erectile Dysfunction", "atc_classification": "G04BE03 - sildenafil ; Belongs to the class of drugs used in erectile dysfunction." -}] \ No newline at end of file +}] From 535f6a5b0df2257f974dde024c4bfd21acda6c5d Mon Sep 17 00:00:00 2001 From: Ankur Prabhu <85862184+AnkurPrabhu@users.noreply.github.com> Date: Sun, 24 Mar 2024 22:00:31 +0530 Subject: [PATCH 27/32] [BUG] fix Location/ Bed Management issue ( District Lab Admin Account) (#1959) * fixing permission issue * added tests --- .../models/mixins/permissions/facility.py | 25 +++++++++++++++++-- .../facility/tests/test_asset_location_api.py | 25 +++++++++++++++++++ care/facility/tests/test_facilityuser_api.py | 20 +++++++++++++++ 3 files changed, 68 insertions(+), 2 deletions(-) diff --git a/care/facility/models/mixins/permissions/facility.py b/care/facility/models/mixins/permissions/facility.py index 9d51abc58c..a92a3ba0cf 100644 --- a/care/facility/models/mixins/permissions/facility.py +++ b/care/facility/models/mixins/permissions/facility.py @@ -67,8 +67,19 @@ def has_cover_image_delete_permission(request): ) def has_object_read_permission(self, request): - return super().has_object_read_permission(request) or self.users.contains( - request.user + return ( + super().has_object_read_permission(request) + or self.users.contains(request.user) + or ( + hasattr(self, "state") + and request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] + and request.user.state == self.state + ) + or ( + hasattr(self, "district") + and request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] + and request.user.district == self.district + ) ) def has_object_write_permission(self, request): @@ -124,6 +135,16 @@ def has_object_read_permission(self, request): return ( super().has_object_read_permission(request) or request.user.is_superuser + or ( + hasattr(self.facility, "state") + and request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] + and request.user.state == self.facility.state + ) + or ( + hasattr(self.facility, "district") + and request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] + and request.user.district == self.facility.district + ) or request.user in self.facility.users.all() ) diff --git a/care/facility/tests/test_asset_location_api.py b/care/facility/tests/test_asset_location_api.py index 5dbd1df4ed..c2f95b8940 100644 --- a/care/facility/tests/test_asset_location_api.py +++ b/care/facility/tests/test_asset_location_api.py @@ -112,3 +112,28 @@ def test_delete_asset_location_with_no_assets_and_beds(self): f"/api/v1/facility/{self.facility.external_id}/asset_location/{self.asset_location.external_id}/", ) self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_user_access_to_asset_location_on_user_type(self): + # when a user is a state_lab_admin or a district_lab_admin + state_lab_admin = self.create_user( + "state_lab_admin", self.district, user_type=35 + ) + district_lab_admin = self.create_user( + "district_lab_admin", self.district, user_type=25 + ) + + self.client.force_authenticate(user=state_lab_admin) + + # when they try to access a asset_location in their state or district then they + # should be able to do so without permission issue + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/{self.asset_location_with_linked_bed.external_id}/" + ) + + self.assertIs(response.status_code, status.HTTP_200_OK) + + self.client.force_authenticate(user=district_lab_admin) + response = self.client.get( + f"/api/v1/facility/{self.facility.external_id}/asset_location/{self.asset_location_with_linked_bed.external_id}/" + ) + self.assertIs(response.status_code, status.HTTP_200_OK) diff --git a/care/facility/tests/test_facilityuser_api.py b/care/facility/tests/test_facilityuser_api.py index 8b7f0d8c2d..9ade78e875 100644 --- a/care/facility/tests/test_facilityuser_api.py +++ b/care/facility/tests/test_facilityuser_api.py @@ -45,3 +45,23 @@ def test_link_existing_facility(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(len(response.data["results"]), 2) + + def test_user_access_to_facility_on_user_type(self): + # when a user is a state_lab_admin or a district_lab_admin + state_lab_admin = self.create_user( + "state_lab_admin", self.district, user_type=35 + ) + district_lab_admin = self.create_user( + "district_lab_admin", self.district, user_type=25 + ) + + self.client.force_authenticate(user=state_lab_admin) + + # when they try to access a facility in their state or district then they + # should be able to do so without permission issue + response = self.client.get(f"/api/v1/facility/{self.facility.external_id}/") + self.assertIs(response.status_code, status.HTTP_200_OK) + + self.client.force_authenticate(user=district_lab_admin) + response = self.client.get(f"/api/v1/facility/{self.facility.external_id}/") + self.assertIs(response.status_code, status.HTTP_200_OK) From 55a75811ba05b11338bb3621b2366b87a02144ed Mon Sep 17 00:00:00 2001 From: Onkar Jadhav <56870381+Omkar76@users.noreply.github.com> Date: Sun, 24 Mar 2024 22:01:46 +0530 Subject: [PATCH 28/32] track previous consultation (#1907) * update facility.json with more consultations for same patient * track previous consulation * use Aakashs's suggestion - sort queryset by created_date descending Co-authored-by: Aakash Singh * Lint --------- Co-authored-by: Aakash Singh --- .../api/serializers/patient_consultation.py | 1 + ...tientconsultation_previous_consultation.py | 52 ++ care/facility/models/patient_consultation.py | 6 + data/dummy/facility.json | 568 ++++++++++++++++++ 4 files changed, 627 insertions(+) create mode 100644 care/facility/migrations/0415_patientconsultation_previous_consultation.py diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index f67b1cc49e..bfb18b1669 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -378,6 +378,7 @@ def create(self, validated_data): consultation.created_by = self.context["request"].user consultation.last_edited_by = self.context["request"].user patient = consultation.patient + consultation.previous_consultation = patient.last_consultation last_consultation = patient.last_consultation if ( last_consultation diff --git a/care/facility/migrations/0415_patientconsultation_previous_consultation.py b/care/facility/migrations/0415_patientconsultation_previous_consultation.py new file mode 100644 index 0000000000..4d9d6f7dbd --- /dev/null +++ b/care/facility/migrations/0415_patientconsultation_previous_consultation.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.10 on 2024-02-18 06:37 +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0414_remove_bed_old_name"), + ] + + def fill_previous_consultation(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + bulk_update_list = [] + qs = PatientConsultation.objects.all().order_by("-created_date") + + def get_previous_consultation(consultation): + previous_consultation = qs.filter( + patient=consultation.patient, + created_date__lt=consultation.created_date, + ).latest("created_date") + + return previous_consultation + + for consultation in qs: + try: + consultation.previous_consultation = get_previous_consultation( + consultation + ) + bulk_update_list.append(consultation) + except PatientConsultation.DoesNotExist: + pass + + PatientConsultation.objects.bulk_update( + bulk_update_list, ["previous_consultation"], batch_size=1000 + ) + + operations = [ + migrations.AddField( + model_name="patientconsultation", + name="previous_consultation", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.PROTECT, + to="facility.patientconsultation", + ), + ), + migrations.RunPython( + fill_previous_consultation, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 91e18dd351..a6230d18f6 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -126,6 +126,12 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): default="", null=True, blank=True ) referred_by_external = models.TextField(default="", null=True, blank=True) + previous_consultation = models.ForeignKey( + "self", + null=True, + blank=True, + on_delete=models.PROTECT, + ) is_readmission = models.BooleanField(default=False) admitted = models.BooleanField(default=False) # Deprecated encounter_date = models.DateTimeField(default=timezone.now, db_index=True) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 16e4c2fab2..cf4a399781 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -3197,6 +3197,574 @@ "discharge_advice": {} } }, + { + "model": "facility.patientconsultation", + "pk": 19, + "fields": { + "external_id": "40faecd6-6199-48cd-bc2a-dd9e73b920f9", + "created_date": "2023-12-07T08:47:53.746Z", + "modified_date": "2023-12-07T08:47:53.751Z", + "deleted": false, + "patient": 18, + "patient_no": "IP008", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2023-12-07T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "pk": 20, + "fields": { + "external_id": "41faecc6-6199-48cd-bc2a-dd9e73b920f9", + "created_date": "2023-12-15T08:47:53.746Z", + "modified_date": "2023-12-15T08:47:53.751Z", + "deleted": false, + "patient": 18, + "patient_no": "IP009", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2023-12-15T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "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", + "deleted": false, + "patient": 18, + "patient_no": "IP0010", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2024-1-30T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "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", + "deleted": false, + "patient": 18, + "patient_no": "IP011", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2024-2-28T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "pk": 23, + "fields": { + "external_id": "40faecc6-61a9-48cd-bc2a-dd9e73b920f9", + "created_date": "2024-04-01T08:47:53.746Z", + "modified_date": "2024-04-01T08:47:53.746Z", + "deleted": false, + "patient": 18, + "patient_no": "IP012", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2024-04-01T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "pk": 24, + "fields": { + "external_id": "40faecb6-6199-48cd-bc2a-dd9e73b920f9", + "created_date": "2022-05-06T08:47:53.746Z", + "modified_date": "2022-05-06T08:47:53.751Z", + "deleted": false, + "patient": 18, + "patient_no": "IP013", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2022-05-06T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "pk": 25, + "fields": { + "external_id": "400aecc6-6199-48cd-bc2a-dd9e73b920f9", + "created_date": "2022-02-06T08:47:53.746Z", + "modified_date": "2022-02-06T08:47:53.746Z", + "deleted": false, + "patient": 18, + "patient_no": "IP014", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2022-02-06T08:47:53.746Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, + { + "model": "facility.patientconsultation", + "pk": 26, + "fields": { + "external_id": "40faecc6-6599-48cd-bc2a-dd9e73b920f9", + "created_date": "2023-08-06T08:47:53.746Z", + "modified_date": "2023-08-06T08:47:53.751Z", + "deleted": false, + "patient": 18, + "patient_no": "IP015", + "facility": 1, + "deprecated_diagnosis": "", + "deprecated_icd11_provisional_diagnoses": "[]", + "deprecated_icd11_diagnoses": "[]", + "deprecated_icd11_principal_diagnosis": "", + "symptoms": "1", + "other_symptoms": "", + "symptoms_onset_date": null, + "deprecated_covid_category": null, + "category": "Stable", + "examination_details": "Examination details and Clinical conditions", + "history_of_present_illness": "history", + "treatment_plan": "", + "consultation_notes": "generalnote", + "course_in_facility": null, + "investigation": [], + "prescriptions": {}, + "procedure": [], + "suggestion": "A", + "route_to_facility": 10, + "review_interval": -1, + "referred_to": null, + "referred_to_external": "", + "transferred_from_location": null, + "referred_from_facility": null, + "referred_from_facility_external": "", + "referred_by_external": "", + "is_readmission": false, + "admitted": true, + "encounter_date": "2023-12-06T08:47:34.395Z", + "icu_admission_date": null, + "discharge_date": null, + "discharge_reason": null, + "new_discharge_reason": null, + "discharge_notes": "", + "discharge_prescription": {}, + "discharge_prn_prescription": {}, + "death_datetime": null, + "death_confirmed_doctor": "", + "bed_number": null, + "is_kasp": false, + "kasp_enabled_date": null, + "is_telemedicine": false, + "last_updated_by_telemedicine": false, + "assigned_to": null, + "medico_legal_case": false, + "deprecated_verified_by": "", + "treating_physician": 21, + "created_by": 2, + "last_edited_by": 2, + "last_daily_round": null, + "current_bed": null, + "height": 70.0, + "weight": 170.0, + "operation": null, + "special_instruction": "", + "intubation_history": [], + "prn_prescription": {}, + "discharge_advice": {} + } + }, { "model": "facility.bed", "pk": 1, From e73b454f133b1b4ad4e7a783aa59c28fdb6e70a6 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 25 Mar 2024 14:17:52 +0530 Subject: [PATCH 29/32] Adds missing merge migrations (#2011) --- care/facility/migrations/0422_merge_20240325_1411.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 care/facility/migrations/0422_merge_20240325_1411.py diff --git a/care/facility/migrations/0422_merge_20240325_1411.py b/care/facility/migrations/0422_merge_20240325_1411.py new file mode 100644 index 0000000000..e581f0d598 --- /dev/null +++ b/care/facility/migrations/0422_merge_20240325_1411.py @@ -0,0 +1,12 @@ +# Generated by Django 4.2.8 on 2024-03-25 08:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0415_patientconsultation_previous_consultation"), + ("facility", "0421_merge_20240318_1434"), + ] + + operations = [] From ce7073e4d789dd19272d257cc79c2500d3d53e75 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 25 Mar 2024 19:07:04 +0530 Subject: [PATCH 30/32] Fixes discharged patients CSV export (#2010) * Fixes discharged patients CSV export * default `is_active` to False --- care/facility/api/viewsets/patient.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 0ce204bdf9..704dc98bbb 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -410,7 +410,8 @@ def filter_queryset(self, queryset: QuerySet) -> QuerySet: if self.action == "list" and settings.CSV_REQUEST_PARAMETER in self.request.GET: for backend in (PatientDRYFilter, filters.DjangoFilterBackend): queryset = backend().filter_queryset(self.request, queryset, self) - return queryset.filter(last_consultation__discharge_date__isnull=True) + is_active = self.request.GET.get("is_active", "False") == "True" + return queryset.filter(last_consultation__discharge_date__isnull=is_active) return super().filter_queryset(queryset) From 12c645d7c65d5c74f23c7b00b418e1464798ef81 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 25 Mar 2024 21:14:41 +0530 Subject: [PATCH 31/32] Adds support for validating integrity of fixtures in tests workflow (#2013) --- .github/workflows/test-base.yml | 7 +++++-- data/dummy/facility.json | 4 ++-- data/dummy/users.json | 24 ++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-base.yml b/.github/workflows/test-base.yml index 9246ad80e9..e295ac88c9 100644 --- a/.github/workflows/test-base.yml +++ b/.github/workflows/test-base.yml @@ -25,8 +25,8 @@ jobs: with: load: true set: | - *.cache-from=type=local,src=/tmp/.buildx-cache - *.cache-to=type=local,dest=/tmp/.buildx-cache-new + *.cache-from=type=local,src=/tmp/.buildx-cache + *.cache-to=type=local,dest=/tmp/.buildx-cache-new files: docker-compose.yaml,docker-compose.local.yaml - name: Start services @@ -35,6 +35,9 @@ jobs: - name: Check migrations run: make checkmigration + - name: Validate integrity of fixtures + run: make load-dummy-data + - name: Run tests run: make test-coverage diff --git a/data/dummy/facility.json b/data/dummy/facility.json index cf4a399781..0960cccb12 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -3206,7 +3206,7 @@ "modified_date": "2023-12-07T08:47:53.751Z", "deleted": false, "patient": 18, - "patient_no": "IP008", + "patient_no": "IP02578", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", @@ -3277,7 +3277,7 @@ "modified_date": "2023-12-15T08:47:53.751Z", "deleted": false, "patient": 18, - "patient_no": "IP009", + "patient_no": "IP1009", "facility": 1, "deprecated_diagnosis": "", "deprecated_icd11_provisional_diagnoses": "[]", diff --git a/data/dummy/users.json b/data/dummy/users.json index e952cb5a44..5f20d2f3b6 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -22,6 +22,7 @@ "phone_number": "+919696969696", "alt_phone_number": null, "gender": 3, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": false, @@ -57,6 +58,7 @@ "phone_number": "+911234567891", "alt_phone_number": "+911234567891", "gender": 3, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -92,6 +94,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -127,6 +130,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -162,6 +166,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -197,6 +202,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -232,6 +238,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -267,6 +274,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -302,6 +310,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -337,6 +346,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -372,6 +382,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -407,6 +418,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -442,6 +454,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -477,6 +490,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -512,6 +526,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -547,6 +562,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -582,6 +598,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -617,6 +634,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -652,6 +670,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -687,6 +706,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": null, "verified": true, @@ -722,6 +742,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, @@ -757,6 +778,7 @@ "phone_number": "+919876543219", "alt_phone_number": "+919876543219", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, @@ -792,6 +814,7 @@ "phone_number": "+918878825662", "alt_phone_number": "+918878825662", "gender": 2, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, @@ -827,6 +850,7 @@ "phone_number": "+918744587566", "alt_phone_number": "+918744587566", "gender": 1, + "video_connect_link": null, "date_of_birth": "2005-01-01", "home_facility": 1, "verified": true, From fa46f2f50bbc2f03053688cca6b2365f459c5860 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Thu, 28 Mar 2024 19:46:25 +0530 Subject: [PATCH 32/32] Added consent records to patient consultation (#2006) * added consent records to patient consultation * fix migrations * fix warning * updated perms * fix permission-error * merged migrations * updates * added migration * lint * tests and migrations * refactor associating id logic * remove archive logic --------- Co-authored-by: Rithvik Nishad Co-authored-by: Aakash Singh --- care/facility/api/serializers/file_upload.py | 31 +++++++--- ...ntconsultation_consent_records_and_more.py | 58 ++++++++++++++++++ care/facility/models/file_upload.py | 1 + .../models/json_schema/consultation.py | 18 ++++++ care/facility/models/patient_consultation.py | 17 ++---- care/facility/tests/test_file_upload.py | 61 +++++++++++++++++++ care/users/models.py | 14 ++++- 7 files changed, 181 insertions(+), 19 deletions(-) create mode 100644 care/facility/migrations/0423_patientconsultation_consent_records_and_more.py diff --git a/care/facility/api/serializers/file_upload.py b/care/facility/api/serializers/file_upload.py index 962688da7a..e991cf045a 100644 --- a/care/facility/api/serializers/file_upload.py +++ b/care/facility/api/serializers/file_upload.py @@ -34,13 +34,12 @@ def check_permissions(file_type, associating_id, user, action="create"): return patient.id elif file_type == FileUpload.FileType.CONSULTATION.value: consultation = PatientConsultation.objects.get(external_id=associating_id) - if consultation.discharge_date: - if not action == "read": - raise serializers.ValidationError( - { - "consultation": "Cannot upload file for a discharged consultation." - } - ) + if consultation.discharge_date and not action == "read": + raise serializers.ValidationError( + { + "consultation": "Cannot upload file for a discharged consultation." + } + ) if consultation.patient.assigned_to: if user == consultation.patient.assigned_to: return consultation.id @@ -53,6 +52,24 @@ def check_permissions(file_type, associating_id, user, action="create"): ): raise Exception("No Permission") return consultation.id + elif file_type == FileUpload.FileType.CONSENT_RECORD.value: + consultation = PatientConsultation.objects.get( + consent_records__contains=[{"id": associating_id}] + ) + if consultation.discharge_date and not action == "read": + raise serializers.ValidationError( + { + "consultation": "Cannot upload file for a discharged consultation." + } + ) + if ( + user == consultation.assigned_to + or user == consultation.patient.assigned_to + or has_facility_permission(user, consultation.facility) + or has_facility_permission(user, consultation.patient.facility) + ): + return associating_id + raise Exception("No Permission") elif file_type == FileUpload.FileType.DISCHARGE_SUMMARY.value: consultation = PatientConsultation.objects.get(external_id=associating_id) if ( diff --git a/care/facility/migrations/0423_patientconsultation_consent_records_and_more.py b/care/facility/migrations/0423_patientconsultation_consent_records_and_more.py new file mode 100644 index 0000000000..5ace7eac4f --- /dev/null +++ b/care/facility/migrations/0423_patientconsultation_consent_records_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.10 on 2024-03-27 17:32 + +from django.db import migrations, models + +import care.utils.models.validators + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0422_merge_20240325_1411"), + ] + + operations = [ + migrations.AddField( + model_name="patientconsultation", + name="consent_records", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "deleted": {"type": "boolean"}, + "id": {"type": "string"}, + "patient_code_status": {"type": "number"}, + "type": {"type": "number"}, + }, + "required": ["id", "type"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="fileupload", + name="file_type", + field=models.IntegerField( + choices=[ + (1, "PATIENT"), + (2, "CONSULTATION"), + (3, "SAMPLE_MANAGEMENT"), + (4, "CLAIM"), + (5, "DISCHARGE_SUMMARY"), + (6, "COMMUNICATION"), + (7, "CONSENT_RECORD"), + ], + default=1, + ), + ), + ] diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 4aec7366ca..9f5e1e04af 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -28,6 +28,7 @@ class FileType(enum.Enum): CLAIM = 4 DISCHARGE_SUMMARY = 5 COMMUNICATION = 6 + CONSENT_RECORD = 7 class FileCategory(enum.Enum): UNSPECIFIED = "UNSPECIFIED" diff --git a/care/facility/models/json_schema/consultation.py b/care/facility/models/json_schema/consultation.py index 91e68402c2..5446cf4632 100644 --- a/care/facility/models/json_schema/consultation.py +++ b/care/facility/models/json_schema/consultation.py @@ -17,3 +17,21 @@ } ], } + +CONSENT_RECORDS = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "array", + "items": [ + { + "type": "object", + "properties": { + "id": {"type": "string"}, + "type": {"type": "number"}, + "patient_code_status": {"type": "number"}, + "deleted": {"type": "boolean"}, + }, + "additionalProperties": False, + "required": ["id", "type"], + } + ], +} diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index a6230d18f6..8f6797f997 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -11,6 +11,7 @@ COVID_CATEGORY_CHOICES, PatientBaseModel, ) +from care.facility.models.json_schema.consultation import CONSENT_RECORDS from care.facility.models.mixins.permissions.patient import ( ConsultationRelatedPermissionMixin, ) @@ -25,6 +26,7 @@ reverse_choices, ) from care.users.models import User +from care.utils.models.validators import JSONFieldSchemaValidator class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): @@ -244,6 +246,10 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): prn_prescription = JSONField(default=dict) discharge_advice = JSONField(default=dict) + consent_records = JSONField( + default=list, validators=[JSONFieldSchemaValidator(CONSENT_RECORDS)] + ) + def get_related_consultation(self): return self @@ -280,17 +286,6 @@ def get_related_consultation(self): def __str__(self): return f"{self.patient.name}<>{self.facility.name}" - def save(self, *args, **kwargs): - """ - # Removing Patient Hospital Change on Referral - if not self.pk or self.referred_to is not None: - # pk is None when the consultation is created - # referred to is not null when the person is being referred to a new facility - self.patient.facility = self.referred_to or self.facility - self.patient.save() - """ - super(PatientConsultation, self).save(*args, **kwargs) - class Meta: constraints = [ models.CheckConstraint( diff --git a/care/facility/tests/test_file_upload.py b/care/facility/tests/test_file_upload.py index de521ef8b4..45a65e1f79 100644 --- a/care/facility/tests/test_file_upload.py +++ b/care/facility/tests/test_file_upload.py @@ -1,6 +1,9 @@ +import json + from rest_framework import status from rest_framework.test import APITestCase +from care.facility.models.file_upload import FileUpload from care.utils.tests.test_utils import TestUtils @@ -46,3 +49,61 @@ def test_file_upload_whitelist(self): }, ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class ConsentFileUploadApiTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls) -> None: + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user("nurse", cls.district, home_facility=cls.facility) + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.consultation = cls.create_consultation(cls.patient, cls.facility) + + def test_consent_file_upload(self): + response = self.client.patch( + f"/api/v1/consultation/{self.consultation.external_id}/", + { + "consent_records": json.dumps( + [ + { + "id": "consent-12345", + "type": 2, + "patient_code_status": 1, + } + ] + ) + }, + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + upload_response = self.client.post( + "/api/v1/files/", + { + "original_name": "test.pdf", + "file_type": "CONSENT_RECORD", + "name": "Test File", + "associating_id": "consent-12345", + "file_category": "UNSPECIFIED", + "mime_type": "application/pdf", + }, + ) + + self.assertEqual(upload_response.status_code, status.HTTP_201_CREATED) + + self.assertEqual( + FileUpload.objects.filter(associating_id="consent-12345").count(), 1 + ) + + all_files = self.client.get( + "/api/v1/files/?associating_id=consent-12345&file_type=CONSENT_RECORD&is_archived=false" + ) + + self.assertEqual(all_files.status_code, status.HTTP_200_OK) + self.assertEqual(all_files.data["count"], 1) + self.assertEqual(all_files.data["results"][0]["name"], "Test File") diff --git a/care/users/models.py b/care/users/models.py index c3099816b3..c06ae3e43c 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -1,5 +1,6 @@ import uuid +from django.conf import settings from django.contrib.auth.models import AbstractUser, UserManager from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models @@ -130,7 +131,18 @@ def get_entire_queryset(self): return super().get_queryset().select_related("local_body", "district", "state") def create_superuser(self, username, email, password, **extra_fields): - district = District.objects.all()[0] + district = District.objects.all().first() + data_command = ( + "load_data" if settings.IS_PRODUCTION is True else "load_dummy_data" + ) + if not district: + proceed = input( + f"It looks like you haven't loaded district data. It is recommended to populate district data before you create a super user. Please run `python manage.py {data_command}`.\n Proceed anyway? [y/N]" + ) + if proceed.lower() != "y": + raise Exception("Aborted Superuser Creation") + district = None + extra_fields["district"] = district extra_fields["phone_number"] = "+919696969696" extra_fields["gender"] = 3