From 2bb73576f4103af0ae55a3b37f1e9b7c22b6102f Mon Sep 17 00:00:00 2001 From: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:56:42 +0530 Subject: [PATCH 01/46] Add feature flags (#2429) * Added Facility Flag Implementation * added caching and user flags * Fix class name * update migrations * fixes * format * add tests * fix tests * Fix variable naming * abstract feature flag model * cleanup * fix migrations * fix failing tests * Add merge migration * Clean migrations * Fix linting * fix lint --------- Co-authored-by: Aakash Singh --- care/facility/admin.py | 18 +++++ care/facility/api/serializers/facility.py | 6 ++ ...yflag_facilityflag_unique_facility_flag.py | 62 +++++++++++++++++ care/facility/models/__init__.py | 1 + care/facility/models/facility.py | 4 ++ care/facility/models/facility_flag.py | 40 +++++++++++ care/facility/tests/test_facility_flags.py | 55 +++++++++++++++ care/users/admin.py | 27 +++++++- care/users/api/serializers/user.py | 6 ++ care/users/migrations/0017_userflag.py | 62 +++++++++++++++++ care/users/models.py | 40 ++++++++++- care/users/tests/test_api.py | 1 + care/users/tests/test_user_flags.py | 44 ++++++++++++ care/utils/models/base.py | 63 +++++++++++++++++ care/utils/registries/__init__.py | 0 care/utils/registries/feature_flag.py | 68 +++++++++++++++++++ care/utils/tests/test_feature_flags.py | 68 +++++++++++++++++++ config/urls.py | 2 +- 18 files changed, 563 insertions(+), 4 deletions(-) create mode 100644 care/facility/migrations/0458_facilityflag_facilityflag_unique_facility_flag.py create mode 100644 care/facility/models/facility_flag.py create mode 100644 care/facility/tests/test_facility_flags.py create mode 100644 care/users/migrations/0017_userflag.py create mode 100644 care/users/tests/test_user_flags.py create mode 100644 care/utils/registries/__init__.py create mode 100644 care/utils/registries/feature_flag.py create mode 100644 care/utils/tests/test_feature_flags.py diff --git a/care/facility/admin.py b/care/facility/admin.py index e48c3a6c00..7e06f1442b 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -1,3 +1,4 @@ +from django import forms from django.contrib import admin from django.contrib.admin import SimpleListFilter from djangoql.admin import DjangoQLSearchMixin @@ -12,12 +13,14 @@ PatientConsultation, ) from care.facility.models.patient_sample import PatientSample +from care.utils.registries.feature_flag import FlagRegistry, FlagType from .models import ( Building, Disease, Facility, FacilityCapacity, + FacilityFlag, FacilityInventoryItem, FacilityInventoryItemTag, FacilityInventoryUnit, @@ -188,6 +191,19 @@ class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin): actions = ["export_as_csv"] +class FacilityFlagAdmin(admin.ModelAdmin): + class FacilityFeatureFlagForm(forms.ModelForm): + flag = forms.ChoiceField( + choices=lambda: FlagRegistry.get_all_flags_as_choices(FlagType.FACILITY) + ) + + class Meta: + fields = "__all__" + model = FacilityFlag + + form = FacilityFeatureFlagForm + + admin.site.register(Facility, FacilityAdmin) admin.site.register(FacilityStaff, FacilityStaffAdmin) admin.site.register(FacilityCapacity, FacilityCapacityAdmin) @@ -217,3 +233,5 @@ class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin): admin.site.register(PatientConsent) admin.site.register(FileUpload) admin.site.register(PatientConsultation) + +admin.site.register(FacilityFlag, FacilityFlagAdmin) diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index dba13cd5b1..99c00bbe69 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -105,6 +105,11 @@ class FacilitySerializer(FacilityBasicInfoSerializer): ) bed_count = serializers.SerializerMethodField() + facility_flags = serializers.SerializerMethodField() + + def get_facility_flags(self, facility): + return facility.get_facility_flags() + class Meta: model = Facility fields = [ @@ -140,6 +145,7 @@ class Meta: "read_cover_image_url", "patient_count", "bed_count", + "facility_flags", ] read_only_fields = ("modified_date", "created_date") diff --git a/care/facility/migrations/0458_facilityflag_facilityflag_unique_facility_flag.py b/care/facility/migrations/0458_facilityflag_facilityflag_unique_facility_flag.py new file mode 100644 index 0000000000..d47a1f7c14 --- /dev/null +++ b/care/facility/migrations/0458_facilityflag_facilityflag_unique_facility_flag.py @@ -0,0 +1,62 @@ +# Generated by Django 4.2.10 on 2024-09-19 12:58 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("facility", "0457_patientmetainfo_domestic_healthcare_support_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="FacilityFlag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("flag", models.CharField(max_length=1024)), + ( + "facility", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="facility.facility", + ), + ), + ], + options={ + "verbose_name": "Facility Flag", + }, + ), + migrations.AddConstraint( + model_name="facilityflag", + constraint=models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("facility", "flag"), + name="unique_facility_flag", + ), + ), + ] diff --git a/care/facility/models/__init__.py b/care/facility/models/__init__.py index 8993152ef2..df41476768 100644 --- a/care/facility/models/__init__.py +++ b/care/facility/models/__init__.py @@ -8,6 +8,7 @@ from .encounter_symptom import * # noqa from .events import * # noqa from .facility import * # noqa +from .facility_flag import * # noqa from .icd11_diagnosis import * # noqa from .inventory import * # noqa from .patient import * # noqa diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index 8b231d5a50..ec2f79a35c 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -6,6 +6,7 @@ from simple_history.models import HistoricalRecords from care.facility.models import FacilityBaseModel, reverse_choices +from care.facility.models.facility_flag import FacilityFlag from care.facility.models.mixins.permissions.facility import ( FacilityPermissionMixin, FacilityRelatedPermissionMixin, @@ -274,6 +275,9 @@ def get_features_display(self): return [] return [FacilityFeature(f).label for f in self.features] + def get_facility_flags(self): + return FacilityFlag.get_all_flags(self.id) + CSV_MAPPING = { "name": "Facility Name", "facility_type": "Facility Type", diff --git a/care/facility/models/facility_flag.py b/care/facility/models/facility_flag.py new file mode 100644 index 0000000000..e5cf3b4441 --- /dev/null +++ b/care/facility/models/facility_flag.py @@ -0,0 +1,40 @@ +from django.db import models + +from care.utils.models.base import BaseFlag +from care.utils.registries.feature_flag import FlagName, FlagType + +FACILITY_FLAG_CACHE_KEY = "facility_flag_cache:{facility_id}:{flag_name}" +FACILITY_ALL_FLAGS_CACHE_KEY = "facility_all_flags_cache:{facility_id}" +FACILITY_FLAG_CACHE_TTL = 60 * 60 * 24 # 1 Day + + +class FacilityFlag(BaseFlag): + facility = models.ForeignKey( + "facility.Facility", on_delete=models.CASCADE, null=False, blank=False + ) + + cache_key_template = "facility_flag_cache:{entity_id}:{flag_name}" + all_flags_cache_key_template = "facility_all_flags_cache:{entity_id}" + flag_type = FlagType.FACILITY + entity_field_name = "facility" + + def __str__(self) -> str: + return f"Facility Flag: {self.facility.name} - {self.flag}" + + class Meta: + verbose_name = "Facility Flag" + constraints = [ + models.UniqueConstraint( + fields=["facility", "flag"], + condition=models.Q(deleted=False), + name="unique_facility_flag", + ) + ] + + @classmethod + def check_facility_has_flag(cls, facility_id: int, flag_name: FlagName) -> bool: + return cls.check_entity_has_flag(facility_id, flag_name) + + @classmethod + def get_all_flags(cls, facility_id: int) -> tuple[FlagName]: + return super().get_all_flags(facility_id) diff --git a/care/facility/tests/test_facility_flags.py b/care/facility/tests/test_facility_flags.py new file mode 100644 index 0000000000..5141459ec7 --- /dev/null +++ b/care/facility/tests/test_facility_flags.py @@ -0,0 +1,55 @@ +from django.db import IntegrityError +from rest_framework.test import APITestCase + +from care.facility.models.facility_flag import FacilityFlag +from care.utils.registries.feature_flag import FlagRegistry, FlagType +from care.utils.tests.test_utils import TestUtils + + +class FacilityFlagsTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + FlagRegistry.register(FlagType.FACILITY, "TEST_FLAG") + FlagRegistry.register(FlagType.FACILITY, "TEST_FLAG_2") + cls.district = cls.create_district(cls.create_state()) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + + def setUp(self) -> None: + self.facility = self.create_facility( + self.super_user, self.district, self.local_body + ) + + def test_facility_flags(self): + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG") + self.assertTrue( + FacilityFlag.check_facility_has_flag(self.facility.id, "TEST_FLAG") + ) + + def test_facility_flags_negative(self): + self.assertFalse( + FacilityFlag.check_facility_has_flag(self.facility.id, "TEST_FLAG") + ) + + def test_create_duplicate_flag(self): + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG") + with self.assertRaises(IntegrityError): + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG") + + def test_get_all_flags(self): + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG") + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG_2") + self.assertEqual( + FacilityFlag.get_all_flags(self.facility.id), ("TEST_FLAG", "TEST_FLAG_2") + ) + + def test_get_user_flags_api(self): + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG") + FacilityFlag.objects.create(facility=self.facility, flag="TEST_FLAG_2") + user = self.create_user("user", self.district, home_facility=self.facility) + self.client.force_authenticate(user=user) + response = self.client.get(f"/api/v1/facility/{self.facility.external_id}/") + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json()["facility_flags"], ["TEST_FLAG", "TEST_FLAG_2"] + ) diff --git a/care/users/admin.py b/care/users/admin.py index 32b64980dd..5aebd9f966 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -1,10 +1,20 @@ +from django import forms from django.contrib import admin from django.contrib.auth import admin as auth_admin from django.contrib.auth import get_user_model from djqscsv import render_to_csv_response from care.users.forms import UserChangeForm, UserCreationForm -from care.users.models import District, LocalBody, Skill, State, UserSkill, Ward +from care.users.models import ( + District, + LocalBody, + Skill, + State, + UserFlag, + UserSkill, + Ward, +) +from care.utils.registries.feature_flag import FlagRegistry, FlagType User = get_user_model() @@ -72,6 +82,19 @@ class WardAdmin(admin.ModelAdmin): autocomplete_fields = ["local_body"] -admin.site.register(Skill) +@admin.register(UserFlag) +class UserFlagAdmin(admin.ModelAdmin): + class UserFlagForm(forms.ModelForm): + flag = forms.ChoiceField( + choices=lambda: FlagRegistry.get_all_flags_as_choices(FlagType.USER) + ) + + class Meta: + fields = "__all__" + model = UserFlag + form = UserFlagForm + + +admin.site.register(Skill) admin.site.register(UserSkill) diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 9e267ac98e..8040ed71dc 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -284,6 +284,11 @@ class UserSerializer(SignUpSerializer): date_of_birth = serializers.DateField(required=True) + user_flags = serializers.SerializerMethodField() + + def get_user_flags(self, user) -> tuple[str]: + return user.get_all_flags() + class Meta: model = User fields = ( @@ -316,6 +321,7 @@ class Meta: "pf_endpoint", "pf_p256dh", "pf_auth", + "user_flags", ) read_only_fields = ( "is_superuser", diff --git a/care/users/migrations/0017_userflag.py b/care/users/migrations/0017_userflag.py new file mode 100644 index 0000000000..a862ab9d08 --- /dev/null +++ b/care/users/migrations/0017_userflag.py @@ -0,0 +1,62 @@ +# Generated by Django 5.1.1 on 2024-09-19 12:22 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0016_upgrade_user_skills"), + ] + + operations = [ + migrations.CreateModel( + name="UserFlag", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ("flag", models.CharField(max_length=1024)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "User Flag", + "constraints": [ + models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("user", "flag"), + name="unique_user_flag", + ) + ], + }, + ), + ] diff --git a/care/users/models.py b/care/users/models.py index 5ea7e17e6e..1ef1280f54 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -8,12 +8,17 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from care.utils.models.base import BaseModel +from care.utils.models.base import BaseFlag, BaseModel from care.utils.models.validators import ( UsernameValidator, mobile_or_landline_number_validator, mobile_validator, ) +from care.utils.registries.feature_flag import FlagName, FlagType + +USER_FLAG_CACHE_KEY = "user_flag_cache:{user_id}:{flag_name}" +USER_ALL_FLAGS_CACHE_KEY = "user_all_flags_cache:{user_id}" +USER_FLAG_CACHE_TTL = 60 * 60 * 24 # 1 Day def reverse_choices(choices): @@ -368,6 +373,9 @@ def delete(self, *args, **kwargs): def get_absolute_url(self): return reverse("users:detail", kwargs={"username": self.username}) + def get_all_flags(self): + return UserFlag.get_all_flags(self.id) + def save(self, *args, **kwargs) -> None: """ While saving, if the local body is not null, then district will be local body's district @@ -391,3 +399,33 @@ class UserFacilityAllocation(models.Model): ) start_date = models.DateTimeField(default=now) end_date = models.DateTimeField(null=True, blank=True) + + +class UserFlag(BaseFlag): + user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) + + cache_key_template = "user_flag_cache:{entity_id}:{flag_name}" + all_flags_cache_key_template = "user_all_flags_cache:{entity_id}" + flag_type = FlagType.USER + entity_field_name = "user" + + def __str__(self): + return f"User Flag: {self.user.get_full_name()} - {self.flag}" + + class Meta: + verbose_name = "User Flag" + constraints = [ + models.UniqueConstraint( + fields=["user", "flag"], + condition=models.Q(deleted=False), + name="unique_user_flag", + ) + ] + + @classmethod + def check_user_has_flag(cls, user_id: int, flag_name: FlagName) -> bool: + return cls.check_entity_has_flag(user_id, flag_name) + + @classmethod + def get_all_flags(cls, user_id: int) -> tuple[FlagName]: + return super().get_all_flags(user_id) diff --git a/care/users/tests/test_api.py b/care/users/tests/test_api.py index ef58f25e7c..28b87af4bf 100644 --- a/care/users/tests/test_api.py +++ b/care/users/tests/test_api.py @@ -46,6 +46,7 @@ def get_detail_representation(self, obj=None) -> dict: "doctor_qualification": obj.doctor_qualification, "weekly_working_hours": obj.weekly_working_hours, "video_connect_link": obj.video_connect_link, + "user_flags": [], **self.get_local_body_district_state_representation(obj), } diff --git a/care/users/tests/test_user_flags.py b/care/users/tests/test_user_flags.py new file mode 100644 index 0000000000..88f2450049 --- /dev/null +++ b/care/users/tests/test_user_flags.py @@ -0,0 +1,44 @@ +from django.db import IntegrityError +from rest_framework.test import APITestCase + +from care.users.models import UserFlag +from care.utils.registries.feature_flag import FlagRegistry, FlagType +from care.utils.tests.test_utils import TestUtils + + +class UserFlagsTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + FlagRegistry.register(FlagType.USER, "TEST_FLAG_2") + cls.district = cls.create_district(cls.create_state()) + + def setUp(self) -> None: + self.user = self.create_user("user", self.district) + super().setUp() + + def test_user_flags(self): + UserFlag.objects.create(user=self.user, flag="TEST_FLAG") + self.assertTrue(UserFlag.check_user_has_flag(self.user.id, "TEST_FLAG")) + + def test_user_flags_negative(self): + self.assertFalse(UserFlag.check_user_has_flag(self.user.id, "TEST_FLAG")) + + def test_create_duplicate_flag(self): + UserFlag.objects.create(user=self.user, flag="TEST_FLAG") + with self.assertRaises(IntegrityError): + UserFlag.objects.create(user=self.user, flag="TEST_FLAG") + + def test_get_all_flags(self): + UserFlag.objects.create(user=self.user, flag="TEST_FLAG") + UserFlag.objects.create(user=self.user, flag="TEST_FLAG_2") + self.assertEqual( + UserFlag.get_all_flags(self.user.id), ("TEST_FLAG", "TEST_FLAG_2") + ) + + def test_get_user_flags_api(self): + UserFlag.objects.create(user=self.user, flag="TEST_FLAG") + UserFlag.objects.create(user=self.user, flag="TEST_FLAG_2") + response = self.client.get("/api/v1/users/getcurrentuser/") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["user_flags"], ["TEST_FLAG", "TEST_FLAG_2"]) diff --git a/care/utils/models/base.py b/care/utils/models/base.py index 6e61afb567..f38fb5a501 100644 --- a/care/utils/models/base.py +++ b/care/utils/models/base.py @@ -1,7 +1,10 @@ from uuid import uuid4 +from django.core.cache import cache from django.db import models +from care.utils.registries.feature_flag import FlagName, FlagRegistry + class BaseManager(models.Manager): def get_queryset(self): @@ -33,3 +36,63 @@ class Meta: def delete(self, *args): self.deleted = True self.save(update_fields=["deleted"]) + + +FLAGS_CACHE_TTL = 60 * 60 * 24 # 1 Day + + +class BaseFlag(BaseModel): + flag = models.CharField(max_length=1024) + + cache_key_template = "" + all_flags_cache_key_template = "" + flag_type = None + entity_field_name = "" + + class Meta: + abstract = True + + @property + def entity(self): + return getattr(self, self.entity_field_name) + + @property + def entity_id(self): + return getattr(self, f"{self.entity_field_name}_id") + + @classmethod + def validate_flag(cls, flag_name: FlagName): + FlagRegistry.validate_flag_name(cls.flag_type, flag_name) + + def save(self, *args, **kwargs): + self.validate_flag(self.flag) + cache.delete( + self.cache_key_template.format( + entity_id=self.entity_id, flag_name=self.flag + ) + ) + cache.delete(self.all_flags_cache_key_template.format(entity_id=self.entity_id)) + return super().save(*args, **kwargs) + + @classmethod + def check_entity_has_flag(cls, entity_id: int, flag_name: FlagName) -> bool: + cls.validate_flag(flag_name) + return cache.get_or_set( + cls.cache_key_template.format(entity_id=entity_id, flag_name=flag_name), + default=lambda: cls.objects.filter( + **{f"{cls.entity_field_name}_id": entity_id, "flag": flag_name} + ).exists(), + timeout=FLAGS_CACHE_TTL, + ) + + @classmethod + def get_all_flags(cls, entity_id: int) -> tuple[FlagName]: + return cache.get_or_set( + cls.all_flags_cache_key_template.format(entity_id=entity_id), + default=lambda: tuple( + cls.objects.filter( + **{f"{cls.entity_field_name}_id": entity_id} + ).values_list("flag", flat=True) + ), + timeout=FLAGS_CACHE_TTL, + ) diff --git a/care/utils/registries/__init__.py b/care/utils/registries/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/registries/feature_flag.py b/care/utils/registries/feature_flag.py new file mode 100644 index 0000000000..dba42888e5 --- /dev/null +++ b/care/utils/registries/feature_flag.py @@ -0,0 +1,68 @@ +import enum +import logging +from typing import TypeAlias + +from django.core.exceptions import ValidationError + +logger = logging.getLogger(__name__) + + +class FlagNotFoundException(ValidationError): + pass + + +class FlagType(enum.Enum): + USER = "USER" + FACILITY = "FACILITY" + + +# TODO: convert to type in python 3.12 +FlagName = str +FlagTypeRegistry: TypeAlias = dict[FlagType, dict[FlagName, bool]] + + +class FlagRegistry: + _flags: FlagTypeRegistry = {} + + @classmethod + def register(cls, flag_type: FlagType, flag_name: FlagName) -> None: + if flag_type not in cls._flags: + cls._flags[flag_type] = {} + cls._flags[flag_type][flag_name] = True + + @classmethod + def unregister(cls, flag_type, flag_name) -> None: + try: + del cls._flags[flag_type][flag_name] + except KeyError as e: + logger.warning(f"Flag {flag_name} not found in {flag_type}: {e}") + + @classmethod + def register_wrapper(cls, flag_type, flag_name) -> None: + def inner_wrapper(wrapped_class): + cls.register(cls, flag_type, flag_name) + return wrapped_class + + return inner_wrapper + + @classmethod + def validate_flag_type(cls, flag_type: FlagType) -> None: + if flag_type not in cls._flags: + raise FlagNotFoundException("Invalid Flag Type") + + @classmethod + def validate_flag_name(cls, flag_type: FlagType, flag_name): + cls.validate_flag_type(flag_type) + if flag_name not in cls._flags[flag_type]: + raise FlagNotFoundException("Flag not registered") + + @classmethod + def get_all_flags(cls, flag_type: FlagType) -> list[FlagName]: + cls.validate_flag_type(flag_type) + return list(cls._flags[flag_type].keys()) + + @classmethod + def get_all_flags_as_choices( + cls, flag_type: FlagType + ) -> list[tuple[FlagName, FlagName]]: + return ((x, x) for x in cls._flags.get(flag_type, {}).keys()) diff --git a/care/utils/tests/test_feature_flags.py b/care/utils/tests/test_feature_flags.py new file mode 100644 index 0000000000..13324f20f6 --- /dev/null +++ b/care/utils/tests/test_feature_flags.py @@ -0,0 +1,68 @@ +from django.test import TestCase + +from care.utils.registries.feature_flag import ( + FlagNotFoundException, + FlagRegistry, + FlagType, +) + + +class FeatureFlagTestCase(TestCase): + def setUp(self): + FlagRegistry._flags = {} + super().setUp() + + def test_register_flag(self): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + self.assertTrue(FlagRegistry._flags[FlagType.USER]["TEST_FLAG"]) + FlagRegistry.register(FlagType.USER, "TEST_FLAG_2") + self.assertTrue(FlagRegistry._flags[FlagType.USER]["TEST_FLAG_2"]) + + def test_unregister_flag(self): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + self.assertTrue(FlagRegistry._flags[FlagType.USER]["TEST_FLAG"]) + FlagRegistry.unregister(FlagType.USER, "TEST_FLAG") + self.assertFalse(FlagRegistry._flags[FlagType.USER].get("TEST_FLAG")) + + def test_unregister_flag_not_found(self): + FlagRegistry.unregister(FlagType.USER, "TEST_FLAG") + self.assertEqual(FlagRegistry._flags, {}) + + def test_validate_flag_type(self): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + self.assertIsNone(FlagRegistry.validate_flag_type(FlagType.USER)) + + def test_validate_flag_type_invalid(self): + with self.assertRaises(FlagNotFoundException): + FlagRegistry.validate_flag_type( + FlagType.USER + ) # FlagType.USER is not registered + + def test_validate_flag_name(self): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + self.assertIsNone(FlagRegistry.validate_flag_name(FlagType.USER, "TEST_FLAG")) + + def test_validate_flag_name_invalid(self): + with self.assertRaises(FlagNotFoundException) as ectx: + FlagRegistry.validate_flag_name(FlagType.USER, "TEST_OTHER_FLAG") + self.assertEqual(ectx.exception.message, "Invalid Flag Type") + + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + with self.assertRaises(FlagNotFoundException) as ectx: + FlagRegistry.validate_flag_name(FlagType.USER, "TEST_OTHER_FLAG") + self.assertEqual(ectx.exception.message, "Flag not registered") + + def test_get_all_flags(self): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + FlagRegistry.register(FlagType.USER, "TEST_FLAG_2") + self.assertEqual( + FlagRegistry.get_all_flags(FlagType.USER), ["TEST_FLAG", "TEST_FLAG_2"] + ) + + def test_get_all_flags_as_choices(self): + FlagRegistry.register(FlagType.USER, "TEST_FLAG") + FlagRegistry.register(FlagType.USER, "TEST_FLAG_2") + self.assertEqual( + list(FlagRegistry.get_all_flags_as_choices(FlagType.USER)), + [("TEST_FLAG", "TEST_FLAG"), ("TEST_FLAG_2", "TEST_FLAG_2")], + ) diff --git a/config/urls.py b/config/urls.py index 4d112c686a..923c8413ce 100644 --- a/config/urls.py +++ b/config/urls.py @@ -40,7 +40,7 @@ path("ping/", ping, name="ping"), path("app_version/", app_version, name="app_version"), # Django Admin, use {% url 'admin:index' %} - path(settings.ADMIN_URL, admin.site.urls), + path(f"{settings.ADMIN_URL}/", admin.site.urls), # Rest API path("api/v1/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path( From d36b5d906c78aaaebf988b4b41cf562819d29530 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 19 Sep 2024 19:58:21 +0530 Subject: [PATCH 02/46] added timeout to middleware requests (#2434) * added timeout to middleware requests * updated the reference for timeout value in function --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/utils/assetintegration/base.py | 11 +++++++++++ config/settings/base.py | 6 ++++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index d43690b39d..3a430dcad0 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -16,6 +16,7 @@ def __init__(self, meta): self.host = self.meta["local_ip_address"] self.middleware_hostname = self.meta["middleware_hostname"] self.insecure_connection = self.meta.get("insecure_connection", False) + self.timeout = settings.MIDDLEWARE_REQUEST_TIMEOUT def handle_action(self, action): pass @@ -37,12 +38,17 @@ def api_post(self, url, data=None): url, json=data, headers=self.get_headers(), + timeout=self.timeout, ) try: response = req.json() if req.status_code >= 400: raise APIException(response, req.status_code) return response + + except requests.Timeout: + raise APIException({"error": "Request Timeout"}, 504) + except json.decoder.JSONDecodeError: raise APIException({"error": "Invalid Response"}, req.status_code) @@ -51,11 +57,16 @@ def api_get(self, url, data=None): url, params=data, headers=self.get_headers(), + timeout=self.timeout, ) try: if req.status_code >= 400: raise APIException(req.text, req.status_code) response = req.json() return response + + except requests.Timeout: + raise APIException({"error": "Request Timeout"}, 504) + except json.decoder.JSONDecodeError: raise APIException({"error": "Invalid Response"}, req.status_code) diff --git a/config/settings/base.py b/config/settings/base.py index e12c643dec..7c934f830b 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -6,10 +6,10 @@ import json from datetime import datetime, timedelta from pathlib import Path -from django.utils.translation import gettext_lazy as _ import environ from authlib.jose import JsonWebKey +from django.utils.translation import gettext_lazy as _ from healthy_django.healthcheck.celery_queue_length import ( DjangoCeleryQueueLengthHealthCheck, ) @@ -55,7 +55,6 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#locale-paths LOCALE_PATHS = [str(BASE_DIR / "locale")] - LANGUAGES = [ ("en-us", _("English")), ("ml", _("Malayalam")), @@ -663,3 +662,6 @@ TASK_SUMMARIZE_DISTRICT_PATIENT = env.bool( "TASK_SUMMARIZE_DISTRICT_PATIENT", default=True ) + +# Timeout for middleware request (in seconds) +MIDDLEWARE_REQUEST_TIMEOUT = env.int("MIDDLEWARE_REQUEST_TIMEOUT", 20) From 0683faa66c1b962045e2bc683969054db4fbcec4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 19 Sep 2024 20:45:26 +0530 Subject: [PATCH 03/46] update unique_bed_name_per_location to ignore deleted beds (#2432) update unique_bed_name_per_location to ignore deleted beds (#2432) --------- Co-authored-by: vigneshhari --- ...d_unique_bed_name_per_location_and_more.py | 27 +++++++++++++++++++ care/facility/models/bed.py | 1 + care/facility/tests/test_bed_create.py | 21 +++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 care/facility/migrations/0459_remove_bed_unique_bed_name_per_location_and_more.py diff --git a/care/facility/migrations/0459_remove_bed_unique_bed_name_per_location_and_more.py b/care/facility/migrations/0459_remove_bed_unique_bed_name_per_location_and_more.py new file mode 100644 index 0000000000..9ed655e03b --- /dev/null +++ b/care/facility/migrations/0459_remove_bed_unique_bed_name_per_location_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.10 on 2024-09-19 14:44 + +import django.db.models.functions.text +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("facility", "0458_facilityflag_facilityflag_unique_facility_flag"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="bed", + name="unique_bed_name_per_location", + ), + migrations.AddConstraint( + model_name="bed", + constraint=models.UniqueConstraint( + django.db.models.functions.text.Lower("name"), + models.F("location"), + condition=models.Q(("deleted", False)), + name="unique_bed_name_per_location", + ), + ), + ] diff --git a/care/facility/models/bed.py b/care/facility/models/bed.py index 1f6ae30777..a06db2729c 100644 --- a/care/facility/models/bed.py +++ b/care/facility/models/bed.py @@ -37,6 +37,7 @@ class Meta: models.functions.Lower("name"), "location", name="unique_bed_name_per_location", + condition=models.Q(deleted=False), ) ] diff --git a/care/facility/tests/test_bed_create.py b/care/facility/tests/test_bed_create.py index 26a18a69be..5f0e160c5c 100644 --- a/care/facility/tests/test_bed_create.py +++ b/care/facility/tests/test_bed_create.py @@ -63,6 +63,27 @@ def test_create_with_same_name(self): self.assertEqual(Bed.objects.filter(facility=self.facility).count(), 1) + def test_create_with_name_previously_deleted(self): + sample_data = { + "bed_type": "REGULAR", + "description": "Testing creation of beds.", + "facility": self.facility.external_id, + "location": self.asset_location.external_id, + "name": "Test Bed", + "number_of_beds": 1, + } + response = self.client.post("/api/v1/bed/", sample_data, format="json") + self.assertIs(response.status_code, status.HTTP_201_CREATED) + + bed = Bed.objects.get(name="Test Bed") + bed.deleted = True + bed.save() + + response = self.client.post("/api/v1/bed/", sample_data, format="json") + self.assertIs(response.status_code, status.HTTP_201_CREATED) + + self.assertEqual(Bed.objects.filter(facility=self.facility).count(), 1) + class MultipleBedTest(TestUtils, APITestCase): @classmethod From 28e2ac0b6ae478a8f87e6793a876a65014770744 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 19 Sep 2024 20:47:12 +0530 Subject: [PATCH 04/46] fix string representation of prescription and mar (#2412) fix string representation of prescription and mar (#2412) --- care/facility/models/prescription.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index 05ecee76fc..1ddffd563b 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -175,7 +175,7 @@ def has_object_write_permission(self, request): return ConsultationRelatedPermissionMixin.has_write_permission(request) def __str__(self): - return self.medicine + " - " + self.consultation.patient.name + return f"Prescription({self.id}) {self.consultation_id} - {self.medicine_id}" class MedicineAdministration(BaseModel, ConsultationRelatedPermissionMixin): @@ -204,11 +204,7 @@ class MedicineAdministration(BaseModel, ConsultationRelatedPermissionMixin): ) def __str__(self): - return ( - self.prescription.medicine - + " - " - + self.prescription.consultation.patient.name - ) + return f"MedicineAdministration({self.id}) {self.prescription_id} - {self.administered_date}" def get_related_consultation(self): return self.prescription.consultation From d5a2278561a5d7fea3b56bacb36b57228ee6fe8e Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:47:53 +0530 Subject: [PATCH 05/46] Convert enum choices to integer choices in Daily Rounds (#2462) Convert enum choices to integer choices in Daily Rounds (#2462) --- care/facility/api/serializers/daily_round.py | 26 +-- care/facility/api/viewsets/daily_round.py | 4 +- care/facility/api/viewsets/patient.py | 5 +- care/facility/models/daily_round.py | 188 ++++++++----------- 4 files changed, 99 insertions(+), 124 deletions(-) diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 4f2f61c32b..0a506143d9 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -39,7 +39,7 @@ class DailyRoundSerializer(serializers.ModelSerializer): taken_at = serializers.DateTimeField(required=True) - rounds_type = ChoiceField(choices=DailyRound.RoundsTypeChoice, required=True) + rounds_type = ChoiceField(choices=DailyRound.RoundsType.choices, required=True) # Community Nurse's Log @@ -65,38 +65,38 @@ class DailyRoundSerializer(serializers.ModelSerializer): # Critical Care Components consciousness_level = ChoiceField( - choices=DailyRound.ConsciousnessChoice, required=False + choices=DailyRound.ConsciousnessTypeChoice.choices, required=False ) left_pupil_light_reaction = ChoiceField( - choices=DailyRound.PupilReactionChoice, required=False + choices=DailyRound.PupilReactionType.choices, required=False ) right_pupil_light_reaction = ChoiceField( - choices=DailyRound.PupilReactionChoice, required=False + choices=DailyRound.PupilReactionType.choices, required=False ) limb_response_upper_extremity_right = ChoiceField( - choices=DailyRound.LimbResponseChoice, required=False + choices=DailyRound.LimbResponseType.choices, required=False ) limb_response_upper_extremity_left = ChoiceField( - choices=DailyRound.LimbResponseChoice, required=False + choices=DailyRound.LimbResponseType.choices, required=False ) limb_response_lower_extremity_left = ChoiceField( - choices=DailyRound.LimbResponseChoice, required=False + choices=DailyRound.LimbResponseType.choices, required=False ) limb_response_lower_extremity_right = ChoiceField( - choices=DailyRound.LimbResponseChoice, required=False + choices=DailyRound.LimbResponseType.choices, required=False ) - rhythm = ChoiceField(choices=DailyRound.RythmnChoice, required=False) + rhythm = ChoiceField(choices=DailyRound.RythmnType.choices, required=False) ventilator_interface = ChoiceField( - choices=DailyRound.VentilatorInterfaceChoice, required=False + choices=DailyRound.VentilatorInterfaceType.choices, required=False ) ventilator_mode = ChoiceField( - choices=DailyRound.VentilatorModeChoice, required=False + choices=DailyRound.VentilatorModeType.choices, required=False ) ventilator_oxygen_modality = ChoiceField( - choices=DailyRound.VentilatorOxygenModalityChoice, required=False + choices=DailyRound.VentilatorOxygenModalityType.choices, required=False ) insulin_intake_frequency = ChoiceField( - choices=DailyRound.InsulinIntakeFrequencyChoice, required=False + choices=DailyRound.InsulinIntakeFrequencyType.choices, required=False ) last_edited_by = UserBaseMinimumSerializer(read_only=True) diff --git a/care/facility/api/viewsets/daily_round.py b/care/facility/api/viewsets/daily_round.py index 96257fd410..1423b7e50e 100644 --- a/care/facility/api/viewsets/daily_round.py +++ b/care/facility/api/viewsets/daily_round.py @@ -23,10 +23,10 @@ class DailyRoundFilterSet(filters.FilterSet): def filter_rounds_type(self, queryset, name, value): rounds_type = set() - values = value.split(",") + values = value.upper().split(",") for v in values: try: - rounds_type.add(DailyRound.RoundsTypeDict[v]) + rounds_type.add(DailyRound.RoundsType[v].value) except KeyError: pass return queryset.filter(rounds_type__in=list(rounds_type)) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index f216aa6362..7287cd52e4 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -94,7 +94,6 @@ REVERSE_FACILITY_TYPES = covert_choice_dict(FACILITY_TYPES) REVERSE_BED_TYPES = covert_choice_dict(BedTypeChoices) DISCHARGE_REASONS = [choice[0] for choice in DISCHARGE_REASON_CHOICES] -VENTILATOR_CHOICES = covert_choice_dict(DailyRound.VentilatorInterfaceChoice) class PatientFilterSet(filters.FilterSet): @@ -220,7 +219,9 @@ def filter_by_bed_type(self, queryset, name, value): ) ventilator_interface = CareChoiceFilter( field_name="last_consultation__last_daily_round__ventilator_interface", - choice_dict=VENTILATOR_CHOICES, + choice_dict={ + label: value for value, label in DailyRound.VentilatorInterfaceType.choices + }, ) # Vaccination Filters diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 58a288ed91..62caaf0058 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -1,5 +1,3 @@ -import enum - from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import JSONField @@ -10,7 +8,6 @@ COVID_CATEGORY_CHOICES, PatientBaseModel, ) -from care.facility.models.base import covert_choice_dict from care.facility.models.bed import AssetBed from care.facility.models.json_schema.daily_round import ( BLOOD_PRESSURE, @@ -29,28 +26,23 @@ class DailyRound(PatientBaseModel): - class RoundsType(enum.Enum): - NORMAL = 0 - COMMUNITY_NURSES_LOG = 30 - DOCTORS_LOG = 50 - VENTILATOR = 100 - ICU = 200 - AUTOMATED = 300 - TELEMEDICINE = 400 - - RoundsTypeChoice = [(e.value, e.name) for e in RoundsType] - RoundsTypeDict = covert_choice_dict(RoundsTypeChoice) - - class ConsciousnessType(enum.Enum): - UNKNOWN = 0 - ALERT = 5 - RESPONDS_TO_VOICE = 10 - RESPONDS_TO_PAIN = 15 - UNRESPONSIVE = 20 - AGITATED_OR_CONFUSED = 25 - ONSET_OF_AGITATION_AND_CONFUSION = 30 - - ConsciousnessChoice = [(e.value, e.name) for e in ConsciousnessType] + class RoundsType(models.IntegerChoices): + NORMAL = 0, "NORMAL" + COMMUNITY_NURSES_LOG = 30, "COMMUNITY_NURSES_LOG" + DOCTORS_LOG = 50, "DOCTORS_LOG" + VENTILATOR = 100, "VENTILATOR" + ICU = 200, "ICU" + AUTOMATED = 300, "AUTOMATED" + TELEMEDICINE = 400, "TELEMEDICINE" + + class ConsciousnessTypeChoice(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + ALERT = 5, "ALERT" + RESPONDS_TO_VOICE = 10, "RESPONDS_TO_VOICE" + RESPONDS_TO_PAIN = 15, "RESPONDS_TO_PAIN" + UNRESPONSIVE = 20, "UNRESPONSIVE" + AGITATED_OR_CONFUSED = 25, "AGITATED_OR_CONFUSED" + ONSET_OF_AGITATION_AND_CONFUSION = 30, "ONSET_OF_AGITATION_AND_CONFUSION" class BowelDifficultyType(models.IntegerChoices): NO_DIFFICULTY = 0, "NO_DIFFICULTY" @@ -103,75 +95,57 @@ class AppetiteType(models.IntegerChoices): NO_TASTE_FOR_FOOD = 4, "NO_TASTE_FOR_FOOD" CANNOT_BE_ASSESSED = 5, "CANNOT_BE_ASSESSED" - class PupilReactionType(enum.Enum): - UNKNOWN = 0 - BRISK = 5 - SLUGGISH = 10 - FIXED = 15 - CANNOT_BE_ASSESSED = 20 - - PupilReactionChoice = [(e.value, e.name) for e in PupilReactionType] - - class LimbResponseType(enum.Enum): - UNKNOWN = 0 - STRONG = 5 - MODERATE = 10 - WEAK = 15 - FLEXION = 20 - EXTENSION = 25 - NONE = 30 - - LimbResponseChoice = [(e.value, e.name) for e in LimbResponseType] - - class RythmnType(enum.Enum): - UNKNOWN = 0 - REGULAR = 5 - IRREGULAR = 10 - - RythmnChoice = [(e.value, e.name) for e in RythmnType] - - class VentilatorInterfaceType(enum.Enum): - UNKNOWN = 0 - INVASIVE = 5 - NON_INVASIVE = 10 - OXYGEN_SUPPORT = 15 - - VentilatorInterfaceChoice = [(e.value, e.name) for e in VentilatorInterfaceType] - - class VentilatorModeType(enum.Enum): - UNKNOWN = 0 - VCV = 5 - PCV = 10 - PRVC = 15 - APRV = 20 - VC_SIMV = 25 - PC_SIMV = 30 - PRVC_SIMV = 40 - ASV = 45 - PSV = 50 - - VentilatorModeChoice = [(e.value, e.name) for e in VentilatorModeType] - - class VentilatorOxygenModalityType(enum.Enum): - UNKNOWN = 0 - NASAL_PRONGS = 5 - SIMPLE_FACE_MASK = 10 - NON_REBREATHING_MASK = 15 - HIGH_FLOW_NASAL_CANNULA = 20 - - VentilatorOxygenModalityChoice = [ - (e.value, e.name) for e in VentilatorOxygenModalityType - ] - - class InsulinIntakeFrequencyType(enum.Enum): - UNKNOWN = 0 - OD = 5 - BD = 10 - TD = 15 - - InsulinIntakeFrequencyChoice = [ - (e.value, e.name) for e in InsulinIntakeFrequencyType - ] + class PupilReactionType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + BRISK = 5, "BRISK" + SLUGGISH = 10, "SLUGGISH" + FIXED = 15, "FIXED" + CANNOT_BE_ASSESSED = 20, "CANNOT_BE_ASSESSED" + + class LimbResponseType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + STRONG = 5, "STRONG" + MODERATE = 10, "MODERATE" + WEAK = 15, "WEAK" + FLEXION = 20, "FLEXION" + EXTENSION = 25, "EXTENSION" + NONE = 30, "NONE" + + class RythmnType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + REGULAR = 5, "REGULAR" + IRREGULAR = 10, "IRREGULAR" + + class VentilatorInterfaceType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + INVASIVE = 5, "INVASIVE" + NON_INVASIVE = 10, "NON_INVASIVE" + OXYGEN_SUPPORT = 15, "OXYGEN_SUPPORT" + + class VentilatorModeType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + VCV = 5, "VCV" + PCV = 10, "PCV" + PRVC = 15, "PRVC" + APRV = 20, "APRV" + VC_SIMV = 25, "VC_SIMV" + PC_SIMV = 30, "PC_SIMV" + PRVC_SIMV = 40, "PRVC_SIMV" + ASV = 45, "ASV" + PSV = 50, "PSV" + + class VentilatorOxygenModalityType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + NASAL_PRONGS = 5, "NASAL_PRONGS" + SIMPLE_FACE_MASK = 10, "SIMPLE_FACE_MASK" + NON_REBREATHING_MASK = 15, "NON_REBREATHING_MASK" + HIGH_FLOW_NASAL_CANNULA = 20, "HIGH_FLOW_NASAL_CANNULA" + + class InsulinIntakeFrequencyType(models.IntegerChoices): + UNKNOWN = 0, "UNKNOWN" + OD = 5, "OD" + BD = 10, "BD" + TD = 15, "TD" consultation = models.ForeignKey( PatientConsultation, @@ -223,7 +197,7 @@ class InsulinIntakeFrequencyType(enum.Enum): taken_at = models.DateTimeField(null=True, blank=True, db_index=True) rounds_type = models.IntegerField( - choices=RoundsTypeChoice, default=RoundsType.NORMAL.value + choices=RoundsType.choices, default=RoundsType.NORMAL.value ) is_parsed_by_ocr = models.BooleanField(default=False) @@ -258,7 +232,7 @@ class InsulinIntakeFrequencyType(enum.Enum): # Critical Care Attributes consciousness_level = models.IntegerField( - choices=ConsciousnessChoice, default=None, null=True + choices=ConsciousnessTypeChoice.choices, default=None, null=True ) consciousness_level_detail = models.TextField(default=None, null=True, blank=True) @@ -272,7 +246,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=None, null=True + choices=PupilReactionType.choices, default=None, null=True ) left_pupil_light_reaction_detail = models.TextField( default=None, null=True, blank=True @@ -285,7 +259,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=None, null=True + choices=PupilReactionType.choices, default=None, null=True ) right_pupil_light_reaction_detail = models.TextField( default=None, null=True, blank=True @@ -311,16 +285,16 @@ class InsulinIntakeFrequencyType(enum.Enum): validators=[MinValueValidator(3), MaxValueValidator(15)], ) limb_response_upper_extremity_right = models.IntegerField( - choices=LimbResponseChoice, default=None, null=True + choices=LimbResponseType.choices, default=None, null=True ) limb_response_upper_extremity_left = models.IntegerField( - choices=LimbResponseChoice, default=None, null=True + choices=LimbResponseType.choices, default=None, null=True ) limb_response_lower_extremity_left = models.IntegerField( - choices=LimbResponseChoice, default=None, null=True + choices=LimbResponseType.choices, default=None, null=True ) limb_response_lower_extremity_right = models.IntegerField( - choices=LimbResponseChoice, default=None, null=True + choices=LimbResponseType.choices, default=None, null=True ) bp = JSONField(default=dict, validators=[JSONFieldSchemaValidator(BLOOD_PRESSURE)]) pulse = models.IntegerField( @@ -333,15 +307,15 @@ class InsulinIntakeFrequencyType(enum.Enum): null=True, validators=[MinValueValidator(0), MaxValueValidator(150)], ) - rhythm = models.IntegerField(choices=RythmnChoice, default=None, null=True) + rhythm = models.IntegerField(choices=RythmnType.choices, default=None, null=True) rhythm_detail = models.TextField(default=None, null=True, blank=True) ventilator_interface = models.IntegerField( - choices=VentilatorInterfaceChoice, + choices=VentilatorInterfaceType.choices, default=None, null=True, ) ventilator_mode = models.IntegerField( - choices=VentilatorModeChoice, default=None, null=True + choices=VentilatorModeType.choices, default=None, null=True ) ventilator_peep = models.DecimalField( decimal_places=2, @@ -377,7 +351,7 @@ class InsulinIntakeFrequencyType(enum.Enum): validators=[MinValueValidator(0), MaxValueValidator(1000)], ) ventilator_oxygen_modality = models.IntegerField( - choices=VentilatorOxygenModalityChoice, default=None, null=True + choices=VentilatorOxygenModalityType.choices, default=None, null=True ) ventilator_oxygen_modality_oxygen_rate = models.IntegerField( default=None, @@ -483,7 +457,7 @@ class InsulinIntakeFrequencyType(enum.Enum): validators=[MinValueValidator(0), MaxValueValidator(100)], ) insulin_intake_frequency = models.IntegerField( - choices=InsulinIntakeFrequencyChoice, + choices=InsulinIntakeFrequencyType.choices, default=None, null=True, ) From 30395b69926c13ccda4b60b88dd8fbed53d00708 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 19 Sep 2024 23:15:08 +0530 Subject: [PATCH 06/46] lint all files (#2468) --- care/abdm/utils/fhir.py | 146 ++++++++++-------- .../0003_set_site_domain_and_name.py | 1 + .../api/serializers/patient_sample.py | 8 +- .../migrations/0439_encounter_symptoms.py | 16 +- care/facility/models/patient_external_test.py | 2 +- .../utils/summarisation/facility_capacity.py | 6 +- .../utils/summarisation/tests_summary.py | 6 +- care/hcx/utils/hcx/__init__.py | 6 +- .../commands/populate_investigations.py | 16 +- config/asgi.py | 1 + config/wsgi.py | 1 + locale/hi/LC_MESSAGES/django.po | 1 - locale/kn/LC_MESSAGES/django.po | 1 - locale/ml/LC_MESSAGES/django.po | 1 - locale/ta/LC_MESSAGES/django.po | 1 - 15 files changed, 120 insertions(+), 93 deletions(-) diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index d862f079fe..c0eaa47a57 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -190,12 +190,12 @@ def _procedure(self, procedure): text=procedure["procedure"], ), subject=self._reference(self._patient()), - performedDateTime=f"{procedure['time']}:00+05:30" - if not procedure["repetitive"] - else None, - performedString=f"Every {procedure['frequency']}" - if procedure["repetitive"] - else None, + performedDateTime=( + f"{procedure['time']}:00+05:30" if not procedure["repetitive"] else None + ), + performedString=( + f"Every {procedure['frequency']}" if procedure["repetitive"] else None + ), ) self._procedure_profiles.append(procedure_profile) @@ -213,9 +213,11 @@ def _careplan(self): description="This includes Treatment Summary, Prescribed Medication, General Notes and Special Instructions", period=Period( start=self.consultation.encounter_date.isoformat(), - end=self.consultation.discharge_date.isoformat() - if self.consultation.discharge_date - else None, + end=( + self.consultation.discharge_date.isoformat() + if self.consultation.discharge_date + else None + ), ), note=[ Annotation(text=self.consultation.treatment_plan), @@ -260,36 +262,46 @@ def _diagnostic_report(self): return self._diagnostic_report_profile def _observation(self, title, value, id, date): - if not value or (type(value) == dict and not value["value"]): + if not value or (isinstance(value, dict) and not value["value"]): return return Observation( - id=f"{id}.{title.replace(' ', '').replace('_', '-')}" - if id and title - else str(uuid()), + id=( + f"{id}.{title.replace(' ', '').replace('_', '-')}" + if id and title + else str(uuid()) + ), status="final", effectiveDateTime=date if date else None, code=CodeableConcept(text=title), - valueQuantity=Quantity(value=str(value["value"]), unit=value["unit"]) - if type(value) == dict - else None, - valueString=value if type(value) == str else None, - component=list( - map( - lambda component: ObservationComponent( - code=CodeableConcept(text=component["title"]), - valueQuantity=Quantity( - value=component["value"], unit=component["unit"] - ) - if type(component) == dict - else None, - valueString=component if type(component) == str else None, - ), - value, + valueQuantity=( + Quantity(value=str(value["value"]), unit=value["unit"]) + if isinstance(value, dict) + else None + ), + valueString=value if isinstance(value, str) else None, + component=( + list( + map( + lambda component: ObservationComponent( + code=CodeableConcept(text=component["title"]), + valueQuantity=( + Quantity( + value=component["value"], unit=component["unit"] + ) + if isinstance(component, dict) + else None + ), + valueString=( + component if isinstance(component, str) else None + ), + ), + value, + ) ) - ) - if type(value) == list - else None, + if isinstance(value, list) + else None + ), ) def _observations_from_daily_round(self, daily_round): @@ -322,20 +334,22 @@ def _observations_from_daily_round(self, daily_round): ), self._observation( "Blood Pressure", - [ - { - "title": "Systolic Blood Pressure", - "value": daily_round.bp["systolic"], - "unit": "mmHg", - }, - { - "title": "Diastolic Blood Pressure", - "value": daily_round.bp["diastolic"], - "unit": "mmHg", - }, - ] - if "systolic" in daily_round.bp and "diastolic" in daily_round.bp - else None, + ( + [ + { + "title": "Systolic Blood Pressure", + "value": daily_round.bp["systolic"], + "unit": "mmHg", + }, + { + "title": "Diastolic Blood Pressure", + "value": daily_round.bp["diastolic"], + "unit": "mmHg", + }, + ] + if "systolic" in daily_round.bp and "diastolic" in daily_round.bp + else None + ), id, date, ), @@ -369,21 +383,23 @@ def _encounter(self, include_diagnosis=False): "class": Coding(code="IMP", display="Inpatient Encounter"), "subject": self._reference(self._patient()), "period": Period(start=period_start, end=period_end), - "diagnosis": list( - map( - lambda consultation_diagnosis: EncounterDiagnosis( - condition=self._reference( - self._condition( - consultation_diagnosis.diagnosis_id, - consultation_diagnosis.verification_status, - ), - ) - ), - self.consultation.diagnoses.all(), + "diagnosis": ( + list( + map( + lambda consultation_diagnosis: EncounterDiagnosis( + condition=self._reference( + self._condition( + consultation_diagnosis.diagnosis_id, + consultation_diagnosis.verification_status, + ), + ) + ), + self.consultation.diagnoses.all(), + ) ) - ) - if include_diagnosis - else None, + if include_diagnosis + else None + ), } ) @@ -655,10 +671,12 @@ def _immunization_composition(self): else [] ) ], - emptyReason=None - if self._immunization() - else CodeableConcept( - coding=[Coding(code="notasked", display="Not Asked")] + emptyReason=( + None + if self._immunization() + else CodeableConcept( + coding=[Coding(code="notasked", display="Not Asked")] + ) ), ), ], diff --git a/care/contrib/sites/migrations/0003_set_site_domain_and_name.py b/care/contrib/sites/migrations/0003_set_site_domain_and_name.py index d1473c9d6c..2078c433f6 100644 --- a/care/contrib/sites/migrations/0003_set_site_domain_and_name.py +++ b/care/contrib/sites/migrations/0003_set_site_domain_and_name.py @@ -3,6 +3,7 @@ http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django """ + from django.conf import settings from django.db import migrations diff --git a/care/facility/api/serializers/patient_sample.py b/care/facility/api/serializers/patient_sample.py index 2308dcac1b..261d4f9ca3 100644 --- a/care/facility/api/serializers/patient_sample.py +++ b/care/facility/api/serializers/patient_sample.py @@ -108,9 +108,11 @@ def update(self, instance, validated_data): is_completed = validated_data.get("result") in [1, 2] new_status = validated_data.get( "status", - PatientSample.SAMPLE_TEST_FLOW_MAP["COMPLETED"] - if is_completed - else None, + ( + PatientSample.SAMPLE_TEST_FLOW_MAP["COMPLETED"] + if is_completed + else None + ), ) choice = PatientSample.SAMPLE_TEST_FLOW_CHOICES[new_status - 1][1] if is_completed: diff --git a/care/facility/migrations/0439_encounter_symptoms.py b/care/facility/migrations/0439_encounter_symptoms.py index 67f9b17f45..8982cae5d9 100644 --- a/care/facility/migrations/0439_encounter_symptoms.py +++ b/care/facility/migrations/0439_encounter_symptoms.py @@ -39,9 +39,11 @@ def backfill_symptoms_table(apps, schema_editor): bulk.append( EncounterSymptom( symptom=symptom_id, - other_symptom=consultation.deprecated_other_symptoms - if symptom_id == 9 # Other symptom - else "", + other_symptom=( + consultation.deprecated_other_symptoms + if symptom_id == 9 # Other symptom + else "" + ), onset_date=consultation.deprecated_symptoms_onset_date or consultation.encounter_date, created_date=consultation.created_date, @@ -85,9 +87,11 @@ def backfill_symptoms_table(apps, schema_editor): bulk.append( EncounterSymptom( symptom=symptom_id, - other_symptom=daily_round.deprecated_other_symptoms - if symptom_id == 9 # Other symptom - else "", + other_symptom=( + daily_round.deprecated_other_symptoms + if symptom_id == 9 # Other symptom + else "" + ), onset_date=daily_round.created_date, created_date=daily_round.created_date, created_by=daily_round.created_by, diff --git a/care/facility/models/patient_external_test.py b/care/facility/models/patient_external_test.py index 1e13c8f5c2..297ccbfd63 100644 --- a/care/facility/models/patient_external_test.py +++ b/care/facility/models/patient_external_test.py @@ -78,7 +78,7 @@ class PatientExternalTest(FacilityBaseModel): "sample_type": "Sample Type", "result": "Final Result", "sample_collection_date": "Sample Collection Date", - "source": "Source" + "source": "Source", # "result_date": "", } diff --git a/care/facility/utils/summarisation/facility_capacity.py b/care/facility/utils/summarisation/facility_capacity.py index 63548b9c3c..a41ef71ec9 100644 --- a/care/facility/utils/summarisation/facility_capacity.py +++ b/care/facility/utils/summarisation/facility_capacity.py @@ -30,9 +30,9 @@ def facility_capacity_summary(): capacity_summary[facility_obj.id]["features"] = list( capacity_summary[facility_obj.id]["features"] ) - capacity_summary[facility_obj.id][ - "actual_live_patients" - ] = patients_in_facility.filter(is_active=True).count() + capacity_summary[facility_obj.id]["actual_live_patients"] = ( + patients_in_facility.filter(is_active=True).count() + ) discharge_patients = patients_in_facility.filter(is_active=False) capacity_summary[facility_obj.id][ "actual_discharged_patients" diff --git a/care/facility/utils/summarisation/tests_summary.py b/care/facility/utils/summarisation/tests_summary.py index 4854a8fa5d..a9da8c8a61 100644 --- a/care/facility/utils/summarisation/tests_summary.py +++ b/care/facility/utils/summarisation/tests_summary.py @@ -48,9 +48,9 @@ def tests_summary(): if facility_test_summary.data != facility_tests_summarised_data: facility_test_summary.data = facility_tests_summarised_data latest_modification_date = timezone.now() - facility_test_summary.data[ - "modified_date" - ] = latest_modification_date.strftime("%d-%m-%Y %H:%M") + facility_test_summary.data["modified_date"] = ( + latest_modification_date.strftime("%d-%m-%Y %H:%M") + ) facility_test_summary.save() except ObjectDoesNotExist: modified_date = timezone.now() diff --git a/care/hcx/utils/hcx/__init__.py b/care/hcx/utils/hcx/__init__.py index 533e0a9bbc..17207ff45d 100644 --- a/care/hcx/utils/hcx/__init__.py +++ b/care/hcx/utils/hcx/__init__.py @@ -70,9 +70,9 @@ def createHeaders(self, recipientCode=None, correlationId=None): .replace(microsecond=0) .isoformat(), "x-hcx-sender_code": self.participantCode, - "x-hcx-correlation_id": correlationId - if correlationId - else str(uuid.uuid4()), + "x-hcx-correlation_id": ( + correlationId if correlationId else str(uuid.uuid4()) + ), # "x-hcx-workflow_id": str(uuid.uuid4()), "x-hcx-api_call_id": str(uuid.uuid4()), # "x-hcx-status": "response.complete", diff --git a/care/users/management/commands/populate_investigations.py b/care/users/management/commands/populate_investigations.py index 21847ee190..39b2162913 100644 --- a/care/users/management/commands/populate_investigations.py +++ b/care/users/management/commands/populate_investigations.py @@ -47,12 +47,16 @@ def handle(self, *args, **options): "name": investigation["name"], "unit": investigation.get("unit", ""), "ideal_value": investigation.get("ideal_value", ""), - "min_value": None - if investigation.get("min_value") is None - else float(investigation.get("min_value")), - "max_value": None - if investigation.get("max_value") is None - else float(investigation.get("max_value")), + "min_value": ( + None + if investigation.get("min_value") is None + else float(investigation.get("min_value")) + ), + "max_value": ( + None + if investigation.get("max_value") is None + else float(investigation.get("max_value")) + ), "investigation_type": investigation["type"], "choices": investigation.get("choices", ""), } diff --git a/config/asgi.py b/config/asgi.py index 724751777f..156e6f0e37 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -7,6 +7,7 @@ https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ """ + import os import sys from pathlib import Path diff --git a/config/wsgi.py b/config/wsgi.py index 835d5c2ece..c9f43409b4 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -13,6 +13,7 @@ framework. """ + import os import sys from pathlib import Path diff --git a/locale/hi/LC_MESSAGES/django.po b/locale/hi/LC_MESSAGES/django.po index a63e6bcfa7..5117bd02bc 100644 --- a/locale/hi/LC_MESSAGES/django.po +++ b/locale/hi/LC_MESSAGES/django.po @@ -230,4 +230,3 @@ msgstr "आपके पासवर्ड में कम से कम 1 प #: config/validators.py:66 msgid "The domain name is invalid. It should not start with scheme and should not end with a trailing slash." msgstr "डोमेन नाम अमान्य है। इसे स्कीम से शुरू नहीं करना चाहिए और अंत में स्लैश से नहीं होना चाहिए।" - diff --git a/locale/kn/LC_MESSAGES/django.po b/locale/kn/LC_MESSAGES/django.po index 39aa3fc1df..46a9b390b2 100644 --- a/locale/kn/LC_MESSAGES/django.po +++ b/locale/kn/LC_MESSAGES/django.po @@ -230,4 +230,3 @@ msgstr "ನಿಮ್ಮ ಪಾಸ್‌ವರ್ಡ್ ಕನಿಷ್ಠ 1 ಚ #: config/validators.py:66 msgid "The domain name is invalid. It should not start with scheme and should not end with a trailing slash." msgstr "ಡೊಮೇನ್ ಹೆಸರು ಅಮಾನ್ಯವಾಗಿದೆ. ಇದು ಸ್ಕೀಮ್‌ನೊಂದಿಗೆ ಪ್ರಾರಂಭವಾಗಬಾರದು ಮತ್ತು ಟ್ರೇಲಿಂಗ್ ಸ್ಲ್ಯಾಷ್‌ನೊಂದಿಗೆ ಕೊನೆಗೊಳ್ಳಬಾರದು." - diff --git a/locale/ml/LC_MESSAGES/django.po b/locale/ml/LC_MESSAGES/django.po index 5b0e3c8386..048485bd7b 100644 --- a/locale/ml/LC_MESSAGES/django.po +++ b/locale/ml/LC_MESSAGES/django.po @@ -230,4 +230,3 @@ msgstr "നിങ്ങളുടെ പാസ്‌വേഡിൽ കുറഞ #: config/validators.py:66 msgid "The domain name is invalid. It should not start with scheme and should not end with a trailing slash." msgstr "ഡൊമെയ്ൻ നാമം അസാധുവാണ്. ഇത് സ്കീമിൽ ആരംഭിക്കരുത്, ഒരു ട്രെയിലിംഗ് സ്ലാഷിൽ അവസാനിക്കരുത്." - diff --git a/locale/ta/LC_MESSAGES/django.po b/locale/ta/LC_MESSAGES/django.po index e1ff832ea4..60e1f4946f 100644 --- a/locale/ta/LC_MESSAGES/django.po +++ b/locale/ta/LC_MESSAGES/django.po @@ -230,4 +230,3 @@ msgstr "உங்கள் கடவுச்சொல்லில் குற #: config/validators.py:66 msgid "The domain name is invalid. It should not start with scheme and should not end with a trailing slash." msgstr "டொமைன் பெயர் தவறானது. இது திட்டத்துடன் தொடங்கக்கூடாது மற்றும் பின்னோக்கி சாய்வுடன் முடிவடையக்கூடாது." - From 96992068f5765989ead685a436728ff46edd151f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 19 Sep 2024 23:46:26 +0530 Subject: [PATCH 07/46] Major dependencies update (#2449) Major dependencies update (#2449) --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> Co-authored-by: vigneshhari --- .github/dependabot.yml | 8 +- .github/workflows/combine-dependencies.yml | 23 - .github/workflows/docs.yml | 4 +- Pipfile | 59 +- Pipfile.lock | 1430 +++++++++-------- care/abdm/models/consent.py | 12 +- care/facility/admin.py | 40 +- .../migrations/0001_initial_squashed.py | 6 +- ...sultation_deprecated_diagnosis_and_more.py | 2 +- ...ntconsultation_consent_records_and_more.py | 4 +- .../migrations_old/0030_auto_20200327_0619.py | 2 +- .../migrations_old/0035_auto_20200328_0442.py | 4 +- .../migrations_old/0338_auto_20230323_1249.py | 2 +- care/facility/models/asset.py | 2 +- care/facility/models/encounter_symptom.py | 4 +- care/facility/models/events.py | 2 +- care/facility/models/facility.py | 4 +- care/facility/models/file_upload.py | 4 +- care/facility/models/icd11_diagnosis.py | 2 +- care/facility/models/patient.py | 4 +- care/facility/models/patient_consultation.py | 14 +- care/facility/models/prescription.py | 2 +- care/facility/tests/test_patient_api.py | 9 +- care/users/admin.py | 3 +- care/users/models.py | 31 + config/authentication.py | 6 +- config/settings/local.py | 15 - docker/dev.Dockerfile | 10 +- docker/prod.Dockerfile | 51 +- docs/django-configuration/configuration.rst | 14 +- 30 files changed, 883 insertions(+), 890 deletions(-) delete mode 100644 .github/workflows/combine-dependencies.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e9a27996a6..ea748496cf 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,9 +10,13 @@ updates: schedule: interval: "weekly" groups: - boto: + all-dependencies: patterns: - - "boto*" + - "*" + ignore: + - dependency-name: "redis-om" + - dependency-name: "pydantic" + - dependency-name: "fhir.resources" - package-ecosystem: "docker" directory: "/docker" diff --git a/.github/workflows/combine-dependencies.yml b/.github/workflows/combine-dependencies.yml deleted file mode 100644 index d44461ff5a..0000000000 --- a/.github/workflows/combine-dependencies.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Combine Dependencies - -on: workflow_dispatch - -# The minimum permissions required to run this Action -permissions: - contents: write - pull-requests: write - checks: read - -jobs: - combine-prs: - runs-on: ubuntu-latest - - steps: - - name: Combine dependencies - id: combine-dependencies - uses: github/combine-prs@v5.0.0 - with: - pr_title: Combined dependencies # The title of the pull request to create - select_label: dependencies # The label which marks PRs that should be combined. - labels: combined-dependencies # Add a label to the combined PR - ci_required: "false" # Whether or not CI should be passing to combine the PR diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 11ed3918b4..19189155e7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -23,11 +23,11 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.11" + python-version: "3.12" cache: 'pipenv' - name: Install pipenv - run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python + run: pip install pipenv - name: Install dependencies run: pipenv sync --categories "docs" diff --git a/Pipfile b/Pipfile index 56f0fd7421..445f0f9265 100644 --- a/Pipfile +++ b/Pipfile @@ -5,15 +5,14 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" -authlib = "==1.3.1" -boto3 = "==1.35.0" +authlib = "==1.3.2" +boto3 = "==1.35.19" celery = "==5.4.0" -django = "==4.2.15" +django = "==5.1.1" django-environ = "==0.11.2" -django-cors-headers = "==4.3.1" -django-filter = "==24.2" +django-cors-headers = "==4.4.0" +django-filter = "==24.3" django-maintenance-mode = "==0.21.1" -django-model-utils = "==4.5.1" django-queryset-csv = "==1.1.0" django-ratelimit = "==4.1.0" django-redis = "==5.4.0" @@ -26,51 +25,49 @@ dry-rest-permissions = "==0.1.10" drf-nested-routers = "==0.94.1" drf-spectacular = "==0.27.2" "fhir.resources" = "==6.5.0" -gunicorn = "==22.0.0" +gunicorn = "==23.0.0" healthy-django = "==0.1.0" -jsonschema = "==4.22.0" +jsonschema = "==4.23.0" jwcrypto = "==1.5.6" newrelic = "==9.13.0" -pillow = "==10.3.0" +pillow = "==10.4.0" psycopg = { extras = ["c"], version = "==3.1.19" } pycryptodome = "==3.20.0" pydantic = "==1.10.15" # fix for fhir.resources < 7.0.2 -pyjwt = "==2.8.0" +pyjwt = "==2.9.0" python-slugify = "==8.0.4" pywebpush = "==2.0.0" redis = { extras = ["hiredis"], version = "==5.0.5" } # constraint for redis-om -redis-om = "==0.3.1" +redis-om = "==0.3.1" # > 0.3.1 broken with pydantic < 2 requests = "==2.32.3" -sentry-sdk = "==2.13.0" -whitenoise = "==6.6.0" +sentry-sdk = "==2.14.0" +whitenoise = "==6.7.0" [dev-packages] -black = "==24.4.2" -boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.0" } -coverage = "==7.5.3" -debugpy = "==1.8.1" +black = "==24.8.0" +boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.19" } +coverage = "==7.6.1" +debugpy = "==1.8.5" django-coverage-plugin = "==3.1.0" -django-debug-toolbar = "==4.4.2" django-extensions = "==3.2.3" -django-silk = "==5.1.0" -django-stubs = "==5.0.2" -djangorestframework-stubs = "==3.15.0" -factory-boy = "==3.3.0" +django-silk = "==5.2.0" +djangorestframework-stubs = "==3.15.1" +factory-boy = "==3.3.1" flake8 = "==7.1.1" freezegun = "==1.5.1" -ipython = "==8.25.0" +ipython = "==8.27.0" isort = "==5.13.2" -mypy = "==1.10.0" -pre-commit = "==3.7.1" +mypy = "==1.11.2" +pre-commit = "==3.8.0" requests-mock = "==1.12.1" tblib = "==3.0.0" -watchdog = "==4.0.1" -werkzeug = "==3.0.3" +watchdog = "==5.0.2" +werkzeug = "==3.0.4" [docs] -furo = "==2024.5.6" -sphinx = "==7.3.7" -myst-parser = "==3.0.1" +furo = "==2024.8.6" +sphinx = "==8.0.2" +myst-parser = "==4.0.0" [requires] -python_version = "3.11" +python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock index a05d26ac90..c5b9fbdda4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,11 +1,11 @@ { "_meta": { "hash": { - "sha256": "81cab9840716883c05f3a21732b616658bab7dc3d0636e93e5612316c6319acf" + "sha256": "84feb1d63404ac8bd49709c07717eeb2670f7766b6339f8ec277ed2e33a9366f" }, "pipfile-spec": 6, "requires": { - "python_version": "3.11" + "python_version": "3.12" }, "sources": [ { @@ -191,12 +191,12 @@ }, "authlib": { "hashes": [ - "sha256:7ae843f03c06c5c0debd63c9db91f9fda64fa62a42a77419fa15fbb7e7a58917", - "sha256:d35800b973099bbadc49b42b256ecb80041ad56b7fe1216a362c7943c088f377" + "sha256:4b16130117f9eb82aa6eec97f6dd4673c3f960ac0283ccdae2897ee4bc030ba2", + "sha256:ede026a95e9f5cdc2d4364a52103f5405e75aa156357e831ef2bfd0bc5094dfc" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.3.1" + "version": "==1.3.2" }, "billiard": { "hashes": [ @@ -208,20 +208,20 @@ }, "boto3": { "hashes": [ - "sha256:ada32dab854c46a877cf967b8a55ab1a7d356c3c87f1c8bd556d446ff03dfd95", - "sha256:bdc242e3ea81decc6ea551b04b2c122f088c29269d8e093b55862946aa0fcfc6" + "sha256:84b3fe1727945bc3cada832d969ddb3dc0d08fce1677064ca8bdc13a89c1a143", + "sha256:9979fe674780a0b7100eae9156d74ee374cd1638a9f61c77277e3ce712f3e496" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.0" + "version": "==1.35.19" }, "botocore": { "hashes": [ - "sha256:10195e5ca764745f02b9a51df048b996ddbdc1899a44a2caf35dfb225dfea489", - "sha256:4cc51a6a486915aedc140f9d027b7e156646b7a0f7b33b1000762c81aff9a12f" + "sha256:18362b7ec748561d786aebf1dd5c9faf22c4732efbf89344314199f96d3bbb65", + "sha256:d9bc656e7dde0b3e3f3080fc54bacff6a97fd7806b98acbcc21c7f9d4d0102b9" ], "markers": "python_version >= '3.8'", - "version": "==1.35.4" + "version": "==1.35.22" }, "celery": { "hashes": [ @@ -234,84 +234,84 @@ }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "cffi": { "hashes": [ - "sha256:011aff3524d578a9412c8b3cfaa50f2c0bd78e03eb7af7aa5e0df59b158efb2f", - "sha256:0a048d4f6630113e54bb4b77e315e1ba32a5a31512c31a273807d0027a7e69ab", - "sha256:0bb15e7acf8ab35ca8b24b90af52c8b391690ef5c4aec3d31f38f0d37d2cc499", - "sha256:0d46ee4764b88b91f16661a8befc6bfb24806d885e27436fdc292ed7e6f6d058", - "sha256:0e60821d312f99d3e1569202518dddf10ae547e799d75aef3bca3a2d9e8ee693", - "sha256:0fdacad9e0d9fc23e519efd5ea24a70348305e8d7d85ecbb1a5fa66dc834e7fb", - "sha256:14b9cbc8f7ac98a739558eb86fabc283d4d564dafed50216e7f7ee62d0d25377", - "sha256:17c6d6d3260c7f2d94f657e6872591fe8733872a86ed1345bda872cfc8c74885", - "sha256:1a2ddbac59dc3716bc79f27906c010406155031a1c801410f1bafff17ea304d2", - "sha256:2404f3de742f47cb62d023f0ba7c5a916c9c653d5b368cc966382ae4e57da401", - "sha256:24658baf6224d8f280e827f0a50c46ad819ec8ba380a42448e24459daf809cf4", - "sha256:24aa705a5f5bd3a8bcfa4d123f03413de5d86e497435693b638cbffb7d5d8a1b", - "sha256:2770bb0d5e3cc0e31e7318db06efcbcdb7b31bcb1a70086d3177692a02256f59", - "sha256:331ad15c39c9fe9186ceaf87203a9ecf5ae0ba2538c9e898e3a6967e8ad3db6f", - "sha256:3aa9d43b02a0c681f0bfbc12d476d47b2b2b6a3f9287f11ee42989a268a1833c", - "sha256:41f4915e09218744d8bae14759f983e466ab69b178de38066f7579892ff2a555", - "sha256:4304d4416ff032ed50ad6bb87416d802e67139e31c0bde4628f36a47a3164bfa", - "sha256:435a22d00ec7d7ea533db494da8581b05977f9c37338c80bc86314bec2619424", - "sha256:45f7cd36186db767d803b1473b3c659d57a23b5fa491ad83c6d40f2af58e4dbb", - "sha256:48b389b1fd5144603d61d752afd7167dfd205973a43151ae5045b35793232aa2", - "sha256:4e67d26532bfd8b7f7c05d5a766d6f437b362c1bf203a3a5ce3593a645e870b8", - "sha256:516a405f174fd3b88829eabfe4bb296ac602d6a0f68e0d64d5ac9456194a5b7e", - "sha256:5ba5c243f4004c750836f81606a9fcb7841f8874ad8f3bf204ff5e56332b72b9", - "sha256:5bdc0f1f610d067c70aa3737ed06e2726fd9d6f7bfee4a351f4c40b6831f4e82", - "sha256:6107e445faf057c118d5050560695e46d272e5301feffda3c41849641222a828", - "sha256:6327b572f5770293fc062a7ec04160e89741e8552bf1c358d1a23eba68166759", - "sha256:669b29a9eca6146465cc574659058ed949748f0809a2582d1f1a324eb91054dc", - "sha256:6ce01337d23884b21c03869d2f68c5523d43174d4fc405490eb0091057943118", - "sha256:6d872186c1617d143969defeadac5a904e6e374183e07977eedef9c07c8953bf", - "sha256:6f76a90c345796c01d85e6332e81cab6d70de83b829cf1d9762d0a3da59c7932", - "sha256:70d2aa9fb00cf52034feac4b913181a6e10356019b18ef89bc7c12a283bf5f5a", - "sha256:7cbc78dc018596315d4e7841c8c3a7ae31cc4d638c9b627f87d52e8abaaf2d29", - "sha256:856bf0924d24e7f93b8aee12a3a1095c34085600aa805693fb7f5d1962393206", - "sha256:8a98748ed1a1df4ee1d6f927e151ed6c1a09d5ec21684de879c7ea6aa96f58f2", - "sha256:93a7350f6706b31f457c1457d3a3259ff9071a66f312ae64dc024f049055f72c", - "sha256:964823b2fc77b55355999ade496c54dde161c621cb1f6eac61dc30ed1b63cd4c", - "sha256:a003ac9edc22d99ae1286b0875c460351f4e101f8c9d9d2576e78d7e048f64e0", - "sha256:a0ce71725cacc9ebf839630772b07eeec220cbb5f03be1399e0457a1464f8e1a", - "sha256:a47eef975d2b8b721775a0fa286f50eab535b9d56c70a6e62842134cf7841195", - "sha256:a8b5b9712783415695663bd463990e2f00c6750562e6ad1d28e072a611c5f2a6", - "sha256:a9015f5b8af1bb6837a3fcb0cdf3b874fe3385ff6274e8b7925d81ccaec3c5c9", - "sha256:aec510255ce690d240f7cb23d7114f6b351c733a74c279a84def763660a2c3bc", - "sha256:b00e7bcd71caa0282cbe3c90966f738e2db91e64092a877c3ff7f19a1628fdcb", - "sha256:b50aaac7d05c2c26dfd50c3321199f019ba76bb650e346a6ef3616306eed67b0", - "sha256:b7b6ea9e36d32582cda3465f54c4b454f62f23cb083ebc7a94e2ca6ef011c3a7", - "sha256:bb9333f58fc3a2296fb1d54576138d4cf5d496a2cc118422bd77835e6ae0b9cb", - "sha256:c1c13185b90bbd3f8b5963cd8ce7ad4ff441924c31e23c975cb150e27c2bf67a", - "sha256:c3b8bd3133cd50f6b637bb4322822c94c5ce4bf0d724ed5ae70afce62187c492", - "sha256:c5d97162c196ce54af6700949ddf9409e9833ef1003b4741c2b39ef46f1d9720", - "sha256:c815270206f983309915a6844fe994b2fa47e5d05c4c4cef267c3b30e34dbe42", - "sha256:cab2eba3830bf4f6d91e2d6718e0e1c14a2f5ad1af68a89d24ace0c6b17cced7", - "sha256:d1df34588123fcc88c872f5acb6f74ae59e9d182a2707097f9e28275ec26a12d", - "sha256:d6bdcd415ba87846fd317bee0774e412e8792832e7805938987e4ede1d13046d", - "sha256:db9a30ec064129d605d0f1aedc93e00894b9334ec74ba9c6bdd08147434b33eb", - "sha256:dbc183e7bef690c9abe5ea67b7b60fdbca81aa8da43468287dae7b5c046107d4", - "sha256:dca802c8db0720ce1c49cce1149ff7b06e91ba15fa84b1d59144fef1a1bc7ac2", - "sha256:dec6b307ce928e8e112a6bb9921a1cb00a0e14979bf28b98e084a4b8a742bd9b", - "sha256:df8bb0010fdd0a743b7542589223a2816bdde4d94bb5ad67884348fa2c1c67e8", - "sha256:e4094c7b464cf0a858e75cd14b03509e84789abf7b79f8537e6a72152109c76e", - "sha256:e4760a68cab57bfaa628938e9c2971137e05ce48e762a9cb53b76c9b569f1204", - "sha256:eb09b82377233b902d4c3fbeeb7ad731cdab579c6c6fda1f763cd779139e47c3", - "sha256:eb862356ee9391dc5a0b3cbc00f416b48c1b9a52d252d898e5b7696a5f9fe150", - "sha256:ef9528915df81b8f4c7612b19b8628214c65c9b7f74db2e34a646a0a2a0da2d4", - "sha256:f3157624b7558b914cb039fd1af735e5e8049a87c817cc215109ad1c8779df76", - "sha256:f3e0992f23bbb0be00a921eae5363329253c3b86287db27092461c887b791e5e", - "sha256:f9338cc05451f1942d0d8203ec2c346c830f8e86469903d5126c1f0a13a2bcbb", - "sha256:ffef8fd58a36fb5f1196919638f73dd3ae0db1a878982b27a9a5a176ede4ba91" + "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", + "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", + "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1", + "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", + "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", + "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", + "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8", + "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36", + "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", + "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", + "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc", + "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", + "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", + "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", + "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", + "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", + "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", + "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", + "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", + "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b", + "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", + "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", + "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c", + "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", + "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", + "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", + "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8", + "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1", + "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", + "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", + "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", + "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595", + "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0", + "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", + "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", + "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", + "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", + "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", + "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", + "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16", + "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", + "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e", + "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", + "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964", + "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", + "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576", + "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", + "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3", + "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662", + "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", + "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", + "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", + "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", + "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", + "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", + "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", + "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", + "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9", + "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7", + "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", + "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a", + "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", + "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", + "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", + "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", + "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87", + "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b" ], "markers": "platform_python_implementation != 'PyPy'", - "version": "==1.17.0" + "version": "==1.17.1" }, "charset-normalizer": { "hashes": [ @@ -442,54 +442,54 @@ }, "cryptography": { "hashes": [ - "sha256:0663585d02f76929792470451a5ba64424acc3cd5227b03921dab0e2f27b1709", - "sha256:08a24a7070b2b6804c1940ff0f910ff728932a9d0e80e7814234269f9d46d069", - "sha256:232ce02943a579095a339ac4b390fbbe97f5b5d5d107f8a08260ea2768be8cc2", - "sha256:2905ccf93a8a2a416f3ec01b1a7911c3fe4073ef35640e7ee5296754e30b762b", - "sha256:299d3da8e00b7e2b54bb02ef58d73cd5f55fb31f33ebbf33bd00d9aa6807df7e", - "sha256:2c6d112bf61c5ef44042c253e4859b3cbbb50df2f78fa8fae6747a7814484a70", - "sha256:31e44a986ceccec3d0498e16f3d27b2ee5fdf69ce2ab89b52eaad1d2f33d8778", - "sha256:3d9a1eca329405219b605fac09ecfc09ac09e595d6def650a437523fcd08dd22", - "sha256:3dcdedae5c7710b9f97ac6bba7e1052b95c7083c9d0e9df96e02a1932e777895", - "sha256:47ca71115e545954e6c1d207dd13461ab81f4eccfcb1345eac874828b5e3eaaf", - "sha256:4a997df8c1c2aae1e1e5ac49c2e4f610ad037fc5a3aadc7b64e39dea42249431", - "sha256:51956cf8730665e2bdf8ddb8da0056f699c1a5715648c1b0144670c1ba00b48f", - "sha256:5bcb8a5620008a8034d39bce21dc3e23735dfdb6a33a06974739bfa04f853947", - "sha256:64c3f16e2a4fc51c0d06af28441881f98c5d91009b8caaff40cf3548089e9c74", - "sha256:6e2b11c55d260d03a8cf29ac9b5e0608d35f08077d8c087be96287f43af3ccdc", - "sha256:7b3f5fe74a5ca32d4d0f302ffe6680fcc5c28f8ef0dc0ae8f40c0f3a1b4fca66", - "sha256:844b6d608374e7d08f4f6e6f9f7b951f9256db41421917dfb2d003dde4cd6b66", - "sha256:9a8d6802e0825767476f62aafed40532bd435e8a5f7d23bd8b4f5fd04cc80ecf", - "sha256:aae4d918f6b180a8ab8bf6511a419473d107df4dbb4225c7b48c5c9602c38c7f", - "sha256:ac1955ce000cb29ab40def14fd1bbfa7af2017cca696ee696925615cafd0dce5", - "sha256:b88075ada2d51aa9f18283532c9f60e72170041bba88d7f37e49cbb10275299e", - "sha256:cb013933d4c127349b3948aa8aaf2f12c0353ad0eccd715ca789c8a0f671646f", - "sha256:cc70b4b581f28d0a254d006f26949245e3657d40d8857066c2ae22a61222ef55", - "sha256:e9c5266c432a1e23738d178e51c2c7a5e2ddf790f248be939448c0ba2021f9d1", - "sha256:ea9e57f8ea880eeea38ab5abf9fbe39f923544d7884228ec67d666abd60f5a47", - "sha256:ee0c405832ade84d4de74b9029bedb7b31200600fa524d218fc29bfa371e97f5", - "sha256:fdcb265de28585de5b859ae13e3846a8e805268a823a12a4da2597f1f5afc9f0" + "sha256:014f58110f53237ace6a408b5beb6c427b64e084eb451ef25a28308270086494", + "sha256:1bbcce1a551e262dfbafb6e6252f1ae36a248e615ca44ba302df077a846a8806", + "sha256:203e92a75716d8cfb491dc47c79e17d0d9207ccffcbcb35f598fbe463ae3444d", + "sha256:27e613d7077ac613e399270253259d9d53872aaf657471473ebfc9a52935c062", + "sha256:2bd51274dcd59f09dd952afb696bf9c61a7a49dfc764c04dd33ef7a6b502a1e2", + "sha256:38926c50cff6f533f8a2dae3d7f19541432610d114a70808f0926d5aaa7121e4", + "sha256:511f4273808ab590912a93ddb4e3914dfd8a388fed883361b02dea3791f292e1", + "sha256:58d4e9129985185a06d849aa6df265bdd5a74ca6e1b736a77959b498e0505b85", + "sha256:5b43d1ea6b378b54a1dc99dd8a2b5be47658fe9a7ce0a58ff0b55f4b43ef2b84", + "sha256:61ec41068b7b74268fa86e3e9e12b9f0c21fcf65434571dbb13d954bceb08042", + "sha256:666ae11966643886c2987b3b721899d250855718d6d9ce41b521252a17985f4d", + "sha256:68aaecc4178e90719e95298515979814bda0cbada1256a4485414860bd7ab962", + "sha256:7c05650fe8023c5ed0d46793d4b7d7e6cd9c04e68eabe5b0aeea836e37bdcec2", + "sha256:80eda8b3e173f0f247f711eef62be51b599b5d425c429b5d4ca6a05e9e856baa", + "sha256:8385d98f6a3bf8bb2d65a73e17ed87a3ba84f6991c155691c51112075f9ffc5d", + "sha256:88cce104c36870d70c49c7c8fd22885875d950d9ee6ab54df2745f83ba0dc365", + "sha256:9d3cdb25fa98afdd3d0892d132b8d7139e2c087da1712041f6b762e4f807cc96", + "sha256:a575913fb06e05e6b4b814d7f7468c2c660e8bb16d8d5a1faf9b33ccc569dd47", + "sha256:ac119bb76b9faa00f48128b7f5679e1d8d437365c5d26f1c2c3f0da4ce1b553d", + "sha256:c1332724be35d23a854994ff0b66530119500b6053d0bd3363265f7e5e77288d", + "sha256:d03a475165f3134f773d1388aeb19c2d25ba88b6a9733c5c590b9ff7bbfa2e0c", + "sha256:d75601ad10b059ec832e78823b348bfa1a59f6b8d545db3a24fd44362a1564cb", + "sha256:de41fd81a41e53267cb020bb3a7212861da53a7d39f863585d13ea11049cf277", + "sha256:e710bf40870f4db63c3d7d929aa9e09e4e7ee219e703f949ec4073b4294f6172", + "sha256:ea25acb556320250756e53f9e20a4177515f012c9eaea17eb7587a8c4d8ae034", + "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", + "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], "markers": "python_version >= '3.7'", - "version": "==43.0.0" + "version": "==43.0.1" }, "django": { "hashes": [ - "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", - "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a" + "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", + "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.2.15" + "markers": "python_version >= '3.10'", + "version": "==5.1.1" }, "django-cors-headers": { "hashes": [ - "sha256:0b1fd19297e37417fc9f835d39e45c8c642938ddba1acce0c1753d3edef04f36", - "sha256:0bf65ef45e606aff1994d35503e6b677c0b26cafff6506f8fd7187f3be840207" + "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", + "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.3.1" + "version": "==4.4.0" }, "django-environ": { "hashes": [ @@ -502,12 +502,12 @@ }, "django-filter": { "hashes": [ - "sha256:48e5fc1da3ccd6ca0d5f9bb550973518ce977a4edde9d2a8a154a7f4f0b9f96e", - "sha256:df2ee9857e18d38bed203c8745f62a803fa0f31688c9fe6f8e868120b1848e48" + "sha256:c4852822928ce17fb699bcfccd644b3574f1a2d80aeb2b4ff4f16b02dd49dc64", + "sha256:d8ccaf6732afd21ca0542f6733b11591030fa98669f8d15599b358e24a2cd9c3" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.2" + "version": "==24.3" }, "django-maintenance-mode": { "hashes": [ @@ -517,15 +517,6 @@ "index": "pypi", "version": "==0.21.1" }, - "django-model-utils": { - "hashes": [ - "sha256:1220f22d9a467d53a1e0f4cda4857df0b2f757edf9a29955c42461988caa648a", - "sha256:f1141fc71796242edeffed5ad53a8cc57f00d345eb5a3a63e3f69401cd562ee2" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.5.1" - }, "django-queryset-csv": { "hashes": [ "sha256:46b4fd55686d40c81d4ee725155bde73c9ffd201b7f87d9abfea3679cc7a4a86" @@ -727,12 +718,12 @@ }, "gunicorn": { "hashes": [ - "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", - "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==22.0.0" + "version": "==23.0.0" }, "healthy-django": { "hashes": [ @@ -850,11 +841,11 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "inflection": { "hashes": [ @@ -874,12 +865,12 @@ }, "jsonschema": { "hashes": [ - "sha256:5b22d434a45935119af990552c862e5d6d564e8f6601206b305a61fdf661a2b7", - "sha256:ff4cfd6b1367a40e7bc6411caec72effadd3db0bbe5017de188f2d6108335802" + "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4", + "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==4.22.0" + "version": "==4.23.0" }, "jsonschema-specifications": { "hashes": [ @@ -900,115 +891,117 @@ }, "kombu": { "hashes": [ - "sha256:ad200a8dbdaaa2bbc5f26d2ee7d707d9a1fded353a0f4bd751ce8c7d9f449c60", - "sha256:c8dd99820467610b4febbc7a9e8a0d3d7da2d35116b67184418b51cc520ea6b6" + "sha256:1c05178826dab811f8cab5b0a154d42a7a33d8bcdde9fa3d7b4582e43c3c03db", + "sha256:621d365f234e4c089596f3a2510f1ade07026efc28caca426161d8f458786cab" ], "markers": "python_version >= '3.8'", - "version": "==5.4.0" + "version": "==5.4.1" }, "more-itertools": { "hashes": [ - "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27", - "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923" + "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", + "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6" ], "markers": "python_version >= '3.8'", - "version": "==10.4.0" + "version": "==10.5.0" }, "multidict": { "hashes": [ - "sha256:01265f5e40f5a17f8241d52656ed27192be03bfa8764d88e8220141d1e4b3556", - "sha256:0275e35209c27a3f7951e1ce7aaf93ce0d163b28948444bec61dd7badc6d3f8c", - "sha256:04bde7a7b3de05732a4eb39c94574db1ec99abb56162d6c520ad26f83267de29", - "sha256:04da1bb8c8dbadf2a18a452639771951c662c5ad03aefe4884775454be322c9b", - "sha256:09a892e4a9fb47331da06948690ae38eaa2426de97b4ccbfafbdcbe5c8f37ff8", - "sha256:0d63c74e3d7ab26de115c49bffc92cc77ed23395303d496eae515d4204a625e7", - "sha256:107c0cdefe028703fb5dafe640a409cb146d44a6ae201e55b35a4af8e95457dd", - "sha256:141b43360bfd3bdd75f15ed811850763555a251e38b2405967f8e25fb43f7d40", - "sha256:14c2976aa9038c2629efa2c148022ed5eb4cb939e15ec7aace7ca932f48f9ba6", - "sha256:19fe01cea168585ba0f678cad6f58133db2aa14eccaf22f88e4a6dccadfad8b3", - "sha256:1d147090048129ce3c453f0292e7697d333db95e52616b3793922945804a433c", - "sha256:1d9ea7a7e779d7a3561aade7d596649fbecfa5c08a7674b11b423783217933f9", - "sha256:215ed703caf15f578dca76ee6f6b21b7603791ae090fbf1ef9d865571039ade5", - "sha256:21fd81c4ebdb4f214161be351eb5bcf385426bf023041da2fd9e60681f3cebae", - "sha256:220dd781e3f7af2c2c1053da9fa96d9cf3072ca58f057f4c5adaaa1cab8fc442", - "sha256:228b644ae063c10e7f324ab1ab6b548bdf6f8b47f3ec234fef1093bc2735e5f9", - "sha256:29bfeb0dff5cb5fdab2023a7a9947b3b4af63e9c47cae2a10ad58394b517fddc", - "sha256:2f4848aa3baa109e6ab81fe2006c77ed4d3cd1e0ac2c1fbddb7b1277c168788c", - "sha256:2faa5ae9376faba05f630d7e5e6be05be22913782b927b19d12b8145968a85ea", - "sha256:2ffc42c922dbfddb4a4c3b438eb056828719f07608af27d163191cb3e3aa6cc5", - "sha256:37b15024f864916b4951adb95d3a80c9431299080341ab9544ed148091b53f50", - "sha256:3cc2ad10255f903656017363cd59436f2111443a76f996584d1077e43ee51182", - "sha256:3d25f19500588cbc47dc19081d78131c32637c25804df8414463ec908631e453", - "sha256:403c0911cd5d5791605808b942c88a8155c2592e05332d2bf78f18697a5fa15e", - "sha256:411bf8515f3be9813d06004cac41ccf7d1cd46dfe233705933dd163b60e37600", - "sha256:425bf820055005bfc8aa9a0b99ccb52cc2f4070153e34b701acc98d201693733", - "sha256:435a0984199d81ca178b9ae2c26ec3d49692d20ee29bc4c11a2a8d4514c67eda", - "sha256:4a6a4f196f08c58c59e0b8ef8ec441d12aee4125a7d4f4fef000ccb22f8d7241", - "sha256:4cc0ef8b962ac7a5e62b9e826bd0cd5040e7d401bc45a6835910ed699037a461", - "sha256:51d035609b86722963404f711db441cf7134f1889107fb171a970c9701f92e1e", - "sha256:53689bb4e102200a4fafa9de9c7c3c212ab40a7ab2c8e474491914d2305f187e", - "sha256:55205d03e8a598cfc688c71ca8ea5f66447164efff8869517f175ea632c7cb7b", - "sha256:5c0631926c4f58e9a5ccce555ad7747d9a9f8b10619621f22f9635f069f6233e", - "sha256:5cb241881eefd96b46f89b1a056187ea8e9ba14ab88ba632e68d7a2ecb7aadf7", - "sha256:60d698e8179a42ec85172d12f50b1668254628425a6bd611aba022257cac1386", - "sha256:612d1156111ae11d14afaf3a0669ebf6c170dbb735e510a7438ffe2369a847fd", - "sha256:6214c5a5571802c33f80e6c84713b2c79e024995b9c5897f794b43e714daeec9", - "sha256:6939c95381e003f54cd4c5516740faba40cf5ad3eeff460c3ad1d3e0ea2549bf", - "sha256:69db76c09796b313331bb7048229e3bee7928eb62bab5e071e9f7fcc4879caee", - "sha256:6bf7a982604375a8d49b6cc1b781c1747f243d91b81035a9b43a2126c04766f5", - "sha256:766c8f7511df26d9f11cd3a8be623e59cca73d44643abab3f8c8c07620524e4a", - "sha256:76c0de87358b192de7ea9649beb392f107dcad9ad27276324c24c91774ca5271", - "sha256:76f067f5121dcecf0d63a67f29080b26c43c71a98b10c701b0677e4a065fbd54", - "sha256:7901c05ead4b3fb75113fb1dd33eb1253c6d3ee37ce93305acd9d38e0b5f21a4", - "sha256:79660376075cfd4b2c80f295528aa6beb2058fd289f4c9252f986751a4cd0496", - "sha256:79a6d2ba910adb2cbafc95dad936f8b9386e77c84c35bc0add315b856d7c3abb", - "sha256:7afcdd1fc07befad18ec4523a782cde4e93e0a2bf71239894b8d61ee578c1319", - "sha256:7be7047bd08accdb7487737631d25735c9a04327911de89ff1b26b81745bd4e3", - "sha256:7c6390cf87ff6234643428991b7359b5f59cc15155695deb4eda5c777d2b880f", - "sha256:7df704ca8cf4a073334e0427ae2345323613e4df18cc224f647f251e5e75a527", - "sha256:85f67aed7bb647f93e7520633d8f51d3cbc6ab96957c71272b286b2f30dc70ed", - "sha256:896ebdcf62683551312c30e20614305f53125750803b614e9e6ce74a96232604", - "sha256:92d16a3e275e38293623ebf639c471d3e03bb20b8ebb845237e0d3664914caef", - "sha256:99f60d34c048c5c2fabc766108c103612344c46e35d4ed9ae0673d33c8fb26e8", - "sha256:9fe7b0653ba3d9d65cbe7698cca585bf0f8c83dbbcc710db9c90f478e175f2d5", - "sha256:a3145cb08d8625b2d3fee1b2d596a8766352979c9bffe5d7833e0503d0f0b5e5", - "sha256:aeaf541ddbad8311a87dd695ed9642401131ea39ad7bc8cf3ef3967fd093b626", - "sha256:b55358304d7a73d7bdf5de62494aaf70bd33015831ffd98bc498b433dfe5b10c", - "sha256:b82cc8ace10ab5bd93235dfaab2021c70637005e1ac787031f4d1da63d493c1d", - "sha256:c0868d64af83169e4d4152ec612637a543f7a336e4a307b119e98042e852ad9c", - "sha256:c1c1496e73051918fcd4f58ff2e0f2f3066d1c76a0c6aeffd9b45d53243702cc", - "sha256:c9bf56195c6bbd293340ea82eafd0071cb3d450c703d2c93afb89f93b8386ccc", - "sha256:cbebcd5bcaf1eaf302617c114aa67569dd3f090dd0ce8ba9e35e9985b41ac35b", - "sha256:cd6c8fca38178e12c00418de737aef1261576bd1b6e8c6134d3e729a4e858b38", - "sha256:ceb3b7e6a0135e092de86110c5a74e46bda4bd4fbfeeb3a3bcec79c0f861e450", - "sha256:cf590b134eb70629e350691ecca88eac3e3b8b3c86992042fb82e3cb1830d5e1", - "sha256:d3eb1ceec286eba8220c26f3b0096cf189aea7057b6e7b7a2e60ed36b373b77f", - "sha256:d65f25da8e248202bd47445cec78e0025c0fe7582b23ec69c3b27a640dd7a8e3", - "sha256:d6f6d4f185481c9669b9447bf9d9cf3b95a0e9df9d169bbc17e363b7d5487755", - "sha256:d84a5c3a5f7ce6db1f999fb9438f686bc2e09d38143f2d93d8406ed2dd6b9226", - "sha256:d946b0a9eb8aaa590df1fe082cee553ceab173e6cb5b03239716338629c50c7a", - "sha256:dce1c6912ab9ff5f179eaf6efe7365c1f425ed690b03341911bf4939ef2f3046", - "sha256:de170c7b4fe6859beb8926e84f7d7d6c693dfe8e27372ce3b76f01c46e489fcf", - "sha256:e02021f87a5b6932fa6ce916ca004c4d441509d33bbdbeca70d05dff5e9d2479", - "sha256:e030047e85cbcedbfc073f71836d62dd5dadfbe7531cae27789ff66bc551bd5e", - "sha256:e0e79d91e71b9867c73323a3444724d496c037e578a0e1755ae159ba14f4f3d1", - "sha256:e4428b29611e989719874670fd152b6625500ad6c686d464e99f5aaeeaca175a", - "sha256:e4972624066095e52b569e02b5ca97dbd7a7ddd4294bf4e7247d52635630dd83", - "sha256:e7be68734bd8c9a513f2b0cfd508802d6609da068f40dc57d4e3494cefc92929", - "sha256:e8e94e6912639a02ce173341ff62cc1201232ab86b8a8fcc05572741a5dc7d93", - "sha256:ea1456df2a27c73ce51120fa2f519f1bea2f4a03a917f4a43c8707cf4cbbae1a", - "sha256:ebd8d160f91a764652d3e51ce0d2956b38efe37c9231cd82cfc0bed2e40b581c", - "sha256:eca2e9d0cc5a889850e9bbd68e98314ada174ff6ccd1129500103df7a94a7a44", - "sha256:edd08e6f2f1a390bf137080507e44ccc086353c8e98c657e666c017718561b89", - "sha256:f285e862d2f153a70586579c15c44656f888806ed0e5b56b64489afe4a2dbfba", - "sha256:f2a1dee728b52b33eebff5072817176c172050d44d67befd681609b4746e1c2e", - "sha256:f7e301075edaf50500f0b341543c41194d8df3ae5caf4702f2095f3ca73dd8da", - "sha256:fb616be3538599e797a2017cccca78e354c767165e8858ab5116813146041a24", - "sha256:fce28b3c8a81b6b36dfac9feb1de115bab619b3c13905b419ec71d03a3fc1423", - "sha256:fe5d7785250541f7f5019ab9cba2c71169dc7d74d0f45253f8313f436458a4ef" - ], - "markers": "python_version >= '3.7'", - "version": "==6.0.5" + "sha256:052e10d2d37810b99cc170b785945421141bf7bb7d2f8799d431e7db229c385f", + "sha256:06809f4f0f7ab7ea2cabf9caca7d79c22c0758b58a71f9d32943ae13c7ace056", + "sha256:071120490b47aa997cca00666923a83f02c7fbb44f71cf7f136df753f7fa8761", + "sha256:0c3f390dc53279cbc8ba976e5f8035eab997829066756d811616b652b00a23a3", + "sha256:0e2b90b43e696f25c62656389d32236e049568b39320e2735d51f08fd362761b", + "sha256:0e5f362e895bc5b9e67fe6e4ded2492d8124bdf817827f33c5b46c2fe3ffaca6", + "sha256:10524ebd769727ac77ef2278390fb0068d83f3acb7773792a5080f2b0abf7748", + "sha256:10a9b09aba0c5b48c53761b7c720aaaf7cf236d5fe394cd399c7ba662d5f9966", + "sha256:16e5f4bf4e603eb1fdd5d8180f1a25f30056f22e55ce51fb3d6ad4ab29f7d96f", + "sha256:188215fc0aafb8e03341995e7c4797860181562380f81ed0a87ff455b70bf1f1", + "sha256:189f652a87e876098bbc67b4da1049afb5f5dfbaa310dd67c594b01c10388db6", + "sha256:1ca0083e80e791cffc6efce7660ad24af66c8d4079d2a750b29001b53ff59ada", + "sha256:1e16bf3e5fc9f44632affb159d30a437bfe286ce9e02754759be5536b169b305", + "sha256:2090f6a85cafc5b2db085124d752757c9d251548cedabe9bd31afe6363e0aff2", + "sha256:20b9b5fbe0b88d0bdef2012ef7dee867f874b72528cf1d08f1d59b0e3850129d", + "sha256:22ae2ebf9b0c69d206c003e2f6a914ea33f0a932d4aa16f236afc049d9958f4a", + "sha256:22f3105d4fb15c8f57ff3959a58fcab6ce36814486500cd7485651230ad4d4ef", + "sha256:23bfd518810af7de1116313ebd9092cb9aa629beb12f6ed631ad53356ed6b86c", + "sha256:27e5fc84ccef8dfaabb09d82b7d179c7cf1a3fbc8a966f8274fcb4ab2eb4cadb", + "sha256:3380252550e372e8511d49481bd836264c009adb826b23fefcc5dd3c69692f60", + "sha256:3702ea6872c5a2a4eeefa6ffd36b042e9773f05b1f37ae3ef7264b1163c2dcf6", + "sha256:37bb93b2178e02b7b618893990941900fd25b6b9ac0fa49931a40aecdf083fe4", + "sha256:3914f5aaa0f36d5d60e8ece6a308ee1c9784cd75ec8151062614657a114c4478", + "sha256:3a37ffb35399029b45c6cc33640a92bef403c9fd388acce75cdc88f58bd19a81", + "sha256:3c8b88a2ccf5493b6c8da9076fb151ba106960a2df90c2633f342f120751a9e7", + "sha256:3e97b5e938051226dc025ec80980c285b053ffb1e25a3db2a3aa3bc046bf7f56", + "sha256:3ec660d19bbc671e3a6443325f07263be452c453ac9e512f5eb935e7d4ac28b3", + "sha256:3efe2c2cb5763f2f1b275ad2bf7a287d3f7ebbef35648a9726e3b69284a4f3d6", + "sha256:483a6aea59cb89904e1ceabd2b47368b5600fb7de78a6e4a2c2987b2d256cf30", + "sha256:4867cafcbc6585e4b678876c489b9273b13e9fff9f6d6d66add5e15d11d926cb", + "sha256:48e171e52d1c4d33888e529b999e5900356b9ae588c2f09a52dcefb158b27506", + "sha256:4a9cb68166a34117d6646c0023c7b759bf197bee5ad4272f420a0141d7eb03a0", + "sha256:4b820514bfc0b98a30e3d85462084779900347e4d49267f747ff54060cc33925", + "sha256:4e18b656c5e844539d506a0a06432274d7bd52a7487e6828c63a63d69185626c", + "sha256:4e9f48f58c2c523d5a06faea47866cd35b32655c46b443f163d08c6d0ddb17d6", + "sha256:50b3a2710631848991d0bf7de077502e8994c804bb805aeb2925a981de58ec2e", + "sha256:55b6d90641869892caa9ca42ff913f7ff1c5ece06474fbd32fb2cf6834726c95", + "sha256:57feec87371dbb3520da6192213c7d6fc892d5589a93db548331954de8248fd2", + "sha256:58130ecf8f7b8112cdb841486404f1282b9c86ccb30d3519faf301b2e5659133", + "sha256:5845c1fd4866bb5dd3125d89b90e57ed3138241540897de748cdf19de8a2fca2", + "sha256:59bfeae4b25ec05b34f1956eaa1cb38032282cd4dfabc5056d0a1ec4d696d3aa", + "sha256:5b48204e8d955c47c55b72779802b219a39acc3ee3d0116d5080c388970b76e3", + "sha256:5c09fcfdccdd0b57867577b719c69e347a436b86cd83747f179dbf0cc0d4c1f3", + "sha256:6180c0ae073bddeb5a97a38c03f30c233e0a4d39cd86166251617d1bbd0af436", + "sha256:682b987361e5fd7a139ed565e30d81fd81e9629acc7d925a205366877d8c8657", + "sha256:6b5d83030255983181005e6cfbac1617ce9746b219bc2aad52201ad121226581", + "sha256:6bb5992037f7a9eff7991ebe4273ea7f51f1c1c511e6a2ce511d0e7bdb754492", + "sha256:73eae06aa53af2ea5270cc066dcaf02cc60d2994bbb2c4ef5764949257d10f43", + "sha256:76f364861c3bfc98cbbcbd402d83454ed9e01a5224bb3a28bf70002a230f73e2", + "sha256:820c661588bd01a0aa62a1283f20d2be4281b086f80dad9e955e690c75fb54a2", + "sha256:82176036e65644a6cc5bd619f65f6f19781e8ec2e5330f51aa9ada7504cc1926", + "sha256:87701f25a2352e5bf7454caa64757642734da9f6b11384c1f9d1a8e699758057", + "sha256:9079dfc6a70abe341f521f78405b8949f96db48da98aeb43f9907f342f627cdc", + "sha256:90f8717cb649eea3504091e640a1b8568faad18bd4b9fcd692853a04475a4b80", + "sha256:957cf8e4b6e123a9eea554fa7ebc85674674b713551de587eb318a2df3e00255", + "sha256:99f826cbf970077383d7de805c0681799491cb939c25450b9b5b3ced03ca99f1", + "sha256:9f636b730f7e8cb19feb87094949ba54ee5357440b9658b2a32a5ce4bce53972", + "sha256:a114d03b938376557927ab23f1e950827c3b893ccb94b62fd95d430fd0e5cf53", + "sha256:a185f876e69897a6f3325c3f19f26a297fa058c5e456bfcff8015e9a27e83ae1", + "sha256:a7a9541cd308eed5e30318430a9c74d2132e9a8cb46b901326272d780bf2d423", + "sha256:aa466da5b15ccea564bdab9c89175c762bc12825f4659c11227f515cee76fa4a", + "sha256:aaed8b0562be4a0876ee3b6946f6869b7bcdb571a5d1496683505944e268b160", + "sha256:ab7c4ceb38d91570a650dba194e1ca87c2b543488fe9309b4212694174fd539c", + "sha256:ac10f4c2b9e770c4e393876e35a7046879d195cd123b4f116d299d442b335bcd", + "sha256:b04772ed465fa3cc947db808fa306d79b43e896beb677a56fb2347ca1a49c1fa", + "sha256:b1c416351ee6271b2f49b56ad7f308072f6f44b37118d69c2cad94f3fa8a40d5", + "sha256:b225d95519a5bf73860323e633a664b0d85ad3d5bede6d30d95b35d4dfe8805b", + "sha256:b2f59caeaf7632cc633b5cf6fc449372b83bbdf0da4ae04d5be36118e46cc0aa", + "sha256:b58c621844d55e71c1b7f7c498ce5aa6985d743a1a59034c57a905b3f153c1ef", + "sha256:bf6bea52ec97e95560af5ae576bdac3aa3aae0b6758c6efa115236d9e07dae44", + "sha256:c08be4f460903e5a9d0f76818db3250f12e9c344e79314d1d570fc69d7f4eae4", + "sha256:c7053d3b0353a8b9de430a4f4b4268ac9a4fb3481af37dfe49825bf45ca24156", + "sha256:c943a53e9186688b45b323602298ab727d8865d8c9ee0b17f8d62d14b56f0753", + "sha256:ce2186a7df133a9c895dea3331ddc5ddad42cdd0d1ea2f0a51e5d161e4762f28", + "sha256:d093be959277cb7dee84b801eb1af388b6ad3ca6a6b6bf1ed7585895789d027d", + "sha256:d094ddec350a2fb899fec68d8353c78233debde9b7d8b4beeafa70825f1c281a", + "sha256:d1a9dd711d0877a1ece3d2e4fea11a8e75741ca21954c919406b44e7cf971304", + "sha256:d569388c381b24671589335a3be6e1d45546c2988c2ebe30fdcada8457a31008", + "sha256:d618649d4e70ac6efcbba75be98b26ef5078faad23592f9b51ca492953012429", + "sha256:d83a047959d38a7ff552ff94be767b7fd79b831ad1cd9920662db05fec24fe72", + "sha256:d8fff389528cad1618fb4b26b95550327495462cd745d879a8c7c2115248e399", + "sha256:da1758c76f50c39a2efd5e9859ce7d776317eb1dd34317c8152ac9251fc574a3", + "sha256:db7457bac39421addd0c8449933ac32d8042aae84a14911a757ae6ca3eef1392", + "sha256:e27bbb6d14416713a8bd7aaa1313c0fc8d44ee48d74497a0ff4c3a1b6ccb5167", + "sha256:e617fb6b0b6953fffd762669610c1c4ffd05632c138d61ac7e14ad187870669c", + "sha256:e9aa71e15d9d9beaad2c6b9319edcdc0a49a43ef5c0a4c8265ca9ee7d6c67774", + "sha256:ec2abea24d98246b94913b76a125e855eb5c434f7c46546046372fe60f666351", + "sha256:f179dee3b863ab1c59580ff60f9d99f632f34ccb38bf67a33ec6b3ecadd0fd76", + "sha256:f4c035da3f544b1882bac24115f3e2e8760f10a0107614fc9839fd232200b875", + "sha256:f67f217af4b1ff66c68a87318012de788dd95fcfeb24cc889011f4e1c7454dfd", + "sha256:f90c822a402cb865e396a504f9fc8173ef34212a342d92e362ca498cad308e28", + "sha256:ff3827aef427c89a25cc96ded1759271a93603aba9fb977a6d264648ebf989db" + ], + "markers": "python_version >= '3.8'", + "version": "==6.1.0" }, "newrelic": { "hashes": [ @@ -1056,79 +1049,90 @@ }, "pillow": { "hashes": [ - "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c", - "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2", - "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb", - "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d", - "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa", - "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3", - "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1", - "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a", - "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd", - "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8", - "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999", - "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599", - "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936", - "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375", - "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d", - "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b", - "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60", - "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572", - "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3", - "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced", - "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f", - "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b", - "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19", - "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f", - "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d", - "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383", - "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795", - "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355", - "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57", - "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09", - "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b", - "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462", - "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf", - "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f", - "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a", - "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad", - "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9", - "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d", - "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45", - "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994", - "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d", - "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338", - "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463", - "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451", - "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591", - "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c", - "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd", - "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32", - "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9", - "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf", - "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5", - "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828", - "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3", - "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5", - "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2", - "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b", - "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2", - "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475", - "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3", - "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb", - "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef", - "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015", - "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002", - "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170", - "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84", - "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57", - "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f", - "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27", - "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a" + "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885", + "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea", + "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df", + "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5", + "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c", + "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d", + "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd", + "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06", + "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908", + "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a", + "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be", + "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0", + "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b", + "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80", + "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a", + "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e", + "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9", + "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696", + "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b", + "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309", + "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e", + "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab", + "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d", + "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060", + "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d", + "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d", + "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4", + "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3", + "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6", + "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb", + "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94", + "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b", + "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496", + "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0", + "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319", + "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b", + "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856", + "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef", + "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680", + "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b", + "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42", + "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e", + "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597", + "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a", + "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8", + "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3", + "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736", + "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da", + "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126", + "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd", + "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5", + "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b", + "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026", + "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b", + "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc", + "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46", + "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2", + "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c", + "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe", + "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984", + "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a", + "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70", + "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca", + "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b", + "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91", + "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3", + "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84", + "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1", + "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5", + "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be", + "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f", + "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc", + "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9", + "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e", + "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141", + "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef", + "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22", + "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27", + "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e", + "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==10.3.0" + "version": "==10.4.0" }, "ply": { "hashes": [ @@ -1263,12 +1267,12 @@ }, "pyjwt": { "hashes": [ - "sha256:57e28d156e3d5c10088e0c68abb90bfac3df82b40a71bd0daa20c65ccd5c23de", - "sha256:59127c392cc44c2da5bb3192169a91f429924e17aff6534d70fdc02ab3e04320" + "sha256:3b02fb0f44517787776cf48f2ae25d8e14f300e6d7545a4315cee571a415e850", + "sha256:7e1e5b56cc735432a7369cbfa0efe50fa113ebecdc04ae6922deba8b84582d0c" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.8.0" + "markers": "python_version >= '3.8'", + "version": "==2.9.0" }, "python-dateutil": { "hashes": [ @@ -1524,12 +1528,20 @@ }, "sentry-sdk": { "hashes": [ - "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", - "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260" + "sha256:1e0e2eaf6dad918c7d1e0edac868a7bf20017b177f242cefe2a6bcd47955961d", + "sha256:b8bc3dc51d06590df1291b7519b85c75e2ced4f28d9ea655b6d54033503b5bf4" ], "index": "pypi", "markers": "python_version >= '3.6'", - "version": "==2.13.0" + "version": "==2.14.0" + }, + "setuptools": { + "hashes": [ + "sha256:6c1fccdac05a97e598fb0ae3bbed5904ccb317337a51139dcd51453611bbb987", + "sha256:c636ac361bc47580504644275c9ad802c50415c7522212252c033bd15f301f32" + ], + "markers": "python_version >= '3.12'", + "version": "==69.5.1" }, "six": { "hashes": [ @@ -1572,19 +1584,19 @@ }, "types-redis": { "hashes": [ - "sha256:08f51f550ad41d0152bd98d77ac9d6d8f761369121710a213642f6036b9a7183", - "sha256:86db9af6f0033154e12bc22c77236cef0907b995fda8c9f0f0eacd59943ed2fc" + "sha256:0e7537e5c085fe96b7d468d5edae0cf667b4ba4b62c6e4a5dfc340bd3b868c23", + "sha256:4bab1a378dbf23c2c95c370dfdb89a8f033957c4fd1a53fee71b529c182fe008" ], "markers": "python_version >= '3.8'", - "version": "==4.6.0.20240819" + "version": "==4.6.0.20240903" }, "types-setuptools": { "hashes": [ - "sha256:3a060681098eb3fbc2fea0a86f7f6af6aa1ca71906039d88d891ea2cecdd4dbf", - "sha256:b9eba9b68546031317a0fa506d4973641d987d74f79e7dd8369ad4f7a93dea17" + "sha256:06f78307e68d1bbde6938072c57b81cf8a99bc84bd6dc7e4c5014730b097dc0c", + "sha256:12f12a165e7ed383f31def705e5c0fa1c26215dd466b0af34bd042f7d5331f55" ], "markers": "python_version >= '3.8'", - "version": "==73.0.0.20240822" + "version": "==75.1.0.20240917" }, "typing-extensions": { "hashes": [ @@ -1618,11 +1630,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" }, "vine": { "hashes": [ @@ -1641,108 +1653,110 @@ }, "whitenoise": { "hashes": [ - "sha256:8998f7370973447fac1e8ef6e8ded2c5209a7b1f67c1012866dbcd09681c3251", - "sha256:b1f9db9bf67dc183484d760b99f4080185633136a273a03f6436034a41064146" + "sha256:58c7a6cd811e275a6c91af22e96e87da0b1109e9a53bb7464116ef4c963bf636", + "sha256:a1ae85e01fdc9815d12fa33f17765bc132ed2c54fa76daf9e39e879dd93566f6" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==6.6.0" + "version": "==6.7.0" }, "yarl": { "hashes": [ - "sha256:008d3e808d03ef28542372d01057fd09168419cdc8f848efe2804f894ae03e51", - "sha256:03caa9507d3d3c83bca08650678e25364e1843b484f19986a527630ca376ecce", - "sha256:07574b007ee20e5c375a8fe4a0789fad26db905f9813be0f9fef5a68080de559", - "sha256:09efe4615ada057ba2d30df871d2f668af661e971dfeedf0c159927d48bbeff0", - "sha256:0d2454f0aef65ea81037759be5ca9947539667eecebca092733b2eb43c965a81", - "sha256:0e9d124c191d5b881060a9e5060627694c3bdd1fe24c5eecc8d5d7d0eb6faabc", - "sha256:18580f672e44ce1238b82f7fb87d727c4a131f3a9d33a5e0e82b793362bf18b4", - "sha256:1f23e4fe1e8794f74b6027d7cf19dc25f8b63af1483d91d595d4a07eca1fb26c", - "sha256:206a55215e6d05dbc6c98ce598a59e6fbd0c493e2de4ea6cc2f4934d5a18d130", - "sha256:23d32a2594cb5d565d358a92e151315d1b2268bc10f4610d098f96b147370136", - "sha256:26a1dc6285e03f3cc9e839a2da83bcbf31dcb0d004c72d0730e755b33466c30e", - "sha256:29e0f83f37610f173eb7e7b5562dd71467993495e568e708d99e9d1944f561ec", - "sha256:2b134fd795e2322b7684155b7855cc99409d10b2e408056db2b93b51a52accc7", - "sha256:2d47552b6e52c3319fede1b60b3de120fe83bde9b7bddad11a69fb0af7db32f1", - "sha256:357495293086c5b6d34ca9616a43d329317feab7917518bc97a08f9e55648455", - "sha256:35a2b9396879ce32754bd457d31a51ff0a9d426fd9e0e3c33394bf4b9036b099", - "sha256:3777ce5536d17989c91696db1d459574e9a9bd37660ea7ee4d3344579bb6f129", - "sha256:3986b6f41ad22988e53d5778f91855dc0399b043fc8946d4f2e68af22ee9ff10", - "sha256:44d8ffbb9c06e5a7f529f38f53eda23e50d1ed33c6c869e01481d3fafa6b8142", - "sha256:49a180c2e0743d5d6e0b4d1a9e5f633c62eca3f8a86ba5dd3c471060e352ca98", - "sha256:4aa9741085f635934f3a2583e16fcf62ba835719a8b2b28fb2917bb0537c1dfa", - "sha256:4b21516d181cd77ebd06ce160ef8cc2a5e9ad35fb1c5930882baff5ac865eee7", - "sha256:4b3c1ffe10069f655ea2d731808e76e0f452fc6c749bea04781daf18e6039525", - "sha256:4c7d56b293cc071e82532f70adcbd8b61909eec973ae9d2d1f9b233f3d943f2c", - "sha256:4e9035df8d0880b2f1c7f5031f33f69e071dfe72ee9310cfc76f7b605958ceb9", - "sha256:54525ae423d7b7a8ee81ba189f131054defdb122cde31ff17477951464c1691c", - "sha256:549d19c84c55d11687ddbd47eeb348a89df9cb30e1993f1b128f4685cd0ebbf8", - "sha256:54beabb809ffcacbd9d28ac57b0db46e42a6e341a030293fb3185c409e626b8b", - "sha256:566db86717cf8080b99b58b083b773a908ae40f06681e87e589a976faf8246bf", - "sha256:5a2e2433eb9344a163aced6a5f6c9222c0786e5a9e9cac2c89f0b28433f56e23", - "sha256:5aef935237d60a51a62b86249839b51345f47564208c6ee615ed2a40878dccdd", - "sha256:604f31d97fa493083ea21bd9b92c419012531c4e17ea6da0f65cacdcf5d0bd27", - "sha256:63b20738b5aac74e239622d2fe30df4fca4942a86e31bf47a81a0e94c14df94f", - "sha256:686a0c2f85f83463272ddffd4deb5e591c98aac1897d65e92319f729c320eece", - "sha256:6a962e04b8f91f8c4e5917e518d17958e3bdee71fd1d8b88cdce74dd0ebbf434", - "sha256:6ad6d10ed9b67a382b45f29ea028f92d25bc0bc1daf6c5b801b90b5aa70fb9ec", - "sha256:6f5cb257bc2ec58f437da2b37a8cd48f666db96d47b8a3115c29f316313654ff", - "sha256:6fe79f998a4052d79e1c30eeb7d6c1c1056ad33300f682465e1b4e9b5a188b78", - "sha256:7855426dfbddac81896b6e533ebefc0af2f132d4a47340cee6d22cac7190022d", - "sha256:7d5aaac37d19b2904bb9dfe12cdb08c8443e7ba7d2852894ad448d4b8f442863", - "sha256:801e9264d19643548651b9db361ce3287176671fb0117f96b5ac0ee1c3530d53", - "sha256:81eb57278deb6098a5b62e88ad8281b2ba09f2f1147c4767522353eaa6260b31", - "sha256:824d6c50492add5da9374875ce72db7a0733b29c2394890aef23d533106e2b15", - "sha256:8397a3817d7dcdd14bb266283cd1d6fc7264a48c186b986f32e86d86d35fbac5", - "sha256:848cd2a1df56ddbffeb375535fb62c9d1645dde33ca4d51341378b3f5954429b", - "sha256:84fc30f71689d7fc9168b92788abc977dc8cefa806909565fc2951d02f6b7d57", - "sha256:8619d6915b3b0b34420cf9b2bb6d81ef59d984cb0fde7544e9ece32b4b3043c3", - "sha256:8a854227cf581330ffa2c4824d96e52ee621dd571078a252c25e3a3b3d94a1b1", - "sha256:8be9e837ea9113676e5754b43b940b50cce76d9ed7d2461df1af39a8ee674d9f", - "sha256:928cecb0ef9d5a7946eb6ff58417ad2fe9375762382f1bf5c55e61645f2c43ad", - "sha256:957b4774373cf6f709359e5c8c4a0af9f6d7875db657adb0feaf8d6cb3c3964c", - "sha256:992f18e0ea248ee03b5a6e8b3b4738850ae7dbb172cc41c966462801cbf62cf7", - "sha256:9fc5fc1eeb029757349ad26bbc5880557389a03fa6ada41703db5e068881e5f2", - "sha256:a00862fb23195b6b8322f7d781b0dc1d82cb3bcac346d1e38689370cc1cc398b", - "sha256:a3a6ed1d525bfb91b3fc9b690c5a21bb52de28c018530ad85093cc488bee2dd2", - "sha256:a6327976c7c2f4ee6816eff196e25385ccc02cb81427952414a64811037bbc8b", - "sha256:a7409f968456111140c1c95301cadf071bd30a81cbd7ab829169fb9e3d72eae9", - "sha256:a825ec844298c791fd28ed14ed1bffc56a98d15b8c58a20e0e08c1f5f2bea1be", - "sha256:a8c1df72eb746f4136fe9a2e72b0c9dc1da1cbd23b5372f94b5820ff8ae30e0e", - "sha256:a9bd00dc3bc395a662900f33f74feb3e757429e545d831eef5bb280252631984", - "sha256:aa102d6d280a5455ad6a0f9e6d769989638718e938a6a0a2ff3f4a7ff8c62cc4", - "sha256:aaaea1e536f98754a6e5c56091baa1b6ce2f2700cc4a00b0d49eca8dea471074", - "sha256:ad4d7a90a92e528aadf4965d685c17dacff3df282db1121136c382dc0b6014d2", - "sha256:b8477c1ee4bd47c57d49621a062121c3023609f7a13b8a46953eb6c9716ca392", - "sha256:ba6f52cbc7809cd8d74604cce9c14868306ae4aa0282016b641c661f981a6e91", - "sha256:bac8d525a8dbc2a1507ec731d2867025d11ceadcb4dd421423a5d42c56818541", - "sha256:bef596fdaa8f26e3d66af846bbe77057237cb6e8efff8cd7cc8dff9a62278bbf", - "sha256:c0ec0ed476f77db9fb29bca17f0a8fcc7bc97ad4c6c1d8959c507decb22e8572", - "sha256:c38c9ddb6103ceae4e4498f9c08fac9b590c5c71b0370f98714768e22ac6fa66", - "sha256:c7224cab95645c7ab53791022ae77a4509472613e839dab722a72abe5a684575", - "sha256:c74018551e31269d56fab81a728f683667e7c28c04e807ba08f8c9e3bba32f14", - "sha256:ca06675212f94e7a610e85ca36948bb8fc023e458dd6c63ef71abfd482481aa5", - "sha256:d1d2532b340b692880261c15aee4dc94dd22ca5d61b9db9a8a361953d36410b1", - "sha256:d25039a474c4c72a5ad4b52495056f843a7ff07b632c1b92ea9043a3d9950f6e", - "sha256:d5ff2c858f5f6a42c2a8e751100f237c5e869cbde669a724f2062d4c4ef93551", - "sha256:d7d7f7de27b8944f1fee2c26a88b4dabc2409d2fea7a9ed3df79b67277644e17", - "sha256:d7eeb6d22331e2fd42fce928a81c697c9ee2d51400bd1a28803965883e13cead", - "sha256:d8a1c6c0be645c745a081c192e747c5de06e944a0d21245f4cf7c05e457c36e0", - "sha256:d8b889777de69897406c9fb0b76cdf2fd0f31267861ae7501d93003d55f54fbe", - "sha256:d9e09c9d74f4566e905a0b8fa668c58109f7624db96a2171f21747abc7524234", - "sha256:db8e58b9d79200c76956cefd14d5c90af54416ff5353c5bfd7cbe58818e26ef0", - "sha256:ddb2a5c08a4eaaba605340fdee8fc08e406c56617566d9643ad8bf6852778fc7", - "sha256:e0381b4ce23ff92f8170080c97678040fc5b08da85e9e292292aba67fdac6c34", - "sha256:e23a6d84d9d1738dbc6e38167776107e63307dfc8ad108e580548d1f2c587f42", - "sha256:e516dc8baf7b380e6c1c26792610230f37147bb754d6426462ab115a02944385", - "sha256:ea65804b5dc88dacd4a40279af0cdadcfe74b3e5b4c897aa0d81cf86927fee78", - "sha256:ec61d826d80fc293ed46c9dd26995921e3a82146feacd952ef0757236fc137be", - "sha256:ee04010f26d5102399bd17f8df8bc38dc7ccd7701dc77f4a68c5b8d733406958", - "sha256:f3bc6af6e2b8f92eced34ef6a96ffb248e863af20ef4fde9448cc8c9b858b749", - "sha256:f7d6b36dd2e029b6bcb8a13cf19664c7b8e19ab3a58e0fefbb5b8461447ed5ec" - ], - "markers": "python_version >= '3.7'", - "version": "==1.9.4" + "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", + "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", + "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", + "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", + "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", + "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", + "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", + "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", + "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", + "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", + "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", + "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", + "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", + "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", + "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", + "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", + "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", + "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", + "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", + "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", + "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", + "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", + "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", + "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", + "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", + "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", + "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", + "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", + "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", + "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", + "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", + "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", + "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", + "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", + "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", + "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", + "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", + "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", + "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", + "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", + "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", + "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", + "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", + "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", + "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", + "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", + "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", + "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", + "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", + "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", + "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", + "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", + "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", + "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", + "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", + "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", + "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", + "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", + "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", + "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", + "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", + "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", + "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", + "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", + "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", + "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", + "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", + "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", + "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", + "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", + "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", + "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", + "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", + "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", + "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", + "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", + "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", + "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", + "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", + "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", + "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", + "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", + "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", + "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", + "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", + "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", + "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", + "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", + "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", + "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", + "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", + "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" + ], + "markers": "python_version >= '3.8'", + "version": "==1.11.1" } }, "develop": { @@ -1771,41 +1785,41 @@ }, "black": { "hashes": [ - "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474", - "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1", - "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0", - "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8", - "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96", - "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1", - "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04", - "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021", - "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94", - "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d", - "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c", - "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7", - "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c", - "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc", - "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7", - "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d", - "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c", - "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741", - "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce", - "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb", - "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063", - "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e" + "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", + "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", + "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", + "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", + "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", + "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", + "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", + "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", + "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", + "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", + "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", + "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", + "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", + "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", + "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", + "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", + "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", + "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", + "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", + "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", + "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", + "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==24.4.2" + "version": "==24.8.0" }, "boto3": { "hashes": [ - "sha256:ada32dab854c46a877cf967b8a55ab1a7d356c3c87f1c8bd556d446ff03dfd95", - "sha256:bdc242e3ea81decc6ea551b04b2c122f088c29269d8e093b55862946aa0fcfc6" + "sha256:84b3fe1727945bc3cada832d969ddb3dc0d08fce1677064ca8bdc13a89c1a143", + "sha256:9979fe674780a0b7100eae9156d74ee374cd1638a9f61c77277e3ce712f3e496" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.0" + "version": "==1.35.19" }, "boto3-stubs": { "extras": [ @@ -1813,35 +1827,35 @@ "s3" ], "hashes": [ - "sha256:786d93ec9df8bfdb7ee21e4e4ecadcf0be4e67cbe09d16148ceba26f1706d9e1", - "sha256:80cf941607a0d9c756e13c9c19533aec29f63bdead3dbaa97ef2a501f7159fd8" + "sha256:6adace32995ae7b88675cf0bbde3b4f31876cbf57520db1ec1f392ac32660b4c", + "sha256:c5842cd82d4a1570613f178831c2b6d1b60f511b87f56cc014f2a216c03ecf5a" ], "markers": "python_version >= '3.8'", - "version": "==1.35.0" + "version": "==1.35.19" }, "botocore": { "hashes": [ - "sha256:10195e5ca764745f02b9a51df048b996ddbdc1899a44a2caf35dfb225dfea489", - "sha256:4cc51a6a486915aedc140f9d027b7e156646b7a0f7b33b1000762c81aff9a12f" + "sha256:18362b7ec748561d786aebf1dd5c9faf22c4732efbf89344314199f96d3bbb65", + "sha256:d9bc656e7dde0b3e3f3080fc54bacff6a97fd7806b98acbcc21c7f9d4d0102b9" ], "markers": "python_version >= '3.8'", - "version": "==1.35.4" + "version": "==1.35.22" }, "botocore-stubs": { "hashes": [ - "sha256:2a0a2a1c058d4b2f3070ba68e17dbcaae8b8699e81879c5de4058c0580d26368", - "sha256:ad89ec214ffa29894db9515aa69a3b44256edd30f1ba78f89bb24d146e2048fa" + "sha256:14dec6b00d974dce4747d6917a791bcea0d8efd1c8959821d8ce6ca69ce40d70", + "sha256:dae8cc433df3044ece464f4985e67a154bab51fd042757d0c59e4a9ea26d0376" ], - "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==1.35.4" + "markers": "python_version >= '3.8'", + "version": "==1.35.22" }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "cfgv": { "hashes": [ @@ -1957,91 +1971,111 @@ }, "coverage": { "hashes": [ - "sha256:015eddc5ccd5364dcb902eaecf9515636806fa1e0d5bef5769d06d0f31b54523", - "sha256:04aefca5190d1dc7a53a4c1a5a7f8568811306d7a8ee231c42fb69215571944f", - "sha256:05ac5f60faa0c704c0f7e6a5cbfd6f02101ed05e0aee4d2822637a9e672c998d", - "sha256:0bbddc54bbacfc09b3edaec644d4ac90c08ee8ed4844b0f86227dcda2d428fcb", - "sha256:1d2a830ade66d3563bb61d1e3c77c8def97b30ed91e166c67d0632c018f380f0", - "sha256:239a4e75e09c2b12ea478d28815acf83334d32e722e7433471fbf641c606344c", - "sha256:244f509f126dc71369393ce5fea17c0592c40ee44e607b6d855e9c4ac57aac98", - "sha256:25a5caf742c6195e08002d3b6c2dd6947e50efc5fc2c2205f61ecb47592d2d83", - "sha256:296a7d9bbc598e8744c00f7a6cecf1da9b30ae9ad51c566291ff1314e6cbbed8", - "sha256:2e079c9ec772fedbade9d7ebc36202a1d9ef7291bc9b3a024ca395c4d52853d7", - "sha256:33ca90a0eb29225f195e30684ba4a6db05dbef03c2ccd50b9077714c48153cac", - "sha256:33fc65740267222fc02975c061eb7167185fef4cc8f2770267ee8bf7d6a42f84", - "sha256:341dd8f61c26337c37988345ca5c8ccabeff33093a26953a1ac72e7d0103c4fb", - "sha256:34d6d21d8795a97b14d503dcaf74226ae51eb1f2bd41015d3ef332a24d0a17b3", - "sha256:3538d8fb1ee9bdd2e2692b3b18c22bb1c19ffbefd06880f5ac496e42d7bb3884", - "sha256:38a3b98dae8a7c9057bd91fbf3415c05e700a5114c5f1b5b0ea5f8f429ba6614", - "sha256:3d5a67f0da401e105753d474369ab034c7bae51a4c31c77d94030d59e41df5bd", - "sha256:50084d3516aa263791198913a17354bd1dc627d3c1639209640b9cac3fef5807", - "sha256:55f689f846661e3f26efa535071775d0483388a1ccfab899df72924805e9e7cd", - "sha256:5bc5a8c87714b0c67cfeb4c7caa82b2d71e8864d1a46aa990b5588fa953673b8", - "sha256:62bda40da1e68898186f274f832ef3e759ce929da9a9fd9fcf265956de269dbc", - "sha256:705f3d7c2b098c40f5b81790a5fedb274113373d4d1a69e65f8b68b0cc26f6db", - "sha256:75e3f4e86804023e991096b29e147e635f5e2568f77883a1e6eed74512659ab0", - "sha256:7b2a19e13dfb5c8e145c7a6ea959485ee8e2204699903c88c7d25283584bfc08", - "sha256:7cec2af81f9e7569280822be68bd57e51b86d42e59ea30d10ebdbb22d2cb7232", - "sha256:8383a6c8cefba1b7cecc0149415046b6fc38836295bc4c84e820872eb5478b3d", - "sha256:8c836309931839cca658a78a888dab9676b5c988d0dd34ca247f5f3e679f4e7a", - "sha256:8e317953bb4c074c06c798a11dbdd2cf9979dbcaa8ccc0fa4701d80042d4ebf1", - "sha256:923b7b1c717bd0f0f92d862d1ff51d9b2b55dbbd133e05680204465f454bb286", - "sha256:990fb20b32990b2ce2c5f974c3e738c9358b2735bc05075d50a6f36721b8f303", - "sha256:9aad68c3f2566dfae84bf46295a79e79d904e1c21ccfc66de88cd446f8686341", - "sha256:a5812840d1d00eafae6585aba38021f90a705a25b8216ec7f66aebe5b619fb84", - "sha256:a6519d917abb15e12380406d721e37613e2a67d166f9fb7e5a8ce0375744cd45", - "sha256:ab0b028165eea880af12f66086694768f2c3139b2c31ad5e032c8edbafca6ffc", - "sha256:aea7da970f1feccf48be7335f8b2ca64baf9b589d79e05b9397a06696ce1a1ec", - "sha256:b1196e13c45e327d6cd0b6e471530a1882f1017eb83c6229fc613cd1a11b53cd", - "sha256:b368e1aee1b9b75757942d44d7598dcd22a9dbb126affcbba82d15917f0cc155", - "sha256:bde997cac85fcac227b27d4fb2c7608a2c5f6558469b0eb704c5726ae49e1c52", - "sha256:c4c2872b3c91f9baa836147ca33650dc5c172e9273c808c3c3199c75490e709d", - "sha256:c59d2ad092dc0551d9f79d9d44d005c945ba95832a6798f98f9216ede3d5f485", - "sha256:d1da0a2e3b37b745a2b2a678a4c796462cf753aebf94edcc87dcc6b8641eae31", - "sha256:d8b7339180d00de83e930358223c617cc343dd08e1aa5ec7b06c3a121aec4e1d", - "sha256:dd4b3355b01273a56b20c219e74e7549e14370b31a4ffe42706a8cda91f19f6d", - "sha256:e08c470c2eb01977d221fd87495b44867a56d4d594f43739a8028f8646a51e0d", - "sha256:f5102a92855d518b0996eb197772f5ac2a527c0ec617124ad5242a3af5e25f85", - "sha256:f542287b1489c7a860d43a7d8883e27ca62ab84ca53c965d11dac1d3a1fab7ce", - "sha256:f78300789a708ac1f17e134593f577407d52d0417305435b134805c4fb135adb", - "sha256:f81bc26d609bf0fbc622c7122ba6307993c83c795d2d6f6f6fd8c000a770d974", - "sha256:f836c174c3a7f639bded48ec913f348c4761cbf49de4a20a956d3431a7c9cb24", - "sha256:fa21a04112c59ad54f69d80e376f7f9d0f5f9123ab87ecd18fbb9ec3a2beed56", - "sha256:fcf7d1d6f5da887ca04302db8e0e0cf56ce9a5e05f202720e49b3e8157ddb9a9", - "sha256:fd27d8b49e574e50caa65196d908f80e4dff64d7e592d0c59788b45aad7e8b35" + "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", + "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d", + "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6", + "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989", + "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c", + "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b", + "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223", + "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f", + "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56", + "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3", + "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8", + "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb", + "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388", + "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0", + "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a", + "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8", + "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f", + "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a", + "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962", + "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8", + "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391", + "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc", + "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2", + "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155", + "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb", + "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0", + "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c", + "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a", + "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004", + "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060", + "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232", + "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93", + "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129", + "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163", + "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de", + "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6", + "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23", + "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569", + "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d", + "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778", + "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d", + "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36", + "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a", + "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6", + "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34", + "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704", + "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106", + "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9", + "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862", + "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b", + "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255", + "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16", + "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3", + "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133", + "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb", + "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657", + "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d", + "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca", + "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36", + "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c", + "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e", + "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff", + "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7", + "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5", + "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02", + "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c", + "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df", + "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3", + "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a", + "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959", + "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234", + "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==7.5.3" + "version": "==7.6.1" }, "debugpy": { "hashes": [ - "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" + "sha256:0a1029a2869d01cb777216af8c53cda0476875ef02a2b6ff8b2f2c9a4b04176c", + "sha256:1cd04a73eb2769eb0bfe43f5bfde1215c5923d6924b9b90f94d15f207a402226", + "sha256:28ced650c974aaf179231668a293ecd5c63c0a671ae6d56b8795ecc5d2f48d3c", + "sha256:345d6a0206e81eb68b1493ce2fbffd57c3088e2ce4b46592077a943d2b968ca3", + "sha256:3df6692351172a42af7558daa5019651f898fc67450bf091335aa8a18fbf6f3a", + "sha256:4413b7a3ede757dc33a273a17d685ea2b0c09dbd312cc03f5534a0fd4d40750a", + "sha256:4fbb3b39ae1aa3e5ad578f37a48a7a303dad9a3d018d369bc9ec629c1cfa7408", + "sha256:55919dce65b471eff25901acf82d328bbd5b833526b6c1364bd5133754777a44", + "sha256:5b5c770977c8ec6c40c60d6f58cacc7f7fe5a45960363d6974ddb9b62dbee156", + "sha256:606bccba19f7188b6ea9579c8a4f5a5364ecd0bf5a0659c8a5d0e10dcee3032a", + "sha256:7b0fe36ed9d26cb6836b0a51453653f8f2e347ba7348f2bbfe76bfeb670bfb1c", + "sha256:7e4d594367d6407a120b76bdaa03886e9eb652c05ba7f87e37418426ad2079f7", + "sha256:8f913ee8e9fcf9d38a751f56e6de12a297ae7832749d35de26d960f14280750a", + "sha256:a697beca97dad3780b89a7fb525d5e79f33821a8bc0c06faf1f1289e549743cf", + "sha256:ad84b7cde7fd96cf6eea34ff6c4a1b7887e0fe2ea46e099e53234856f9d99a34", + "sha256:b2112cfeb34b4507399d298fe7023a16656fc553ed5246536060ca7bd0e668d0", + "sha256:b78c1250441ce893cb5035dd6f5fc12db968cc07f91cc06996b2087f7cefdd8e", + "sha256:c0a65b00b7cdd2ee0c2cf4c7335fef31e15f1b7056c7fdbce9e90193e1a8c8cb", + "sha256:c9f7c15ea1da18d2fcc2709e9f3d6de98b69a5b0fff1807fb80bc55f906691f7", + "sha256:db9fb642938a7a609a6c865c32ecd0d795d56c1aaa7a7a5722d77855d5e77f2b", + "sha256:dd3811bd63632bb25eda6bd73bea8e0521794cda02be41fa3160eb26fc29e7ed", + "sha256:e84c276489e141ed0b93b0af648eef891546143d6a48f610945416453a8ad406" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.8.1" + "version": "==1.8.5" }, "decorator": { "hashes": [ @@ -2060,12 +2094,12 @@ }, "django": { "hashes": [ - "sha256:61ee4a130efb8c451ef3467c67ca99fdce400fedd768634efc86a68c18d80d30", - "sha256:c77f926b81129493961e19c0e02188f8d07c112a1162df69bfab178ae447f94a" + "sha256:021ffb7fdab3d2d388bc8c7c2434eb9c1f6f4d09e6119010bbb1694dda286bc2", + "sha256:71603f27dac22a6533fb38d83072eea9ddb4017fead6f67f2562a40402d61c3f" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.2.15" + "markers": "python_version >= '3.10'", + "version": "==5.1.1" }, "django-coverage-plugin": { "hashes": [ @@ -2075,15 +2109,6 @@ "index": "pypi", "version": "==3.1.0" }, - "django-debug-toolbar": { - "hashes": [ - "sha256:5d7afb2ea5f8730241e5b0735396e16cd1fd8c6b53a2f3e1e30bbab9abb23728", - "sha256:9204050fcb1e4f74216c5b024bc76081451926a6303993d6c513f5e142675927" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.4.2" - }, "django-extensions": { "hashes": [ "sha256:44d27919d04e23b3f40231c4ab7af4e61ce832ef46d610cc650d53e68328410a", @@ -2095,21 +2120,20 @@ }, "django-silk": { "hashes": [ - "sha256:34abb5852315f0f3303d45b7ab4a2caa9cf670102b614dbb2ac40a5d2d5cbffb", - "sha256:35a2051672b0be86af4ce734a0df0b6674c8c63f2df730b3756ec6e52923707d" + "sha256:39ddeda80469d5495d1cbb53590a9bdd4ce30a7474dc16ac26b6cbc0d9521f50", + "sha256:b3f01ccbf46611073603a6ac2b84f578bde978ad44785f42994c3d6f81398fdc" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.1.0" + "version": "==5.2.0" }, "django-stubs": { "hashes": [ - "sha256:236bc5606e5607cb968f92b648471f9edaa461a774bc013bf9e6bff8730f6bdf", - "sha256:cb0c506cb5c54c64612e4a2ee8d6b913c6178560ec168009fe847c09747c304b" + "sha256:78e3764488fdfd2695f12502136548ec22f8d4b1780541a835042b8238d11514", + "sha256:c2502f5ecbae50c68f9a86d52b5b2447d8648fd205036dad0ccb41e19a445927" ], - "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==5.0.2" + "version": "==5.0.4" }, "django-stubs-ext": { "hashes": [ @@ -2121,45 +2145,45 @@ }, "djangorestframework-stubs": { "hashes": [ - "sha256:6c634f16fe1f9b1654cfd921eca64cd4188ce8534ab5e3ec7e44aaa0ca969d93", - "sha256:f60ee1c80abb01a77acc0169969e07c45c2739ae64667b9a0dd4a2e32697dcab" + "sha256:34539871895d66d382b6ae3655d9f95c1de7733cf50bc29097638d367ed3117d", + "sha256:79dc9018f5d5fa420f9981eec9f1e820ecbd04719791f144419cdc6c5b8e29bd" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.15.0" + "version": "==3.15.1" }, "executing": { "hashes": [ - "sha256:35afe2ce3affba8ee97f2d69927fa823b08b472b7b994e36a52a964b93d16147", - "sha256:eac49ca94516ccc753f9fb5ce82603156e590b27525a8bc32cce8ae302eb61bc" + "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf", + "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab" ], - "markers": "python_version >= '3.5'", - "version": "==2.0.1" + "markers": "python_version >= '3.8'", + "version": "==2.1.0" }, "factory-boy": { "hashes": [ - "sha256:a2cdbdb63228177aa4f1c52f4b6d83fab2b8623bf602c7dedd7eb83c0f69c04c", - "sha256:bc76d97d1a65bbd9842a6d722882098eb549ec8ee1081f9fb2e8ff29f0c300f1" + "sha256:7b1113c49736e1e9995bc2a18f4dbf2c52cf0f841103517010b1d825712ce3ca", + "sha256:8317aa5289cdfc45f9cae570feb07a6177316c82e34d14df3c2e1f22f26abef0" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.3.0" + "markers": "python_version >= '3.8'", + "version": "==3.3.1" }, "faker": { "hashes": [ - "sha256:1c44d4bdcad7237516c9a829b6a0bcb031c6a4cb0506207c480c79f74d8922bf", - "sha256:4ce108fc96053bbba3abf848e3a2885f05faa938deb987f97e4420deaec541c4" + "sha256:4294d169255a045990720d6f3fa4134b764a4cdf46ef0d3c7553d2506f1adaa1", + "sha256:e59c01d1e8b8e20a83255ab8232c143cb2af3b4f5ab6a3f5ce495f385ad8ab4c" ], "markers": "python_version >= '3.8'", - "version": "==27.4.0" + "version": "==28.4.1" }, "filelock": { "hashes": [ - "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb", - "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7" + "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0", + "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435" ], "markers": "python_version >= '3.8'", - "version": "==3.15.4" + "version": "==3.16.1" }, "flake8": { "hashes": [ @@ -2189,28 +2213,28 @@ }, "identify": { "hashes": [ - "sha256:cb171c685bdc31bcc4c1734698736a7d5b6c8bf2e0c15117f4d469c8640ae5cf", - "sha256:e79ae4406387a9d300332b5fd366d8994f1525e8414984e1a59e058b2eda2dd0" + "sha256:53863bcac7caf8d2ed85bd20312ea5dcfc22226800f6d6881f232d861db5a8f0", + "sha256:91478c5fb7c3aac5ff7bf9b4344f803843dc586832d5f110d672b19aa1984c98" ], "markers": "python_version >= '3.8'", - "version": "==2.6.0" + "version": "==2.6.1" }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "ipython": { "hashes": [ - "sha256:53eee7ad44df903a06655871cbab66d156a051fd86f3ec6750470ac9604ac1ab", - "sha256:c6ed726a140b6e725b911528f80439c534fac915246af3efc39440a6b0f9d716" + "sha256:0b99a2dc9f15fd68692e898e5568725c6d49c527d36a9fb5960ffbdeaa82ff7e", + "sha256:f68b3cb8bde357a5d7adc9598d57e22a45dfbea19eb6b98286fa3b288c9cd55c" ], "index": "pypi", "markers": "python_version >= '3.10'", - "version": "==8.25.0" + "version": "==8.27.0" }, "isort": { "hashes": [ @@ -2321,44 +2345,44 @@ }, "mypy": { "hashes": [ - "sha256:075cbf81f3e134eadaf247de187bd604748171d6b79736fa9b6c9685b4083061", - "sha256:12b6bfc1b1a66095ab413160a6e520e1dc076a28f3e22f7fb25ba3b000b4ef99", - "sha256:1ec404a7cbe9fc0e92cb0e67f55ce0c025014e26d33e54d9e506a0f2d07fe5de", - "sha256:28d0e038361b45f099cc086d9dd99c15ff14d0188f44ac883010e172ce86c38a", - "sha256:2b0695d605ddcd3eb2f736cd8b4e388288c21e7de85001e9f85df9187f2b50f9", - "sha256:3236a4c8f535a0631f85f5fcdffba71c7feeef76a6002fcba7c1a8e57c8be1ec", - "sha256:3be66771aa5c97602f382230165b856c231d1277c511c9a8dd058be4784472e1", - "sha256:3d087fcbec056c4ee34974da493a826ce316947485cef3901f511848e687c131", - "sha256:3f298531bca95ff615b6e9f2fc0333aae27fa48052903a0ac90215021cdcfa4f", - "sha256:4a2b5cdbb5dd35aa08ea9114436e0d79aceb2f38e32c21684dcf8e24e1e92821", - "sha256:4cf18f9d0efa1b16478c4c129eabec36148032575391095f73cae2e722fcf9d5", - "sha256:8b2cbaca148d0754a54d44121b5825ae71868c7592a53b7292eeb0f3fdae95ee", - "sha256:8f55583b12156c399dce2df7d16f8a5095291354f1e839c252ec6c0611e86e2e", - "sha256:92f93b21c0fe73dc00abf91022234c79d793318b8a96faac147cd579c1671746", - "sha256:9e36fb078cce9904c7989b9693e41cb9711e0600139ce3970c6ef814b6ebc2b2", - "sha256:9fd50226364cd2737351c79807775136b0abe084433b55b2e29181a4c3c878c0", - "sha256:a781f6ad4bab20eef8b65174a57e5203f4be627b46291f4589879bf4e257b97b", - "sha256:a87dbfa85971e8d59c9cc1fcf534efe664d8949e4c0b6b44e8ca548e746a8d53", - "sha256:b808e12113505b97d9023b0b5e0c0705a90571c6feefc6f215c1df9381256e30", - "sha256:bc6ac273b23c6b82da3bb25f4136c4fd42665f17f2cd850771cb600bdd2ebeda", - "sha256:cd777b780312ddb135bceb9bc8722a73ec95e042f911cc279e2ec3c667076051", - "sha256:da1cbf08fb3b851ab3b9523a884c232774008267b1f83371ace57f412fe308c2", - "sha256:e22e1527dc3d4aa94311d246b59e47f6455b8729f4968765ac1eacf9a4760bc7", - "sha256:f8c083976eb530019175aabadb60921e73b4f45736760826aa1689dda8208aee", - "sha256:f90cff89eea89273727d8783fef5d4a934be2fdca11b47def50cf5d311aff727", - "sha256:fa7ef5244615a2523b56c034becde4e9e3f9b034854c93639adb667ec9ec2976", - "sha256:fcfc70599efde5c67862a07a1aaf50e55bce629ace26bb19dc17cece5dd31ca4" + "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", + "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", + "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", + "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", + "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", + "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", + "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", + "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", + "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", + "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", + "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", + "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", + "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", + "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", + "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", + "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", + "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", + "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", + "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", + "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", + "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", + "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", + "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", + "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", + "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", + "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", + "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.10.0" + "version": "==1.11.2" }, "mypy-boto3-s3": { "hashes": [ - "sha256:74d8f3492eeff768ff6f69ac6d40bf68b40aa6e54ebe10a8d098fc3d24a54abf", - "sha256:f7300b559dee5435872625448becf159abe36b19cd7006dd78e0d51610312183" + "sha256:5b5aec56e16ef48766ae81b1807263127eebc4e6bfbeeef3f2f2cb7c30c06d03", + "sha256:9f64e1196ffecc2c6ab7bee95e848692b5f464a1df14211361ea6bbbc2038387" ], - "version": "==1.35.2" + "version": "==1.35.22" }, "mypy-extensions": { "hashes": [ @@ -2410,20 +2434,20 @@ }, "platformdirs": { "hashes": [ - "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee", - "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3" + "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", + "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb" ], "markers": "python_version >= '3.8'", - "version": "==4.2.2" + "version": "==4.3.6" }, "pre-commit": { "hashes": [ - "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a", - "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5" + "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af", + "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f" ], "index": "pypi", "markers": "python_version >= '3.9'", - "version": "==3.7.1" + "version": "==3.8.0" }, "prompt-toolkit": { "hashes": [ @@ -2606,35 +2630,35 @@ }, "types-awscrt": { "hashes": [ - "sha256:0839fe12f0f914d8f7d63ed777c728cb4eccc2d5d79a26e377d12b0604e7bf0e", - "sha256:84a9f4f422ec525c314fdf54c23a1e73edfbcec968560943ca2d41cfae623b38" + "sha256:117ff2b1bb657f09d01b7e0ce3fe3fa6e039be12d30b826896182725c9ce85b1", + "sha256:9f7f47de68799cb2bcb9e486f48d77b9f58962b92fba43cb8860da70b3c57d1b" ], - "markers": "python_version >= '3.7' and python_version < '4.0'", - "version": "==0.21.2" + "markers": "python_version >= '3.8'", + "version": "==0.21.5" }, "types-pyyaml": { "hashes": [ - "sha256:b8f76ddbd7f65440a8bda5526a9607e4c7a322dc2f8e1a8c405644f9a6f4b9af", - "sha256:deda34c5c655265fc517b546c902aa6eed2ef8d3e921e4765fe606fe2afe8d35" + "sha256:392b267f1c0fe6022952462bf5d6523f31e37f6cea49b14cee7ad634b6301570", + "sha256:d1405a86f9576682234ef83bcb4e6fff7c9305c8b1fbad5e0bcd4f7dbdc9c587" ], "markers": "python_version >= '3.8'", - "version": "==6.0.12.20240808" + "version": "==6.0.12.20240917" }, "types-requests": { "hashes": [ - "sha256:90c079ff05e549f6bf50e02e910210b98b8ff1ebdd18e19c873cd237737c1358", - "sha256:f754283e152c752e46e70942fa2a146b5bc70393522257bb85bd1ef7e019dcc3" + "sha256:2850e178db3919d9bf809e434eef65ba49d0e7e33ac92d588f4a5e295fffd405", + "sha256:59c2f673eb55f32a99b2894faf6020e1a9f4a402ad0f192bfee0b64469054310" ], "markers": "python_version >= '3.8'", - "version": "==2.32.0.20240712" + "version": "==2.32.0.20240914" }, "types-s3transfer": { "hashes": [ - "sha256:02154cce46528287ad76ad1a0153840e0492239a0887e8833466eccf84b98da0", - "sha256:49a7c81fa609ac1532f8de3756e64b58afcecad8767933310228002ec7adff74" + "sha256:60167a3bfb5c536ec6cdb5818f7f9a28edca9dc3e0b5ff85ae374526fc5e576e", + "sha256:7a3fec8cd632e2b5efb665a355ef93c2a87fdd5a45b74a949f95a9e628a86356" ], - "markers": "python_version >= '3.8' and python_version < '4.0'", - "version": "==0.10.1" + "markers": "python_version >= '3.8'", + "version": "==0.10.2" }, "typing-extensions": { "hashes": [ @@ -2646,58 +2670,56 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" }, "virtualenv": { "hashes": [ - "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a", - "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589" + "sha256:4f3ac17b81fba3ce3bd6f4ead2749a72da5929c01774948e243db9ba41df4ff6", + "sha256:ce489cac131aa58f4b25e321d6d186171f78e6cb13fafbf32a840cee67733ff4" ], "markers": "python_version >= '3.7'", - "version": "==20.26.3" + "version": "==20.26.5" }, "watchdog": { "hashes": [ - "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7", - "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767", - "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175", - "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459", - "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5", - "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429", - "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6", - "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d", - "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7", - "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28", - "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235", - "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57", - "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a", - "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5", - "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709", - "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee", - "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84", - "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd", - "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba", - "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db", - "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682", - "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35", - "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d", - "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645", - "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253", - "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193", - "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b", - "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44", - "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b", - "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625", - "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e", - "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5" + "sha256:14dd4ed023d79d1f670aa659f449bcd2733c33a35c8ffd88689d9d243885198b", + "sha256:29e4a2607bd407d9552c502d38b45a05ec26a8e40cc7e94db9bb48f861fa5abc", + "sha256:3960136b2b619510569b90f0cd96408591d6c251a75c97690f4553ca88889769", + "sha256:3e8d5ff39f0a9968952cce548e8e08f849141a4fcc1290b1c17c032ba697b9d7", + "sha256:53ed1bf71fcb8475dd0ef4912ab139c294c87b903724b6f4a8bd98e026862e6d", + "sha256:5597c051587f8757798216f2485e85eac583c3b343e9aa09127a3a6f82c65ee8", + "sha256:638bcca3d5b1885c6ec47be67bf712b00a9ab3d4b22ec0881f4889ad870bc7e8", + "sha256:6bec703ad90b35a848e05e1b40bf0050da7ca28ead7ac4be724ae5ac2653a1a0", + "sha256:726eef8f8c634ac6584f86c9c53353a010d9f311f6c15a034f3800a7a891d941", + "sha256:72990192cb63872c47d5e5fefe230a401b87fd59d257ee577d61c9e5564c62e5", + "sha256:7d1aa7e4bb0f0c65a1a91ba37c10e19dabf7eaaa282c5787e51371f090748f4b", + "sha256:8c47150aa12f775e22efff1eee9f0f6beee542a7aa1a985c271b1997d340184f", + "sha256:901ee48c23f70193d1a7bc2d9ee297df66081dd5f46f0ca011be4f70dec80dab", + "sha256:963f7c4c91e3f51c998eeff1b3fb24a52a8a34da4f956e470f4b068bb47b78ee", + "sha256:9814adb768c23727a27792c77812cf4e2fd9853cd280eafa2bcfa62a99e8bd6e", + "sha256:aa9cd6e24126d4afb3752a3e70fce39f92d0e1a58a236ddf6ee823ff7dba28ee", + "sha256:b6dc8f1d770a8280997e4beae7b9a75a33b268c59e033e72c8a10990097e5fde", + "sha256:b84bff0391ad4abe25c2740c7aec0e3de316fdf7764007f41e248422a7760a7f", + "sha256:ba32efcccfe2c58f4d01115440d1672b4eb26cdd6fc5b5818f1fb41f7c3e1889", + "sha256:bda40c57115684d0216556671875e008279dea2dc00fcd3dde126ac8e0d7a2fb", + "sha256:c4a440f725f3b99133de610bfec93d570b13826f89616377715b9cd60424db6e", + "sha256:d010be060c996db725fbce7e3ef14687cdcc76f4ca0e4339a68cc4532c382a73", + "sha256:d2ab34adc9bf1489452965cdb16a924e97d4452fcf88a50b21859068b50b5c3b", + "sha256:d7594a6d32cda2b49df3fd9abf9b37c8d2f3eab5df45c24056b4a671ac661619", + "sha256:d961f4123bb3c447d9fcdcb67e1530c366f10ab3a0c7d1c0c9943050936d4877", + "sha256:dae7a1879918f6544201d33666909b040a46421054a50e0f773e0d870ed7438d", + "sha256:dcebf7e475001d2cdeb020be630dc5b687e9acdd60d16fea6bb4508e7b94cf76", + "sha256:f627c5bf5759fdd90195b0c0431f99cff4867d212a67b384442c51136a098ed7", + "sha256:f8b2918c19e0d48f5f20df458c84692e2a054f02d9df25e6c3c930063eca64c1", + "sha256:fb223456db6e5f7bd9bbd5cd969f05aae82ae21acc00643b60d81c770abd402b" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==4.0.1" + "markers": "python_version >= '3.9'", + "version": "==5.0.2" }, "wcwidth": { "hashes": [ @@ -2708,22 +2730,22 @@ }, "werkzeug": { "hashes": [ - "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", - "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" + "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", + "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==3.0.3" + "version": "==3.0.4" } }, "docs": { "alabaster": { "hashes": [ - "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", - "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92" + "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", + "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b" ], - "markers": "python_version >= '3.9'", - "version": "==0.7.16" + "markers": "python_version >= '3.10'", + "version": "==1.0.0" }, "babel": { "hashes": [ @@ -2743,11 +2765,11 @@ }, "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", + "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" ], "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "version": "==2024.8.30" }, "charset-normalizer": { "hashes": [ @@ -2855,20 +2877,20 @@ }, "furo": { "hashes": [ - "sha256:490a00d08c0a37ecc90de03ae9227e8eb5d6f7f750edf9807f398a2bdf2358de", - "sha256:81f205a6605ebccbb883350432b4831c0196dd3d1bc92f61e1f459045b3d2b0b" + "sha256:6cd97c58b47813d3619e63e9081169880fbe331f0ca883c871ff1f3f11814f5c", + "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==2024.5.6" + "version": "==2024.8.6" }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.5'", - "version": "==3.7" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "imagesize": { "hashes": [ @@ -2962,11 +2984,11 @@ }, "mdit-py-plugins": { "hashes": [ - "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a", - "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c" + "sha256:0c673c3f889399a33b95e88d2f0d111b4447bdfea7f237dab2d488f459835636", + "sha256:5f2cd1fdb606ddf152d37ec30e46101a60512bc0e5fa1a7002c36647b09e26b5" ], "markers": "python_version >= '3.8'", - "version": "==0.4.1" + "version": "==0.4.2" }, "mdurl": { "hashes": [ @@ -2978,12 +3000,12 @@ }, "myst-parser": { "hashes": [ - "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", - "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87" + "sha256:851c9dfb44e36e56d15d05e72f02b80da21a9e0d07cba96baf5e2d476bb91531", + "sha256:b9317997552424448c6096c2558872fdb6f81d3ecb3a40ce84a7518798f3f28d" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.0.1" + "markers": "python_version >= '3.10'", + "version": "==4.0.0" }, "packaging": { "hashes": [ @@ -3086,12 +3108,12 @@ }, "sphinx": { "hashes": [ - "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3", - "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc" + "sha256:0cce1ddcc4fd3532cf1dd283bc7d886758362c5c1de6598696579ce96d8ffa5b", + "sha256:56173572ae6c1b9a38911786e206a110c9749116745873feae4f9ce88e59391d" ], "index": "pypi", - "markers": "python_version >= '3.9'", - "version": "==7.3.7" + "markers": "python_version >= '3.10'", + "version": "==8.0.2" }, "sphinx-basic-ng": { "hashes": [ @@ -3151,11 +3173,11 @@ }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", + "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" ], "markers": "python_version >= '3.8'", - "version": "==2.2.2" + "version": "==2.2.3" } } } diff --git a/care/abdm/models/consent.py b/care/abdm/models/consent.py index aafe01d726..ee4b0f8264 100644 --- a/care/abdm/models/consent.py +++ b/care/abdm/models/consent.py @@ -42,14 +42,12 @@ def default_to_time(): default=list, validators=[JSONFieldSchemaValidator(CARE_CONTEXTS)] ) - status = models.CharField( - choices=Status.choices, max_length=20, default=Status.REQUESTED.value - ) + status = models.CharField(choices=Status, max_length=20, default=Status.REQUESTED) purpose = models.CharField( - choices=Purpose.choices, max_length=20, default=Purpose.CARE_MANAGEMENT.value + choices=Purpose, max_length=20, default=Purpose.CARE_MANAGEMENT ) hi_types = ArrayField( - models.CharField(choices=HealthInformationTypes.choices, max_length=20), + models.CharField(choices=HealthInformationTypes, max_length=20), default=list, ) @@ -61,14 +59,14 @@ def default_to_time(): ) access_mode = models.CharField( - choices=AccessMode.choices, max_length=20, default=AccessMode.VIEW.value + choices=AccessMode, max_length=20, default=AccessMode.VIEW ) from_time = models.DateTimeField(null=True, blank=True, default=default_from_time) to_time = models.DateTimeField(null=True, blank=True, default=default_to_time) expiry = models.DateTimeField(null=True, blank=True, default=default_expiry) frequency_unit = models.CharField( - choices=FrequencyUnit.choices, max_length=20, default=FrequencyUnit.HOUR.value + choices=FrequencyUnit, max_length=20, default=FrequencyUnit.HOUR ) frequency_value = models.PositiveSmallIntegerField( default=1, validators=[MinValueValidator(1)] diff --git a/care/facility/admin.py b/care/facility/admin.py index 7e06f1442b..6e22e90f5d 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -40,6 +40,7 @@ ) +@admin.register(Building) class BuildingAdmin(admin.ModelAdmin): autocomplete_fields = ["facility"] search_fields = ["name"] @@ -93,43 +94,51 @@ def queryset(self, request, queryset): return queryset.filter(state__name=self.value()) +@admin.register(Facility) class FacilityAdmin(DjangoQLSearchMixin, admin.ModelAdmin): search_fields = ["name"] list_filter = [StateFilter, DistrictFilter] djangoql_completion_enabled_by_default = True +@admin.register(FacilityStaff) class FacilityStaffAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["facility", "staff"] djangoql_completion_enabled_by_default = True +@admin.register(FacilityCapacity) class FacilityCapacityAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["facility"] djangoql_completion_enabled_by_default = True +@admin.register(FacilityVolunteer) class FacilityVolunteerAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["facility", "volunteer"] djangoql_completion_enabled_by_default = True +@admin.register(Inventory) class InventoryAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["facility", "item"] djangoql_completion_enabled_by_default = True +@admin.register(InventoryItem) class InventoryItemAdmin(DjangoQLSearchMixin, admin.ModelAdmin): search_fields = ["name", "description"] djangoql_completion_enabled_by_default = True +@admin.register(Room) class RoomAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["building"] search_fields = ["building", "num"] djangoql_completion_enabled_by_default = True +@admin.register(StaffRoomAllocation) class StaffRoomAllocationAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["staff", "room"] djangoql_completion_enabled_by_default = True @@ -140,6 +149,7 @@ class AmbulanceDriverInline(DjangoQLSearchMixin, admin.TabularInline): djangoql_completion_enabled_by_default = True +@admin.register(Ambulance) class AmbulanceAdmin(admin.ModelAdmin): search_fields = ["vehicle_number"] inlines = [ @@ -148,33 +158,40 @@ class AmbulanceAdmin(admin.ModelAdmin): djangoql_completion_enabled_by_default = True +@admin.register(AmbulanceDriver) class AmbulanceDriverAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["ambulance"] djangoql_completion_enabled_by_default = True +@admin.register(PatientRegistration) class PatientAdmin(DjangoQLSearchMixin, admin.ModelAdmin): list_display = ("id", "name", "year_of_birth", "gender") djangoql_completion_enabled_by_default = True +@admin.register(PatientSample) class PatientSampleAdmin(DjangoQLSearchMixin, admin.ModelAdmin): djangoql_completion_enabled_by_default = True +@admin.register(PatientExternalTest) class PatientExternalTestAdmin(admin.ModelAdmin): pass +@admin.register(PatientInvestigation) class PatientTestAdmin(admin.ModelAdmin): pass +@admin.register(PatientInvestigationGroup) class PatientTestGroupAdmin(admin.ModelAdmin): pass class ExportCsvMixin: + @admin.action(description="Export Selected") def export_as_csv(self, request, queryset): queryset = FacilityUser.objects.all().values(*FacilityUser.CSV_MAPPING.keys()) return render_to_csv_response( @@ -183,14 +200,14 @@ def export_as_csv(self, request, queryset): field_serializer_map=FacilityUser.CSV_MAKE_PRETTY, ) - export_as_csv.short_description = "Export Selected" - +@admin.register(FacilityUser) class FacilityUserAdmin(DjangoQLSearchMixin, admin.ModelAdmin, ExportCsvMixin): djangoql_completion_enabled_by_default = True actions = ["export_as_csv"] +@admin.register(FacilityFlag) class FacilityFlagAdmin(admin.ModelAdmin): class FacilityFeatureFlagForm(forms.ModelForm): flag = forms.ChoiceField( @@ -204,34 +221,15 @@ class Meta: form = FacilityFeatureFlagForm -admin.site.register(Facility, FacilityAdmin) -admin.site.register(FacilityStaff, FacilityStaffAdmin) -admin.site.register(FacilityCapacity, FacilityCapacityAdmin) -admin.site.register(FacilityVolunteer, FacilityVolunteerAdmin) -admin.site.register(FacilityUser, FacilityUserAdmin) -admin.site.register(Building, BuildingAdmin) -admin.site.register(Room, RoomAdmin) -admin.site.register(StaffRoomAllocation, StaffRoomAllocationAdmin) -admin.site.register(InventoryItem, InventoryItemAdmin) -admin.site.register(Inventory, InventoryAdmin) admin.site.register(InventoryLog) -admin.site.register(Ambulance, AmbulanceAdmin) -admin.site.register(AmbulanceDriver, AmbulanceDriverAdmin) -admin.site.register(PatientRegistration, PatientAdmin) -admin.site.register(PatientSample, PatientSampleAdmin) admin.site.register(Disease) admin.site.register(FacilityInventoryUnit) admin.site.register(FacilityInventoryUnitConverter) admin.site.register(FacilityInventoryItem) admin.site.register(FacilityInventoryItemTag) -admin.site.register(PatientExternalTest, PatientExternalTestAdmin) -admin.site.register(PatientInvestigation, PatientTestAdmin) -admin.site.register(PatientInvestigationGroup, PatientTestGroupAdmin) admin.site.register(AssetBed) admin.site.register(Asset) admin.site.register(Bed) admin.site.register(PatientConsent) admin.site.register(FileUpload) admin.site.register(PatientConsultation) - -admin.site.register(FacilityFlag, FacilityFlagAdmin) diff --git a/care/facility/migrations/0001_initial_squashed.py b/care/facility/migrations/0001_initial_squashed.py index 9d2ed01273..45e36e44c9 100644 --- a/care/facility/migrations/0001_initial_squashed.py +++ b/care/facility/migrations/0001_initial_squashed.py @@ -6913,7 +6913,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="patientconsultation", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( models.Q(_negated=True, suggestion="R"), ("referred_to__isnull", False), ("referred_to_external__isnull", False), @@ -6925,7 +6925,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="patientconsultation", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( ("admitted", False), ("admission_date__isnull", False), _connector="OR", @@ -6996,7 +6996,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="facilitylocalgovtbody", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( ("local_body__isnull", False), ("district__isnull", False), _connector="OR", diff --git a/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py b/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py index d5f67b6f30..f13fea7311 100644 --- a/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py +++ b/care/facility/migrations/0393_rename_diagnosis_patientconsultation_deprecated_diagnosis_and_more.py @@ -174,7 +174,7 @@ def populate_consultation_diagnosis(apps, schema_editor): migrations.AddConstraint( model_name="consultationdiagnosis", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( ("is_principal", False), models.Q( ("verification_status__in", ["refuted", "entered-in-error"]), diff --git a/care/facility/migrations/0443_remove_patientconsultation_consent_records_and_more.py b/care/facility/migrations/0443_remove_patientconsultation_consent_records_and_more.py index b9e012db41..789da80658 100644 --- a/care/facility/migrations/0443_remove_patientconsultation_consent_records_and_more.py +++ b/care/facility/migrations/0443_remove_patientconsultation_consent_records_and_more.py @@ -181,7 +181,7 @@ def reverse_migrate(apps, schema_editor): migrations.AddConstraint( model_name="patientconsent", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( models.Q(("type", 2), _negated=True), ("patient_code_status__isnull", False), _connector="OR", @@ -192,7 +192,7 @@ def reverse_migrate(apps, schema_editor): migrations.AddConstraint( model_name="patientconsent", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( ("type", 2), ("patient_code_status__isnull", True), _connector="OR" ), name="patient_code_status_not_required", diff --git a/care/facility/migrations_old/0030_auto_20200327_0619.py b/care/facility/migrations_old/0030_auto_20200327_0619.py index b1bf984873..cb042f882f 100644 --- a/care/facility/migrations_old/0030_auto_20200327_0619.py +++ b/care/facility/migrations_old/0030_auto_20200327_0619.py @@ -170,7 +170,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="facilitylocalgovtbody", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( ("local_body__isnull", False), ("district__isnull", False), _connector="OR", diff --git a/care/facility/migrations_old/0035_auto_20200328_0442.py b/care/facility/migrations_old/0035_auto_20200328_0442.py index 88e31db6f7..63fe6dd53c 100644 --- a/care/facility/migrations_old/0035_auto_20200328_0442.py +++ b/care/facility/migrations_old/0035_auto_20200328_0442.py @@ -64,7 +64,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="patientconsultation", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( models.Q(_negated=True, suggestion="R"), ("referred_to__isnull", False), _connector="OR", @@ -75,7 +75,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="patientconsultation", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( ("admitted", False), ("admission_date__isnull", False), _connector="OR", diff --git a/care/facility/migrations_old/0338_auto_20230323_1249.py b/care/facility/migrations_old/0338_auto_20230323_1249.py index 56eb54275a..f86d9351a8 100644 --- a/care/facility/migrations_old/0338_auto_20230323_1249.py +++ b/care/facility/migrations_old/0338_auto_20230323_1249.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): migrations.AddConstraint( model_name="patientconsultation", constraint=models.CheckConstraint( - check=models.Q( + condition=models.Q( models.Q(_negated=True, suggestion="R"), ("referred_to__isnull", False), ("referred_to_external__isnull", False), diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index 9608a1e482..7cb027283d 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -219,7 +219,7 @@ class AvailabilityRecord(BaseModel): content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE) object_external_id = models.UUIDField() status = models.CharField( - choices=AvailabilityStatus.choices, + choices=AvailabilityStatus, default=AvailabilityStatus.NOT_MONITORED, max_length=20, ) diff --git a/care/facility/models/encounter_symptom.py b/care/facility/models/encounter_symptom.py index 1322decbf1..9412927aff 100644 --- a/care/facility/models/encounter_symptom.py +++ b/care/facility/models/encounter_symptom.py @@ -54,13 +54,13 @@ class Symptom(models.IntegerChoices): class EncounterSymptom(BaseModel, ConsultationRelatedPermissionMixin): - symptom = models.SmallIntegerField(choices=Symptom.choices, null=False, blank=False) + symptom = models.SmallIntegerField(choices=Symptom, null=False, blank=False) other_symptom = models.CharField(default="", blank=True, null=False) onset_date = models.DateTimeField(null=False, blank=False) cure_date = models.DateTimeField(null=True, blank=True) clinical_impression_status = models.CharField( max_length=255, - choices=ClinicalImpressionStatus.choices, + choices=ClinicalImpressionStatus, default=ClinicalImpressionStatus.IN_PROGRESS, ) consultation = models.ForeignKey( diff --git a/care/facility/models/events.py b/care/facility/models/events.py index 9eafe47388..9b7fbfc56a 100644 --- a/care/facility/models/events.py +++ b/care/facility/models/events.py @@ -65,7 +65,7 @@ class PatientConsultationEvent(models.Model): meta = models.JSONField(default=dict, encoder=CustomJSONEncoder) value = models.JSONField(default=dict, encoder=CustomJSONEncoder) change_type = models.CharField( - max_length=10, choices=ChangeType.choices, default=ChangeType.CREATED + max_length=10, choices=ChangeType, default=ChangeType.CREATED ) def __str__(self) -> str: diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index ec2f79a35c..da1ab2229a 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -189,7 +189,7 @@ class Facility(FacilityBaseModel, FacilityPermissionMixin): facility_type = models.IntegerField(choices=FACILITY_TYPES) kasp_empanelled = models.BooleanField(default=False, blank=False, null=False) features = ArrayField( - models.SmallIntegerField(choices=FacilityFeature.choices), + models.SmallIntegerField(choices=FacilityFeature), blank=True, null=True, ) @@ -317,7 +317,7 @@ class Meta: constraints = [ models.CheckConstraint( name="cons_facilitylocalgovtbody_only_one_null", - check=models.Q(local_body__isnull=False) + condition=models.Q(local_body__isnull=False) | models.Q(district__isnull=False), ) ] diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 2b71c65d46..4093e15e53 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -28,7 +28,7 @@ class FileCategory(models.TextChoices): associating_id = models.CharField(max_length=100, blank=False, null=False) file_type = models.IntegerField(default=0) file_category = models.CharField( - choices=FileCategory.choices, + choices=FileCategory, default=FileCategory.UNSPECIFIED, max_length=100, ) @@ -141,7 +141,7 @@ class FileType(models.IntegerChoices): CONSENT_RECORD = 7, "CONSENT_RECORD" ABDM_HEALTH_INFORMATION = 8, "ABDM_HEALTH_INFORMATION" - file_type = models.IntegerField(choices=FileType.choices, default=FileType.PATIENT) + file_type = models.IntegerField(choices=FileType, default=FileType.PATIENT) is_archived = models.BooleanField(default=False) archive_reason = models.TextField(blank=True) uploaded_by = models.ForeignKey( diff --git a/care/facility/models/icd11_diagnosis.py b/care/facility/models/icd11_diagnosis.py index c4747f59e6..68d2aa8816 100644 --- a/care/facility/models/icd11_diagnosis.py +++ b/care/facility/models/icd11_diagnosis.py @@ -102,7 +102,7 @@ class Meta: ), # Diagnosis cannot be principal if verification status is one of refuted/entered-in-error. models.CheckConstraint( - check=( + condition=( models.Q(is_principal=False) | ~models.Q( verification_status__in=INACTIVE_CONDITION_VERIFICATION_STATUSES diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 17eb7f248c..6d328414ff 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -149,7 +149,7 @@ class TestTypeEnum(enum.Enum): verbose_name="Passport Number of Foreign Patients", ) ration_card_category = models.CharField( - choices=RationCardCategory.choices, null=True, max_length=8 + choices=RationCardCategory, null=True, max_length=8 ) is_medical_worker = models.BooleanField( @@ -795,7 +795,7 @@ class PatientNotes(FacilityBaseModel, ConsultationRelatedPermissionMixin): null=True, ) thread = models.SmallIntegerField( - choices=PatientNoteThreadChoices.choices, + choices=PatientNoteThreadChoices, db_index=True, default=PatientNoteThreadChoices.DOCTORS, ) diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 24e027f608..afea466020 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -85,7 +85,7 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): procedure = JSONField(default=dict) suggestion = models.CharField(max_length=4, choices=SUGGESTION_CHOICES) route_to_facility = models.SmallIntegerField( - choices=RouteToFacility.choices, blank=True, null=True + choices=RouteToFacility, blank=True, null=True ) review_interval = models.IntegerField(default=-1) referred_to = models.ForeignKey( @@ -220,7 +220,7 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): intubation_history = JSONField(default=list) has_consents = ArrayField( - models.IntegerField(choices=ConsentType.choices), + models.IntegerField(choices=ConsentType), default=list, ) @@ -278,7 +278,7 @@ class Meta: constraints = [ models.CheckConstraint( name="if_referral_suggested", - check=~models.Q(suggestion=SuggestionChoices.R) + condition=~models.Q(suggestion=SuggestionChoices.R) | models.Q(referred_to__isnull=False) | models.Q(referred_to_external__isnull=False), ), @@ -358,9 +358,9 @@ class PatientConsent(BaseModel, ConsultationRelatedPermissionMixin): consultation = models.ForeignKey( PatientConsultation, on_delete=models.CASCADE, related_name="consents" ) - type = models.IntegerField(choices=ConsentType.choices) + type = models.IntegerField(choices=ConsentType) patient_code_status = models.IntegerField( - choices=PatientCodeStatusType.choices, null=True, blank=True + choices=PatientCodeStatusType, null=True, blank=True ) archived = models.BooleanField(default=False) archived_by = models.ForeignKey( @@ -388,12 +388,12 @@ class Meta: ), models.CheckConstraint( name="patient_code_status_required", - check=~models.Q(type=ConsentType.PATIENT_CODE_STATUS) + condition=~models.Q(type=ConsentType.PATIENT_CODE_STATUS) | models.Q(patient_code_status__isnull=False), ), models.CheckConstraint( name="patient_code_status_not_required", - check=models.Q(type=ConsentType.PATIENT_CODE_STATUS) + condition=models.Q(type=ConsentType.PATIENT_CODE_STATUS) | models.Q(patient_code_status__isnull=True), ), ] diff --git a/care/facility/models/prescription.py b/care/facility/models/prescription.py index 1ddffd563b..e0cf683b8d 100644 --- a/care/facility/models/prescription.py +++ b/care/facility/models/prescription.py @@ -113,7 +113,7 @@ class Prescription(BaseModel, ConsultationRelatedPermissionMixin): ) dosage_type = models.CharField( max_length=100, - choices=PrescriptionDosageType.choices, + choices=PrescriptionDosageType, default=PrescriptionDosageType.REGULAR.value, ) diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index 98f75613ea..d4ea975c2f 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -344,12 +344,14 @@ def test_update_patient_with_meta_info(self): format="json", ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertDictContainsSubset( - { + res_meta = res.data.get("meta_info") + self.assertEqual( + res_meta, + res_meta + | { "socioeconomic_status": "VERY_POOR", "domestic_healthcare_support": "FAMILY_MEMBER", }, - res.data.get("meta_info"), ) def test_has_consent(self): @@ -646,7 +648,6 @@ def test_filter_by_review_missed(self): self.assertIsNone(patient["review_time"]) def test_filter_by_has_consents(self): - choices = ["1", "2", "3", "4", "5", "None"] self.client.force_authenticate(user=self.user) diff --git a/care/users/admin.py b/care/users/admin.py index 5aebd9f966..d8d066e931 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -20,6 +20,7 @@ class ExportCsvMixin: + @admin.action(description="Export Selected") def export_as_csv(self, request, queryset): queryset = User.objects.filter(is_superuser=False).values( *User.CSV_MAPPING.keys() @@ -30,8 +31,6 @@ def export_as_csv(self, request, queryset): field_serializer_map=User.CSV_MAKE_PRETTY, ) - export_as_csv.short_description = "Export Selected" - @admin.register(User) class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): diff --git a/care/users/models.py b/care/users/models.py index 1ef1280f54..1f6d3d7d8d 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -1,3 +1,5 @@ +import secrets +import string import uuid from django.conf import settings @@ -154,6 +156,35 @@ def create_superuser(self, username, email, password, **extra_fields): extra_fields["user_type"] = 40 return super().create_superuser(username, email, password, **extra_fields) + def make_random_password( + self, + length: int = 10, + secure_random: bool = True, + allowed_chars: str = string.ascii_letters + string.digits + string.punctuation, + ) -> str: + """ + Generate a random password with the specified length and allowed characters. + + If secure_random is True the allowed_chars parameter is ignored and, + the generated password will contain: + - At least one lowercase letter. + - At least one uppercase letter. + - At least length // 4 digits. + """ + if secure_random: + allowed_chars = string.ascii_letters + string.digits + string.punctuation + while True: + password = "".join(secrets.choice(allowed_chars) for i in range(length)) + if ( + any(c.islower() for c in password) + and any(c.isupper() for c in password) + and sum(c.isdigit() for c in password) >= (length // 4) + ): + break + else: + password = "".join(secrets.choice(allowed_chars) for _ in range(length)) + return password + class Skill(BaseModel): name = models.CharField(max_length=255, unique=True) diff --git a/config/authentication.py b/config/authentication.py index b4efe830e0..275aaf468a 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -184,7 +184,7 @@ def get_user(self, validated_token, facility): asset_user = User( username=f"asset{str(asset_obj.external_id)}", email="support@ohc.network", - password=f"{password}123", # The 123 makes it inaccessible without hashing + password=f"{password}xyz", # The xyz makes it inaccessible without hashing gender=3, phone_number="919999999999", user_type=User.TYPE_VALUE_MAP["Nurse"], @@ -209,7 +209,7 @@ def authenticate_header(self, request): return "Bearer" def authenticate(self, request): - jwt_token = request.META.get("HTTP_AUTHORIZATION") + jwt_token = self.get_header(request) if jwt_token is None: return None jwt_token = self.get_jwt_token(jwt_token) @@ -236,7 +236,7 @@ def get_user(self, validated_token): user = User( username=settings.ABDM_USERNAME, email="hcx@ohc.network", - password=f"{password}123", + password=f"{password}xyz", gender=3, phone_number="917777777777", user_type=User.TYPE_VALUE_MAP["Volunteer"], diff --git a/config/settings/local.py b/config/settings/local.py index 9ead7719f1..a5e9516c48 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -13,23 +13,8 @@ # https://github.com/jazzband/django-silk#requirements INSTALLED_APPS += ["silk"] # noqa F405 MIDDLEWARE += ["silk.middleware.SilkyMiddleware"] # noqa F405 -# https://github.com/jazzband/django-silk#profiling SILKY_PYTHON_PROFILER = True -# django-debug-toolbar -# ------------------------------------------------------------------------------ -# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#prerequisites -INSTALLED_APPS += ["debug_toolbar"] # noqa F405 -# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#middleware -MIDDLEWARE += ["debug_toolbar.middleware.DebugToolbarMiddleware"] # noqa F405 -# https://django-debug-toolbar.readthedocs.io/en/latest/configuration.html#debug-toolbar-config -DEBUG_TOOLBAR_CONFIG = { - "DISABLE_PANELS": ["debug_toolbar.panels.redirects.RedirectsPanel"], - "SHOW_TEMPLATE_CONTEXT": True, -} -# https://django-debug-toolbar.readthedocs.io/en/latest/installation.html#internal-ips -INTERNAL_IPS = ["127.0.0.1", "10.0.2.2"] - # django-extensions # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration diff --git a/docker/dev.Dockerfile b/docker/dev.Dockerfile index 4cfd78d1c3..f83f059d2a 100644 --- a/docker/dev.Dockerfile +++ b/docker/dev.Dockerfile @@ -1,12 +1,9 @@ -FROM python:3.11-slim-bullseye - -ENV PYTHONUNBUFFERED 1 -ENV PYTHONDONTWRITEBYTECODE 1 - -ENV PATH /venv/bin:$PATH +FROM python:3.12-slim-bookworm ARG TYPST_VERSION=0.11.0 +ENV PATH=/venv/bin:$PATH + RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev \ libpq-dev gettext wget curl gnupg git \ @@ -36,7 +33,6 @@ RUN pip install pipenv COPY Pipfile Pipfile.lock ./ RUN pipenv install --system --categories "packages dev-packages" - COPY . /app RUN python3 /app/install_plugins.py diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index a1fc627425..82e17739ae 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -1,19 +1,22 @@ -ARG PYTHON_VERSION=3.11-slim-bullseye +FROM python:3.12-slim-bookworm AS base -FROM python:${PYTHON_VERSION} AS base +ARG APP_HOME=/app +ARG TYPST_VERSION=0.11.0 -ENV PYTHONUNBUFFERED 1 -ENV PYTHONDONTWRITEBYTECODE 1 +ARG BUILD_ENVIRONMENT="production" +ARG APP_VERSION="unknown" +ENV BUILD_ENVIRONMENT=$BUILD_ENVIRONMENT +ENV APP_VERSION=$APP_VERSION +ENV PYTHONUNBUFFERED=1 +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PATH=/venv/bin:$PATH + +WORKDIR $APP_HOME # --- FROM base AS builder -ARG BUILD_ENVIRONMENT=production -ARG TYPST_VERSION=0.11.0 - -ENV PATH /venv/bin:$PATH - RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential libjpeg-dev zlib1g-dev libpq-dev git wget \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ @@ -38,44 +41,28 @@ RUN ARCH=$(dpkg --print-architecture) && \ RUN python -m venv /venv RUN pip install pipenv - -COPY Pipfile Pipfile.lock ./ +COPY Pipfile Pipfile.lock $APP_HOME RUN pipenv sync --system --categories "packages" -COPY . /app - -RUN python3 /app/install_plugins.py - +COPY plugs/ $APP_HOME/plugs/ +COPY install_plugins.py plug_config.py $APP_HOME +RUN python3 $APP_HOME/install_plugins.py # --- FROM base AS runtime -ARG BUILD_ENVIRONMENT=production -ARG APP_HOME=/app -ARG APP_VERSION="unknown" - -ENV PYTHONUNBUFFERED 1 -ENV PYTHONDONTWRITEBYTECODE 1 -ENV BUILD_ENV ${BUILD_ENVIRONMENT} -ENV APP_VERSION ${APP_VERSION} - -ENV PATH /venv/bin:$PATH - -WORKDIR ${APP_HOME} - RUN apt-get update && apt-get install --no-install-recommends -y \ libpq-dev gettext wget curl gnupg \ && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \ && rm -rf /var/lib/apt/lists/* -# copy typst binary from builder stage COPY --from=builder --chmod=0755 /usr/local/bin/typst /usr/local/bin/typst -# copy in Python environment COPY --from=builder /venv /venv +COPY --chmod=0755 ./scripts/*.sh $APP_HOME -COPY --chmod=0755 ./scripts/*.sh ./ +COPY . $APP_HOME HEALTHCHECK \ --interval=30s \ @@ -84,6 +71,4 @@ HEALTHCHECK \ --retries=12 \ CMD ["/app/healthcheck.sh"] -COPY . ${APP_HOME} - EXPOSE 9000 diff --git a/docs/django-configuration/configuration.rst b/docs/django-configuration/configuration.rst index 1f83b31891..191b7cbf1b 100644 --- a/docs/django-configuration/configuration.rst +++ b/docs/django-configuration/configuration.rst @@ -1,32 +1,32 @@ Environment Variables -=============== +===================== ``MIN_ENCOUNTER_DATE`` ---------------------- +---------------------- Default value is `2020-01-01`. This is the minimum date for a possible consultation encounter. Example: `MIN_ENCOUNTER_DATE=2000-01-01` ``TASK_SUMMARIZE_TRIAGE`` ---------------------- +------------------------- Default value is `True`. If set to `False`, the celery task to summarize triage data will not be executed. Example: `TASK_SUMMARIZE_TRIAGE=False` ``TASK_SUMMARIZE_TESTS`` ---------------------- +------------------------ Default value is `True`. If set to `False`, the celery task to summarize test data will not be executed. Example: `TASK_SUMMARIZE_TESTS=False` ``TASK_SUMMARIZE_FACILITY_CAPACITY`` ---------------------- +------------------------------------ Default value is `True`. If set to `False`, the celery task to summarize facility capacity data will not be executed. Example: `TASK_SUMMARIZE_FACILITY_CAPACITY=False` ``TASK_SUMMARIZE_PATIENT`` ---------------------- +-------------------------- Default value is `True`. If set to `False`, the celery task to summarize patient data will not be executed. Example: `TASK_SUMMARIZE_PATIENT=False` ``TASK_SUMMARIZE_DISTRICT_PATIENT`` ---------------------- +----------------------------------- Default value is `True`. If set to `False`, the celery task to summarize district patient data will not be executed. Example: `TASK_SUMMARIZE_DISTRICT_PATIENT=False` From 857fe7e01b647eb94611ff322478a3f0cced370d Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Thu, 19 Sep 2024 23:56:55 +0530 Subject: [PATCH 08/46] fixing rhytmic_detail missing issue in event logs (#2467) corrected the spelling --- care/facility/management/commands/load_event_types.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index c6a92064f5..ad3511f79d 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -120,7 +120,7 @@ class Command(BaseCommand): {"name": "PULSE", "fields": ("pulse",)}, {"name": "BLOOD_PRESSURE", "fields": ("bp",)}, {"name": "RESPIRATORY_RATE", "fields": ("resp",)}, - {"name": "RHYTHM", "fields": ("rhythm", "rhythm_details")}, + {"name": "RHYTHM", "fields": ("rhythm", "rhythm_detail")}, {"name": "PAIN_SCALE", "fields": ("pain_scale_enhanced",)}, ), }, From 2788679d2c981fe1ad8fe5285c42601941cd248e Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sat, 21 Sep 2024 09:52:38 +0530 Subject: [PATCH 09/46] Update JSON schema validations of Daily Round fields (#2386) --------- Co-authored-by: Aakash Singh --- care/facility/api/serializers/daily_round.py | 7 + ...ound_bp_alter_dailyround_feeds_and_more.py | 479 ++++++++++++++++++ care/facility/models/daily_round.py | 6 +- .../models/json_schema/daily_round.py | 129 ++++- .../tests/test_patient_daily_rounds_api.py | 223 +++++++- 5 files changed, 795 insertions(+), 49 deletions(-) create mode 100644 care/facility/migrations/0460_alter_dailyround_bp_alter_dailyround_feeds_and_more.py diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 0a506143d9..1f4639ec0d 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -116,6 +116,13 @@ class Meta: ) exclude = ("deleted",) + def validate_bp(self, value): + if value is not None: + sys, dia = value.get("systolic"), value.get("diastolic") + if sys is not None and dia is not None and sys < dia: + raise ValidationError("Systolic must be greater than diastolic") + return value + def update(self, instance, validated_data): instance.last_edited_by = self.context["request"].user diff --git a/care/facility/migrations/0460_alter_dailyround_bp_alter_dailyround_feeds_and_more.py b/care/facility/migrations/0460_alter_dailyround_bp_alter_dailyround_feeds_and_more.py new file mode 100644 index 0000000000..d2a00e818b --- /dev/null +++ b/care/facility/migrations/0460_alter_dailyround_bp_alter_dailyround_feeds_and_more.py @@ -0,0 +1,479 @@ +# Generated by Django 4.2.15 on 2024-09-06 05:21 + +from django.core.paginator import Paginator +from django.db import migrations, models +from django.db.models import Q + +import care.utils.models.validators + + +def replace_io_balance_name(instance, find, replace): + for entry in instance.infusions: + if entry["name"] == find: + entry["name"] = replace + + for entry in instance.iv_fluids: + if entry["name"] == find: + entry["name"] = replace + + for entry in instance.feeds: + if entry["name"] == find: + entry["name"] = replace + + for entry in instance.output: + if entry["name"] == find: + entry["name"] = replace + + +class Migration(migrations.Migration): + """ + 1. Improves JSON Schema Validation of JSON Fields in DailyRounds model. + 2. Fills name with "Unknown" for I/O balance field items for ones with + empty string. + 3. Update blood pressure column to `None` for empty objects (`{}`). + """ + + dependencies = [ + ("facility", "0459_remove_bed_unique_bed_name_per_location_and_more"), + ] + + def forward_fill_empty_io_balance_field_names(apps, schema_editor): + DailyRound = apps.get_model("facility", "DailyRound") + + paginator = Paginator( + DailyRound.objects.filter( + Q(infusions__contains=[{"name": ""}]) + | Q(iv_fluids__contains=[{"name": ""}]) + | Q(feeds__contains=[{"name": ""}]) + | Q(output__contains=[{"name": ""}]) + ).order_by("id"), + 1000, + ) + + for page_number in paginator.page_range: + bulk = [] + for instance in paginator.page(page_number).object_list: + replace_io_balance_name(instance, find="", replace="Unknown") + bulk.append(instance) + DailyRound.objects.bulk_update( + bulk, ["infusions", "iv_fluids", "feeds", "output"] + ) + + def reverse_fill_empty_io_balance_field_names(apps, schema_editor): + DailyRound = apps.get_model("facility", "DailyRound") + + paginator = Paginator( + DailyRound.objects.filter( + Q(infusions__contains=[{"name": "Unknown"}]) + | Q(iv_fluids__contains=[{"name": "Unknown"}]) + | Q(feeds__contains=[{"name": "Unknown"}]) + | Q(output__contains=[{"name": "Unknown"}]) + ).order_by("id"), + 1000, + ) + + for page_number in paginator.page_range: + bulk = [] + for instance in paginator.page(page_number).object_list: + replace_io_balance_name(instance, find="Unknown", replace="") + bulk.append(instance) + DailyRound.objects.bulk_update( + bulk, ["infusions", "iv_fluids", "feeds", "output"] + ) + + def forward_set_empty_bp_to_null(apps, schema_editor): + DailyRound = apps.get_model("facility", "DailyRound") + DailyRound.objects.filter(bp={}).update(bp=None) + + def reverse_set_empty_bp_to_null(apps, schema_editor): + DailyRound = apps.get_model("facility", "DailyRound") + DailyRound.objects.filter(bp=None).update(bp={}) + + operations = [ + migrations.RunPython( + forward_fill_empty_io_balance_field_names, + reverse_code=reverse_fill_empty_io_balance_field_names, + ), + migrations.AlterField( + model_name="dailyround", + name="bp", + field=models.JSONField( + default=None, + null=True, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": False, + "properties": { + "diastolic": { + "maximum": 400, + "minimum": 0, + "type": "number", + }, + "systolic": { + "maximum": 400, + "minimum": 0, + "type": "number", + }, + }, + "required": ["systolic", "diastolic"], + "type": "object", + } + ) + ], + ), + ), + migrations.RunPython( + forward_set_empty_bp_to_null, + reverse_code=reverse_set_empty_bp_to_null, + ), + migrations.AlterField( + model_name="dailyround", + name="feeds", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "name": {"enum": ["Ryles Tube", "Normal Feed"]}, + "quantity": {"minimum": 0, "type": "number"}, + }, + "required": ["name", "quantity"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="dailyround", + name="infusions", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "name": { + "enum": [ + "Adrenalin", + "Noradrenalin", + "Vasopressin", + "Dopamine", + "Dobutamine", + ] + }, + "quantity": {"minimum": 0, "type": "number"}, + }, + "required": ["name", "quantity"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="dailyround", + name="iv_fluids", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "name": {"enum": ["RL", "NS", "DNS"]}, + "quantity": {"minimum": 0, "type": "number"}, + }, + "required": ["name", "quantity"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="dailyround", + name="nursing", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "description": {"type": "string"}, + "procedure": { + "enum": [ + "oral_care", + "hair_care", + "bed_bath", + "eye_care", + "perineal_care", + "skin_care", + "pre_enema", + "wound_dressing", + "lymphedema_care", + "ascitic_tapping", + "colostomy_care", + "colostomy_change", + "personal_hygiene", + "positioning", + "suctioning", + "ryles_tube_care", + "ryles_tube_change", + "iv_sitecare", + "nubulisation", + "dressing", + "dvt_pump_stocking", + "restrain", + "chest_tube_care", + "tracheostomy_care", + "tracheostomy_tube_change", + "stoma_care", + "catheter_care", + "catheter_change", + ] + }, + }, + "required": ["procedure", "description"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="dailyround", + name="output", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "name": { + "enum": [ + "Urine", + "Ryles Tube Aspiration", + "ICD", + "Abdominal Drain", + ] + }, + "quantity": {"minimum": 0, "type": "number"}, + }, + "required": ["name", "quantity"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="dailyround", + name="pain_scale_enhanced", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "description": {"type": "string"}, + "region": { + "enum": [ + "AnteriorHead", + "AnteriorNeck", + "AnteriorRightShoulder", + "AnteriorRightChest", + "AnteriorRightArm", + "AnteriorRightForearm", + "AnteriorRightHand", + "AnteriorLeftHand", + "AnteriorLeftChest", + "AnteriorLeftShoulder", + "AnteriorLeftArm", + "AnteriorLeftForearm", + "AnteriorRightFoot", + "AnteriorLeftFoot", + "AnteriorRightLeg", + "AnteriorLowerChest", + "AnteriorAbdomen", + "AnteriorLeftLeg", + "AnteriorRightThigh", + "AnteriorLeftThigh", + "AnteriorGroin", + "PosteriorHead", + "PosteriorNeck", + "PosteriorLeftChest", + "PosteriorRightChest", + "PosteriorAbdomen", + "PosteriorLeftShoulder", + "PosteriorRightShoulder", + "PosteriorLeftArm", + "PosteriorLeftForearm", + "PosteriorLeftHand", + "PosteriorRightArm", + "PosteriorRightForearm", + "PosteriorRightHand", + "PosteriorLeftThighAndButtock", + "PosteriorRightThighAndButtock", + "PosteriorLeftLeg", + "PosteriorRightLeg", + "PosteriorLeftFoot", + "PosteriorRightFoot", + ] + }, + "scale": { + "maximum": 10, + "minimum": 1, + "type": "number", + }, + }, + "required": ["region", "scale"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + migrations.AlterField( + model_name="dailyround", + name="pressure_sore", + field=models.JSONField( + default=list, + validators=[ + care.utils.models.validators.JSONFieldSchemaValidator( + { + "$schema": "http://json-schema.org/draft-07/schema#", + "items": [ + { + "additionalProperties": False, + "properties": { + "description": {"type": "string"}, + "exudate_amount": { + "enum": [ + "None", + "Light", + "Moderate", + "Heavy", + ] + }, + "length": {"minimum": 0, "type": "number"}, + "push_score": { + "maximum": 19, + "minimum": 0, + "type": "number", + }, + "region": { + "enum": [ + "AnteriorHead", + "AnteriorNeck", + "AnteriorRightShoulder", + "AnteriorRightChest", + "AnteriorRightArm", + "AnteriorRightForearm", + "AnteriorRightHand", + "AnteriorLeftHand", + "AnteriorLeftChest", + "AnteriorLeftShoulder", + "AnteriorLeftArm", + "AnteriorLeftForearm", + "AnteriorRightFoot", + "AnteriorLeftFoot", + "AnteriorRightLeg", + "AnteriorLowerChest", + "AnteriorAbdomen", + "AnteriorLeftLeg", + "AnteriorRightThigh", + "AnteriorLeftThigh", + "AnteriorGroin", + "PosteriorHead", + "PosteriorNeck", + "PosteriorLeftChest", + "PosteriorRightChest", + "PosteriorAbdomen", + "PosteriorLeftShoulder", + "PosteriorRightShoulder", + "PosteriorLeftArm", + "PosteriorLeftForearm", + "PosteriorLeftHand", + "PosteriorRightArm", + "PosteriorRightForearm", + "PosteriorRightHand", + "PosteriorLeftThighAndButtock", + "PosteriorRightThighAndButtock", + "PosteriorLeftLeg", + "PosteriorRightLeg", + "PosteriorLeftFoot", + "PosteriorRightFoot", + ] + }, + "scale": { + "maximum": 5, + "minimum": 1, + "type": "number", + }, + "tissue_type": { + "enum": [ + "Closed", + "Epithelial", + "Granulation", + "Slough", + "Necrotic", + ] + }, + "width": {"minimum": 0, "type": "number"}, + }, + "required": ["region", "width", "length"], + "type": "object", + } + ], + "type": "array", + } + ) + ], + ), + ), + ] diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 62caaf0058..fdb0e71572 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -296,7 +296,9 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): limb_response_lower_extremity_right = models.IntegerField( choices=LimbResponseType.choices, default=None, null=True ) - bp = JSONField(default=dict, validators=[JSONFieldSchemaValidator(BLOOD_PRESSURE)]) + bp = JSONField( + default=None, validators=[JSONFieldSchemaValidator(BLOOD_PRESSURE)], null=True + ) pulse = models.IntegerField( default=None, null=True, @@ -498,7 +500,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): def cztn(self, value): """ - Cast Zero to null values + Cast null to zero values """ if not value: return 0 diff --git a/care/facility/models/json_schema/daily_round.py b/care/facility/models/json_schema/daily_round.py index 2ae76ff9a4..90560f202f 100644 --- a/care/facility/models/json_schema/daily_round.py +++ b/care/facility/models/json_schema/daily_round.py @@ -2,10 +2,10 @@ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { - "systolic": {"type": "number"}, - "diastolic": {"type": "number"}, - "mean": {"type": "number"}, + "systolic": {"type": "number", "minimum": 0, "maximum": 400}, + "diastolic": {"type": "number", "minimum": 0, "maximum": 400}, }, + "required": ["systolic", "diastolic"], "additionalProperties": False, } @@ -16,10 +16,16 @@ { "type": "object", "properties": { - "name": {"type": "string"}, - "quantity": {"type": "number"}, - "concentration": {"type": "number"}, - "conc_unit": {"type": "string"}, + "name": { + "enum": [ + "Adrenalin", + "Noradrenalin", + "Vasopressin", + "Dopamine", + "Dobutamine", + ] + }, + "quantity": {"type": "number", "minimum": 0}, }, "additionalProperties": False, "required": ["name", "quantity"], @@ -34,8 +40,8 @@ { "type": "object", "properties": { - "name": {"type": "string"}, - "quantity": {"type": "number"}, + "name": {"enum": ["RL", "NS", "DNS"]}, + "quantity": {"type": "number", "minimum": 0}, }, "additionalProperties": False, "required": ["name", "quantity"], @@ -50,9 +56,8 @@ { "type": "object", "properties": { - "name": {"type": "string"}, - "quantity": {"type": "number"}, - "calories": {"type": "number"}, + "name": {"enum": ["Ryles Tube", "Normal Feed"]}, + "quantity": {"type": "number", "minimum": 0}, }, "additionalProperties": False, "required": ["name", "quantity"], @@ -67,8 +72,15 @@ { "type": "object", "properties": { - "name": {"type": "string"}, - "quantity": {"type": "number"}, + "name": { + "enum": [ + "Urine", + "Ryles Tube Aspiration", + "ICD", + "Abdominal Drain", + ], + }, + "quantity": {"type": "number", "minimum": 0}, }, "additionalProperties": False, "required": ["name", "quantity"], @@ -76,6 +88,49 @@ ], } +HUMAN_BODY_REGIONS = [ + "AnteriorHead", + "AnteriorNeck", + "AnteriorRightShoulder", + "AnteriorRightChest", + "AnteriorRightArm", + "AnteriorRightForearm", + "AnteriorRightHand", + "AnteriorLeftHand", + "AnteriorLeftChest", + "AnteriorLeftShoulder", + "AnteriorLeftArm", + "AnteriorLeftForearm", + "AnteriorRightFoot", + "AnteriorLeftFoot", + "AnteriorRightLeg", + "AnteriorLowerChest", + "AnteriorAbdomen", + "AnteriorLeftLeg", + "AnteriorRightThigh", + "AnteriorLeftThigh", + "AnteriorGroin", + "PosteriorHead", + "PosteriorNeck", + "PosteriorLeftChest", + "PosteriorRightChest", + "PosteriorAbdomen", + "PosteriorLeftShoulder", + "PosteriorRightShoulder", + "PosteriorLeftArm", + "PosteriorLeftForearm", + "PosteriorLeftHand", + "PosteriorRightArm", + "PosteriorRightForearm", + "PosteriorRightHand", + "PosteriorLeftThighAndButtock", + "PosteriorRightThighAndButtock", + "PosteriorLeftLeg", + "PosteriorRightLeg", + "PosteriorLeftFoot", + "PosteriorRightFoot", +] + PRESSURE_SORE = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "array", @@ -83,9 +138,8 @@ { "type": "object", "properties": { - "base_score": {"type": "number"}, - "length": {"type": "number"}, - "width": {"type": "number"}, + "length": {"type": "number", "minimum": 0}, + "width": {"type": "number", "minimum": 0}, "exudate_amount": {"enum": ["None", "Light", "Moderate", "Heavy"]}, "tissue_type": { "enum": [ @@ -97,12 +151,12 @@ ] }, "description": {"type": "string"}, - "push_score": {"type": "number"}, - "region": {"type": "string"}, + "push_score": {"type": "number", "minimum": 0, "maximum": 19}, + "region": {"enum": HUMAN_BODY_REGIONS}, "scale": {"type": "number", "minimum": 1, "maximum": 5}, }, "additionalProperties": False, - "required": [], + "required": ["region", "width", "length"], } ], } @@ -114,7 +168,7 @@ { "type": "object", "properties": { - "region": {"type": "string"}, + "region": {"enum": HUMAN_BODY_REGIONS}, "scale": {"type": "number", "minimum": 1, "maximum": 10}, "description": {"type": "string"}, }, @@ -131,7 +185,38 @@ { "type": "object", "properties": { - "procedure": {"type": "string"}, + "procedure": { + "enum": [ + "oral_care", + "hair_care", + "bed_bath", + "eye_care", + "perineal_care", + "skin_care", + "pre_enema", + "wound_dressing", + "lymphedema_care", + "ascitic_tapping", + "colostomy_care", + "colostomy_change", + "personal_hygiene", + "positioning", + "suctioning", + "ryles_tube_care", + "ryles_tube_change", + "iv_sitecare", + "nubulisation", + "dressing", + "dvt_pump_stocking", + "restrain", + "chest_tube_care", + "tracheostomy_care", + "tracheostomy_tube_change", + "stoma_care", + "catheter_care", + "catheter_change", + ], + }, "description": {"type": "string"}, }, "additionalProperties": False, diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index 1ea519c3eb..e636dfc37a 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -57,19 +57,21 @@ def setUpTestData(cls) -> None: def get_url(self, external_consultation_id=None): return f"/api/v1/consultation/{external_consultation_id}/daily_rounds/analyse/" + def create_log_update(self, **kwargs): + consultation = kwargs.pop("consultation", self.consultation_with_bed) + return self.client.post( + f"/api/v1/consultation/{consultation.external_id}/daily_rounds/", + data={**self.log_update, **kwargs}, + format="json", + ) + def test_external_consultation_does_not_exists_returns_404(self): sample_uuid = "e4a3d84a-d678-4992-9287-114f029046d8" response = self.client.get(self.get_url(sample_uuid)) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - def test_action_in_log_update( - self, - ): - response = self.client.post( - f"/api/v1/consultation/{self.consultation_with_bed.external_id}/daily_rounds/", - data=self.log_update, - format="json", - ) + def test_action_in_log_update(self): + response = self.create_log_update(consultation=self.consultation_with_bed) self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.data["patient_category"], "Comfort Care") self.assertEqual(response.data["rounds_type"], "NORMAL") @@ -98,13 +100,9 @@ def test_log_update_access_by_district_admin(self): ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) - def test_log_update_without_bed_for_admission( - self, - ): - response = self.client.post( - f"/api/v1/consultation/{self.admission_consultation_no_bed.external_id}/daily_rounds/", - data=self.log_update, - format="json", + def test_log_update_without_bed_for_admission(self): + response = self.create_log_update( + consultation=self.admission_consultation_no_bed ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual( @@ -112,23 +110,198 @@ def test_log_update_without_bed_for_admission( "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", + def test_log_update_without_bed_for_domiciliary(self): + response = self.create_log_update( + consultation=self.domiciliary_consultation_no_bed ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) def test_doctors_log_update(self): - response = self.client.post( - f"/api/v1/consultation/{self.consultation_with_bed.external_id}/daily_rounds/", - data={**self.log_update, "rounds_type": "DOCTORS_LOG"}, + response = self.create_log_update(rounds_type="DOCTORS_LOG") + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_log_update_with_blood_pressure_empty(self): + response = self.create_log_update(bp={}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_partial_blood_pressure(self): + response = self.create_log_update(bp={"systolic": 60}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_out_of_range_blood_pressure(self): + response = self.create_log_update(bp={"systolic": 1000, "diastolic": 60}) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_systolic_below_diastolic_blood_pressure(self): + response = self.create_log_update( + bp={"systolic": 60, "diastolic": 90}, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_valid_blood_pressure(self): + response = self.create_log_update( + bp={"systolic": 90, "diastolic": 60}, + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_log_update_with_incorrect_infusion_name(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + infusions=[{"name": "", "quantity": 1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_infusions_quantity_out_of_range(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + infusions=[{"name": "Adrenalin", "quantity": -1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_valid_infusions(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + infusions=[{"name": "Adrenalin", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_log_update_with_incorrect_iv_fluids_name(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + iv_fluids=[{"name": "", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_iv_fluids_quantity_out_of_range(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + iv_fluids=[{"name": "RL", "quantity": -1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_valid_iv_fluids(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + iv_fluids=[{"name": "RL", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_log_update_with_incorrect_feeds_name(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + feeds=[{"name": "", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_feeds_quantity_out_of_range(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + feeds=[{"name": "Ryles Tube", "quantity": -1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_valid_feeds(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + feeds=[{"name": "Ryles Tube", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_log_update_with_incorrect_output_name(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"name": "", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_output_quantity_out_of_range(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"name": "Abdominal Drain", "quantity": -1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_log_update_with_valid_output(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"name": "Abdominal Drain", "quantity": 10}], ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_create_log_update_with_incorrect_nursing_procedure(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"name": "Abdominal Drain", "quantity": 10}], + ) + self.assertEqual(response.status_code, status.HTTP_201_CREATED) + + def test_create_pressure_sore_with_invalid_region(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[ + { + "region": "", + "length": 1, + "width": 1, + # "exudate_amount": "None", + # "tissue_type": "Closed", + # "push_score": 1, + # "scale": 1, + # "description": "Description", + } + ], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_pressure_sore_with_length_out_of_range(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"region": "PosteriorNeck", "length": -10, "width": 1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_pressure_sore_with_width_out_of_range(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"region": "PosteriorNeck", "length": 1, "width": -1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_pressure_sore_with_missing_region(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[{"length": 1, "width": -1}], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_pressure_sore_with_invalid_exudate_amount(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[ + { + "region": "PosteriorNeck", + "length": 1, + "width": 1, + "exudate_amount": "", + } + ], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_create_pressure_sore_with_invalid_tissue_type(self): + response = self.create_log_update( + rounds_type="VENTILATOR", + output=[ + { + "region": "PosteriorNeck", + "length": 1, + "width": 1, + "tissue_type": "Closed", + } + ], + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_community_nurses_log_update(self): response = self.client.post( f"/api/v1/consultation/{self.consultation_with_bed.external_id}/daily_rounds/", From 8513421173d8e6dc5f77b85d05e92b233a7979c5 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sat, 21 Sep 2024 10:05:17 +0530 Subject: [PATCH 10/46] Switch investigation default from `dict` to `list` and remove deprecated `prescriptions` (#2410) --- ...ientconsultation_prescriptions_and_more.py | 33 +++++++ care/facility/models/daily_round.py | 1 - care/facility/models/patient_consultation.py | 3 +- data/dummy/facility.json | 99 +++++++++++-------- 4 files changed, 92 insertions(+), 44 deletions(-) create mode 100644 care/facility/migrations/0461_remove_patientconsultation_prescriptions_and_more.py diff --git a/care/facility/migrations/0461_remove_patientconsultation_prescriptions_and_more.py b/care/facility/migrations/0461_remove_patientconsultation_prescriptions_and_more.py new file mode 100644 index 0000000000..87faccf001 --- /dev/null +++ b/care/facility/migrations/0461_remove_patientconsultation_prescriptions_and_more.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.10 on 2024-08-29 16:49 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0460_alter_dailyround_bp_alter_dailyround_feeds_and_more"), + ] + + def forward_investigations_dict_to_array(apps, schema_editor): + PatientConsultation = apps.get_model("facility", "PatientConsultation") + PatientConsultation.objects.filter(investigation={}).update(investigation=[]) + + operations = [ + migrations.RemoveField( + model_name="patientconsultation", + name="prescriptions", + ), + migrations.RemoveField( + model_name="dailyround", + name="medication_given", + ), + migrations.AlterField( + model_name="patientconsultation", + name="investigation", + field=models.JSONField(default=list), + ), + migrations.RunPython( + forward_investigations_dict_to_array, + reverse_code=migrations.RunPython.noop, + ), + ] diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index fdb0e71572..6904809f84 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -175,7 +175,6 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): choices=CATEGORY_CHOICES, max_length=13, blank=False, null=True ) other_details = models.TextField(null=True, blank=True) - medication_given = JSONField(default=dict) # To be Used Later on last_updated_by_telemedicine = models.BooleanField(default=False) created_by_telemedicine = models.BooleanField(default=False) diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index afea466020..bf241779a0 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -80,8 +80,7 @@ class PatientConsultation(PatientBaseModel, ConsultationRelatedPermissionMixin): treatment_plan = models.TextField(null=True, blank=True) consultation_notes = models.TextField(null=True, blank=True) course_in_facility = models.TextField(null=True, blank=True) - investigation = JSONField(default=dict) - prescriptions = JSONField(default=dict) # Deprecated + investigation = JSONField(default=list) procedure = JSONField(default=dict) suggestion = models.CharField(max_length=4, choices=SUGGESTION_CHOICES) route_to_facility = models.SmallIntegerField( diff --git a/data/dummy/facility.json b/data/dummy/facility.json index 7314e0d17f..b33eaf7ad8 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -12,7 +12,14 @@ "verified": false, "facility_type": 2, "kasp_empanelled": false, - "features": [1, 2, 3, 4, 5, 6], + "features": [ + 1, + 2, + 3, + 4, + 5, + 6 + ], "longitude": null, "latitude": null, "pincode": 670000, @@ -49,7 +56,10 @@ "verified": false, "facility_type": 1300, "kasp_empanelled": false, - "features": [1, 6], + "features": [ + 1, + 6 + ], "longitude": null, "latitude": null, "pincode": 670112, @@ -86,7 +96,11 @@ "verified": false, "facility_type": 1500, "kasp_empanelled": false, - "features": [1, 4, 6], + "features": [ + 1, + 4, + 6 + ], "longitude": "78.6757364624373000", "latitude": "21.4009146842158660", "pincode": 670000, @@ -123,7 +137,11 @@ "verified": false, "facility_type": 1510, "kasp_empanelled": false, - "features": [1, 3, 5], + "features": [ + 1, + 3, + 5 + ], "longitude": "75.2139014820876600", "latitude": "18.2774285038890340", "pincode": 670000, @@ -1956,7 +1974,6 @@ "consultation_notes": "Transfer", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2016,7 +2033,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2076,7 +2092,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2136,7 +2151,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2196,7 +2210,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2256,7 +2269,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2316,7 +2328,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2376,7 +2387,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2436,7 +2446,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2496,7 +2505,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2556,7 +2564,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2616,7 +2623,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2676,7 +2682,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2736,7 +2741,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2796,7 +2800,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2856,7 +2859,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2916,7 +2918,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -2976,7 +2977,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3036,7 +3036,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3096,7 +3095,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3156,7 +3154,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3216,7 +3213,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3276,7 +3272,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3336,7 +3331,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3396,7 +3390,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3456,7 +3449,6 @@ "consultation_notes": "generalnote", "course_in_facility": null, "investigation": [], - "prescriptions": {}, "procedure": [], "suggestion": "A", "route_to_facility": 10, @@ -3993,8 +3985,14 @@ "default_unit": 1, "description": "", "min_quantity": 150.0, - "allowed_units": [1, 2], - "tags": [1, 2] + "allowed_units": [ + 1, + 2 + ], + "tags": [ + 1, + 2 + ] } }, { @@ -4005,8 +4003,13 @@ "default_unit": 1, "description": "", "min_quantity": 2.0, - "allowed_units": [1, 2], - "tags": [2] + "allowed_units": [ + 1, + 2 + ], + "tags": [ + 2 + ] } }, { @@ -4017,8 +4020,12 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [7], - "tags": [2] + "allowed_units": [ + 7 + ], + "tags": [ + 2 + ] } }, { @@ -4029,7 +4036,9 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [4], + "allowed_units": [ + 4 + ], "tags": [] } }, @@ -4041,7 +4050,9 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [4], + "allowed_units": [ + 4 + ], "tags": [] } }, @@ -4053,7 +4064,9 @@ "default_unit": 4, "description": "", "min_quantity": 100.0, - "allowed_units": [4], + "allowed_units": [ + 4 + ], "tags": [] } }, @@ -4065,8 +4078,12 @@ "default_unit": 7, "description": "", "min_quantity": 10.0, - "allowed_units": [7], - "tags": [2] + "allowed_units": [ + 7 + ], + "tags": [ + 2 + ] } }, { From 664739e9e5bc36a8eaab538fc09bab0346fb95a4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sat, 21 Sep 2024 14:12:37 +0530 Subject: [PATCH 11/46] use random uuid suffix in cover image key with size validation (#2472) use random uuid suffix in cover image key with size validation (#2472) Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/api/serializers/facility.py | 21 +++- care/utils/models/validators.py | 125 ++++++++++++++++++++++ 2 files changed, 143 insertions(+), 3 deletions(-) diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index 99c00bbe69..1e50f33271 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -1,3 +1,5 @@ +import uuid + import boto3 from django.conf import settings from django.contrib.auth import get_user_model @@ -14,6 +16,10 @@ WardSerializer, ) from care.utils.csp.config import BucketType, get_client_config +from care.utils.models.validators import ( + cover_image_validator, + custom_image_extension_validator, +) from config.serializers import ChoiceField from config.validators import MiddlewareDomainAddressValidator @@ -173,7 +179,11 @@ def create(self, validated_data): class FacilityImageUploadSerializer(serializers.ModelSerializer): - cover_image = serializers.ImageField(required=True, write_only=True) + cover_image = serializers.ImageField( + required=True, + write_only=True, + validators=[custom_image_extension_validator, cover_image_validator], + ) read_cover_image_url = serializers.URLField(read_only=True) class Meta: @@ -184,10 +194,15 @@ class Meta: def save(self, **kwargs): facility = self.instance image = self.validated_data["cover_image"] - image_extension = image.name.rsplit(".", 1)[-1] + config, bucket_name = get_client_config(BucketType.FACILITY) s3 = boto3.client("s3", **config) - image_location = f"cover_images/{facility.external_id}_cover.{image_extension}" + + if facility.cover_image_url: + s3.delete_object(Bucket=bucket_name, Key=facility.cover_image_url) + + image_extension = image.name.rsplit(".", 1)[-1] + image_location = f"cover_images/{facility.external_id}_{str(uuid.uuid4())[0:8]}.{image_extension}" boto_params = { "Bucket": bucket_name, "Key": image_location, diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 1ef8dc2625..9a2cd68965 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -1,12 +1,15 @@ import re +from fractions import Fraction from typing import Iterable, List import jsonschema from django.core import validators from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import UploadedFile from django.core.validators import RegexValidator from django.utils.deconstruct import deconstructible from django.utils.translation import gettext_lazy as _ +from PIL import Image @deconstructible @@ -194,3 +197,125 @@ def __eq__(self, __value: object) -> bool: # pragma: no cover allow_floats=True, precision=4, ) + + +@deconstructible +class ImageSizeValidator: + message: dict[str, str] = { + "min_width": _( + "Image width is less than the minimum allowed width of %(min_width)s pixels." + ), + "max_width": _( + "Image width is greater than the maximum allowed width of %(max_width)s pixels." + ), + "min_height": _( + "Image height is less than the minimum allowed height of %(min_height)s pixels." + ), + "max_height": _( + "Image height is greater than the maximum allowed height of %(max_height)s pixels." + ), + "aspect_ratio": _( + "Image aspect ratio is not within the allowed range of %(aspect_ratio)s." + ), + "min_size": _( + "Image size is less than the minimum allowed size of %(min_size)s." + ), + "max_size": _( + "Image size is greater than the maximum allowed size of %(max_size)s." + ), + } + + def __init__( + self, + min_width: int | None = None, + max_width: int | None = None, + min_height: int | None = None, + max_height: int | None = None, + aspect_ratio: float | None = None, + min_size: int | None = None, + max_size: int | None = None, + ) -> None: + self.min_width = min_width + self.max_width = max_width + self.min_height = min_height + self.max_height = max_height + self.aspect_ratio = aspect_ratio + self.min_size = min_size + self.max_size = max_size + + def __call__(self, value: UploadedFile) -> None: + with Image.open(value.file) as image: + width, height = image.size + size: int = value.size + + errors: list[str] = [] + + if self.min_width and width < self.min_width: + errors.append(self.message["min_width"] % {"min_width": self.min_width}) + + if self.max_width and width > self.max_width: + errors.append(self.message["max_width"] % {"max_width": self.max_width}) + + if self.min_height and height < self.min_height: + errors.append( + self.message["min_height"] % {"min_height": self.min_height} + ) + + if self.max_height and height > self.max_height: + errors.append( + self.message["max_height"] % {"max_height": self.max_height} + ) + + if self.aspect_ratio: + if not (1 / self.aspect_ratio) < (width / height) < self.aspect_ratio: + aspect_ratio_fraction = Fraction( + self.aspect_ratio + ).limit_denominator() + aspect_ratio_str = f"{aspect_ratio_fraction.numerator}:{aspect_ratio_fraction.denominator}" + + errors.append( + self.message["aspect_ratio"] + % {"aspect_ratio": aspect_ratio_str} + ) + + if self.min_size and size < self.min_size: + errors.append(self.message["min_size"] % {"min_size": self.min_size}) + + if self.max_size and size > self.max_size: + errors.append(self.message["max_size"] % {"max_size": self.max_size}) + + if errors: + raise ValidationError(errors) + + value.seek(0) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, ImageSizeValidator): + return False + return all( + getattr(self, attr) == getattr(other, attr) + for attr in [ + "min_width", + "max_width", + "min_height", + "max_height", + "aspect_ratio", + "min_size", + "max_size", + ] + ) + + +cover_image_validator = ImageSizeValidator( + min_width=400, + min_height=400, + max_width=1024, + max_height=1024, + aspect_ratio=1 / 1, + min_size=1024, + max_size=1024 * 1024 * 2, +) + +custom_image_extension_validator = validators.FileExtensionValidator( + allowed_extensions=["jpg", "jpeg", "png"] +) From 51b5c00fe3498a7281dcfbc2baaeb88d59ae2da1 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sat, 21 Sep 2024 16:14:07 +0530 Subject: [PATCH 12/46] convert image validator aspect ratio to list (#2475) --- care/facility/tests/test_facility_api.py | 53 +++++++++++++++++ care/utils/models/validators.py | 49 ++++++++++------ care/utils/tests/test_image_validator.py | 74 ++++++++++++++++++++++++ 3 files changed, 159 insertions(+), 17 deletions(-) create mode 100644 care/utils/tests/test_image_validator.py diff --git a/care/facility/tests/test_facility_api.py b/care/facility/tests/test_facility_api.py index 0f6fef256d..36aa09ca30 100644 --- a/care/facility/tests/test_facility_api.py +++ b/care/facility/tests/test_facility_api.py @@ -1,3 +1,7 @@ +import io + +from django.core.files.uploadedfile import SimpleUploadedFile +from PIL import Image from rest_framework import status from rest_framework.test import APITestCase @@ -147,3 +151,52 @@ def test_delete_with_active_patients(self): self.client.force_authenticate(user=state_admin) response = self.client.delete(f"/api/v1/facility/{facility.external_id}/") self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + + +class FacilityCoverImageTests(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("staff", cls.district, home_facility=cls.facility) + + def test_valid_image(self): + self.facility.cover_image = "http://example.com/test.jpg" + self.facility.save() + image = Image.new("RGB", (400, 400)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = SimpleUploadedFile("test.jpg", file.getvalue(), "image/jpeg") + test_file.size = 2048 + + payload = {"cover_image": test_file} + + response = self.client.post( + f"/api/v1/facility/{self.facility.external_id}/cover_image/", + payload, + format="multipart", + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + + def test_invalid_image_too_small(self): + image = Image.new("RGB", (100, 100)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = SimpleUploadedFile("test.jpg", file.getvalue(), "image/jpeg") + test_file.size = 1000 + + payload = {"cover_image": test_file} + + response = self.client.post( + f"/api/v1/facility/{self.facility.external_id}/cover_image/", + payload, + format="multipart", + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + self.assertEqual( + response.data["cover_image"][0], + "Image width is less than the minimum allowed width of 400 pixels.", + ) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 9a2cd68965..e20233c3fb 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -215,7 +215,7 @@ class ImageSizeValidator: "Image height is greater than the maximum allowed height of %(max_height)s pixels." ), "aspect_ratio": _( - "Image aspect ratio is not within the allowed range of %(aspect_ratio)s." + "Image aspect ratio must be one of the following: %(aspect_ratio)s." ), "min_size": _( "Image size is less than the minimum allowed size of %(min_size)s." @@ -231,17 +231,23 @@ def __init__( max_width: int | None = None, min_height: int | None = None, max_height: int | None = None, - aspect_ratio: float | None = None, - min_size: int | None = None, - max_size: int | None = None, + aspect_ratio: list[float] | None = None, + min_size: int | None = None, # in bytes + max_size: int | None = None, # in bytes ) -> None: self.min_width = min_width self.max_width = max_width self.min_height = min_height self.max_height = max_height - self.aspect_ratio = aspect_ratio self.min_size = min_size self.max_size = max_size + if aspect_ratio: + self.aspect_ratio = set( + Fraction(ratio).limit_denominator(10) for ratio in aspect_ratio + ) + self.aspect_ratio_str = ", ".join( + f"{ratio.numerator}:{ratio.denominator}" for ratio in self.aspect_ratio + ) def __call__(self, value: UploadedFile) -> None: with Image.open(value.file) as image: @@ -267,22 +273,24 @@ def __call__(self, value: UploadedFile) -> None: ) if self.aspect_ratio: - if not (1 / self.aspect_ratio) < (width / height) < self.aspect_ratio: - aspect_ratio_fraction = Fraction( - self.aspect_ratio - ).limit_denominator() - aspect_ratio_str = f"{aspect_ratio_fraction.numerator}:{aspect_ratio_fraction.denominator}" - + image_aspect_ratio = Fraction(width / height).limit_denominator(10) + if image_aspect_ratio not in self.aspect_ratio: errors.append( self.message["aspect_ratio"] - % {"aspect_ratio": aspect_ratio_str} + % {"aspect_ratio": self.aspect_ratio_str} ) if self.min_size and size < self.min_size: - errors.append(self.message["min_size"] % {"min_size": self.min_size}) + errors.append( + self.message["min_size"] + % {"min_size": self._humanize_bytes(self.min_size)} + ) if self.max_size and size > self.max_size: - errors.append(self.message["max_size"] % {"max_size": self.max_size}) + errors.append( + self.message["max_size"] + % {"max_size": self._humanize_bytes(self.max_size)} + ) if errors: raise ValidationError(errors) @@ -305,15 +313,22 @@ def __eq__(self, other: object) -> bool: ] ) + def _humanize_bytes(self, size: int) -> str: + for unit in ["B", "KB"]: + if size < 1024.0: + return f"{f"{size:.2f}".rstrip(".0")} {unit}" + size /= 1024.0 + return f"{f"{size:.2f}".rstrip(".0")} MB" + cover_image_validator = ImageSizeValidator( min_width=400, min_height=400, max_width=1024, max_height=1024, - aspect_ratio=1 / 1, - min_size=1024, - max_size=1024 * 1024 * 2, + aspect_ratio=[1 / 1], + min_size=1024, # 1 KB + max_size=1024 * 1024 * 2, # 2 MB ) custom_image_extension_validator = validators.FileExtensionValidator( diff --git a/care/utils/tests/test_image_validator.py b/care/utils/tests/test_image_validator.py new file mode 100644 index 0000000000..1c8d2656f0 --- /dev/null +++ b/care/utils/tests/test_image_validator.py @@ -0,0 +1,74 @@ +import io + +from django.core.exceptions import ValidationError +from django.core.files.uploadedfile import UploadedFile +from django.test import TestCase +from PIL import Image + +from care.utils.models.validators import ImageSizeValidator + + +class CoverImageValidatorTests(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.cover_image_validator = ImageSizeValidator( + min_width=400, + min_height=400, + max_width=1024, + max_height=1024, + aspect_ratio=[1 / 1], + min_size=1024, + max_size=1024 * 1024 * 2, + ) + + def test_valid_image(self): + image = Image.new("RGB", (400, 400)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = UploadedFile(file, "test.jpg", "image/jpeg", 2048) + self.assertIsNone(self.cover_image_validator(test_file)) + + def test_invalid_image_too_small(self): + image = Image.new("RGB", (100, 100)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = UploadedFile(file, "test.jpg", "image/jpeg", 1000) + with self.assertRaises(ValidationError) as cm: + self.cover_image_validator(test_file) + self.assertEqual( + cm.exception.messages, + [ + "Image width is less than the minimum allowed width of 400 pixels.", + "Image height is less than the minimum allowed height of 400 pixels.", + "Image size is less than the minimum allowed size of 1 KB.", + ], + ) + + def test_invalid_image_too_large(self): + image = Image.new("RGB", (2000, 2000)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = UploadedFile(file, "test.jpg", "image/jpeg", 1024 * 1024 * 3) + with self.assertRaises(ValidationError) as cm: + self.cover_image_validator(test_file) + self.assertEqual( + cm.exception.messages, + [ + "Image width is greater than the maximum allowed width of 1024 pixels.", + "Image height is greater than the maximum allowed height of 1024 pixels.", + "Image size is greater than the maximum allowed size of 2 MB.", + ], + ) + + def test_invalid_image_aspect_ratio(self): + image = Image.new("RGB", (400, 800)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = UploadedFile(file, "test.jpg", "image/jpeg", 2048) + with self.assertRaises(ValidationError) as cm: + self.cover_image_validator(test_file) + self.assertEqual( + cm.exception.messages, + ["Image aspect ratio must be one of the following: 1:1."], + ) From cedd28c1afd4c0ff51c7d6ad992f68cbda4b34b4 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sun, 22 Sep 2024 13:06:13 +0530 Subject: [PATCH 13/46] Fix UnorderedObjectListWarning in patient search api (#2478) --- care/facility/api/viewsets/patient.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 7287cd52e4..6ac7f960d2 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -285,7 +285,6 @@ def filter_by_diagnoses(self, queryset, name, value): ) def filter_by_has_consents(self, queryset, name, value: str): - if not value: return queryset @@ -805,7 +804,7 @@ class PatientSearchViewSet(ListModelMixin, GenericViewSet): "facility", "allow_transfer", "is_active", - ) + ).order_by("id") serializer_class = PatientSearchSerializer permission_classes = (IsAuthenticated, DRYPermissions) pagination_class = PatientSearchSetPagination From 09ee8a37fe84b9548c7a08f24b7e88a5d6b2bb2a Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sun, 22 Sep 2024 13:06:38 +0530 Subject: [PATCH 14/46] Avoid printing typst version in tests (#2477) --- care/facility/tests/test_pdf_generation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/tests/test_pdf_generation.py b/care/facility/tests/test_pdf_generation.py index 6754c0c43a..c0c7d2fca6 100644 --- a/care/facility/tests/test_pdf_generation.py +++ b/care/facility/tests/test_pdf_generation.py @@ -103,7 +103,7 @@ def test_compile_typ(data): class TestTypstInstallation(TestCase): def test_typst_installed(self): try: - subprocess.run(["typst", "--version"], check=True) + subprocess.run(["typst", "--version"], check=True, capture_output=True) typst_installed = True except subprocess.CalledProcessError: typst_installed = False From 22a91f9bdf8a54e8af290658693bd79c27797f12 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sun, 22 Sep 2024 13:08:17 +0530 Subject: [PATCH 15/46] Fix warnings from drf where min/max values of DecimalField were not instances of Decimal (#2476) --- care/facility/models/daily_round.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 6904809f84..ddfb409cd1 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -1,3 +1,5 @@ +from decimal import Decimal + from django.core.validators import MaxValueValidator, MinValueValidator from django.db import models from django.db.models import JSONField @@ -158,7 +160,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(95), MaxValueValidator(106)], + validators=[MinValueValidator(Decimal(95)), MaxValueValidator(Decimal(106))], ) spo2 = models.DecimalField( max_digits=4, decimal_places=2, blank=True, null=True, default=None @@ -324,7 +326,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(0), MaxValueValidator(30)], + validators=[MinValueValidator(Decimal(0)), MaxValueValidator(Decimal(30))], ) ventilator_pip = models.IntegerField( default=None, @@ -395,7 +397,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(0), MaxValueValidator(10)], + validators=[MinValueValidator(Decimal(0)), MaxValueValidator(Decimal(10))], ) pco2 = models.IntegerField( default=None, @@ -413,7 +415,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(5), MaxValueValidator(80)], + validators=[MinValueValidator(Decimal(5)), MaxValueValidator(Decimal(80))], ) base_excess = models.IntegerField( default=None, @@ -426,7 +428,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(0), MaxValueValidator(20)], + validators=[MinValueValidator(Decimal(0)), MaxValueValidator(Decimal(20))], ) sodium = models.DecimalField( decimal_places=2, @@ -434,7 +436,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(100), MaxValueValidator(170)], + validators=[MinValueValidator(Decimal(100)), MaxValueValidator(Decimal(170))], ) potassium = models.DecimalField( decimal_places=2, @@ -442,7 +444,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(1), MaxValueValidator(10)], + validators=[MinValueValidator(Decimal(1)), MaxValueValidator(Decimal(10))], ) blood_sugar_level = models.IntegerField( default=None, @@ -455,7 +457,7 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): blank=True, default=None, null=True, - validators=[MinValueValidator(0), MaxValueValidator(100)], + validators=[MinValueValidator(Decimal(0)), MaxValueValidator(Decimal(100))], ) insulin_intake_frequency = models.IntegerField( choices=InsulinIntakeFrequencyType.choices, From 0ee6d8afaeb6e2b0c36ee3a331be21798f4b850e Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Sun, 22 Sep 2024 16:02:21 +0530 Subject: [PATCH 16/46] Fixes default admin url (#2474) Fixes default admin url (#2474) --------- Co-authored-by: Aakash Singh --- config/settings/base.py | 2 +- config/urls.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/settings/base.py b/config/settings/base.py index 7c934f830b..21627b8ab7 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -309,7 +309,7 @@ # MANAGERS = ADMINS # Django Admin URL. -ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/") +ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin") # LOGGING # ------------------------------------------------------------------------------ diff --git a/config/urls.py b/config/urls.py index 923c8413ce..832f8168a4 100644 --- a/config/urls.py +++ b/config/urls.py @@ -40,7 +40,7 @@ path("ping/", ping, name="ping"), path("app_version/", app_version, name="app_version"), # Django Admin, use {% url 'admin:index' %} - path(f"{settings.ADMIN_URL}/", admin.site.urls), + path(f"{settings.ADMIN_URL.rstrip("/")}/", admin.site.urls), # Rest API path("api/v1/auth/login/", TokenObtainPairView.as_view(), name="token_obtain_pair"), path( From d3a01620d05bb35545b24094c1b8e1f3db4593e2 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:07:37 +0530 Subject: [PATCH 17/46] added a request time logging middleware (#2361) added a request time logging middleware (#2361) --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- config/middleware.py | 15 +++++++++++++++ config/settings/base.py | 24 ++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 config/middleware.py diff --git a/config/middleware.py b/config/middleware.py new file mode 100644 index 0000000000..3243201a56 --- /dev/null +++ b/config/middleware.py @@ -0,0 +1,15 @@ +import logging +import time + + +class RequestTimeLoggingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + self.logger = logging.getLogger("time_logging_middleware") + + def __call__(self, request): + request.start_time = time.time() + response = self.get_response(request) + duration = time.time() - request.start_time + self.logger.info(f"Request to {request.path} took {duration:.4f} seconds") + return response diff --git a/config/settings/base.py b/config/settings/base.py index 21627b8ab7..265abac9ab 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -200,6 +200,10 @@ "care.audit_log.middleware.AuditLogMiddleware", ] +# add RequestTimeLoggingMiddleware based on the environment variable +if env.bool("ENABLE_REQUEST_TIME_LOGGING", default=False): + MIDDLEWARE.insert(0, "config.middleware.RequestTimeLoggingMiddleware") + # STATIC # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#static-files @@ -323,14 +327,30 @@ "verbose": { "format": "%(levelname)s %(asctime)s %(module)s " "%(process)d %(thread)d %(message)s" - } + }, + "request_time": { + "format": "INFO %(asctime)s %(message)s", + "datefmt": "%Y-%m-%d %H:%M:%S", + }, }, "handlers": { "console": { "level": "DEBUG", "class": "logging.StreamHandler", "formatter": "verbose", - } + }, + "time_logging": { + "level": "INFO", + "class": "logging.StreamHandler", + "formatter": "request_time", + }, + }, + "loggers": { + "time_logging_middleware": { + "handlers": ["time_logging"], + "level": "INFO", + "propagate": False, + }, }, "root": {"level": "INFO", "handlers": ["console"]}, } From d4f92085533c93986a59d7bdb49cb16fbf4769ca Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sun, 22 Sep 2024 16:15:32 +0530 Subject: [PATCH 18/46] Switch to ruff for lint and format (#1559) * add ruff * fix ruff command * use ruff action * update lint action * update rules --- .flake8 | 14 ----- .github/workflows/linter.yml | 31 ++++------ .pre-commit-config.yaml | 22 ++----- .vscode/extensions.json | 2 +- .vscode/settings.json | 11 ++-- Makefile | 12 ++++ Pipfile | 1 + Pipfile.lock | 63 +++++++++++++------ pyproject.toml | 117 ++++++++++++++++++++++++----------- 9 files changed, 160 insertions(+), 113 deletions(-) delete mode 100644 .flake8 diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 25f2b30205..0000000000 --- a/.flake8 +++ /dev/null @@ -1,14 +0,0 @@ -[flake8] -max-line-length = 88 -extend-ignore = E203,E501,E231 -exclude = - .git, - .gitignore, - *.pot, - *.py[co], - __pycache__, - .venv, - */migrations/*, - */migrations_old/*, - */static/CACHE/*, - docs diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index d17dd681a1..fdc8e815a4 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -10,29 +10,24 @@ on: permissions: { } jobs: - build: + lint: name: Lint Code Base runs-on: ubuntu-latest - permissions: - contents: read - packages: read - statuses: write steps: - name: Checkout Code uses: actions/checkout@v4 + + - name: Ruff check + uses: chartboost/ruff-action@v1 with: - fetch-depth: 0 + version: 0.6.7 + args: "check" + changed-files: "true" - - name: Lint Code Base - uses: super-linter/super-linter/slim@v6 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VALIDATE_ALL_CODEBASE: false - VALIDATE_PYTHON_BLACK: true - VALIDATE_PYTHON_FLAKE8: true - VALIDATE_PYTHON_ISORT: true - LINTER_RULES_PATH: / - PYTHON_BLACK_CONFIG_FILE: "pyproject.toml" - PYTHON_FLAKE8_CONFIG_FILE: ".flake8" - PYTHON_ISORT_CONFIG_FILE: "pyproject.toml" + - name: Ruff format + uses: chartboost/ruff-action@v1 + with: + version: 0.6.7 + args: "format" + changed-files: "true" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c8b61c98e7..4cad6717e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,21 +16,9 @@ repos: - id: check-yaml - id: check-toml - - repo: https://github.com/PyCQA/isort - rev: 5.13.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.6.7 hooks: - - id: isort - additional_dependencies: ["isort[pyproject]"] - - - repo: https://github.com/psf/black - rev: 24.4.2 - hooks: - - id: black - args: ["--config=pyproject.toml"] - - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.0 - hooks: - - id: flake8 - args: ["--config=.flake8"] - additional_dependencies: [flake8-isort] + - id: ruff + args: [ --fix ] + - id: ruff-format diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 259dc3c09e..e9d06bb780 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,6 +3,6 @@ "boto3typed.boto3-ide", "ms-python.python", "ms-python.vscode-pylance", - "ms-python.isort" + "charliermarsh.ruff" ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 88b18e9481..0f0840051d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,10 +3,12 @@ "editor.formatOnSave": false }, "[python]": { + "editor.formatOnSave": true, "editor.codeActionsOnSave": { + "source.fixAll": "explicit", "source.organizeImports": "explicit" }, - "editor.formatOnSave": true + "editor.defaultFormatter": "charliermarsh.ruff" }, "files.associations": { "*.envrc": "shellscript", @@ -19,10 +21,5 @@ "files.trimFinalNewlines": true, "files.trimTrailingWhitespace": true, "githubPullRequests.ignoredPullRequestBranches": ["develop", "staging"], - "python.formatting.blackPath": "${workspaceFolder}/.venv/bin/black", - "python.formatting.provider": "black", - "python.languageServer": "Pylance", - "python.linting.flake8Args": ["--config=.flake8"], - "python.linting.flake8Path": "${workspaceFolder}/.venv/bin/flake8", - "isort.args": ["--profile", "black"] + "python.languageServer": "Pylance" } diff --git a/Makefile b/Makefile index 549d86d8b7..0b84921fc0 100644 --- a/Makefile +++ b/Makefile @@ -52,5 +52,17 @@ reset_db: docker compose exec backend bash -c "python manage.py reset_db --noinput" docker compose exec backend bash -c "python manage.py migrate" +ruff-all: + ruff check . + +ruff: + ruff check --fix $(shell git diff --name-only --staged | grep -E '\.py$$|\/pyproject.toml$$') + +ruff-all-docker: + docker exec care bash -c "ruff check ." + +ruff-docker: + docker exec care bash -c "ruff check --fix $(shell git diff --name-only --staged | grep -E '\.py$$|\/pyproject.toml$$')" + %: docker compose exec backend bash -c "python manage.py $*" diff --git a/Pipfile b/Pipfile index 445f0f9265..a792c1df96 100644 --- a/Pipfile +++ b/Pipfile @@ -63,6 +63,7 @@ requests-mock = "==1.12.1" tblib = "==3.0.0" watchdog = "==5.0.2" werkzeug = "==3.0.4" +ruff = "==0.6.7" [docs] furo = "==2024.8.6" diff --git a/Pipfile.lock b/Pipfile.lock index c5b9fbdda4..52ebb6605e 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "84feb1d63404ac8bd49709c07717eeb2670f7766b6339f8ec277ed2e33a9366f" + "sha256": "bc01a15ab901f4331df57e40d2fbdaef62ff99cf3449949df2b640c9fd179a64" }, "pipfile-spec": 6, "requires": { @@ -200,11 +200,11 @@ }, "billiard": { "hashes": [ - "sha256:07aa978b308f334ff8282bd4a746e681b3513db5c9a514cbdd810cbbdc19714d", - "sha256:9a3c3184cb275aa17a732f93f65b20c525d3d9f253722d26a82194803ade5a2c" + "sha256:12b641b0c539073fc8d3f5b8b7be998956665c4233c7c1fcd66a7e677c4fb36f", + "sha256:40b59a4ac8806ba2c2369ea98d876bc6108b051c227baffd928c644d15d8f3cb" ], "markers": "python_version >= '3.7'", - "version": "==4.2.0" + "version": "==4.2.1" }, "boto3": { "hashes": [ @@ -217,11 +217,11 @@ }, "botocore": { "hashes": [ - "sha256:18362b7ec748561d786aebf1dd5c9faf22c4732efbf89344314199f96d3bbb65", - "sha256:d9bc656e7dde0b3e3f3080fc54bacff6a97fd7806b98acbcc21c7f9d4d0102b9" + "sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b", + "sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3" ], "markers": "python_version >= '3.8'", - "version": "==1.35.22" + "version": "==1.35.24" }, "celery": { "hashes": [ @@ -891,11 +891,11 @@ }, "kombu": { "hashes": [ - "sha256:1c05178826dab811f8cab5b0a154d42a7a33d8bcdde9fa3d7b4582e43c3c03db", - "sha256:621d365f234e4c089596f3a2510f1ade07026efc28caca426161d8f458786cab" + "sha256:14212f5ccf022fc0a70453bb025a1dcc32782a588c49ea866884047d66e14763", + "sha256:eef572dd2fd9fc614b37580e3caeafdd5af46c1eff31e7fba89138cdb406f2cf" ], "markers": "python_version >= '3.8'", - "version": "==5.4.1" + "version": "==5.4.2" }, "more-itertools": { "hashes": [ @@ -1835,19 +1835,19 @@ }, "botocore": { "hashes": [ - "sha256:18362b7ec748561d786aebf1dd5c9faf22c4732efbf89344314199f96d3bbb65", - "sha256:d9bc656e7dde0b3e3f3080fc54bacff6a97fd7806b98acbcc21c7f9d4d0102b9" + "sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b", + "sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3" ], "markers": "python_version >= '3.8'", - "version": "==1.35.22" + "version": "==1.35.24" }, "botocore-stubs": { "hashes": [ - "sha256:14dec6b00d974dce4747d6917a791bcea0d8efd1c8959821d8ce6ca69ce40d70", - "sha256:dae8cc433df3044ece464f4985e67a154bab51fd042757d0c59e4a9ea26d0376" + "sha256:052eead5808ef138988089b6de6786f6271fc7f395ee0d4395e96e06ab04573c", + "sha256:20ca0d0c77215b599ab79e365f2bf5f909a49f4c5440bbbbb08175971a3de442" ], "markers": "python_version >= '3.8'", - "version": "==1.35.22" + "version": "==1.35.24" }, "certifi": { "hashes": [ @@ -2171,11 +2171,11 @@ }, "faker": { "hashes": [ - "sha256:4294d169255a045990720d6f3fa4134b764a4cdf46ef0d3c7553d2506f1adaa1", - "sha256:e59c01d1e8b8e20a83255ab8232c143cb2af3b4f5ab6a3f5ce495f385ad8ab4c" + "sha256:32d0ee7d42925ff06e4a7d906ee7efbf34f5052a41a2a1eb8bb174a422a5498f", + "sha256:34e89aec594cad9773431ca479ee95c7ce03dd9f22fda2524e2373b880a2fa77" ], "markers": "python_version >= '3.8'", - "version": "==28.4.1" + "version": "==29.0.0" }, "filelock": { "hashes": [ @@ -2580,6 +2580,31 @@ "markers": "python_version >= '3.5'", "version": "==1.12.1" }, + "ruff": { + "hashes": [ + "sha256:02b083770e4cdb1495ed313f5694c62808e71764ec6ee5db84eedd82fd32d8f5", + "sha256:08277b217534bfdcc2e1377f7f933e1c7957453e8a79764d004e44c40db923f2", + "sha256:0c05fd37013de36dfa883a3854fae57b3113aaa8abf5dea79202675991d48624", + "sha256:17a86aac6f915932d259f7bec79173e356165518859f94649d8c50b81ff087e9", + "sha256:2f0b62056246234d59cbf2ea66e84812dc9ec4540518e37553513392c171cb18", + "sha256:44e52129d82266fa59b587e2cd74def5637b730a69c4542525dfdecfaae38bd5", + "sha256:525201b77f94d2b54868f0cbe5edc018e64c22563da6c5c2e5c107a4e85c1c0d", + "sha256:533d66b7774ef224e7cf91506a7dafcc9e8ec7c059263ec46629e54e7b1f90ab", + "sha256:590445eec5653f36248584579c06252ad2e110a5d1f32db5420de35fb0e1c977", + "sha256:6b1462fa56c832dc0cea5b4041cfc9c97813505d11cce74ebc6d1aae068de36b", + "sha256:8854450839f339e1049fdbe15d875384242b8e85d5c6947bb2faad33c651020b", + "sha256:9ba4efe5c6dbbb58be58dd83feedb83b5e95c00091bf09987b4baf510fee5c99", + "sha256:a0e1655868164e114ba43a908fd2d64a271a23660195017c17691fb6355d59bb", + "sha256:a939ca435b49f6966a7dd64b765c9df16f1faed0ca3b6f16acdf7731969deb35", + "sha256:b28f0d5e2f771c1fe3c7a45d3f53916fc74a480698c4b5731f0bea61e52137c8", + "sha256:b3f8822defd260ae2460ea3832b24d37d203c3577f48b055590a426a722d50ef", + "sha256:c6707a32e03b791f4448dc0dce24b636cbcdee4dd5607adc24e5ee73fd86c00a", + "sha256:f49c9caa28d9bbfac4a637ae10327b3db00f47d038f3fbb2195c4d682e925b14" + ], + "index": "pypi", + "markers": "python_version >= '3.7'", + "version": "==0.6.7" + }, "s3transfer": { "hashes": [ "sha256:0711534e9356d3cc692fdde846b4a1e4b0cb6519971860796e6bc4c7aea00ef6", diff --git a/pyproject.toml b/pyproject.toml index 930e81fc03..2ab41f7ca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,48 +9,91 @@ omit = [ "*/migrations*/*", "*/asgi.py", "*/wsgi.py", + "docs/*", "manage.py", - ".venv/*" + ".venv/*", ] [tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "raise NotImplementedError" -] +exclude_lines = ["pragma: no cover", "raise NotImplementedError"] ignore_errors = true -[tool.isort] -profile = "black" -known_third_party = [ - "allauth", - "boto3", - "celery", - "crispy_forms", - "dateutil", - "django", - "django_filters", - "django_rest_passwordreset", - "djangoql", - "djqscsv", - "drf_spectacular", - "dry_rest_permissions", - "environ", - "freezegun", - "hardcopy", - "healthy_django", - "jsonschema", - "jwt", - "phonenumber_field", - "phonenumbers", - "pytz", - "pywebpush", - "ratelimit", - "requests", - "rest_framework", - "rest_framework_nested", - "rest_framework_simplejwt", - "sentry_sdk", - "simple_history" +[tool.ruff] +target-version = "py312" +extend-exclude = ["*/migrations_old/*"] +include = ["*.py", "pyproject.toml"] + +[tool.ruff.lint] +# https://docs.astral.sh/ruff/rules/ +select = [ + "F", # pyflakes + "E", # pycodestyle errors + "W", # pycodestyle warnings + "I", # isort + "N", # pep8-naming + "UP", # pyupgrade + # "ANN", # flake8-annotations + "S", # flake8-bandit + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "ISC", # flake8-import-conventions + "ICN", # flake8-import-order + "LOG", # flake8-logging + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-todo + "INT", # flake8-gettext + "ARG", # flake8-unused-arguments + "PTH", # flake8-use-pathlib + "TD", # flake8-todo + "ERA", # eradicate + "PL", # pylint + "FURB", # refurb + "RUF", # ruff +] +unfixable = [ + "T20", # don't remove print statements ] +ignore = [ + "E203", # whitespace-before-punctuation + "E501", # line-too-long + "FBT002", # boolean-default-value-positional-argument + "SIM105", # suppressible-exception + "PLR0913", # too-many-arguments + "DJ001", # django-nullable-model-string-field + "ISC001", # conflicts with format + "COM812", # conflicts with format +] + + +[tool.ruff.format] +line-ending = "lf" + + +[tool.ruff.lint.flake8-builtins] +builtins-ignorelist = ["id", "list", "filter"] + + +[tool.ruff.lint.flake8-quotes] +docstring-quotes = "double" + + +[tool.ruff.lint.per-file-ignores] +"**/__init__.py" = ["E402", "F401"] # for imports +"**/tests/**" = ["DTZ001"] From d9f02879568a413bcb556011bcc5cad218c1ef94 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Sun, 22 Sep 2024 16:37:48 +0530 Subject: [PATCH 19/46] Added Facility Hubs (#2135) Added Facility Hubs (#2135) --------- Co-authored-by: Aakash Singh --- care/facility/admin.py | 7 ++ care/facility/api/serializers/facility.py | 49 ++++++++++- care/facility/api/viewsets/facility.py | 27 +++++- .../migrations/0462_facilityhubspoke.py | 83 +++++++++++++++++++ care/facility/models/facility.py | 56 +++++++++++++ care/facility/tests/test_facility_api.py | 77 +++++++++++++++++ config/api_router.py | 9 +- 7 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 care/facility/migrations/0462_facilityhubspoke.py diff --git a/care/facility/admin.py b/care/facility/admin.py index 6e22e90f5d..11a210d0ed 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -7,6 +7,7 @@ from care.facility.models.ambulance import Ambulance, AmbulanceDriver from care.facility.models.asset import Asset from care.facility.models.bed import AssetBed, Bed +from care.facility.models.facility import FacilityHubSpoke from care.facility.models.file_upload import FileUpload from care.facility.models.patient_consultation import ( PatientConsent, @@ -101,6 +102,12 @@ class FacilityAdmin(DjangoQLSearchMixin, admin.ModelAdmin): djangoql_completion_enabled_by_default = True +@admin.register(FacilityHubSpoke) +class FacilityHubSpokeAdmin(DjangoQLSearchMixin, admin.ModelAdmin): + search_fields = ["name"] + djangoql_completion_enabled_by_default = True + + @admin.register(FacilityStaff) class FacilityStaffAdmin(DjangoQLSearchMixin, admin.ModelAdmin): autocomplete_fields = ["facility", "staff"] diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index 1e50f33271..b75bf06897 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -3,11 +3,12 @@ import boto3 from django.conf import settings from django.contrib.auth import get_user_model +from django.db.models import Q from rest_framework import serializers from care.facility.models import FACILITY_TYPES, Facility, FacilityLocalGovtBody from care.facility.models.bed import Bed -from care.facility.models.facility import FEATURE_CHOICES +from care.facility.models.facility import FEATURE_CHOICES, FacilityHubSpoke from care.facility.models.patient import PatientRegistration from care.users.api.serializers.lsg import ( DistrictSerializer, @@ -20,6 +21,7 @@ cover_image_validator, custom_image_extension_validator, ) +from care.utils.serializer.external_id_field import ExternalIdSerializerField from config.serializers import ChoiceField from config.validators import MiddlewareDomainAddressValidator @@ -178,6 +180,51 @@ def create(self, validated_data): return super().create(validated_data) +class FacilitySpokeSerializer(serializers.ModelSerializer): + id = serializers.UUIDField(source="external_id", read_only=True) + spoke = ExternalIdSerializerField( + queryset=Facility.objects.all(), required=True, write_only=True + ) + hub_object = FacilityBasicInfoSerializer(read_only=True, source="hub") + spoke_object = FacilityBasicInfoSerializer(read_only=True, source="spoke") + + class Meta: + model = FacilityHubSpoke + fields = ( + "id", + "spoke", + "hub_object", + "spoke_object", + "relationship", + "created_date", + "modified_date", + ) + read_only_fields = ( + "id", + "spoke_object", + "hub_object", + "created_date", + "modified_date", + ) + + def validate(self, data): + data["hub"] = self.context["facility"] + return data + + def validate_spoke(self, spoke: Facility): + hub: Facility = self.context["facility"] + + if hub == spoke: + raise serializers.ValidationError("Cannot set a facility as it's own spoke") + + if FacilityHubSpoke.objects.filter( + Q(hub=hub, spoke=spoke) | Q(hub=spoke, spoke=hub) + ).first(): + raise serializers.ValidationError("Facility is already a spoke/hub") + + return spoke + + class FacilityImageUploadSerializer(serializers.ModelSerializer): cover_image = serializers.ImageField( required=True, diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index 93b7e4bd2e..427be23f94 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.shortcuts import get_object_or_404 from django_filters import rest_framework as filters from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view @@ -14,6 +15,7 @@ FacilityBasicInfoSerializer, FacilityImageUploadSerializer, FacilitySerializer, + FacilitySpokeSerializer, ) from care.facility.models import ( Facility, @@ -21,8 +23,9 @@ FacilityPatientStatsHistory, HospitalDoctors, ) -from care.facility.models.facility import FacilityUser +from care.facility.models.facility import FacilityHubSpoke, FacilityUser from care.users.models import User +from care.utils.queryset.facility import get_facility_queryset class FacilityFilter(filters.FilterSet): @@ -183,3 +186,25 @@ class AllFacilityViewSet( filterset_class = FacilityFilter lookup_field = "external_id" search_fields = ["name", "district__name", "state__name"] + + +class FacilitySpokesViewSet(viewsets.ModelViewSet): + queryset = FacilityHubSpoke.objects.all().select_related("spoke", "hub") + serializer_class = FacilitySpokeSerializer + permission_classes = (IsAuthenticated, DRYPermissions) + lookup_field = "external_id" + + def get_queryset(self): + return self.queryset.filter(hub=self.get_facility()) + + def get_facility(self): + facilities = get_facility_queryset(self.request.user) + return get_object_or_404( + facilities.filter(external_id=self.kwargs["facility_external_id"]) + ) + + def get_serializer_context(self): + facility = self.get_facility() + context = super().get_serializer_context() + context["facility"] = facility + return context diff --git a/care/facility/migrations/0462_facilityhubspoke.py b/care/facility/migrations/0462_facilityhubspoke.py new file mode 100644 index 0000000000..f7fb6ca81c --- /dev/null +++ b/care/facility/migrations/0462_facilityhubspoke.py @@ -0,0 +1,83 @@ +# Generated by Django 5.1.1 on 2024-09-21 12:26 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("facility", "0461_remove_patientconsultation_prescriptions_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="FacilityHubSpoke", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "external_id", + models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), + ), + ( + "created_date", + models.DateTimeField(auto_now_add=True, db_index=True, null=True), + ), + ( + "modified_date", + models.DateTimeField(auto_now=True, db_index=True, null=True), + ), + ("deleted", models.BooleanField(db_index=True, default=False)), + ( + "relationship", + models.IntegerField( + choices=[(1, "Regular Hub"), (2, "Tele ICU Hub")], default=1 + ), + ), + ( + "hub", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="spokes", + to="facility.facility", + ), + ), + ( + "spoke", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="hubs", + to="facility.facility", + ), + ), + ], + options={ + "constraints": [ + models.CheckConstraint( + condition=models.Q(("hub", models.F("spoke")), _negated=True), + name="hub_and_spoke_not_same", + ), + models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("hub", "spoke"), + name="unique_hub_spoke", + ), + models.UniqueConstraint( + condition=models.Q(("deleted", False)), + fields=("spoke", "hub"), + name="unique_spoke_hub", + ), + ], + }, + ), + ] diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index da1ab2229a..209dbc3869 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -3,6 +3,10 @@ from django.contrib.postgres.fields import ArrayField from django.core.validators import MinValueValidator from django.db import models +from django.db.models import IntegerChoices +from django.db.models.constraints import CheckConstraint, UniqueConstraint +from django.utils.translation import gettext_lazy as _ +from rest_framework import serializers from simple_history.models import HistoricalRecords from care.facility.models import FacilityBaseModel, reverse_choices @@ -12,6 +16,7 @@ FacilityRelatedPermissionMixin, ) from care.users.models import District, LocalBody, State, Ward +from care.utils.models.base import BaseModel from care.utils.models.validators import mobile_or_landline_number_validator User = get_user_model() @@ -49,6 +54,11 @@ ] +class HubRelationship(IntegerChoices): + REGULAR_HUB = 1, _("Regular Hub") + TELE_ICU_HUB = 2, _("Tele ICU Hub") + + class FacilityFeature(models.IntegerChoices): CT_SCAN_FACILITY = 1, "CT Scan Facility" MATERNITY_CARE = 2, "Maternity Care" @@ -182,6 +192,17 @@ class FacilityFeature(models.IntegerChoices): REVERSE_FEATURE_CHOICES = reverse_choices(FEATURE_CHOICES) +# making sure A -> B -> C -> A does not happen +def check_if_spoke_is_not_ancestor(base_id: int, spoke_id: int): + ancestors_of_base = FacilityHubSpoke.objects.filter(spoke_id=base_id).values_list( + "hub_id", flat=True + ) + if spoke_id in ancestors_of_base: + raise serializers.ValidationError("This facility is already an ancestor hub") + for ancestor in ancestors_of_base: + check_if_spoke_is_not_ancestor(ancestor, spoke_id) + + class Facility(FacilityBaseModel, FacilityPermissionMixin): name = models.CharField(max_length=1000, blank=False, null=False) is_active = models.BooleanField(default=True) @@ -293,6 +314,41 @@ def get_facility_flags(self): CSV_MAKE_PRETTY = {"facility_type": (lambda x: REVERSE_FACILITY_TYPES[x])} +class FacilityHubSpoke(BaseModel, FacilityRelatedPermissionMixin): + hub = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="spokes") + spoke = models.ForeignKey(Facility, on_delete=models.CASCADE, related_name="hubs") + relationship = models.IntegerField( + choices=HubRelationship.choices, default=HubRelationship.REGULAR_HUB + ) + + class Meta: + constraints = [ + # Ensure hub and spoke are not the same + CheckConstraint( + check=~models.Q(hub=models.F("spoke")), + name="hub_and_spoke_not_same", + ), + # bidirectional uniqueness + UniqueConstraint( + fields=["hub", "spoke"], + name="unique_hub_spoke", + condition=models.Q(deleted=False), + ), + UniqueConstraint( + fields=["spoke", "hub"], + name="unique_spoke_hub", + condition=models.Q(deleted=False), + ), + ] + + def save(self, *args, **kwargs): + check_if_spoke_is_not_ancestor(self.hub.id, self.spoke.id) + return super().save(*args, **kwargs) + + def __str__(self): + return f"Hub: {self.hub.name} Spoke: {self.spoke.name}" + + class FacilityLocalGovtBody(models.Model): """ DEPRECATED_FROM: 2020-03-29 diff --git a/care/facility/tests/test_facility_api.py b/care/facility/tests/test_facility_api.py index 36aa09ca30..800f45fb8e 100644 --- a/care/facility/tests/test_facility_api.py +++ b/care/facility/tests/test_facility_api.py @@ -5,6 +5,7 @@ from rest_framework import status from rest_framework.test import APITestCase +from care.facility.models.facility import FacilityHubSpoke from care.utils.tests.test_utils import TestUtils @@ -152,6 +153,82 @@ def test_delete_with_active_patients(self): response = self.client.delete(f"/api/v1/facility/{facility.external_id}/") self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + def test_add_spoke(self): + facility = self.create_facility(self.super_user, self.district, self.local_body) + facility2 = self.create_facility( + self.super_user, self.district, self.local_body + ) + + state_admin = self.create_user("state_admin", self.district, user_type=40) + self.client.force_authenticate(user=state_admin) + response = self.client.post( + f"/api/v1/facility/{facility.external_id}/spokes/", + {"spoke": facility2.external_id}, + ) + self.assertIs(response.status_code, status.HTTP_201_CREATED) + + def test_delete_spoke(self): + facility = self.create_facility(self.super_user, self.district, self.local_body) + facility2 = self.create_facility( + self.super_user, self.district, self.local_body + ) + + state_admin = self.create_user("state_admin", self.district, user_type=40) + + spoke = FacilityHubSpoke.objects.create(hub=facility, spoke=facility2) + self.client.force_authenticate(user=state_admin) + response = self.client.delete( + f"/api/v1/facility/{facility.external_id}/spokes/{spoke.external_id}/" + ) + self.assertIs(response.status_code, status.HTTP_204_NO_CONTENT) + + def test_add_spoke_no_permission(self): + facility = self.create_facility(self.super_user, self.district, self.local_body) + facility2 = self.create_facility( + self.super_user, self.district, self.local_body + ) + + self.client.force_authenticate(user=self.user) + response = self.client.post( + f"/api/v1/facility/{facility.external_id}/spokes/", + {"spoke": facility2.external_id}, + ) + self.assertIs(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_delete_spoke_no_permission(self): + facility = self.create_facility(self.super_user, self.district, self.local_body) + facility2 = self.create_facility( + self.super_user, self.district, self.local_body + ) + + spoke = FacilityHubSpoke.objects.create(hub=facility, spoke=facility2) + self.client.force_authenticate(user=self.user) + response = self.client.delete( + f"/api/v1/facility/{facility.external_id}/spokes/{spoke.external_id}/" + ) + self.assertIs(response.status_code, status.HTTP_403_FORBIDDEN) + + def test_spoke_is_not_ancestor(self): + facility_a = self.create_facility( + self.super_user, self.district, self.local_body + ) + facility_b = self.create_facility( + self.super_user, self.district, self.local_body + ) + facility_c = self.create_facility( + self.super_user, self.district, self.local_body + ) + + FacilityHubSpoke.objects.create(hub=facility_a, spoke=facility_b) + FacilityHubSpoke.objects.create(hub=facility_b, spoke=facility_c) + + self.client.force_authenticate(user=self.super_user) + response = self.client.post( + f"/api/v1/facility/{facility_c.external_id}/spokes/", + {"spoke": facility_a.external_id}, + ) + self.assertIs(response.status_code, status.HTTP_400_BAD_REQUEST) + class FacilityCoverImageTests(TestUtils, APITestCase): @classmethod diff --git a/config/api_router.py b/config/api_router.py index 413106927f..dd79b1c122 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -35,7 +35,11 @@ EventTypeViewSet, PatientConsultationEventViewSet, ) -from care.facility.api.viewsets.facility import AllFacilityViewSet, FacilityViewSet +from care.facility.api.viewsets.facility import ( + AllFacilityViewSet, + FacilitySpokesViewSet, + FacilityViewSet, +) from care.facility.api.viewsets.facility_capacity import FacilityCapacityViewSet from care.facility.api.viewsets.facility_users import FacilityUserViewSet from care.facility.api.viewsets.file_upload import FileUploadViewSet @@ -218,6 +222,9 @@ FacilityDischargedPatientViewSet, basename="facility-discharged-patients", ) +facility_nested_router.register( + r"spokes", FacilitySpokesViewSet, basename="facility-spokes" +) router.register("asset", AssetViewSet, basename="asset") asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") From 4261fa54c4dfc68bd5492d62eafc06ed11593c3b Mon Sep 17 00:00:00 2001 From: Ayush Chauhan <77507609+ayushchauhan12@users.noreply.github.com> Date: Sun, 22 Sep 2024 16:52:30 +0530 Subject: [PATCH 20/46] fix:Hide asset users from users list page (#2107) (#2113) Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/users/api/viewsets/users.py | 1 + 1 file changed, 1 insertion(+) diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index f67ce6ac42..6a908ae940 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -107,6 +107,7 @@ class UserViewSet( created_by_user=F("created_by__username"), ) ) + queryset = queryset.filter(Q(asset__isnull=True)) lookup_field = "username" lookup_value_regex = "[^/]+" permission_classes = ( From ab99a1b697c1d595f65087283a38b9a269e4c4f2 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 22 Sep 2024 16:54:39 +0530 Subject: [PATCH 21/46] Fix linting --- Makefile | 3 + care/abdm/api/viewsets/auth.py | 8 +- care/abdm/api/viewsets/hip.py | 4 +- care/abdm/api/viewsets/monitoring.py | 4 +- care/abdm/service/gateway.py | 26 +++---- care/abdm/utils/api_call.py | 70 ++++++++--------- care/abdm/utils/fhir.py | 34 ++++---- care/audit_log/helpers.py | 12 +-- care/audit_log/middleware.py | 19 ++--- care/audit_log/receivers.py | 18 ++--- care/facility/api/serializers/asset.py | 4 +- care/facility/api/serializers/bed.py | 4 +- .../api/serializers/consultation_diagnosis.py | 2 +- care/facility/api/serializers/file_upload.py | 4 +- care/facility/api/serializers/shifting.py | 9 +-- care/facility/api/viewsets/asset.py | 3 +- care/facility/api/viewsets/mixins/access.py | 10 +-- care/facility/api/viewsets/patient.py | 2 +- .../commands/clean_patient_phone_numbers.py | 3 +- .../management/commands/load_event_types.py | 14 ++-- .../commands/load_icd11_diagnoses_data.py | 2 +- .../commands/load_medicines_data.py | 2 +- .../migrations/0415_auto_20240227_0130.py | 2 +- ...tientconsultation_has_consents_and_more.py | 2 - care/facility/tasks/asset_monitor.py | 4 +- care/facility/tasks/location_monitor.py | 4 +- care/facility/tasks/plausible_stats.py | 2 +- .../templatetags/data_formatting_tags.py | 2 +- .../utils/reports/discharge_summary.py | 2 +- care/hcx/static_data/pmjy_packages.py | 2 +- care/hcx/utils/fhir.py | 32 ++++---- care/users/api/viewsets/users.py | 2 +- .../management/commands/load_lsg_data.py | 5 +- .../management/commands/load_state_data.py | 2 +- .../management/commands/load_ward_data.py | 5 +- .../commands/populate_investigations.py | 2 +- care/users/reset_password_views.py | 2 +- care/users/signals.py | 4 +- care/utils/filters/multiselect.py | 2 +- care/utils/lock.py | 2 +- care/utils/models/validators.py | 4 +- care/utils/notification_handler.py | 77 +++++-------------- care/utils/serializer/external_id_field.py | 2 +- care/utils/tests/test_utils.py | 6 +- care/utils/ulid/ulid.py | 4 +- config/__init__.py | 2 +- config/authentication.py | 2 +- config/ratelimit.py | 2 +- config/settings/local.py | 4 +- config/validators.py | 8 +- merge_production_dotenvs_in_dotenv.py | 4 +- 51 files changed, 195 insertions(+), 255 deletions(-) diff --git a/Makefile b/Makefile index 0b84921fc0..aa4ff85e2a 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,9 @@ reset_db: ruff-all: ruff check . +ruff-fix-all: + ruff check --fix . + ruff: ruff check --fix $(shell git diff --name-only --staged | grep -E '\.py$$|\/pyproject.toml$$') diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index 52211dad8f..ad1d48beea 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -159,7 +159,7 @@ def post(self, request, *args, **kwargs): map( lambda consultation: { "id": str(consultation.external_id), - "name": f"Encounter: {str(consultation.created_date.date())}", + "name": f"Encounter: {consultation.created_date.date()!s}", }, PatientConsultation.objects.filter(patient=patient), ) @@ -213,7 +213,7 @@ def post(self, request, *args, **kwargs): map( lambda consultation: { "id": str(consultation.external_id), - "name": f"Encounter: {str(consultation.created_date.date())}", + "name": f"Encounter: {consultation.created_date.date()!s}", }, PatientConsultation.objects.filter(patient=patient), ) @@ -251,7 +251,7 @@ def post(self, request, *args, **kwargs): consent_id = data["hiRequest"]["consent"]["id"] consent = json.loads(cache.get(consent_id)) if consent_id in cache else None - if not consent or not consent["notification"]["status"] == "GRANTED": + if not consent or consent["notification"]["status"] != "GRANTED": return Response({}, status=status.HTTP_401_UNAUTHORIZED) # TODO: check if from and to are in range and consent expiry is greater than today @@ -269,7 +269,7 @@ def post(self, request, *args, **kwargs): {"request_id": data["requestId"], "transaction_id": data["transactionId"]} ) - if not on_data_request_response.status_code == 202: + if on_data_request_response.status_code != 202: return Response({}, status=status.HTTP_202_ACCEPTED) return Response( on_data_request_response, status=status.HTTP_400_BAD_REQUEST diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index bf7311b390..2de9b9fca8 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from rest_framework import status from rest_framework.decorators import action @@ -118,7 +118,7 @@ def share(self, request, *args, **kwargs): payload = { "requestId": str(uuid.uuid4()), "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") ), "acknowledgement": { "status": "SUCCESS", diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py index 54cfe30069..62b38360ce 100644 --- a/care/abdm/api/viewsets/monitoring.py +++ b/care/abdm/api/viewsets/monitoring.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from rest_framework import status from rest_framework.generics import GenericAPIView @@ -14,7 +14,7 @@ def get(self, request, *args, **kwargs): return Response( { "timestamp": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") ), "status": "UP", "error": None, diff --git a/care/abdm/service/gateway.py b/care/abdm/service/gateway.py index 1a284e2960..45f0c781e0 100644 --- a/care/abdm/service/gateway.py +++ b/care/abdm/service/gateway.py @@ -1,5 +1,5 @@ import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from django.conf import settings from django.db.models import Q @@ -18,7 +18,7 @@ def __init__(self): def consent_requests__init(self, consent: ConsentRequest): data = { "requestId": str(consent.external_id), - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "consent": { @@ -42,14 +42,14 @@ def consent_requests__init(self, consent: ConsentRequest): "permission": { "accessMode": consent.access_mode, "dateRange": { - "from": consent.from_time.astimezone(timezone.utc).strftime( + "from": consent.from_time.astimezone(UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), - "to": consent.to_time.astimezone(timezone.utc).strftime( + "to": consent.to_time.astimezone(UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), }, - "dataEraseAt": consent.expiry.astimezone(timezone.utc).strftime( + "dataEraseAt": consent.expiry.astimezone(UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "frequency": { @@ -67,7 +67,7 @@ def consent_requests__init(self, consent: ConsentRequest): def consent_requests__status(self, consent_request_id: str): data = { "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "consentRequestId": consent_request_id, @@ -80,7 +80,7 @@ def consent_requests__status(self, consent_request_id: str): def consents__hiu__on_notify(self, consent: ConsentRequest, request_id: str): data = { "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "resp": {"requestId": request_id}, @@ -104,7 +104,7 @@ def consents__hiu__on_notify(self, consent: ConsentRequest, request_id: str): def consents__fetch(self, consent_artefact_id: str): data = { "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "consentId": consent_artefact_id, @@ -121,7 +121,7 @@ def health_information__cm__request(self, artefact: ConsentArtefact): data = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "hiRequest": { @@ -132,7 +132,7 @@ def health_information__cm__request(self, artefact: ConsentArtefact): "cryptoAlg": artefact.key_material_algorithm, "curve": artefact.key_material_curve, "dhPublicKey": { - "expiry": artefact.expiry.astimezone(timezone.utc).strftime( + "expiry": artefact.expiry.astimezone(UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "parameters": f"{artefact.key_material_curve}/{artefact.key_material_algorithm}", @@ -169,13 +169,13 @@ def get_hf_id_by_health_id(self, health_id): def health_information__notify(self, artefact: ConsentArtefact): data = { "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "notification": { "consentId": str(artefact.artefact_id), "transactionId": str(artefact.transaction_id), - "doneAt": datetime.now(tz=timezone.utc).strftime( + "doneAt": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "notifier": { @@ -198,7 +198,7 @@ def health_information__notify(self, artefact: ConsentArtefact): def patients__find(self, abha_number: AbhaNumber): data = { "requestId": str(uuid.uuid4()), - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "query": { diff --git a/care/abdm/utils/api_call.py b/care/abdm/utils/api_call.py index 09a23e67de..e357a6f781 100644 --- a/care/abdm/utils/api_call.py +++ b/care/abdm/utils/api_call.py @@ -2,7 +2,7 @@ import logging import uuid from base64 import b64encode -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import requests from Crypto.Cipher import PKCS1_v1_5 @@ -83,7 +83,7 @@ def add_auth_header(self, headers): resp = requests.post( ABDM_TOKEN_URL, data=json.dumps(data), headers=auth_headers ) - logger.info("Token Response Status: {}".format(resp.status_code)) + logger.info(f"Token Response Status: {resp.status_code}") if resp.status_code < 300: # Checking if Content-Type is application/json if resp.headers["Content-Type"] != "application/json": @@ -92,21 +92,21 @@ def add_auth_header(self, headers): resp.headers["Content-Type"] ) ) - logger.info("Response: {}".format(resp.text)) + logger.info(f"Response: {resp.text}") return None else: data = resp.json() token = data["accessToken"] expires_in = data["expiresIn"] - logger.info("New Token: {}".format(token)) - logger.info("Expires in: {}".format(expires_in)) + logger.info(f"New Token: {token}") + logger.info(f"Expires in: {expires_in}") cache.set(ABDM_TOKEN_CACHE_KEY, token, expires_in) else: - logger.info("Bad Response: {}".format(resp.text)) + logger.info(f"Bad Response: {resp.text}") return None # logger.info("Returning Authorization Header: Bearer {}".format(token)) logger.info("Adding Authorization Header") - auth_header = {"Authorization": "Bearer {}".format(token)} + auth_header = {"Authorization": f"Bearer {token}"} return {**headers, **auth_header} def add_additional_headers(self, headers, additional_headers): @@ -118,9 +118,9 @@ def get(self, path, params=None, auth=None): headers = self.add_auth_header(headers) if auth: headers = self.add_user_header(headers, auth) - logger.info("Making GET Request to: {}".format(url)) + logger.info(f"Making GET Request to: {url}") response = requests.get(url, headers=headers, params=params) - logger.info("{} Response: {}".format(response.status_code, response.text)) + logger.info(f"{response.status_code} Response: {response.text}") return response def post(self, path, data=None, auth=None, additional_headers=None, method="POST"): @@ -140,9 +140,9 @@ def post(self, path, data=None, auth=None, additional_headers=None, method="POST # ) data_json = json.dumps(data) # logger.info("curl -X POST {} {} -d {}".format(url, headers_string, data_json)) - logger.info("Posting Request to: {}".format(url)) + logger.info(f"Posting Request to: {url}") response = requests.request(method, url, headers=headers, data=data_json) - logger.info("{} Response: {}".format(response.status_code, response.text)) + logger.info(f"{response.status_code} Response: {response.text}") return response @@ -153,7 +153,7 @@ def __init__(self): def generate_aadhaar_otp(self, data): path = "/v1/registration/aadhaar/generateOtp" response = self.api.post(path, data) - logger.info("{} Response: {}".format(response.status_code, response.text)) + logger.info(f"{response.status_code} Response: {response.text}") return response.json() def resend_aadhaar_otp(self, data): @@ -185,7 +185,7 @@ def verify_mobile_otp(self, data): # /v1/registration/aadhaar/createHealthIdWithPreVerified def create_health_id(self, data): path = "/v1/registration/aadhaar/createHealthIdWithPreVerified" - logger.info("Creating Health ID with data: {}".format(data)) + logger.info(f"Creating Health ID with data: {data}") # data.pop("healthId", None) response = self.api.post(path, data) return response.json() @@ -310,9 +310,9 @@ def get_abha_card_pdf(self, data): def get_qr_code(self, data, auth): path = "/v1/account/qrCode" access_token = self.generate_access_token(data) - logger.info("Getting QR Code for: {}".format(data)) + logger.info(f"Getting QR Code for: {data}") response = self.api.get(path, {}, access_token) - logger.info("QR Code Response: {}".format(response.text)) + logger.info(f"QR Code Response: {response.text}") return response.json() @@ -365,7 +365,7 @@ def get_hip_id_by_health_id(self, health_id): def add_care_context(self, access_token, request_id): if request_id not in self.temp_memory: - return + return None data = self.temp_memory[request_id] @@ -380,7 +380,7 @@ def add_care_context(self, access_token, request_id): "patient_id": str(consultation.patient.external_id), "patient_name": consultation.patient.name, "context_id": str(consultation.external_id), - "context_name": f"Encounter: {str(consultation.created_date.date())}", + "context_name": f"Encounter: {consultation.created_date.date()!s}", } ) @@ -390,7 +390,7 @@ def add_care_context(self, access_token, request_id): def save_linking_token(self, patient, access_token, request_id): if request_id not in self.temp_memory: - return + return None data = self.temp_memory[request_id] health_id = patient and patient["id"] or data["healthId"] @@ -415,11 +415,11 @@ def fetch_modes(self, data): self.temp_memory[request_id] = data if "authMode" in data and data["authMode"] == "DIRECT": self.init(request_id) - return + return None payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "query": { @@ -437,7 +437,7 @@ def fetch_modes(self, data): # "/v0.5/users/auth/init" def init(self, prev_request_id): if prev_request_id not in self.temp_memory: - return + return None path = "/v0.5/users/auth/init" additional_headers = {"X-CM-ID": settings.X_CM_ID} @@ -449,7 +449,7 @@ def init(self, prev_request_id): payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "query": { @@ -468,7 +468,7 @@ def init(self, prev_request_id): # "/v0.5/users/auth/confirm" def confirm(self, transaction_id, prev_request_id): if prev_request_id not in self.temp_memory: - return + return None path = "/v0.5/users/auth/confirm" additional_headers = {"X-CM-ID": settings.X_CM_ID} @@ -480,7 +480,7 @@ def confirm(self, transaction_id, prev_request_id): payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "transactionId": transaction_id, @@ -504,7 +504,7 @@ def auth_on_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "acknowledgement": {"status": "OK"}, @@ -524,7 +524,7 @@ def add_contexts(self, data): payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "link": { @@ -552,7 +552,7 @@ def on_discover(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "transactionId": data["transaction_id"], @@ -584,7 +584,7 @@ def on_link_init(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "transactionId": data["transaction_id"], @@ -615,7 +615,7 @@ def on_link_confirm(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "patient": { @@ -645,7 +645,7 @@ def on_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "acknowledgement": {"status": "OK", "consentId": data["consent_id"]}, @@ -663,7 +663,7 @@ def on_data_request(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "hiRequest": { @@ -718,14 +718,14 @@ def data_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "notification": { "consentId": data["consent_id"], "transactionId": data["transaction_id"], "doneAt": str( - datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z") + datetime.now(tz=UTC).strftime("%Y-%m-%dT%H:%M:%S.000Z") ), "statusNotification": { "sessionStatus": data["session_status"], @@ -754,7 +754,7 @@ def patient_status_on_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "acknowledgement": {"status": "OK"}, @@ -772,7 +772,7 @@ def patient_sms_notify(self, data): request_id = str(uuid.uuid4()) payload = { "requestId": request_id, - "timestamp": datetime.now(tz=timezone.utc).strftime( + "timestamp": datetime.now(tz=UTC).strftime( "%Y-%m-%dT%H:%M:%S.000Z" ), "notification": { diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index c0eaa47a57..5c56c54d81 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -1,5 +1,5 @@ import base64 -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 as uuid from fhir.resources.address import Address @@ -263,7 +263,7 @@ def _diagnostic_report(self): def _observation(self, title, value, id, date): if not value or (isinstance(value, dict) and not value["value"]): - return + return None return Observation( id=( @@ -410,7 +410,7 @@ def _immunization(self): return self._immunization_profile if not self.consultation.patient.is_vaccinated: - return + return None self._immunization_profile = Immunization( id=str(uuid()), @@ -516,7 +516,7 @@ def _prescription_composition(self): ] ), title="Prescription", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=[ CompositionSection( title="In Patient Prescriptions", @@ -560,7 +560,7 @@ def _health_document_composition(self): ] ), title="Health Document Record", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=[ CompositionSection( title="Health Document Record", @@ -605,7 +605,7 @@ def _wellness_composition(self): ] ), title="Wellness Record", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=list( map( lambda daily_round: CompositionSection( @@ -651,7 +651,7 @@ def _immunization_composition(self): ], ), title="Immunization", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=[ CompositionSection( title="IPD Immunization", @@ -701,7 +701,7 @@ def _diagnostic_report_composition(self): ], ), title="Diagnostic Report", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=[ CompositionSection( title="Investigation Report", @@ -738,7 +738,7 @@ def _discharge_summary_composition(self): ] ), title="Discharge Summary Document", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=[ CompositionSection( title="Prescribed medications", @@ -861,7 +861,7 @@ def _op_consultation_composition(self): ] ), title="OP Consultation Document", - date=datetime.now(timezone.utc).isoformat(), + date=datetime.now(UTC).isoformat(), section=[ CompositionSection( title="Prescribed medications", @@ -973,7 +973,7 @@ def _bundle_entry(self, resource): def create_prescription_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), @@ -1003,7 +1003,7 @@ def create_prescription_record(self): def create_wellness_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), @@ -1027,7 +1027,7 @@ def create_wellness_record(self): def create_immunization_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), @@ -1046,7 +1046,7 @@ def create_immunization_record(self): def create_diagnostic_report_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), @@ -1070,7 +1070,7 @@ def create_diagnostic_report_record(self): def create_health_document_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), @@ -1094,7 +1094,7 @@ def create_health_document_record(self): def create_discharge_summary_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), @@ -1149,7 +1149,7 @@ def create_discharge_summary_record(self): def create_op_consultation_record(self): id = str(uuid()) - now = datetime.now(timezone.utc).isoformat() + now = datetime.now(UTC).isoformat() return Bundle( id=id, identifier=Identifier(value=id), diff --git a/care/audit_log/helpers.py b/care/audit_log/helpers.py index 698ebdc5b9..04e89814bc 100644 --- a/care/audit_log/helpers.py +++ b/care/audit_log/helpers.py @@ -1,7 +1,7 @@ import re from fnmatch import fnmatch from functools import lru_cache -from typing import List, NamedTuple +from typing import NamedTuple from django.conf import settings from rest_framework.utils.encoders import JSONEncoder @@ -26,7 +26,7 @@ def seperate_hashable_dict(d: dict): def get_or_create_meta(instance): if not hasattr(instance._meta, "dal"): - setattr(instance._meta, "dal", MetaDataContainer()) + instance._meta.dal = MetaDataContainer() return instance @@ -34,7 +34,9 @@ def get_model_name(instance): return f"{instance._meta.app_label}.{instance.__class__.__name__}" -Search = NamedTuple("Search", [("type", str), ("value", str)]) +class Search(NamedTuple): + type: str + value: str def _make_search(item): @@ -46,7 +48,7 @@ def _make_search(item): def candidate_in_scope( - candidate: str, scope: List, is_application: bool = False + candidate: str, scope: list, is_application: bool = False ) -> bool: """ Check if the candidate string is valid with the scope supplied, @@ -80,7 +82,7 @@ def candidate_in_scope( return False -@lru_cache() +@lru_cache def exclude_model(model_name): if candidate_in_scope( model_name, diff --git a/care/audit_log/middleware.py b/care/audit_log/middleware.py index fdf7f8ffc5..35f23efd4b 100644 --- a/care/audit_log/middleware.py +++ b/care/audit_log/middleware.py @@ -2,21 +2,18 @@ import threading import uuid from hashlib import md5 -from typing import NamedTuple, Optional +from typing import NamedTuple from django.conf import settings from django.contrib.auth.models import AnonymousUser from django.http import HttpRequest, HttpResponse -RequestInformation = NamedTuple( - "RequestInformation", - [ - ("request_id", str), - ("request", HttpRequest), - ("response", Optional[HttpResponse]), - ("exception", Optional[Exception]), - ], -) + +class RequestInformation(NamedTuple): + request_id: str + request: HttpRequest + response: HttpResponse | None + exception: Exception | None logger = logging.getLogger(__name__) @@ -56,7 +53,7 @@ def save(request, response=None, exception=None): f"{md5(request.path.lower().encode('utf-8')).hexdigest()}::" f"{uuid.uuid4().hex}" ) - setattr(request, "dal_request_id", dal_request_id) + request.dal_request_id = dal_request_id AuditLogMiddleware.thread.__dal__ = RequestInformation( dal_request_id, request, response, exception diff --git a/care/audit_log/receivers.py b/care/audit_log/receivers.py index d9b8c8abc5..9b812c42a0 100644 --- a/care/audit_log/receivers.py +++ b/care/audit_log/receivers.py @@ -1,6 +1,6 @@ import json import logging -from typing import NamedTuple, Optional, Union +from typing import NamedTuple from django.conf import settings from django.contrib.auth.models import AbstractUser @@ -22,15 +22,11 @@ logger = logging.getLogger(__name__) -Event = NamedTuple( - "Event", - [ - ("model", str), - ("actor", AbstractUser), - ("entity_id", Union[int, str]), - ("changes", dict), - ], -) +class Event(NamedTuple): + model: str + actor: AbstractUser + entity_id: int | str + changes: dict @receiver(pre_delete, weak=False) @@ -105,7 +101,7 @@ def pre_save_signal(sender, instance, **kwargs) -> None: ) -def _post_processor(instance, event: Optional[Event], operation: Operation): +def _post_processor(instance, event: Event | None, operation: Operation): request_id = AuditLogMiddleware.get_current_request_id() actor = AuditLogMiddleware.get_current_user() model_name = get_model_name(instance) diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index 57526a8ba6..c1543d73c4 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -181,7 +181,7 @@ def validate(self, attrs): facilities = get_facility_queryset(user) if not facilities.filter(id=location.facility.id).exists(): - raise PermissionError() + raise PermissionError del attrs["location"] attrs["current_location"] = location @@ -201,7 +201,7 @@ def validate(self, attrs): ) # validate that last serviced date is not in the future - if "last_serviced_on" in attrs and attrs["last_serviced_on"]: + if attrs.get("last_serviced_on"): if attrs["last_serviced_on"] > datetime.now().date(): raise ValidationError("Last serviced on cannot be in the future") diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index 6daf57d609..ea66efd499 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -73,7 +73,7 @@ def validate(self, attrs): if (not facilities.filter(id=location.facility.id).exists()) or ( not facilities.filter(id=facility.id).exists() ): - raise PermissionError() + raise PermissionError del attrs["location"] attrs["location"] = location attrs["facility"] = facility @@ -109,7 +109,7 @@ def validate(self, attrs): if ( not facilities.filter(id=asset.current_location.facility.id).exists() ) or (not facilities.filter(id=bed.facility.id).exists()): - raise PermissionError() + raise PermissionError if asset.asset_class not in [ AssetClasses.HL7MONITOR.name, AssetClasses.ONVIF.name, diff --git a/care/facility/api/serializers/consultation_diagnosis.py b/care/facility/api/serializers/consultation_diagnosis.py index 62f5d80a6a..be0e64676f 100644 --- a/care/facility/api/serializers/consultation_diagnosis.py +++ b/care/facility/api/serializers/consultation_diagnosis.py @@ -112,7 +112,7 @@ def validate(self, attrs: Any) -> Any: ): validated["is_principal"] = False - if "is_principal" in validated and validated["is_principal"]: + if validated.get("is_principal"): verification_status = validated.get( "verification_status", self.instance.verification_status if self.instance else None, diff --git a/care/facility/api/serializers/file_upload.py b/care/facility/api/serializers/file_upload.py index e1f2c77752..6afc7d8434 100644 --- a/care/facility/api/serializers/file_upload.py +++ b/care/facility/api/serializers/file_upload.py @@ -111,9 +111,7 @@ def check_permissions(file_type, associating_id, user, action="create"): if not has_facility_permission(user, patient.facility): raise Exception("No Permission") return sample.id - elif file_type == FileUpload.FileType.CLAIM.value: - return associating_id - elif file_type == FileUpload.FileType.COMMUNICATION.value: + elif file_type == FileUpload.FileType.CLAIM.value or file_type == FileUpload.FileType.COMMUNICATION.value: return associating_id else: raise Exception("Undefined File Type") diff --git a/care/facility/api/serializers/shifting.py b/care/facility/api/serializers/shifting.py index ec4d416a8c..6c55f56d7a 100644 --- a/care/facility/api/serializers/shifting.py +++ b/care/facility/api/serializers/shifting.py @@ -280,9 +280,7 @@ def update(self, instance, validated_data): if ( status in self.PEACETIME_SHIFTING_STATUS and has_facility_permission(user, instance.origin_facility) - ): - pass - elif ( + ) or ( status in self.PEACETIME_RECIEVING_STATUS and has_facility_permission(user, instance.assigned_facility) ): @@ -294,10 +292,7 @@ def update(self, instance, validated_data): status in self.LIMITED_RECIEVING_STATUS and instance.assigned_facility and not has_facility_permission(user, instance.assigned_facility) - ): - raise ValidationError({"status": ["Permission Denied"]}) - - elif status in self.LIMITED_SHIFTING_STATUS and not has_facility_permission( + ) or status in self.LIMITED_SHIFTING_STATUS and not has_facility_permission( user, instance.shifting_approving_facility ): raise ValidationError({"status": ["Permission Denied"]}) diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 4f33bc6225..cf6ff5790d 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -15,9 +15,8 @@ from djqscsv import render_to_csv_response from drf_spectacular.utils import OpenApiParameter, extend_schema, inline_serializer from dry_rest_permissions.generics import DRYPermissions -from rest_framework import exceptions +from rest_framework import exceptions, status from rest_framework import filters as drf_filters -from rest_framework import status from rest_framework.decorators import action from rest_framework.exceptions import APIException, ValidationError from rest_framework.mixins import ( diff --git a/care/facility/api/viewsets/mixins/access.py b/care/facility/api/viewsets/mixins/access.py index 5cb5ee2c73..09f25f6d18 100644 --- a/care/facility/api/viewsets/mixins/access.py +++ b/care/facility/api/viewsets/mixins/access.py @@ -17,9 +17,8 @@ def get_queryset(self): queryset = queryset.filter( facility__district=self.request.user.district ) - else: - if hasattr(instance, "created_by"): - queryset = queryset.filter(created_by=self.request.user) + elif hasattr(instance, "created_by"): + queryset = queryset.filter(created_by=self.request.user) return queryset def filter_by_user_scope(self, queryset): @@ -34,9 +33,8 @@ def filter_by_user_scope(self, queryset): queryset = queryset.filter( facility__district=self.request.user.district ) - else: - if hasattr(instance, "created_by"): - queryset = queryset.filter(created_by=self.request.user) + elif hasattr(instance, "created_by"): + queryset = queryset.filter(created_by=self.request.user) return queryset def perform_create(self, serializer): diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 6ac7f960d2..a820331a92 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -243,7 +243,7 @@ def filter_by_review_missed(self, queryset, name, value): if isinstance(value, bool): if value: queryset = queryset.filter( - (Q(review_time__isnull=False) & Q(review_time__lt=timezone.now())) + Q(review_time__isnull=False) & Q(review_time__lt=timezone.now()) ) else: queryset = queryset.filter( diff --git a/care/facility/management/commands/clean_patient_phone_numbers.py b/care/facility/management/commands/clean_patient_phone_numbers.py index 059f0b1ec8..e95bb23686 100644 --- a/care/facility/management/commands/clean_patient_phone_numbers.py +++ b/care/facility/management/commands/clean_patient_phone_numbers.py @@ -1,5 +1,4 @@ import json -from typing import Optional import phonenumbers from django.core.management.base import BaseCommand @@ -24,7 +23,7 @@ class Command(BaseCommand): help = "Cleans the phone number field of patient to support E164 field" - def handle(self, *args, **options) -> Optional[str]: + def handle(self, *args, **options) -> str | None: qs = PatientRegistration.objects.all() failed = [] for patient in qs: diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index ad3511f79d..b41966f195 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -1,4 +1,4 @@ -from typing import Optional, Tuple, TypedDict +from typing import TypedDict from django.core.management import BaseCommand @@ -7,9 +7,9 @@ class EventTypeDef(TypedDict, total=False): name: str - model: Optional[str] - children: Tuple["EventType", ...] - fields: Tuple[str, ...] + model: str | None + children: tuple["EventType", ...] + fields: tuple[str, ...] class Command(BaseCommand): @@ -17,7 +17,7 @@ class Command(BaseCommand): Management command to load event types """ - consultation_event_types: Tuple[EventTypeDef, ...] = ( + consultation_event_types: tuple[EventTypeDef, ...] = ( { "name": "CONSULTATION", "model": "PatientConsultation", @@ -262,7 +262,7 @@ class Command(BaseCommand): }, ) - inactive_event_types: Tuple[str, ...] = ( + inactive_event_types: tuple[str, ...] = ( "RESPIRATORY", "INTAKE_OUTPUT", "VENTILATOR_MODES", @@ -271,7 +271,7 @@ class Command(BaseCommand): ) def create_objects( - self, types: Tuple[EventType, ...], model: str = None, parent: EventType = None + self, types: tuple[EventType, ...], model: str = None, parent: EventType = None ): for event_type in types: model = event_type.get("model", model) diff --git a/care/facility/management/commands/load_icd11_diagnoses_data.py b/care/facility/management/commands/load_icd11_diagnoses_data.py index ef47d81105..1354c40c94 100644 --- a/care/facility/management/commands/load_icd11_diagnoses_data.py +++ b/care/facility/management/commands/load_icd11_diagnoses_data.py @@ -6,7 +6,7 @@ def fetch_data(): - with open("data/icd11.json", "r") as json_file: + with open("data/icd11.json") as json_file: return json.load(json_file) diff --git a/care/facility/management/commands/load_medicines_data.py b/care/facility/management/commands/load_medicines_data.py index 6a8666070f..8f909a7424 100644 --- a/care/facility/management/commands/load_medicines_data.py +++ b/care/facility/management/commands/load_medicines_data.py @@ -14,7 +14,7 @@ class Command(BaseCommand): help = "Loads Medibase Medicines into the database from medibase.json" def fetch_data(self): - with open("data/medibase.json", "r") as json_file: + with open("data/medibase.json") as json_file: return json.load(json_file) def handle(self, *args, **options): diff --git a/care/facility/migrations/0415_auto_20240227_0130.py b/care/facility/migrations/0415_auto_20240227_0130.py index 805ec79680..eaa95dd9ca 100644 --- a/care/facility/migrations/0415_auto_20240227_0130.py +++ b/care/facility/migrations/0415_auto_20240227_0130.py @@ -1,7 +1,7 @@ # Generated by Django 4.2.10 on 2024-02-26 20:00 from django.db import migrations -from django.db.models import Count, F, Value, Window +from django.db.models import F, Window from django.db.models.functions import RowNumber diff --git a/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py b/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py index 9126f9b48f..2cb7a52521 100644 --- a/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py +++ b/care/facility/migrations/0444_patientconsultation_has_consents_and_more.py @@ -1,11 +1,9 @@ # Generated by Django 4.2.10 on 2024-07-04 16:20 -import uuid import django.contrib.postgres.fields import django.db.models.deletion from django.db import migrations, models -from django.db.models import Subquery class Migration(migrations.Migration): diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index f3b50a87c6..a977bf1c7b 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -49,7 +49,7 @@ def check_asset_status(): ) if not resolved_middleware: - logger.warn( + logger.warning( f"Asset {asset.external_id} does not have a middleware hostname" ) continue @@ -91,7 +91,7 @@ def check_asset_status(): else: result = asset_class.api_get(asset_class.get_url("devices/status")) except Exception as e: - logger.warn(f"Middleware {resolved_middleware} is down", e) + logger.warning(f"Middleware {resolved_middleware} is down", e) # If no status is returned, setting default status as down if not result or "error" in result: diff --git a/care/facility/tasks/location_monitor.py b/care/facility/tasks/location_monitor.py index de89d5b451..6df3aef7d7 100644 --- a/care/facility/tasks/location_monitor.py +++ b/care/facility/tasks/location_monitor.py @@ -29,7 +29,7 @@ def check_location_status(): ) if not resolved_middleware: - logger.warn( + logger.warning( f"No middleware hostname resolved for location {location.external_id}" ) continue @@ -54,7 +54,7 @@ def check_location_status(): new_status = AvailabilityStatus.OPERATIONAL except Exception as e: - logger.warn(f"Middleware {resolved_middleware} is down", e) + logger.warning(f"Middleware {resolved_middleware} is down", e) # Fetching the last record of the location last_record = ( diff --git a/care/facility/tasks/plausible_stats.py b/care/facility/tasks/plausible_stats.py index ea9e89b1d7..fdc897ebb4 100644 --- a/care/facility/tasks/plausible_stats.py +++ b/care/facility/tasks/plausible_stats.py @@ -148,4 +148,4 @@ def capture_goals(): f"Saved goal property entry for {goal_name} and property {property_name} on {yesterday}" ) except Exception as e: - logger.error(f"Failed to process goal {goal_name} due to error: {str(e)}") + logger.error(f"Failed to process goal {goal_name} due to error: {e!s}") diff --git a/care/facility/templatetags/data_formatting_tags.py b/care/facility/templatetags/data_formatting_tags.py index 18cc43c545..a3e4403ce6 100644 --- a/care/facility/templatetags/data_formatting_tags.py +++ b/care/facility/templatetags/data_formatting_tags.py @@ -14,7 +14,7 @@ def format_empty_data(data): @register.filter(name="format_to_sentence_case") def format_to_sentence_case(data): if data is None: - return + return None def convert_to_sentence_case(s): if s == "ICU": diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index b9fb47d077..7dc6f0f0d7 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -75,7 +75,7 @@ def get_diagnoses_data(consultation: PatientConsultation): diagnoses.append(diagnose) principal, unconfirmed, provisional, differential, confirmed = [], [], [], [], [] - for diagnosis, record in zip(diagnoses, entries): + for diagnosis, record in zip(diagnoses, entries, strict=False): _, verification_status, is_principal = record diagnosis.verification_status = verification_status diff --git a/care/hcx/static_data/pmjy_packages.py b/care/hcx/static_data/pmjy_packages.py index be20f9fc60..6c036f31ef 100644 --- a/care/hcx/static_data/pmjy_packages.py +++ b/care/hcx/static_data/pmjy_packages.py @@ -31,7 +31,7 @@ def get_representation(self) -> PMJYPackageObject: def load_pmjy_packages(): print("Loading PMJY Packages into the redis cache...", end="", flush=True) - with open("data/pmjy_packages.json", "r") as f: + with open("data/pmjy_packages.json") as f: pmjy_packages = json.load(f) for package in pmjy_packages: PMJYPackage( diff --git a/care/hcx/utils/fhir.py b/care/hcx/utils/fhir.py index 93df49ba82..31524a39ee 100644 --- a/care/hcx/utils/fhir.py +++ b/care/hcx/utils/fhir.py @@ -1,6 +1,6 @@ -from datetime import datetime, timezone +from datetime import UTC, datetime from functools import reduce -from typing import List, Literal, TypedDict +from typing import Literal, TypedDict import requests from fhir.resources import ( @@ -408,8 +408,8 @@ def create_coverage_eligibility_request_profile( priority="normal", status="active", purpose="validation", - service_period_start=datetime.now().astimezone(tz=timezone.utc), - service_period_end=datetime.now().astimezone(tz=timezone.utc), + service_period_start=datetime.now().astimezone(tz=UTC), + service_period_end=datetime.now().astimezone(tz=UTC), ): return coverageeligibilityrequest.CoverageEligibilityRequest( id=id, @@ -430,7 +430,7 @@ def create_coverage_eligibility_request_profile( start=service_period_start, end=service_period_end, ), - created=datetime.now().astimezone(tz=timezone.utc), + created=datetime.now().astimezone(tz=UTC), enterer=reference.Reference(reference=self.get_reference_url(enterer)), provider=reference.Reference(reference=self.get_reference_url(provider)), insurer=reference.Reference(reference=self.get_reference_url(insurer)), @@ -447,7 +447,7 @@ def create_claim_profile( self, id: str, identifier_value: str, - items: List[IClaimItem], + items: list[IClaimItem], patient: patient.Patient, provider: organization.Organization, insurer: organization.Organization, @@ -504,7 +504,7 @@ def create_claim_profile( ) ), patient=reference.Reference(reference=self.get_reference_url(patient)), - created=datetime.now().astimezone(tz=timezone.utc), + created=datetime.now().astimezone(tz=UTC), insurer=reference.Reference(reference=self.get_reference_url(insurer)), provider=reference.Reference(reference=self.get_reference_url(provider)), priority=codeableconcept.CodeableConcept( @@ -704,9 +704,9 @@ def create_coverage_eligibility_request_bundle( status="active", priority="normal", purpose="validation", - service_period_start=datetime.now().astimezone(tz=timezone.utc), - service_period_end=datetime.now().astimezone(tz=timezone.utc), - last_upadted=datetime.now().astimezone(tz=timezone.utc), + service_period_start=datetime.now().astimezone(tz=UTC), + service_period_end=datetime.now().astimezone(tz=UTC), + last_upadted=datetime.now().astimezone(tz=UTC), ): provider = self.create_provider_profile( provider_id, provider_name, provider_identifier_value @@ -754,7 +754,7 @@ def create_coverage_eligibility_request_bundle( value=identifier_value, ), type="collection", - timestamp=datetime.now().astimezone(tz=timezone.utc), + timestamp=datetime.now().astimezone(tz=UTC), entry=[ bundle.BundleEntry( fullUrl=self.get_reference_url(coverage_eligibility_request), @@ -804,7 +804,7 @@ def create_claim_bundle( type="institutional", priority="normal", claim_payee_type="provider", - last_updated=datetime.now().astimezone(tz=timezone.utc), + last_updated=datetime.now().astimezone(tz=UTC), supporting_info=[], related_claims=[], procedures=[], @@ -884,7 +884,7 @@ def create_claim_bundle( value=identifier_value, ), type="collection", - timestamp=datetime.now().astimezone(tz=timezone.utc), + timestamp=datetime.now().astimezone(tz=UTC), entry=[ bundle.BundleEntry( fullUrl=self.get_reference_url(claim), @@ -933,7 +933,7 @@ def create_communication_profile( identifier_value: str, payload: list, about: list, - last_updated=datetime.now().astimezone(tz=timezone.utc), + last_updated=datetime.now().astimezone(tz=UTC), ): return communication.Communication( id=id, @@ -980,7 +980,7 @@ def create_communication_bundle( communication_identifier_value: str, payload: list, about: list, - last_updated=datetime.now().astimezone(tz=timezone.utc), + last_updated=datetime.now().astimezone(tz=UTC), ): communication_profile = self.create_communication_profile( communication_id, @@ -1001,7 +1001,7 @@ def create_communication_bundle( value=identifier_value, ), type="collection", - timestamp=datetime.now().astimezone(tz=timezone.utc), + timestamp=datetime.now().astimezone(tz=UTC), entry=[ bundle.BundleEntry( fullUrl=self.get_reference_url(communication_profile), diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index 6a908ae940..7c2bb4ccca 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -29,7 +29,7 @@ def remove_facility_user_cache(user_id): - key = f"user_facilities:{str(user_id)}" + key = f"user_facilities:{user_id!s}" cache.delete(key) return True diff --git a/care/users/management/commands/load_lsg_data.py b/care/users/management/commands/load_lsg_data.py index 9e16d1327b..4135a2c235 100644 --- a/care/users/management/commands/load_lsg_data.py +++ b/care/users/management/commands/load_lsg_data.py @@ -1,7 +1,6 @@ import glob import json from collections import defaultdict -from typing import Optional from django.core.management.base import BaseCommand, CommandParser @@ -19,7 +18,7 @@ class Command(BaseCommand): def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("folder", help="path to the folder of JSONs") - def handle(self, *args, **options) -> Optional[str]: + def handle(self, *args, **options) -> str | None: folder = options["folder"] counter = 0 local_bodies = [] @@ -95,7 +94,7 @@ def create_local_bodies(local_body_list): for f in sorted(glob.glob(f"{folder}/*.json")): counter += 1 - with open(f"{f}", "r") as data_f: + with open(f"{f}") as data_f: data = json.load(data_f) data.pop("wards", None) local_bodies.append(data) diff --git a/care/users/management/commands/load_state_data.py b/care/users/management/commands/load_state_data.py index d72d28b022..cf2895b915 100644 --- a/care/users/management/commands/load_state_data.py +++ b/care/users/management/commands/load_state_data.py @@ -22,7 +22,7 @@ def handle(self, *args, **options): json_file_path = options["json_file_path"] data = [] - with open(json_file_path, "r") as json_file: + with open(json_file_path) as json_file: data = json.load(json_file) for item in data: diff --git a/care/users/management/commands/load_ward_data.py b/care/users/management/commands/load_ward_data.py index 5eb29fdcc0..c89cf47c8b 100644 --- a/care/users/management/commands/load_ward_data.py +++ b/care/users/management/commands/load_ward_data.py @@ -1,6 +1,5 @@ import glob import json -from typing import Optional from django.core.management.base import BaseCommand, CommandParser from django.db import IntegrityError @@ -19,7 +18,7 @@ class Command(BaseCommand): def add_arguments(self, parser: CommandParser) -> None: parser.add_argument("folder", help="path to the folder of JSONs") - def handle(self, *args, **options) -> Optional[str]: + def handle(self, *args, **options) -> str | None: def int_or_zero(value): try: int(value) @@ -60,7 +59,7 @@ def get_local_body(lb): ).first() for f in sorted(glob.glob(f"{folder}/*.json")): - with open(f"{f}", "r") as data_f: + with open(f"{f}") as data_f: data = json.load(data_f) wards = data.pop("wards", None) if wards is None: diff --git a/care/users/management/commands/populate_investigations.py b/care/users/management/commands/populate_investigations.py index 39b2162913..a802c66522 100644 --- a/care/users/management/commands/populate_investigations.py +++ b/care/users/management/commands/populate_investigations.py @@ -8,7 +8,7 @@ PatientInvestigationGroup, ) -with open("data/investigations.json", "r") as investigations_data: +with open("data/investigations.json") as investigations_data: investigations = json.load(investigations_data) with open("data/investigation_groups.json") as investigation_groups_data: diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index 28ab1cfe12..b3bda5db83 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -221,7 +221,7 @@ def post(self, request, *args, **kwargs): # find a user users = User.objects.filter( - **{"{}__exact".format(get_password_reset_lookup_field()): username} + **{f"{get_password_reset_lookup_field()}__exact": username} ) active_user_found = False diff --git a/care/users/signals.py b/care/users/signals.py index 85feb06a19..aba1418aff 100644 --- a/care/users/signals.py +++ b/care/users/signals.py @@ -30,9 +30,7 @@ def password_reset_token_created( "current_user": reset_password_token.user, "username": reset_password_token.user.username, "email": reset_password_token.user.email, - "reset_password_url": "{}/password_reset/{}".format( - settings.CURRENT_DOMAIN, reset_password_token.key - ), + "reset_password_url": f"{settings.CURRENT_DOMAIN}/password_reset/{reset_password_token.key}", } # render email text diff --git a/care/utils/filters/multiselect.py b/care/utils/filters/multiselect.py index 30c91bc446..fa4befe885 100644 --- a/care/utils/filters/multiselect.py +++ b/care/utils/filters/multiselect.py @@ -6,7 +6,7 @@ def filter(self, qs, value): if not value: return qs if not self.field_name: - return + return None values_list = value.split(",") filters = {self.field_name + "__in": values_list} qs = qs.filter(**filters) diff --git a/care/utils/lock.py b/care/utils/lock.py index f622600daf..333f8e5dc8 100644 --- a/care/utils/lock.py +++ b/care/utils/lock.py @@ -16,7 +16,7 @@ def __init__(self, key, timeout=settings.LOCK_TIMEOUT): def acquire(self): if not cache.set(self.key, True, self.timeout, nx=True): - raise ObjectLocked() + raise ObjectLocked def release(self): return cache.delete(self.key) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index e20233c3fb..bf394d9087 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -1,6 +1,6 @@ import re +from collections.abc import Iterable from fractions import Fraction -from typing import Iterable, List import jsonschema from django.core import validators @@ -40,7 +40,7 @@ def __eq__(self, other): def _extract_errors( self, errors: Iterable[jsonschema.ValidationError], - container: List[ValidationError], + container: list[ValidationError], ): for error in errors: if error.context: diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index 6451a9dade..97d0f38970 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -37,8 +37,8 @@ def send_webpush(**kwargs): def get_model_class(model_name): if model_name == "User": - return apps.get_model("users.{}".format(model_name)) - return apps.get_model("facility.{}".format(model_name)) + return apps.get_model(f"users.{model_name}") + return apps.get_model(f"facility.{model_name}") class NotificationGenerator: @@ -166,46 +166,26 @@ def generate_system_message(self): message = "" if isinstance(self.caused_object, PatientRegistration): if self.event == Notification.Event.PATIENT_CREATED.value: - message = "Patient {} was created by {}".format( - self.caused_object.name, self.caused_by.get_full_name() - ) + message = f"Patient {self.caused_object.name} was created by {self.caused_by.get_full_name()}" elif self.event == Notification.Event.PATIENT_UPDATED.value: - message = "Patient {} was updated by {}".format( - self.caused_object.name, self.caused_by.get_full_name() - ) + message = f"Patient {self.caused_object.name} was updated by {self.caused_by.get_full_name()}" if self.event == Notification.Event.PATIENT_DELETED.value: - message = "Patient {} was deleted by {}".format( - self.caused_object.name, self.caused_by.get_full_name() - ) + message = f"Patient {self.caused_object.name} was deleted by {self.caused_by.get_full_name()}" if self.event == Notification.Event.PATIENT_FILE_UPLOAD_CREATED.value: - message = "A file for patient {} was uploaded by {}".format( - self.caused_object.name, self.caused_by.get_full_name() - ) + message = f"A file for patient {self.caused_object.name} was uploaded by {self.caused_by.get_full_name()}" elif isinstance(self.caused_object, PatientConsultation): if self.event == Notification.Event.PATIENT_CONSULTATION_CREATED.value: - message = "Consultation for Patient {} was created by {}".format( - self.caused_object.patient.name, self.caused_by.get_full_name() - ) + message = f"Consultation for Patient {self.caused_object.patient.name} was created by {self.caused_by.get_full_name()}" elif self.event == Notification.Event.PATIENT_CONSULTATION_UPDATED.value: - message = "Consultation for Patient {} was updated by {}".format( - self.caused_object.patient.name, self.caused_by.get_full_name() - ) + message = f"Consultation for Patient {self.caused_object.patient.name} was updated by {self.caused_by.get_full_name()}" if self.event == Notification.Event.PATIENT_CONSULTATION_DELETED.value: - message = "Consultation for Patient {} was deleted by {}".format( - self.caused_object.patient.name, self.caused_by.get_full_name() - ) + message = f"Consultation for Patient {self.caused_object.patient.name} was deleted by {self.caused_by.get_full_name()}" if self.event == Notification.Event.CONSULTATION_FILE_UPLOAD_CREATED.value: - message = "Consultation file for Patient {} was uploaded by {}".format( - self.caused_object.patient.name, self.caused_by.get_full_name() - ) + message = f"Consultation file for Patient {self.caused_object.patient.name} was uploaded by {self.caused_by.get_full_name()}" if self.event == Notification.Event.PATIENT_PRESCRIPTION_CREATED.value: - message = "Prescription for Patient {} was created by {}".format( - self.caused_object.patient.name, self.caused_by.get_full_name() - ) + message = f"Prescription for Patient {self.caused_object.patient.name} was created by {self.caused_by.get_full_name()}" if self.event == Notification.Event.PATIENT_PRESCRIPTION_UPDATED.value: - message = "Prescription for Patient {} was updated by {}".format( - self.caused_object.patient.name, self.caused_by.get_full_name() - ) + message = f"Prescription for Patient {self.caused_object.patient.name} was updated by {self.caused_by.get_full_name()}" elif isinstance(self.caused_object, InvestigationSession): if self.event == Notification.Event.INVESTIGATION_SESSION_CREATED.value: message = ( @@ -216,42 +196,24 @@ def generate_system_message(self): ) elif isinstance(self.caused_object, InvestigationValue): if self.event == Notification.Event.INVESTIGATION_UPDATED.value: - message = "Investigation Value for {} for Patient {} was updated by {}".format( - self.caused_object.investigation.name, - self.caused_object.consultation.patient.name, - self.caused_by.get_full_name(), - ) + message = f"Investigation Value for {self.caused_object.investigation.name} for Patient {self.caused_object.consultation.patient.name} was updated by {self.caused_by.get_full_name()}" elif isinstance(self.caused_object, DailyRound): if ( self.event == Notification.Event.PATIENT_CONSULTATION_UPDATE_CREATED.value ): - message = "Consultation for Patient {} at facility {} was created by {}".format( - self.caused_object.consultation.patient.name, - self.caused_object.consultation.facility.name, - self.caused_by.get_full_name(), - ) + message = f"Consultation for Patient {self.caused_object.consultation.patient.name} at facility {self.caused_object.consultation.facility.name} was created by {self.caused_by.get_full_name()}" elif ( self.event == Notification.Event.PATIENT_CONSULTATION_UPDATE_UPDATED.value ): - message = "Consultation for Patient {} at facility {} was updated by {}".format( - self.caused_object.consultation.patient.name, - self.caused_object.consultation.facility.name, - self.caused_by.get_full_name(), - ) + message = f"Consultation for Patient {self.caused_object.consultation.patient.name} at facility {self.caused_object.consultation.facility.name} was updated by {self.caused_by.get_full_name()}" elif isinstance(self.caused_object, ShiftingRequest): if self.event == Notification.Event.SHIFTING_UPDATED.value: - message = "Shifting for Patient {} was updated by {}".format( - self.caused_object.patient.name, - self.caused_by.get_full_name(), - ) + message = f"Shifting for Patient {self.caused_object.patient.name} was updated by {self.caused_by.get_full_name()}" elif isinstance(self.caused_object, PatientNotes): if self.event == Notification.Event.PATIENT_NOTE_ADDED.value: - message = "Notes for Patient {} was added by {}".format( - self.caused_object.patient.name, - self.caused_by.get_full_name(), - ) + message = f"Notes for Patient {self.caused_object.patient.name} was added by {self.caused_by.get_full_name()}" return message @@ -259,10 +221,7 @@ def generate_sms_message(self): message = "" if isinstance(self.caused_object, ShiftingRequest): if self.event == Notification.Event.SHIFTING_UPDATED.value: - message = "Your Shifting Request to {} has been approved in Care. Please contact {} for any queries".format( - self.caused_object.assigned_facility.name, - self.caused_object.shifting_approving_facility.phone_number, - ) + message = f"Your Shifting Request to {self.caused_object.assigned_facility.name} has been approved in Care. Please contact {self.caused_object.shifting_approving_facility.phone_number} for any queries" return message def generate_sms_phone_numbers(self): diff --git a/care/utils/serializer/external_id_field.py b/care/utils/serializer/external_id_field.py index 88d711731c..43619d4334 100644 --- a/care/utils/serializer/external_id_field.py +++ b/care/utils/serializer/external_id_field.py @@ -5,7 +5,7 @@ from rest_framework.fields import empty -class UUIDValidator(object): +class UUIDValidator: def __call__(self, value): try: return uuid.UUID(value) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 3b340ff5b0..87350be35b 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -115,7 +115,7 @@ def get_base_url(self) -> str: Should return the base url of the testing viewset eg: return "api/v1/facility/" """ - raise NotImplementedError() + raise NotImplementedError @classmethod def create_state(cls, **kwargs) -> State: @@ -752,7 +752,7 @@ def get_list_representation(self, obj) -> dict: :param obj: Object to be represented :return: dict """ - raise NotImplementedError() + raise NotImplementedError def get_detail_representation(self, obj=None) -> dict: """ @@ -761,7 +761,7 @@ def get_detail_representation(self, obj=None) -> dict: :param data: data :return: dict """ - raise NotImplementedError() + raise NotImplementedError def get_local_body_district_state_representation(self, obj): """ diff --git a/care/utils/ulid/ulid.py b/care/utils/ulid/ulid.py index 0bee58bf11..f697c60041 100644 --- a/care/utils/ulid/ulid.py +++ b/care/utils/ulid/ulid.py @@ -22,7 +22,7 @@ def parse(cls, value) -> Self: if len_value == 10: return cls.from_timestamp(int(value)) raise ValueError( - "Cannot create ULID from string of length {}".format(len_value) + f"Cannot create ULID from string of length {len_value}" ) if isinstance(value, (int, float)): return cls.from_int(int(value)) @@ -31,5 +31,5 @@ def parse(cls, value) -> Self: if isinstance(value, memoryview): return cls.from_bytes(value.tobytes()) raise ValueError( - "Cannot create ULID from type {}".format(value.__class__.__name__) + f"Cannot create ULID from type {value.__class__.__name__}" ) diff --git a/config/__init__.py b/config/__init__.py index 35f2dd2419..744181dccb 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -1 +1 @@ -from .celery_app import app # noqa # isort:skip +from .celery_app import app # isort:skip diff --git a/config/authentication.py b/config/authentication.py index 275aaf468a..87d7501b2d 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -182,7 +182,7 @@ def get_user(self, validated_token, facility): if not asset_user: password = User.objects.make_random_password() asset_user = User( - username=f"asset{str(asset_obj.external_id)}", + username=f"asset{asset_obj.external_id!s}", email="support@ohc.network", password=f"{password}xyz", # The xyz makes it inaccessible without hashing gender=3, diff --git a/config/ratelimit.py b/config/ratelimit.py index 4f2594e339..d1faadbf51 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -38,7 +38,7 @@ def ratelimit( group = group key = "ip" else: - group = group + "-{}".format(key) + group = group + f"-{key}" key = GETKEY if is_ratelimited( request, diff --git a/config/settings/local.py b/config/settings/local.py index a5e9516c48..1e9196b582 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -11,14 +11,14 @@ # django-silk # ------------------------------------------------------------------------------ # https://github.com/jazzband/django-silk#requirements -INSTALLED_APPS += ["silk"] # noqa F405 +INSTALLED_APPS += ["silk"] # F405 MIDDLEWARE += ["silk.middleware.SilkyMiddleware"] # noqa F405 SILKY_PYTHON_PROFILER = True # django-extensions # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration -INSTALLED_APPS += ["django_extensions"] # noqa F405 +INSTALLED_APPS += ["django_extensions"] # F405 # Celery diff --git a/config/validators.py b/config/validators.py index fdf70e1628..754cb99496 100644 --- a/config/validators.py +++ b/config/validators.py @@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _ -class NumberValidator(object): +class NumberValidator: def validate(self, password, user=None): if not re.findall(r"\d", password): raise ValidationError( @@ -17,7 +17,7 @@ def get_help_text(self): return _("Your password must contain at least 1 digit, 0-9.") -class UppercaseValidator(object): +class UppercaseValidator: def validate(self, password, user=None): if not re.findall("[A-Z]", password): raise ValidationError( @@ -29,7 +29,7 @@ def get_help_text(self): return _("Your password must contain at least 1 uppercase letter, A-Z.") -class LowercaseValidator(object): +class LowercaseValidator: def validate(self, password, user=None): if not re.findall("[a-z]", password): raise ValidationError( @@ -41,7 +41,7 @@ def get_help_text(self): return _("Your password must contain at least 1 lowercase letter, a-z.") -class SymbolValidator(object): +class SymbolValidator: def validate(self, password, user=None): if not re.findall(r"[()[\]{}|\\`~!@#$%^&*_\-+=;:'\",<>./?]", password): raise ValidationError( diff --git a/merge_production_dotenvs_in_dotenv.py b/merge_production_dotenvs_in_dotenv.py index 89c0fbe477..6fdb55325c 100644 --- a/merge_production_dotenvs_in_dotenv.py +++ b/merge_production_dotenvs_in_dotenv.py @@ -1,5 +1,5 @@ import os -from typing import Sequence +from collections.abc import Sequence ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__)) PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, ".envs", ".production") @@ -15,7 +15,7 @@ def merge( ) -> None: with open(output_file_path, "w") as output_file: for merged_file_path in merged_file_paths: - with open(merged_file_path, "r") as merged_file: + with open(merged_file_path) as merged_file: merged_file_content = merged_file.read() output_file.write(merged_file_content) if append_linesep: From 5bb466ba14740cd909219bc1d4f148125a71df65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 18:01:52 +0530 Subject: [PATCH 22/46] Bump the all-dependencies group across 1 directory with 4 updates (#2479) * Bump the all-dependencies group across 1 directory with 4 updates --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .github/dependabot.yml | 8 ++------ .github/workflows/linter.yml | 4 ++-- Pipfile | 8 ++++---- Pipfile.lock | 38 ++++++++++++++++++------------------ 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ea748496cf..e9a27996a6 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,13 +10,9 @@ updates: schedule: interval: "weekly" groups: - all-dependencies: + boto: patterns: - - "*" - ignore: - - dependency-name: "redis-om" - - dependency-name: "pydantic" - - dependency-name: "fhir.resources" + - "boto*" - package-ecosystem: "docker" directory: "/docker" diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index fdc8e815a4..b7c108dc2f 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -19,14 +19,14 @@ jobs: uses: actions/checkout@v4 - name: Ruff check - uses: chartboost/ruff-action@v1 + uses: chartboost/ruff-action@491342200cdd1cf4d5132a30ddc546b3b5bc531b with: version: 0.6.7 args: "check" changed-files: "true" - name: Ruff format - uses: chartboost/ruff-action@v1 + uses: chartboost/ruff-action@491342200cdd1cf4d5132a30ddc546b3b5bc531b with: version: 0.6.7 args: "format" diff --git a/Pipfile b/Pipfile index a792c1df96..ad018a8b42 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.3.2" -boto3 = "==1.35.19" +boto3 = "==1.35.24" celery = "==5.4.0" django = "==5.1.1" django-environ = "==0.11.2" @@ -31,13 +31,13 @@ jsonschema = "==4.23.0" jwcrypto = "==1.5.6" newrelic = "==9.13.0" pillow = "==10.4.0" -psycopg = { extras = ["c"], version = "==3.1.19" } +psycopg = { extras = ["c"], version = "==3.2.2" } pycryptodome = "==3.20.0" pydantic = "==1.10.15" # fix for fhir.resources < 7.0.2 pyjwt = "==2.9.0" python-slugify = "==8.0.4" pywebpush = "==2.0.0" -redis = { extras = ["hiredis"], version = "==5.0.5" } # constraint for redis-om +redis = { extras = ["hiredis"], version = "==5.0.8" } # constraint for redis-om redis-om = "==0.3.1" # > 0.3.1 broken with pydantic < 2 requests = "==2.32.3" sentry-sdk = "==2.14.0" @@ -45,7 +45,7 @@ whitenoise = "==6.7.0" [dev-packages] black = "==24.8.0" -boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.19" } +boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.24" } coverage = "==7.6.1" debugpy = "==1.8.5" django-coverage-plugin = "==3.1.0" diff --git a/Pipfile.lock b/Pipfile.lock index 52ebb6605e..7e1bbe9223 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "bc01a15ab901f4331df57e40d2fbdaef62ff99cf3449949df2b640c9fd179a64" + "sha256": "5b4f7a6318a74468350c1254bfc9a1d45974283f1f7c73decd6cc672c702e786" }, "pipfile-spec": 6, "requires": { @@ -208,12 +208,12 @@ }, "boto3": { "hashes": [ - "sha256:84b3fe1727945bc3cada832d969ddb3dc0d08fce1677064ca8bdc13a89c1a143", - "sha256:9979fe674780a0b7100eae9156d74ee374cd1638a9f61c77277e3ce712f3e496" + "sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b", + "sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.19" + "version": "==1.35.24" }, "botocore": { "hashes": [ @@ -1154,17 +1154,17 @@ "c" ], "hashes": [ - "sha256:92d7b78ad82426cdcf1a0440678209faa890c6e1721361c2f8901f0dccd62961", - "sha256:dca5e5521c859f6606686432ae1c94e8766d29cc91f2ee595378c510cc5b0731" + "sha256:8bad2e497ce22d556dac1464738cb948f8d6bab450d965cf1d8a8effd52412e0", + "sha256:babf565d459d8f72fb65da5e211dd0b58a52c51e4e1fa9cadecff42d6b7619b2" ], - "markers": "python_version >= '3.7'", - "version": "==3.1.19" + "markers": "python_version >= '3.8'", + "version": "==3.2.2" }, "psycopg-c": { "hashes": [ - "sha256:8e90f53c430e7d661cb3a9298e2761847212ead1b24c5fb058fc9d0fd9616017" + "sha256:de8cac75bc6640ef0f54ad9187b81e07c430206a83c566b73d4cca41ecccb7c8" ], - "version": "==3.1.19" + "version": "==3.2.2" }, "py-vapid": { "hashes": [ @@ -1377,11 +1377,11 @@ "hiredis" ], "hashes": [ - "sha256:30b47d4ebb6b7a0b9b40c1275a19b87bb6f46b3bed82a89012cf56dea4024ada", - "sha256:3417688621acf6ee368dec4a04dd95881be24efd34c79f00d31f62bb528800ae" + "sha256:0c5b10d387568dfe0698c6fad6615750c24170e548ca2deac10c649d463e9870", + "sha256:56134ee08ea909106090934adc36f65c9bcbbaecea5b21ba704ba6fb561f8eb4" ], "markers": "python_version >= '3.7'", - "version": "==5.0.5" + "version": "==5.0.8" }, "redis-om": { "hashes": [ @@ -1814,12 +1814,12 @@ }, "boto3": { "hashes": [ - "sha256:84b3fe1727945bc3cada832d969ddb3dc0d08fce1677064ca8bdc13a89c1a143", - "sha256:9979fe674780a0b7100eae9156d74ee374cd1638a9f61c77277e3ce712f3e496" + "sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b", + "sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.19" + "version": "==1.35.24" }, "boto3-stubs": { "extras": [ @@ -1827,11 +1827,11 @@ "s3" ], "hashes": [ - "sha256:6adace32995ae7b88675cf0bbde3b4f31876cbf57520db1ec1f392ac32660b4c", - "sha256:c5842cd82d4a1570613f178831c2b6d1b60f511b87f56cc014f2a216c03ecf5a" + "sha256:329a9944a75bedd9b343230b03d37c94158e60e7b7a9553e03be9a8f327cd41c", + "sha256:f96c814f26f2ab8fcd42811b7208105f43657fe15ffa82be2cdc7734160e28c4" ], "markers": "python_version >= '3.8'", - "version": "==1.35.19" + "version": "==1.35.24" }, "botocore": { "hashes": [ From 6aa885d70d8ff65932d0ada95e426c4eeb77a6b2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 22:37:36 +0530 Subject: [PATCH 23/46] Bump pydantic from 1.10.15 to 1.10.18 (#2484) Bumps [pydantic](https://github.com/pydantic/pydantic) from 1.10.15 to 1.10.18. - [Release notes](https://github.com/pydantic/pydantic/releases) - [Changelog](https://github.com/pydantic/pydantic/blob/main/HISTORY.md) - [Commits](https://github.com/pydantic/pydantic/compare/v1.10.15...v1.10.18) --- updated-dependencies: - dependency-name: pydantic dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Pipfile | 2 +- Pipfile.lock | 83 ++++++++++++++++++++++++++++------------------------ 2 files changed, 46 insertions(+), 39 deletions(-) diff --git a/Pipfile b/Pipfile index ad018a8b42..1917b7dc31 100644 --- a/Pipfile +++ b/Pipfile @@ -33,7 +33,7 @@ newrelic = "==9.13.0" pillow = "==10.4.0" psycopg = { extras = ["c"], version = "==3.2.2" } pycryptodome = "==3.20.0" -pydantic = "==1.10.15" # fix for fhir.resources < 7.0.2 +pydantic = "==1.10.18" # fix for fhir.resources < 7.0.2 pyjwt = "==2.9.0" python-slugify = "==8.0.4" pywebpush = "==2.0.0" diff --git a/Pipfile.lock b/Pipfile.lock index 7e1bbe9223..9223ef356d 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5b4f7a6318a74468350c1254bfc9a1d45974283f1f7c73decd6cc672c702e786" + "sha256": "20d6dbc4d87c784ecfc03228f4a162a9583de48d46cdc79fd71028f6d04244c9" }, "pipfile-spec": 6, "requires": { @@ -1224,46 +1224,53 @@ "email" ], "hashes": [ - "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de", - "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986", - "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55", - "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4", - "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58", - "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3", - "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12", - "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d", - "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7", - "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53", - "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb", - "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51", - "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948", - "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022", - "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed", - "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383", - "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4", - "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b", - "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2", - "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528", - "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf", - "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8", - "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc", - "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f", - "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0", - "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7", - "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c", - "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44", - "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654", - "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0", - "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb", - "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00", - "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1", - "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c", - "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22", - "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0" + "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620", + "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82", + "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62", + "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c", + "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c", + "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682", + "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048", + "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b", + "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03", + "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f", + "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a", + "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1", + "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe", + "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33", + "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f", + "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518", + "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485", + "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f", + "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec", + "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70", + "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86", + "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf", + "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d", + "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588", + "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481", + "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9", + "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3", + "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab", + "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7", + "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a", + "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0", + "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc", + "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861", + "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357", + "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a", + "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3", + "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80", + "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02", + "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b", + "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5", + "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2", + "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890", + "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f" ], "index": "pypi", "markers": "python_version >= '3.7'", - "version": "==1.10.15" + "version": "==1.10.18" }, "pyjwt": { "hashes": [ From 15debe1f45564ee7f810d3ac9b259ca6b71739a5 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Sun, 22 Sep 2024 22:42:36 +0530 Subject: [PATCH 24/46] Prescriptions and MAR: Disallow users to perform edits on discharged encounters (#2133) Prescriptions and MAR: Disallow users to perform edits on discharged encounters (#2133) --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/api/serializers/prescription.py | 15 +- care/facility/api/viewsets/prescription.py | 11 +- .../test_medicine_administrations_api.py | 189 +++++++++++ care/facility/tests/test_medicine_api.py | 298 ------------------ care/facility/tests/test_prescriptions_api.py | 156 ++++++++- 5 files changed, 359 insertions(+), 310 deletions(-) create mode 100644 care/facility/tests/test_medicine_administrations_api.py delete mode 100644 care/facility/tests/test_medicine_api.py diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index ed68e772a7..f94d3cccff 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -57,6 +57,13 @@ def validate(self, attrs): return super().validate(attrs) + def create(self, validated_data): + if validated_data["prescription"].consultation.discharge_date: + raise serializers.ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) + return super().create(validated_data) + class Meta: model = MedicineAdministration exclude = ("deleted",) @@ -149,4 +156,10 @@ def validate(self, attrs): 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. + + def create(self, validated_data): + if validated_data["consultation"].discharge_date: + raise serializers.ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) + return super().create(validated_data) diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index c949e350e5..a551dd639f 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -8,6 +8,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.serializers import ValidationError from rest_framework.viewsets import GenericViewSet, ViewSet from care.facility.api.serializers.prescription import ( @@ -78,6 +79,10 @@ def get_queryset(self): @extend_schema(tags=["prescription_administration"]) @action(methods=["POST"], detail=True) def archive(self, request, *args, **kwargs): + if self.get_consultation_obj().discharge_date: + raise ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) instance = self.get_object() if instance.archived_on: return Response( @@ -137,12 +142,16 @@ def perform_create(self, serializer): detail=True, ) def discontinue(self, request, *args, **kwargs): + consultation_obj = self.get_consultation_obj() + if consultation_obj.discharge_date: + raise ValidationError( + {"consultation": "Not allowed for discharged consultations"} + ) prescription_obj = self.get_object() prescription_obj.discontinued = True prescription_obj.discontinued_reason = request.data.get( "discontinued_reason", None ) - consultation_obj = self.get_consultation_obj() NotificationGenerator( event=Notification.Event.PATIENT_PRESCRIPTION_UPDATED, caused_by=self.request.user, diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_administrations_api.py new file mode 100644 index 0000000000..bbac0a5274 --- /dev/null +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -0,0 +1,189 @@ +from django.utils import timezone +from rest_framework import status +from rest_framework.test import APITestCase + +from care.facility.models import ( + MedibaseMedicine, + MedicineAdministration, + Prescription, + PrescriptionDosageType, +) +from care.utils.tests.test_utils import TestUtils + + +class MedicineAdministrationsApiTestCase(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("nurse1", cls.district, home_facility=cls.facility) + cls.patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.remote_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.remote_user = cls.create_user( + "remote-nurse", cls.district, home_facility=cls.remote_facility + ) + cls.discharged_patient = cls.create_patient( + cls.district, cls.facility, local_body=cls.local_body + ) + cls.discharged_consultation = cls.create_consultation( + cls.discharged_patient, cls.facility, discharge_date="2024-01-04T00:00:00Z" + ) + + def setUp(self) -> None: + super().setUp() + self.normal_prescription = self.create_prescription() + self.discharged_prescription = self.create_prescription( + consultation=self.discharged_consultation + ) + self.discharged_administration = self.create_medicine_administration( + prescription=self.discharged_prescription + ) + + def create_prescription(self, **kwargs): + patient = kwargs.pop("patient", self.patient) + consultation = kwargs.pop( + "consultation", self.create_consultation(patient, self.facility) + ) + data = { + "consultation": consultation, + "medicine": MedibaseMedicine.objects.first(), + "prescription_type": "REGULAR", + "base_dosage": "1 mg", + "frequency": "OD", + "dosage_type": kwargs.get( + "dosage_type", PrescriptionDosageType.REGULAR.value + ), + } + return Prescription.objects.create( + **{**data, **kwargs, "prescribed_by": self.user} + ) + + def create_medicine_administration(self, prescription, **kwargs): + return MedicineAdministration.objects.create( + prescription=prescription, administered_by=self.user, **kwargs + ) + + def test_administer_for_discharged_consultations(self): + prescription = self.discharged_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_archive_for_discharged_consultations(self): + res = self.client.post( + f"/api/v1/consultation/{self.discharged_prescription.consultation.external_id}/prescription_administration/{self.discharged_administration.external_id}/archive/" + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_non_home_facility(self): + self.client.force_authenticate(self.remote_user) + prescription = self.discharged_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + ) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + + def test_archive_non_home_facility(self): + self.client.force_authenticate(self.remote_user) + res = self.client.post( + f"/api/v1/consultation/{self.discharged_prescription.consultation.external_id}/prescription_administration/{self.discharged_administration.external_id}/archive/" + ) + self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND) + + def test_administer_and_archive(self): + # test administer + prescription = self.normal_prescription + 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_201_CREATED) + + administration_id = res.data["id"] + + # test archive + archive_path = f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/{administration_id}/archive/" + res = self.client.post(archive_path, {}) + self.assertEqual(res.status_code, status.HTTP_200_OK) + + # test archive again + res = self.client.post(archive_path, {}) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + # test list administrations + res = self.client.get( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue( + any([administration_id == x["id"] for x in res.data["results"]]) + ) + + # test archived list administrations + res = self.client.get( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=true" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertTrue( + any([administration_id == x["id"] for x in res.data["results"]]) + ) + + # test archived list administrations + res = self.client.get( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=false" + ) + self.assertEqual(res.status_code, status.HTTP_200_OK) + self.assertFalse( + any([administration_id == x["id"] for x in res.data["results"]]) + ) + + def test_administer_in_future(self): + prescription = self.normal_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes", "administered_date": "2300-09-01T16:34"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_in_past(self): + prescription = self.normal_prescription + res = self.client.post( + f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", + {"notes": "Test Notes", "administered_date": "2019-09-01T16:34"}, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_administer_discontinued(self): + prescription = self.create_prescription( + discontinued=True, discontinued_date=timezone.now() + ) + 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) + + 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_medicine_api.py b/care/facility/tests/test_medicine_api.py deleted file mode 100644 index b683a6c22e..0000000000 --- a/care/facility/tests/test_medicine_api.py +++ /dev/null @@ -1,298 +0,0 @@ -from django.utils import timezone -from rest_framework import status -from rest_framework.test import APITestCase - -from care.facility.models import MedibaseMedicine, Prescription, PrescriptionDosageType -from care.utils.tests.test_utils import TestUtils - - -class MedicinePrescriptionApiTestCase(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("staff1", 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) - meds = MedibaseMedicine.objects.all().values_list("external_id", flat=True)[:2] - cls.medicine1 = str(meds[0]) - - def setUp(self) -> None: - super().setUp() - - def prescription_data(self, **kwargs): - data = { - "medicine": self.medicine1, - "prescription_type": "REGULAR", - "base_dosage": "1 mg", - "frequency": "OD", - "dosage_type": kwargs.get( - "dosage_type", PrescriptionDosageType.REGULAR.value - ), - } - return {**data, **kwargs} - - def test_invalid_dosage(self): - 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()["base_dosage"][0], - "Invalid Input, must be in the format: ", - ) - - def test_dosage_out_of_range(self): - 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()["base_dosage"][0], - "Input amount must be between 0.0001 and 5000", - ) - - 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()["base_dosage"][0], - "Input amount must be between 0.0001 and 5000", - ) - - def test_dosage_precision(self): - 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()["base_dosage"][0], - "Input amount must have at most 4 decimal places", - ) - - def test_dosage_unit_invalid(self): - 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()["base_dosage"][0].startswith("Unit must be one of")) - - def test_dosage_leading_zero(self): - 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()["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(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()["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(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_valid_dosage(self): - 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 - 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("staff1", cls.district, home_facility=cls.facility) - cls.patient = cls.create_patient( - cls.district, cls.facility, local_body=cls.local_body - ) - - def setUp(self) -> None: - super().setUp() - self.normal_prescription = self.create_prescription() - - def create_prescription(self, **kwargs): - data = { - "consultation": self.create_consultation(self.patient, self.facility), - "medicine": MedibaseMedicine.objects.first(), - "prescription_type": "REGULAR", - "base_dosage": "1 mg", - "frequency": "OD", - "dosage_type": kwargs.get( - "dosage_type", PrescriptionDosageType.REGULAR.value - ), - } - return Prescription.objects.create( - **{**data, **kwargs, "prescribed_by": self.user} - ) - - def test_administer_and_archive(self): - # test administer - prescription = self.normal_prescription - 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_201_CREATED) - - administration_id = res.data["id"] - - # test archive - archive_path = f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/{administration_id}/archive/" - res = self.client.post(archive_path, {}) - self.assertEqual(res.status_code, status.HTTP_200_OK) - - # test archive again - res = self.client.post(archive_path, {}) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - # test list administrations - res = self.client.get( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) - - # test archived list administrations - res = self.client.get( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=true" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) - - # test archived list administrations - res = self.client.get( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=false" - ) - self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertFalse( - any([administration_id == x["id"] for x in res.data["results"]]) - ) - - def test_administer_in_future(self): - prescription = self.normal_prescription - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes", "administered_date": "2300-09-01T16:34"}, - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_administer_in_past(self): - prescription = self.normal_prescription - res = self.client.post( - f"/api/v1/consultation/{prescription.consultation.external_id}/prescriptions/{prescription.external_id}/administer/", - {"notes": "Test Notes", "administered_date": "2019-09-01T16:34"}, - ) - self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) - - def test_administer_discontinued(self): - prescription = self.create_prescription( - discontinued=True, discontinued_date=timezone.now() - ) - 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) - - 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 a357e51a39..3168aeb151 100644 --- a/care/facility/tests/test_prescriptions_api.py +++ b/care/facility/tests/test_prescriptions_api.py @@ -1,7 +1,7 @@ from rest_framework import status from rest_framework.test import APITestCase -from care.facility.models import MedibaseMedicine +from care.facility.models import MedibaseMedicine, Prescription from care.utils.tests.test_utils import TestUtils @@ -13,15 +13,23 @@ def setUpTestData(cls) -> None: 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.user = cls.create_user("nurse1", cls.district, home_facility=cls.facility) + cls.remote_facility = cls.create_facility( + cls.super_user, cls.district, cls.local_body + ) + cls.remote_user = cls.create_user( + "remote-nurse", cls.district, home_facility=cls.remote_facility + ) cls.patient = cls.create_patient(cls.district, cls.facility) + cls.consultation = cls.create_consultation(cls.patient, cls.facility) + cls.discharged_patient = cls.create_patient(cls.district, cls.facility) + cls.discharged_consultation = cls.create_consultation( + cls.patient, cls.facility, discharge_date="2002-04-01T16:30:00Z" + ) def setUp(self) -> None: super().setUp() - self.consultation = self.create_consultation(self.patient, self.facility) - self.medicine = MedibaseMedicine.objects.first() - self.medicine2 = MedibaseMedicine.objects.all()[1] - + self.medicine, self.medicine_2 = MedibaseMedicine.objects.all()[:2] self.normal_prescription_data = { "medicine": self.medicine.external_id, "prescription_type": "REGULAR", @@ -29,14 +37,22 @@ def setUp(self) -> None: "frequency": "OD", "dosage_type": "REGULAR", } - - self.normal_prescription_data2 = { - "medicine": self.medicine2.external_id, + self.normal_prescription_data_2 = { + "medicine": self.medicine_2.external_id, "prescription_type": "REGULAR", "base_dosage": "1 mg", "frequency": "OD", "dosage_type": "REGULAR", } + self.discharged_consultation_prescription = Prescription.objects.create( + consultation=self.discharged_consultation, + medicine=self.medicine, + ) + + def prescription_data(self, **kwargs): + data = self.normal_prescription_data + data.update(**kwargs) + return data def test_create_normal_prescription(self): response = self.client.post( @@ -45,6 +61,126 @@ def test_create_normal_prescription(self): ) self.assertEqual(response.status_code, status.HTTP_201_CREATED) + def test_create_prescription_non_home_facility(self): + self.client.force_authenticate(self.remote_user) + response = self.client.post( + f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) + + def test_create_prescription_on_discharged_consultation(self): + response = self.client.post( + f"/api/v1/consultation/{self.discharged_consultation.external_id}/prescriptions/", + self.normal_prescription_data, + ) + self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) + + def test_discontinue_prescription_on_discharged_consultation(self): + res = self.client.post( + f"/api/v1/consultation/{self.discharged_consultation.external_id}/prescriptions/{self.discharged_consultation_prescription.external_id}/discontinue/", + { + "discontinued_reason": "Test Reason", + }, + ) + self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST) + + def test_invalid_dosage(self): + 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()["base_dosage"][0], + "Invalid Input, must be in the format: ", + ) + + def test_dosage_out_of_range(self): + 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()["base_dosage"][0], + "Input amount must be between 0.0001 and 5000", + ) + + 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()["base_dosage"][0], + "Input amount must be between 0.0001 and 5000", + ) + + def test_dosage_precision(self): + 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()["base_dosage"][0], + "Input amount must have at most 4 decimal places", + ) + + def test_dosage_unit_invalid(self): + 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()["base_dosage"][0].startswith("Unit must be one of")) + + def test_dosage_leading_zero(self): + 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()["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(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()["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(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_valid_dosage(self): + 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_prescribe_duplicate_active_medicine_and_discontinue(self): """ 1. Creates a prescription with Medicine A @@ -131,7 +267,7 @@ def test_medicine_filter_for_prescription(self): ) self.client.post( f"/api/v1/consultation/{self.consultation.external_id}/prescriptions/", - self.normal_prescription_data2, + self.normal_prescription_data_2, ) # get all prescriptions without medicine filter From ebc03d9e4005527783bac94faff49e3c4121aa64 Mon Sep 17 00:00:00 2001 From: Uday Sagar <111575806+UdaySagar-Git@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:11:05 +0530 Subject: [PATCH 25/46] Reply feature for Doctor notes (#2160) Reply feature for Doctor notes (#2160) --------- Co-authored-by: Rithvik Nishad Co-authored-by: Khavin Shankar Co-authored-by: rithviknishad Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> Co-authored-by: vigneshhari --- care/facility/api/serializers/patient.py | 39 +++++++++++++++++ care/facility/api/viewsets/patient.py | 4 +- .../migrations/0463_patientnotes_reply_to.py | 25 +++++++++++ care/facility/models/patient.py | 7 +++ care/facility/tests/test_patient_api.py | 43 +++++++++++++++++++ 5 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 care/facility/migrations/0463_patientnotes_reply_to.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 32e378dc9a..b3a88b21e3 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -500,6 +500,21 @@ class Meta: exclude = ("patient_note",) +class ReplyToPatientNoteSerializer(serializers.ModelSerializer): + id = serializers.CharField(source="external_id", read_only=True) + created_by_object = UserBaseMinimumSerializer(source="created_by", read_only=True) + + class Meta: + model = PatientNotes + fields = ( + "id", + "created_by_object", + "created_date", + "user_type", + "note", + ) + + class PatientNotesSerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) facility = FacilityBasicInfoSerializer(read_only=True) @@ -515,6 +530,13 @@ class PatientNotesSerializer(serializers.ModelSerializer): thread = serializers.ChoiceField( choices=PatientNoteThreadChoices, required=False, allow_null=False ) + reply_to = ExternalIdSerializerField( + queryset=PatientNotes.objects.all(), + write_only=True, + required=False, + allow_null=True, + ) + reply_to_object = ReplyToPatientNoteSerializer(source="reply_to", read_only=True) def validate_empty_values(self, data): if not data.get("note", "").strip(): @@ -524,6 +546,10 @@ def validate_empty_values(self, data): def create(self, validated_data): if "thread" not in validated_data: raise serializers.ValidationError({"thread": "This field is required"}) + if "consultation" not in validated_data: + raise serializers.ValidationError( + {"consultation": "This field is required"} + ) user_type = User.REVERSE_TYPE_MAP[validated_data["created_by"].user_type] # If the user is a doctor and the note is being created in the home facility # then the user type is doctor else it is a remote specialist @@ -536,6 +562,17 @@ def create(self, validated_data): # If the user is not a doctor then the user type is the same as the user type validated_data["user_type"] = user_type + if validated_data.get("reply_to"): + reply_to_note = validated_data["reply_to"] + if reply_to_note.thread != validated_data["thread"]: + raise serializers.ValidationError( + "Reply to note should be in the same thread" + ) + if reply_to_note.consultation != validated_data.get("consultation"): + raise serializers.ValidationError( + "Reply to note should be in the same consultation" + ) + user = self.context["request"].user note = validated_data.get("note") with transaction.atomic(): @@ -584,6 +621,8 @@ class Meta: "modified_date", "last_edited_by", "last_edited_date", + "reply_to", + "reply_to_object", ) read_only_fields = ( "id", diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index a820331a92..4953ef54af 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -941,7 +941,9 @@ class PatientNotesViewSet( ): queryset = ( PatientNotes.objects.all() - .select_related("facility", "patient", "created_by") + .select_related( + "facility", "patient", "created_by", "reply_to", "reply_to__created_by" + ) .order_by("-created_date") ) lookup_field = "external_id" diff --git a/care/facility/migrations/0463_patientnotes_reply_to.py b/care/facility/migrations/0463_patientnotes_reply_to.py new file mode 100644 index 0000000000..7b44acd71a --- /dev/null +++ b/care/facility/migrations/0463_patientnotes_reply_to.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.1 on 2024-09-22 17:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("facility", "0462_facilityhubspoke"), + ] + + operations = [ + migrations.AddField( + model_name="patientnotes", + name="reply_to", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="replies", + to="facility.patientnotes", + ), + ), + ] diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index 6d328414ff..f635a773d4 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -799,6 +799,13 @@ class PatientNotes(FacilityBaseModel, ConsultationRelatedPermissionMixin): db_index=True, default=PatientNoteThreadChoices.DOCTORS, ) + reply_to = models.ForeignKey( + "self", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="replies", + ) note = models.TextField(default="", blank=True) def get_related_consultation(self): diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index d4ea975c2f..c243c7a10b 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -27,6 +27,7 @@ class ExpectedPatientNoteKeys(Enum): LAST_EDITED_DATE = "last_edited_date" THREAD = "thread" USER_TYPE = "user_type" + REPLY_TO_OBJECT = "reply_to_object" class ExpectedFacilityKeys(Enum): @@ -234,6 +235,48 @@ def test_patient_notes(self): [item.value for item in ExpectedCreatedByObjectKeys], ) + def test_patient_note_with_reply(self): + patient = self.patient + note = "How is the patient" + created_by = self.user + + data = { + "facility": patient.facility or self.facility, + "note": note, + "thread": PatientNoteThreadChoices.DOCTORS, + } + self.client.force_authenticate(user=created_by) + response = self.client.post( + f"/api/v1/patient/{patient.external_id}/notes/", data=data + ) + reply_data = { + "facility": patient.facility or self.facility, + "note": "Patient is doing fine", + "thread": PatientNoteThreadChoices.DOCTORS, + "reply_to": response.json()["id"], + } + reply_response = self.client.post( + f"/api/v1/patient/{patient.external_id}/notes/", data=reply_data + ) + + # Ensure the reply is created successfully + self.assertEqual(reply_response.status_code, status.HTTP_201_CREATED) + + # Ensure the reply is posted on same thread + self.assertEqual(reply_response.json()["thread"], response.json()["thread"]) + + # Posting reply in other thread should fail + reply_response = self.client.post( + f"/api/v1/patient/{patient.external_id}/notes/", + { + "facility": patient.facility or self.facility, + "note": "Patient is doing fine", + "thread": PatientNoteThreadChoices.NURSES, + "reply_to": response.json()["id"], + }, + ) + self.assertEqual(reply_response.status_code, status.HTTP_400_BAD_REQUEST) + def test_patient_note_edit(self): patientId = self.patient.external_id notes_list_response = self.client.get( From 00ce264597fa8bee5c78b725b2b674b99abf4989 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 22 Sep 2024 23:23:13 +0530 Subject: [PATCH 26/46] Bump cryptography from 43.0.0 to 43.0.1 (#2422) Bumps [cryptography](https://github.com/pyca/cryptography) from 43.0.0 to 43.0.1. - [Changelog](https://github.com/pyca/cryptography/blob/main/CHANGELOG.rst) - [Commits](https://github.com/pyca/cryptography/compare/43.0.0...43.0.1) --- updated-dependencies: - dependency-name: cryptography dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- Pipfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Pipfile.lock b/Pipfile.lock index 9223ef356d..ec55e71efb 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -470,6 +470,7 @@ "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], + "index": "pypi", "markers": "python_version >= '3.7'", "version": "==43.0.1" }, From 12afd7aa04e092414cb4febbf617f36b6815b73f Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Sun, 22 Sep 2024 23:47:39 +0530 Subject: [PATCH 27/46] disable default jwks for production deployments (#2414) * disable default jwks for production deployments * add default jwks for prebuilts * Revert "add default jwks for prebuilts" This reverts commit 68c68206b95761965ef6ed07c234e040f8770e08. --- .gitignore | 2 ++ care/utils/jwks/generate_jwk.py | 13 +++++++++++++ config/settings/base.py | 9 --------- config/settings/deployment.py | 6 ++++++ config/settings/local.py | 29 +++++++++++++++++++++++++---- config/settings/test.py | 26 +++++++++++++++++++++----- 6 files changed, 67 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index b6fcb392f8..162fc2bcba 100644 --- a/.gitignore +++ b/.gitignore @@ -352,3 +352,5 @@ secrets.sh # Redis *.rdb + +jwks.b64.txt diff --git a/care/utils/jwks/generate_jwk.py b/care/utils/jwks/generate_jwk.py index bc1f0454c1..b2e1565a43 100644 --- a/care/utils/jwks/generate_jwk.py +++ b/care/utils/jwks/generate_jwk.py @@ -1,5 +1,6 @@ import base64 import json +from pathlib import Path from authlib.jose import JsonWebKey @@ -11,3 +12,15 @@ def generate_encoded_jwks(): keys = {"keys": [key]} keys_json = json.dumps(keys) return base64.b64encode(keys_json.encode()).decode() + + +def get_jwks_from_file(base_path: Path): + file_path = base_path / "jwks.b64.txt" + try: + with open(file_path, "r") as file: + return file.read() + except FileNotFoundError: + jwks = generate_encoded_jwks() + with open(file_path, "w") as file: + file.write(jwks) + return jwks diff --git a/config/settings/base.py b/config/settings/base.py index 265abac9ab..07c5c5e1aa 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -2,13 +2,10 @@ Base settings to build other settings files upon. """ -import base64 -import json from datetime import datetime, timedelta from pathlib import Path import environ -from authlib.jose import JsonWebKey from django.utils.translation import gettext_lazy as _ from healthy_django.healthcheck.celery_queue_length import ( DjangoCeleryQueueLengthHealthCheck, @@ -17,7 +14,6 @@ from healthy_django.healthcheck.django_database import DjangoDatabaseHealthCheck from care.utils.csp import config as csp_config -from care.utils.jwks.generate_jwk import generate_encoded_jwks from plug_config import manager BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent @@ -630,11 +626,6 @@ CURRENT_DOMAIN = env("CURRENT_DOMAIN", default="localhost:8000") BACKEND_DOMAIN = env("BACKEND_DOMAIN", default="localhost:9000") -# open id connect -JWKS = JsonWebKey.import_key_set( - json.loads(base64.b64decode(env("JWKS_BASE64", default=generate_encoded_jwks()))) -) - APP_VERSION = env("APP_VERSION", default="unknown") # ABDM diff --git a/config/settings/deployment.py b/config/settings/deployment.py index 4d22554be7..e2525bf0f6 100644 --- a/config/settings/deployment.py +++ b/config/settings/deployment.py @@ -1,6 +1,9 @@ +import base64 +import json import logging import sentry_sdk +from authlib.jose import JsonWebKey from sentry_sdk.integrations.celery import CeleryIntegration from sentry_sdk.integrations.django import DjangoIntegration from sentry_sdk.integrations.logging import LoggingIntegration, ignore_logger @@ -118,3 +121,6 @@ SNS_ACCESS_KEY = env("SNS_ACCESS_KEY") SNS_SECRET_KEY = env("SNS_SECRET_KEY") SNS_REGION = "ap-south-1" + +# open id connect +JWKS = JsonWebKey.import_key_set(json.loads(base64.b64decode(env("JWKS_BASE64")))) diff --git a/config/settings/local.py b/config/settings/local.py index 1e9196b582..0edae64230 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -1,4 +1,12 @@ +import base64 +import json + +from authlib.jose import JsonWebKey + +from care.utils.jwks.generate_jwk import get_jwks_from_file + from .base import * # noqa +from .base import BASE_DIR, INSTALLED_APPS, MIDDLEWARE, env # https://github.com/adamchainz/django-cors-headers#cors_allow_all_origins-bool CORS_ORIGIN_ALLOW_ALL = True @@ -6,19 +14,20 @@ # WhiteNoise # ------------------------------------------------------------------------------ # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # noqa F405 +INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS # django-silk # ------------------------------------------------------------------------------ # https://github.com/jazzband/django-silk#requirements -INSTALLED_APPS += ["silk"] # F405 -MIDDLEWARE += ["silk.middleware.SilkyMiddleware"] # noqa F405 +INSTALLED_APPS += ["silk"] +MIDDLEWARE += ["silk.middleware.SilkyMiddleware"] +# https://github.com/jazzband/django-silk#profiling SILKY_PYTHON_PROFILER = True # django-extensions # ------------------------------------------------------------------------------ # https://django-extensions.readthedocs.io/en/latest/installation_instructions.html#configuration -INSTALLED_APPS += ["django_extensions"] # F405 +INSTALLED_APPS += ["django_extensions"] # Celery @@ -33,3 +42,15 @@ RUNSERVER_PLUS_PRINT_SQL_TRUNCATE = 100000 DISABLE_RATELIMIT = True + +# open id connect +JWKS = JsonWebKey.import_key_set( + json.loads( + base64.b64decode( + env( + "JWKS_BASE64", + default=get_jwks_from_file(BASE_DIR), + ) + ) + ) +) diff --git a/config/settings/test.py b/config/settings/test.py index 69b5f54fb8..d06dc5c992 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -1,9 +1,12 @@ -""" -With these settings, tests run faster. -""" +import base64 +import json + +from authlib.jose import JsonWebKey + +from care.utils.jwks.generate_jwk import get_jwks_from_file from .base import * # noqa -from .base import env +from .base import BASE_DIR, TEMPLATES, env # GENERAL # ------------------------------------------------------------------------------ @@ -17,7 +20,7 @@ # TEMPLATES # ------------------------------------------------------------------------------ -TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] # noqa F405 +TEMPLATES[-1]["OPTIONS"]["loaders"] = [ # type: ignore[index] ( "django.template.loaders.cached.Loader", [ @@ -74,3 +77,16 @@ } CELERY_TASK_ALWAYS_EAGER = True + + +# open id connect +JWKS = JsonWebKey.import_key_set( + json.loads( + base64.b64decode( + env( + "JWKS_BASE64", + default=get_jwks_from_file(BASE_DIR), + ) + ) + ) +) From c94ff6f36213140f19106eb6f1f62500eb1830f5 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 23 Sep 2024 10:21:22 +0530 Subject: [PATCH 28/46] update docker actions (#2485) --- .github/workflows/deploy.yml | 38 ++++++++++++++--------------- .github/workflows/docs.yml | 2 +- .github/workflows/reusable-test.yml | 4 ++- 3 files changed, 23 insertions(+), 21 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9463328ce4..e2aa44c12f 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -92,7 +92,7 @@ jobs: ${{ runner.os }}-buildx-build- - name: Build and push image - uses: docker/build-push-action@v5 + uses: docker/build-push-action@v6 with: context: . file: docker/prod.Dockerfile @@ -142,7 +142,7 @@ jobs: uses: actions/checkout@v4 - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v1 + uses: aws-actions/configure-aws-credentials@v4 with: aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} @@ -154,22 +154,6 @@ jobs: IMAGE_TAG: latest-${{ github.run_number }} run: echo "IMAGE_VALUE=`echo ghcr.io/${{ github.repository }}:$IMAGE_TAG`" >> $GITHUB_ENV - - name: Fill Backend Api definition - id: task-def-api - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ${{ env.ECS_TASK_DEFINITION_BACKEND }} - container-name: ${{ env.CONTAINER_NAME_BACKEND }} - image: ${{env.IMAGE_VALUE}} - - - name: Deploy Backend Api - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.task-def-api.outputs.task-definition }} - service: ${{ env.ECS_SERVICE_BACKEND }} - cluster: ${{ env.ECS_CLUSTER }} - wait-for-service-stability: true - - name: Fill Celery Cron definition id: task-def-celery-cron uses: aws-actions/amazon-ecs-render-task-definition@v1 @@ -187,13 +171,29 @@ jobs: image: ${{env.IMAGE_VALUE}} - name: Deploy Backend Celery - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 with: task-definition: ${{ steps.task-def-celery-worker.outputs.task-definition }} service: ${{ env.ECS_SERVICE_CELERY }} cluster: ${{ env.ECS_CLUSTER }} wait-for-service-stability: true + - name: Fill Backend Api definition + id: task-def-api + uses: aws-actions/amazon-ecs-render-task-definition@v1 + with: + task-definition: ${{ env.ECS_TASK_DEFINITION_BACKEND }} + container-name: ${{ env.CONTAINER_NAME_BACKEND }} + image: ${{env.IMAGE_VALUE}} + + - name: Deploy Backend Api + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: ${{ steps.task-def-api.outputs.task-definition }} + service: ${{ env.ECS_SERVICE_BACKEND }} + cluster: ${{ env.ECS_CLUSTER }} + wait-for-service-stability: true + deploy-staging-gcp: needs: build if: github.ref == 'refs/heads/staging' diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 19189155e7..4bceff1ce5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -54,7 +54,7 @@ jobs: - uses: actions/checkout@v4 - name: Download sphinx documentation - uses: actions/download-artifact@v3 + uses: actions/download-artifact@v4 with: name: sphinx-docs path: ./build diff --git a/.github/workflows/reusable-test.yml b/.github/workflows/reusable-test.yml index fbce6e7a24..27d5301ce7 100644 --- a/.github/workflows/reusable-test.yml +++ b/.github/workflows/reusable-test.yml @@ -30,13 +30,15 @@ jobs: ${{ runner.os }}-buildx- - name: Bake docker images - uses: docker/bake-action@v4 + uses: docker/bake-action@v5 with: load: true set: | *.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 + env: + DOCKER_BUILD_NO_SUMMARY: true - name: Start services run: | From ad354aff422f78362405131bf4fe067338dad751 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 23 Sep 2024 10:25:42 +0530 Subject: [PATCH 29/46] Removed redundant room types in FacilityCapacity (#2417) Removed redundant room types in FacilityCapacity (#2417) --------- Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- .../api/serializers/facility_capacity.py | 6 +- ...ter_facilitycapacity_room_type_and_more.py | 90 +++++++++++++++++++ care/facility/models/facility.py | 13 ++- data/dummy/facility.json | 6 +- 4 files changed, 108 insertions(+), 7 deletions(-) create mode 100644 care/facility/migrations/0464_alter_facilitycapacity_room_type_and_more.py diff --git a/care/facility/api/serializers/facility_capacity.py b/care/facility/api/serializers/facility_capacity.py index 887f386235..4db943c69c 100644 --- a/care/facility/api/serializers/facility_capacity.py +++ b/care/facility/api/serializers/facility_capacity.py @@ -1,12 +1,14 @@ from rest_framework import serializers from care.facility.api.serializers import TIMESTAMP_FIELDS -from care.facility.models import ROOM_TYPES, FacilityCapacity +from care.facility.models import FacilityCapacity, RoomType from config.serializers import ChoiceField class FacilityCapacitySerializer(serializers.ModelSerializer): - room_type_text = ChoiceField(choices=ROOM_TYPES, read_only=True, source="room_type") + room_type_text = ChoiceField( + choices=RoomType.choices, read_only=True, source="room_type" + ) id = serializers.UUIDField(source="external_id", read_only=True) def validate(self, data): diff --git a/care/facility/migrations/0464_alter_facilitycapacity_room_type_and_more.py b/care/facility/migrations/0464_alter_facilitycapacity_room_type_and_more.py new file mode 100644 index 0000000000..80bb7eb767 --- /dev/null +++ b/care/facility/migrations/0464_alter_facilitycapacity_room_type_and_more.py @@ -0,0 +1,90 @@ +# Generated by Django 4.2.2 on 2024-09-02 09:42 + +from django.db import migrations, models + +from care.facility.models import RoomType + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0463_patientnotes_reply_to"), + ] + + def migrate_room_type(apps, schema_editor): + FacilityCapacity = apps.get_model("facility", "FacilityCapacity") + + room_type_migration_map = { + 1: RoomType.GENERAL_BED, # General Bed + 10: RoomType.ICU_BED, # ICU + 20: RoomType.ICU_BED, # Ventilator + 30: RoomType.GENERAL_BED, # Covid Beds + 100: RoomType.ICU_BED, # Covid Ventilators + 110: RoomType.ICU_BED, # Covid ICU + 120: RoomType.OXYGEN_BED, # Covid Oxygen beds + 150: RoomType.OXYGEN_BED, # Oxygen beds + 0: RoomType.OTHER, # Total + 2: RoomType.OTHER, # Hostel + 3: RoomType.ISOLATION_BED, # Single Room with Attached Bathroom + 40: RoomType.GENERAL_BED, # KASP Beds + 50: RoomType.ICU_BED, # KASP ICU beds + 60: RoomType.OXYGEN_BED, # KASP Oxygen beds + 70: RoomType.ICU_BED, # KASP Ventilator beds + } + + merged_facility_capacities = {} + + for old_type, new_type in room_type_migration_map.items(): + facility_capacities = FacilityCapacity.objects.filter(room_type=old_type) + + for facility_capacity in facility_capacities: + key = (facility_capacity.facility.external_id, new_type) + + if key not in merged_facility_capacities: + merged_facility_capacities[key] = { + "facility": facility_capacity.facility, + "room_type": new_type, + "total_capacity": facility_capacity.total_capacity, + "current_capacity": facility_capacity.current_capacity, + } + else: + merged_facility_capacities[key]["total_capacity"] += ( + facility_capacity.total_capacity + ) + merged_facility_capacities[key]["current_capacity"] += ( + facility_capacity.current_capacity + ) + + facility_capacity.delete() + + for data in merged_facility_capacities.values(): + FacilityCapacity.objects.create(**data) + + operations = [ + migrations.RunPython(migrate_room_type, migrations.RunPython.noop), + migrations.AlterField( + model_name="facilitycapacity", + name="room_type", + field=models.IntegerField( + choices=[ + (100, "ICU Bed"), + (200, "Ordinary Bed"), + (300, "Oxygen Bed"), + (400, "Isolation Bed"), + (500, "Others"), + ] + ), + ), + migrations.AlterField( + model_name="historicalfacilitycapacity", + name="room_type", + field=models.IntegerField( + choices=[ + (100, "ICU Bed"), + (200, "Ordinary Bed"), + (300, "Oxygen Bed"), + (400, "Isolation Bed"), + (500, "Others"), + ] + ), + ), + ] diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index 209dbc3869..aa929acc7c 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -43,6 +43,15 @@ (70, "KASP Ventilator beds"), ] + +class RoomType(models.IntegerChoices): + ICU_BED = 100, "ICU Bed" + GENERAL_BED = 200, "Ordinary Bed" + OXYGEN_BED = 300, "Oxygen Bed" + ISOLATION_BED = 400, "Isolation Bed" + OTHER = 500, "Others" + + # to be removed in further PR FEATURE_CHOICES = [ (1, "CT Scan Facility"), @@ -426,7 +435,7 @@ class FacilityCapacity(FacilityBaseModel, FacilityRelatedPermissionMixin): facility = models.ForeignKey( "Facility", on_delete=models.CASCADE, null=False, blank=False ) - room_type = models.IntegerField(choices=ROOM_TYPES) + room_type = models.IntegerField(choices=RoomType.choices) total_capacity = models.IntegerField(default=0, validators=[MinValueValidator(0)]) current_capacity = models.IntegerField(default=0, validators=[MinValueValidator(0)]) @@ -462,7 +471,7 @@ def __str__(self): return ( str(self.facility) + " " - + REVERSE_ROOM_TYPES[self.room_type] + + RoomType(self.room_type).label + " " + str(self.total_capacity) ) diff --git a/data/dummy/facility.json b/data/dummy/facility.json index b33eaf7ad8..cea1986085 100644 --- a/data/dummy/facility.json +++ b/data/dummy/facility.json @@ -792,7 +792,7 @@ "modified_date": "2022-09-27T07:00:19.399Z", "deleted": false, "facility": 1, - "room_type": 150, + "room_type": 300, "total_capacity": 1000, "current_capacity": 20 } @@ -806,7 +806,7 @@ "modified_date": "2022-09-27T07:16:52.525Z", "deleted": false, "facility": 2, - "room_type": 150, + "room_type": 300, "total_capacity": 20, "current_capacity": 1 } @@ -820,7 +820,7 @@ "modified_date": "2023-09-15T06:12:31.548Z", "deleted": false, "facility": 4, - "room_type": 150, + "room_type": 300, "total_capacity": 12, "current_capacity": 2 } From d6f9b51ece5ff6b8baa2007c14ab509414b17597 Mon Sep 17 00:00:00 2001 From: Hritesh Shanty <128292557+hrit2773@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:26:32 +0530 Subject: [PATCH 30/46] Soft delete assets from deleted facilities/hospitals (#1996) Soft delete assets from deleted facilities/hospitals (#1996) --------- Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/models/facility.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index aa929acc7c..d2a1db7360 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model from django.contrib.postgres.fields import ArrayField from django.core.validators import MinValueValidator -from django.db import models +from django.db import models, transaction from django.db.models import IntegerChoices from django.db.models.constraints import CheckConstraint, UniqueConstraint from django.utils.translation import gettext_lazy as _ @@ -299,6 +299,18 @@ def save(self, *args, **kwargs) -> None: facility=self, user=self.created_by, created_by=self.created_by ) + @transaction.atomic + def delete(self, *args): + from care.facility.models.asset import Asset, AssetLocation + + AssetLocation.objects.filter(facility_id=self.id).update(deleted=True) + Asset.objects.filter( + current_location_id__in=AssetLocation._base_manager.filter( # noqa: SLF001 + facility_id=self.id + ).values_list("id", flat=True) + ).update(deleted=True) + return super().delete(*args) + @property def get_features_display(self): if not self.features: From e6a55d77eb75fa7a63921ad248c352ef420509b3 Mon Sep 17 00:00:00 2001 From: Uday Sagar <111575806+UdaySagar-Git@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:28:59 +0530 Subject: [PATCH 31/46] Add user profile pictures (#2253) Add user profile pictures (#2253) --------- Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/api/serializers/facility.py | 34 +++------ care/facility/api/viewsets/facility.py | 14 ++-- care/users/api/serializers/user.py | 34 +++++++++ care/users/api/viewsets/users.py | 36 +++++++++- .../0018_user_profile_picture_url.py | 18 +++++ care/users/models.py | 8 +++ care/users/tests/test_api.py | 69 +++++++++++++++++++ care/utils/file_uploads/__init__.py | 0 care/utils/file_uploads/cover_image.py | 53 ++++++++++++++ care/utils/tests/test_utils.py | 2 + data/dummy/users.json | 24 +++++++ 11 files changed, 257 insertions(+), 35 deletions(-) create mode 100644 care/users/migrations/0018_user_profile_picture_url.py create mode 100644 care/utils/file_uploads/__init__.py create mode 100644 care/utils/file_uploads/cover_image.py diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index b75bf06897..92dea0870a 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -1,7 +1,3 @@ -import uuid - -import boto3 -from django.conf import settings from django.contrib.auth import get_user_model from django.db.models import Q from rest_framework import serializers @@ -16,7 +12,7 @@ StateSerializer, WardSerializer, ) -from care.utils.csp.config import BucketType, get_client_config +from care.utils.file_uploads.cover_image import upload_cover_image from care.utils.models.validators import ( cover_image_validator, custom_image_extension_validator, @@ -239,25 +235,13 @@ class Meta: fields = ("cover_image", "read_cover_image_url") def save(self, **kwargs): - facility = self.instance + facility: Facility = self.instance image = self.validated_data["cover_image"] - - config, bucket_name = get_client_config(BucketType.FACILITY) - s3 = boto3.client("s3", **config) - - if facility.cover_image_url: - s3.delete_object(Bucket=bucket_name, Key=facility.cover_image_url) - - image_extension = image.name.rsplit(".", 1)[-1] - image_location = f"cover_images/{facility.external_id}_{str(uuid.uuid4())[0:8]}.{image_extension}" - boto_params = { - "Bucket": bucket_name, - "Key": image_location, - "Body": image.file, - } - if settings.BUCKET_HAS_FINE_ACL: - boto_params["ACL"] = "public-read" - s3.put_object(**boto_params) - facility.cover_image_url = image_location - facility.save() + facility.cover_image_url = upload_cover_image( + image, + str(facility.external_id), + "cover_images", + facility.cover_image_url, + ) + facility.save(update_fields=["cover_image_url"]) return facility diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index 427be23f94..7b9d64c068 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -1,12 +1,13 @@ from django.conf import settings from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator from django_filters import rest_framework as filters from djqscsv import render_to_csv_response from drf_spectacular.utils import extend_schema, extend_schema_view from dry_rest_permissions.generics import DRYPermissionFiltersBase, DRYPermissions from rest_framework import filters as drf_filters from rest_framework import mixins, status, viewsets -from rest_framework.decorators import action +from rest_framework.decorators import action, parser_classes from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -25,6 +26,7 @@ ) from care.facility.models.facility import FacilityHubSpoke, FacilityUser from care.users.models import User +from care.utils.file_uploads.cover_image import delete_cover_image from care.utils.queryset.facility import get_facility_queryset @@ -99,19 +101,13 @@ def initialize_request(self, request, *args, **kwargs): self.action = self.action_map.get(request.method.lower()) return super().initialize_request(request, *args, **kwargs) - def get_parsers(self): - if self.action == "cover_image": - return [MultiPartParser()] - return super().get_parsers() - def get_serializer_class(self): if self.request.query_params.get("all") == "true": return FacilityBasicInfoSerializer if self.action == "cover_image": # Check DRYpermissions before updating return FacilityImageUploadSerializer - else: - return FacilitySerializer + return FacilitySerializer def destroy(self, request, *args, **kwargs): instance = self.get_object() @@ -154,6 +150,7 @@ def list(self, request, *args, **kwargs): return super(FacilityViewSet, self).list(request, *args, **kwargs) @extend_schema(tags=["facility"]) + @method_decorator(parser_classes([MultiPartParser])) @action(methods=["POST"], detail=True) def cover_image(self, request, external_id): facility = self.get_object() @@ -166,6 +163,7 @@ def cover_image(self, request, external_id): @cover_image.mapping.delete def cover_image_delete(self, *args, **kwargs): facility = self.get_object() + delete_cover_image(facility.cover_image_url, "cover_images") facility.cover_image_url = None facility.save() return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 8040ed71dc..edb72c38b4 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -12,6 +12,11 @@ ) from care.users.api.serializers.skill import UserSkillSerializer from care.users.models import GENDER_CHOICES, User +from care.utils.file_uploads.cover_image import upload_cover_image +from care.utils.models.validators import ( + cover_image_validator, + custom_image_extension_validator, +) from care.utils.queryset.facility import get_home_facility_queryset from care.utils.serializer.external_id_field import ExternalIdSerializerField from config.serializers import ChoiceField @@ -279,6 +284,7 @@ class UserSerializer(SignUpSerializer): source="home_facility", read_only=True, ) + read_profile_picture_url = serializers.URLField(read_only=True) home_facility = ExternalIdSerializerField(queryset=Facility.objects.all()) @@ -321,6 +327,7 @@ class Meta: "pf_endpoint", "pf_p256dh", "pf_auth", + "read_profile_picture_url", "user_flags", ) read_only_fields = ( @@ -415,6 +422,7 @@ class UserListSerializer(serializers.ModelSerializer): read_only=True, ) home_facility = ExternalIdSerializerField(queryset=Facility.objects.all()) + read_profile_picture_url = serializers.URLField(read_only=True) class Meta: model = User @@ -437,4 +445,30 @@ class Meta: "home_facility_object", "home_facility", "video_connect_link", + "read_profile_picture_url", + ) + + +class UserImageUploadSerializer(serializers.ModelSerializer): + profile_picture = serializers.ImageField( + required=True, + write_only=True, + validators=[custom_image_extension_validator, cover_image_validator], + ) + read_profile_picture_url = serializers.URLField(read_only=True) + + class Meta: + model = User + fields = ("profile_picture", "read_profile_picture_url") + + def save(self, **kwargs): + user: User = self.instance + image = self.validated_data["profile_picture"] + user.profile_picture_url = upload_cover_image( + image, + str(user.external_id), + "avatars", + user.profile_picture_url, ) + user.save(update_fields=["profile_picture_url"]) + return user diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index 7c2bb4ccca..937a8d3d00 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -4,14 +4,16 @@ from django.db.models import F, Q, Subquery from django.http import Http404 from django.utils import timezone +from django.utils.decorators import method_decorator from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema from dry_rest_permissions.generics import DRYPermissions from rest_framework import filters as drf_filters from rest_framework import filters as rest_framework_filters from rest_framework import mixins, status -from rest_framework.decorators import action +from rest_framework.decorators import action, parser_classes from rest_framework.generics import get_object_or_404 +from rest_framework.parsers import MultiPartParser from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.serializers import ValidationError @@ -21,11 +23,13 @@ from care.facility.models.facility import Facility, FacilityUser from care.users.api.serializers.user import ( UserCreateSerializer, + UserImageUploadSerializer, UserListSerializer, UserSerializer, ) from care.users.models import User from care.utils.cache.cache_allowed_facilities import get_accessible_facilities +from care.utils.file_uploads.cover_image import delete_cover_image def remove_facility_user_cache(user_id): @@ -168,7 +172,7 @@ def get_queryset(self): ) return self.queryset.filter(query) - def get_object(self): + def get_object(self) -> User: try: return super().get_object() except Http404: @@ -181,6 +185,8 @@ def get_serializer_class(self): return UserCreateSerializer # elif self.action == "create": # return SignUpSerializer + elif self.action == "profile_picture": + return UserImageUploadSerializer else: return UserSerializer @@ -388,3 +394,29 @@ def check_availability(self, request, username): if User.check_username_exists(username): return Response(status=status.HTTP_409_CONFLICT) return Response(status=status.HTTP_200_OK) + + def has_profile_image_write_permission(self, request, user): + return request.user.is_superuser or (user.id == request.user.id) + + @extend_schema(tags=["users"]) + @method_decorator(parser_classes([MultiPartParser])) + @action(detail=True, methods=["POST"], permission_classes=[IsAuthenticated]) + def profile_picture(self, request, *args, **kwargs): + user = self.get_object() + if not self.has_profile_image_write_permission(request, user): + return Response(status=status.HTTP_403_FORBIDDEN) + serializer = self.get_serializer(user, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=status.HTTP_200_OK) + + @extend_schema(tags=["users"]) + @profile_picture.mapping.delete + def profile_picture_delete(self, request, *args, **kwargs): + user = self.get_object() + if not self.has_profile_image_write_permission(request, user): + return Response(status=status.HTTP_403_FORBIDDEN) + delete_cover_image(user.profile_picture_url, "avatars") + user.profile_picture_url = None + user.save() + return Response(status=status.HTTP_204_NO_CONTENT) diff --git a/care/users/migrations/0018_user_profile_picture_url.py b/care/users/migrations/0018_user_profile_picture_url.py new file mode 100644 index 0000000000..fea3ac507c --- /dev/null +++ b/care/users/migrations/0018_user_profile_picture_url.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.1 on 2024-09-21 09:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0017_userflag"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="profile_picture_url", + field=models.CharField(blank=True, default=None, max_length=500, null=True), + ), + ] diff --git a/care/users/models.py b/care/users/models.py index 1f6d3d7d8d..2a701a090e 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -283,6 +283,9 @@ class User(AbstractUser): gender = models.IntegerField(choices=GENDER_CHOICES, blank=False) date_of_birth = models.DateField(null=True, blank=True) + profile_picture_url = models.CharField( + blank=True, null=True, default=None, max_length=500 + ) skills = models.ManyToManyField("Skill", through=UserSkill) home_facility = models.ForeignKey( "facility.Facility", on_delete=models.PROTECT, null=True, blank=True @@ -347,6 +350,11 @@ class User(AbstractUser): CSV_MAKE_PRETTY = {"user_type": (lambda x: User.REVERSE_TYPE_MAP[x])} + def read_profile_picture_url(self): + if self.profile_picture_url: + return f"{settings.FACILITY_S3_BUCKET_EXTERNAL_ENDPOINT}/{settings.FACILITY_S3_BUCKET}/{self.profile_picture_url}" + return None + @property def full_name(self): return self.get_full_name() diff --git a/care/users/tests/test_api.py b/care/users/tests/test_api.py index 28b87af4bf..096699deba 100644 --- a/care/users/tests/test_api.py +++ b/care/users/tests/test_api.py @@ -1,6 +1,9 @@ +import io from datetime import date, timedelta +from django.core.files.uploadedfile import SimpleUploadedFile from django.utils import timezone +from PIL import Image from rest_framework import status from rest_framework.test import APITestCase @@ -46,6 +49,7 @@ def get_detail_representation(self, obj=None) -> dict: "doctor_qualification": obj.doctor_qualification, "weekly_working_hours": obj.weekly_working_hours, "video_connect_link": obj.video_connect_link, + "read_profile_picture_url": obj.profile_picture_url, "user_flags": [], **self.get_local_body_district_state_representation(obj), } @@ -292,3 +296,68 @@ def test_home_facility_filter(self): self.assertIn( self.user_5.username, {r["username"] for r in res_data_json["results"]} ) + + +class TestUserProfilePicture(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls) -> None: + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.super_user = cls.create_super_user("su", cls.district) + cls.user = cls.create_user("staff1", cls.district) + + def get_base_url(self) -> str: + return f"/api/v1/users/{self.user.username}/profile_picture/" + + def get_payload(self) -> dict: + image = Image.new("RGB", (400, 400)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = SimpleUploadedFile("test.jpg", file.getvalue(), "image/jpeg") + test_file.size = 2000 + return {"profile_picture": test_file} + + def test_user_can_upload_profile_picture(self): + image = Image.new("RGB", (400, 400)) + file = io.BytesIO() + image.save(file, format="JPEG") + test_file = SimpleUploadedFile("test.jpg", file.getvalue(), "image/jpeg") + test_file.size = 2000 + response = self.client.post( + self.get_base_url(), self.get_payload(), format="multipart" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsNotNone( + User.objects.get(username=self.user.username).profile_picture_url + ) + + def test_user_can_delete_profile_picture(self): + self.user.profile_picture_url = "image.jpg" + self.user.save(update_fields=["profile_picture_url"]) + + response = self.client.delete(self.get_base_url()) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertIsNone( + User.objects.get(username=self.user.username).profile_picture_url + ) + + def test_superuser_can_upload_profile_picture(self): + self.client.force_authenticate(self.super_user) + response = self.client.post( + self.get_base_url(), self.get_payload(), format="multipart" + ) + self.assertEqual(response.status_code, status.HTTP_200_OK) + self.assertIsNotNone( + User.objects.get(username=self.user.username).profile_picture_url + ) + + def test_superuser_can_delete_profile_picture(self): + self.user.profile_picture_url = "image.jpg" + self.user.save(update_fields=["profile_picture_url"]) + + self.client.force_authenticate(self.super_user) + response = self.client.delete(self.get_base_url()) + self.assertEqual(response.status_code, status.HTTP_204_NO_CONTENT) + self.assertIsNone( + User.objects.get(username=self.user.username).profile_picture_url + ) diff --git a/care/utils/file_uploads/__init__.py b/care/utils/file_uploads/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/file_uploads/cover_image.py b/care/utils/file_uploads/cover_image.py new file mode 100644 index 0000000000..55f1183782 --- /dev/null +++ b/care/utils/file_uploads/cover_image.py @@ -0,0 +1,53 @@ +import logging +import secrets +from typing import Literal + +import boto3 +from django.conf import settings +from django.core.files.uploadedfile import UploadedFile + +from care.utils.csp.config import BucketType, get_client_config + +logger = logging.getLogger(__name__) + + +def delete_cover_image(image_key: str, folder: Literal["cover_images", "avatars"]): + config, bucket_name = get_client_config(BucketType.FACILITY) + s3 = boto3.client("s3", **config) + + try: + s3.delete_object(Bucket=bucket_name, Key=image_key) + except Exception: + logger.warning(f"Failed to delete cover image {image_key}") + + +def upload_cover_image( + image: UploadedFile, + object_external_id: str, + folder: Literal["cover_images", "avatars"], + old_key: str = None, +) -> str: + config, bucket_name = get_client_config(BucketType.FACILITY) + s3 = boto3.client("s3", **config) + + if old_key: + try: + s3.delete_object(Bucket=bucket_name, Key=old_key) + except Exception: + logger.warning(f"Failed to delete old cover image {old_key}") + + image_extension = image.name.rsplit(".", 1)[-1] + image_key = ( + f"{folder}/{object_external_id}_{secrets.token_hex(8)}.{image_extension}" + ) + + boto_params = { + "Bucket": bucket_name, + "Key": image_key, + "Body": image.file, + } + if settings.BUCKET_HAS_FINE_ACL: + boto_params["ACL"] = "public-read" + s3.put_object(**boto_params) + + return image_key diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 87350be35b..1f668b7585 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -107,6 +107,8 @@ class TestUtils: Base class for tests, handles most of the test setup and tools for setting up data """ + maxDiff = None + def setUp(self) -> None: self.client.force_login(self.user) diff --git a/data/dummy/users.json b/data/dummy/users.json index 5f20d2f3b6..fc55f68670 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -31,6 +31,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -67,6 +68,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -103,6 +105,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -139,6 +142,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -175,6 +179,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -211,6 +216,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -247,6 +253,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -283,6 +290,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -319,6 +327,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -355,6 +364,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -391,6 +401,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -427,6 +438,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -463,6 +475,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -499,6 +512,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -535,6 +549,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -571,6 +586,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -607,6 +623,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -643,6 +660,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -679,6 +697,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -715,6 +734,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -751,6 +771,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -787,6 +808,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -823,6 +845,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } @@ -859,6 +882,7 @@ "pf_p256dh": null, "pf_auth": null, "asset": null, + "profile_picture_url": null, "groups": [], "user_permissions": [] } From b3ad350f9bdd043f3d3e5139ecc85bda0c16dff0 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Mon, 23 Sep 2024 10:33:49 +0530 Subject: [PATCH 32/46] archived spo2 field to archived_spo2 (#2397) archived spo2 field to archived_spo2 (#2397) --------- Co-authored-by: Aakash Singh --- care/abdm/utils/fhir.py | 17 ++++++++--------- .../management/commands/load_event_types.py | 2 +- ...0464_rename_spo2_dailyround_archived_spo2.py | 17 +++++++++++++++++ care/facility/models/daily_round.py | 4 ++-- 4 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 care/facility/migrations/0464_rename_spo2_dailyround_archived_spo2.py diff --git a/care/abdm/utils/fhir.py b/care/abdm/utils/fhir.py index 5c56c54d81..eb9db58f2e 100644 --- a/care/abdm/utils/fhir.py +++ b/care/abdm/utils/fhir.py @@ -316,7 +316,7 @@ def _observations_from_daily_round(self, daily_round): ), self._observation( "SpO2", - {"value": daily_round.spo2, "unit": "%"}, + {"value": daily_round.ventilator_spo2, "unit": "%"}, id, date, ), @@ -1205,17 +1205,16 @@ def create_op_consultation_record(self): def create_record(self, record_type): if record_type == "Prescription": return self.create_prescription_record() - elif record_type == "WellnessRecord": + if record_type == "WellnessRecord": return self.create_wellness_record() - elif record_type == "ImmunizationRecord": + if record_type == "ImmunizationRecord": return self.create_immunization_record() - elif record_type == "HealthDocumentRecord": + if record_type == "HealthDocumentRecord": return self.create_health_document_record() - elif record_type == "DiagnosticReport": + if record_type == "DiagnosticReport": return self.create_diagnostic_report_record() - elif record_type == "DischargeSummary": + if record_type == "DischargeSummary": return self.create_discharge_summary_record() - elif record_type == "OPConsultation": + if record_type == "OPConsultation": return self.create_op_consultation_record() - else: - return self.create_discharge_summary_record() + return self.create_discharge_summary_record() diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index b41966f195..271f510b79 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -116,7 +116,6 @@ class Command(BaseCommand): "name": "VITALS", "children": ( {"name": "TEMPERATURE", "fields": ("temperature",)}, - {"name": "SPO2", "fields": ("spo2",)}, {"name": "PULSE", "fields": ("pulse",)}, {"name": "BLOOD_PRESSURE", "fields": ("bp",)}, {"name": "RESPIRATORY_RATE", "fields": ("resp",)}, @@ -268,6 +267,7 @@ class Command(BaseCommand): "VENTILATOR_MODES", "SYMPTOMS", "ROUND_SYMPTOMS", + "SPO2", ) def create_objects( diff --git a/care/facility/migrations/0464_rename_spo2_dailyround_archived_spo2.py b/care/facility/migrations/0464_rename_spo2_dailyround_archived_spo2.py new file mode 100644 index 0000000000..f792cb874e --- /dev/null +++ b/care/facility/migrations/0464_rename_spo2_dailyround_archived_spo2.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.1 on 2024-09-22 19:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0463_patientnotes_reply_to"), + ] + + operations = [ + migrations.RenameField( + model_name="dailyround", + old_name="spo2", + new_name="archived_spo2", + ), + ] diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index ddfb409cd1..718b75d058 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -162,9 +162,9 @@ class InsulinIntakeFrequencyType(models.IntegerChoices): null=True, validators=[MinValueValidator(Decimal(95)), MaxValueValidator(Decimal(106))], ) - spo2 = models.DecimalField( + archived_spo2 = models.DecimalField( max_digits=4, decimal_places=2, blank=True, null=True, default=None - ) + ) # Deprecated physical_examination_info = models.TextField(null=True, blank=True) deprecated_covid_category = models.CharField( choices=COVID_CATEGORY_CHOICES, From e02919624c519cd8e8933e812c0680cca7812b17 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 23 Sep 2024 10:46:57 +0530 Subject: [PATCH 33/46] merge migrations (#2486) --- care/facility/migrations/0465_merge_20240923_1045.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 care/facility/migrations/0465_merge_20240923_1045.py diff --git a/care/facility/migrations/0465_merge_20240923_1045.py b/care/facility/migrations/0465_merge_20240923_1045.py new file mode 100644 index 0000000000..e4fc13f67b --- /dev/null +++ b/care/facility/migrations/0465_merge_20240923_1045.py @@ -0,0 +1,12 @@ +# Generated by Django 5.1.1 on 2024-09-23 05:15 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("facility", "0464_alter_facilitycapacity_room_type_and_more"), + ("facility", "0464_rename_spo2_dailyround_archived_spo2"), + ] + + operations = [] From c7637ea61785b1cef1432ba5b0315f5c6ffb28d7 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 23 Sep 2024 12:44:29 +0530 Subject: [PATCH 34/46] Adds functionality for `last_consultation_admitted_bed_type_list` in discharge patient filters (#2204) Adds functionality for `last_consultation_admitted_bed_type_list` in discharge patient filters (#2204) --------- Co-authored-by: Shivank Kacker Co-authored-by: Aakash Singh Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/admin.py | 3 +- care/facility/api/viewsets/patient.py | 100 ++++++++++++---- care/facility/tests/test_patient_api.py | 152 +++++++++++++++++++++++- 3 files changed, 231 insertions(+), 24 deletions(-) diff --git a/care/facility/admin.py b/care/facility/admin.py index 11a210d0ed..55a9bdff65 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -6,7 +6,7 @@ from care.facility.models.ambulance import Ambulance, AmbulanceDriver from care.facility.models.asset import Asset -from care.facility.models.bed import AssetBed, Bed +from care.facility.models.bed import AssetBed, Bed, ConsultationBed from care.facility.models.facility import FacilityHubSpoke from care.facility.models.file_upload import FileUpload from care.facility.models.patient_consultation import ( @@ -237,6 +237,7 @@ class Meta: admin.site.register(AssetBed) admin.site.register(Asset) admin.site.register(Bed) +admin.site.register(ConsultationBed) admin.site.register(PatientConsent) admin.site.register(FileUpload) admin.site.register(PatientConsultation) diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 4953ef54af..f7cb1a05e0 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -67,7 +67,7 @@ ShiftingRequest, ) from care.facility.models.base import covert_choice_dict -from care.facility.models.bed import AssetBed +from care.facility.models.bed import AssetBed, ConsultationBed from care.facility.models.icd11_diagnosis import ( INACTIVE_CONDITION_VERIFICATION_STATUSES, ConditionVerificationStatus, @@ -97,6 +97,9 @@ class PatientFilterSet(filters.FilterSet): + + last_consultation_field = "last_consultation" + source = filters.ChoiceFilter(choices=PatientRegistration.SourceChoices) disease_status = CareChoiceFilter(choice_dict=DISEASE_STATUS_DICT) facility = filters.UUIDFilter(field_name="facility__external_id") @@ -109,14 +112,14 @@ class PatientFilterSet(filters.FilterSet): allow_transfer = filters.BooleanFilter(field_name="allow_transfer") name = filters.CharFilter(field_name="name", lookup_expr="icontains") patient_no = filters.CharFilter( - field_name="last_consultation__patient_no", lookup_expr="iexact" + field_name=f"{last_consultation_field}__patient_no", lookup_expr="iexact" ) gender = filters.NumberFilter(field_name="gender") age = filters.NumberFilter(field_name="age") age_min = filters.NumberFilter(field_name="age", lookup_expr="gte") age_max = filters.NumberFilter(field_name="age", lookup_expr="lte") deprecated_covid_category = filters.ChoiceFilter( - field_name="last_consultation__deprecated_covid_category", + field_name=f"{last_consultation_field}__deprecated_covid_category", choices=COVID_CATEGORY_CHOICES, ) category = filters.ChoiceFilter( @@ -168,24 +171,24 @@ def filter_by_category(self, queryset, name, value): state = filters.NumberFilter(field_name="state__id") state_name = filters.CharFilter(field_name="state__name", lookup_expr="icontains") # Consultation Fields - is_kasp = filters.BooleanFilter(field_name="last_consultation__is_kasp") + is_kasp = filters.BooleanFilter(field_name=f"{last_consultation_field}__is_kasp") last_consultation_kasp_enabled_date = filters.DateFromToRangeFilter( - field_name="last_consultation__kasp_enabled_date" + field_name=f"{last_consultation_field}__kasp_enabled_date" ) last_consultation_encounter_date = filters.DateFromToRangeFilter( - field_name="last_consultation__encounter_date" + field_name=f"{last_consultation_field}__encounter_date" ) last_consultation_discharge_date = filters.DateFromToRangeFilter( - field_name="last_consultation__discharge_date" + field_name=f"{last_consultation_field}__discharge_date" ) last_consultation_admitted_bed_type_list = MultiSelectFilter( method="filter_by_bed_type", ) last_consultation_medico_legal_case = filters.BooleanFilter( - field_name="last_consultation__medico_legal_case" + field_name=f"{last_consultation_field}__medico_legal_case" ) last_consultation_current_bed__location = filters.UUIDFilter( - field_name="last_consultation__current_bed__bed__location__external_id" + field_name=f"{last_consultation_field}__current_bed__bed__location__external_id" ) def filter_by_bed_type(self, queryset, name, value): @@ -204,21 +207,21 @@ def filter_by_bed_type(self, queryset, name, value): return queryset.filter(filter_q) last_consultation_admitted_bed_type = CareChoiceFilter( - field_name="last_consultation__current_bed__bed__bed_type", + field_name=f"{last_consultation_field}__current_bed__bed__bed_type", choice_dict=REVERSE_BED_TYPES, ) last_consultation__new_discharge_reason = filters.ChoiceFilter( - field_name="last_consultation__new_discharge_reason", + field_name=f"{last_consultation_field}__new_discharge_reason", choices=NewDischargeReasonEnum.choices, ) last_consultation_assigned_to = filters.NumberFilter( - field_name="last_consultation__assigned_to" + field_name=f"{last_consultation_field}__assigned_to" ) last_consultation_is_telemedicine = filters.BooleanFilter( - field_name="last_consultation__is_telemedicine" + field_name=f"{last_consultation_field}__is_telemedicine" ) ventilator_interface = CareChoiceFilter( - field_name="last_consultation__last_daily_round__ventilator_interface", + field_name=f"{last_consultation_field}__last_daily_round__ventilator_interface", choice_dict={ label: value for value, label in DailyRound.VentilatorInterfaceType.choices }, @@ -633,19 +636,74 @@ def transfer(self, request, *args, **kwargs): return Response(data=response_serializer.data, status=status.HTTP_200_OK) +class DischargePatientFilterSet(PatientFilterSet): + last_consultation_field = "last_discharge_consultation" + + # Filters patients by the type of bed they have been assigned to. + def filter_by_bed_type(self, queryset, name, value): + if not value: + return queryset + + values = value.split(",") + filter_q = Q() + + # Get the latest consultation bed records for each patient by ordering by patient_id + # and the end_date of the consultation, then selecting distinct patient entries. + last_consultation_bed_ids = ( + ConsultationBed.objects.filter(end_date__isnull=False) + .order_by("consultation__patient_id", "-end_date") + .distinct("consultation__patient_id") + ) + + # patients whose last consultation did not include a bed + if "None" in values: + filter_q |= ~Q( + last_discharge_consultation__id__in=Subquery( + last_consultation_bed_ids.values_list("consultation_id", flat=True) + ) + ) + values.remove("None") + + # If the values list contains valid bed types, apply the filtering for those bed types. + if isinstance(values, list) and len(values) > 0: + filter_q |= Q( + last_discharge_consultation__id__in=Subquery( + ConsultationBed.objects.filter( + id__in=Subquery( + last_consultation_bed_ids.values_list("id", flat=True) + ), # Filter by consultation beds that are part of the latest records for each patient. + bed__bed_type__in=values, # Match the bed types from the provided values list. + ).values_list("consultation_id", flat=True) + ) + ) + + return queryset.filter(filter_q) + + @extend_schema_view(tags=["patient"]) class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): permission_classes = (IsAuthenticated, DRYPermissions) lookup_field = "external_id" serializer_class = PatientListSerializer filter_backends = ( + PatientDRYFilter, filters.DjangoFilterBackend, rest_framework_filters.OrderingFilter, PatientCustomOrderingFilter, ) - filterset_class = PatientFilterSet + filterset_class = DischargePatientFilterSet queryset = ( - PatientRegistration.objects.select_related( + PatientRegistration.objects.annotate( + last_discharge_consultation__id=Subquery( + PatientConsultation.objects.filter( + patient_id=OuterRef("id"), + discharge_date__isnull=False, + ) + .order_by("-discharge_date") + .values("id")[:1] + ) + ) + .select_related( "local_body", "district", "state", @@ -656,8 +714,6 @@ class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): "facility__local_body", "facility__district", "facility__state", - "last_consultation", - "last_consultation__assigned_to", "last_edited", "created_by", ) @@ -702,9 +758,9 @@ class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): "date_declared_positive", "date_of_result", "last_vaccinated_date", - "last_consultation_encounter_date", - "last_consultation_discharge_date", - "last_consultation_symptoms_onset_date", + "last_discharge_consultation_encounter_date", + "last_discharge_consultation_discharge_date", + "last_discharge_consultation_symptoms_onset_date", ] ordering_fields = [ @@ -713,7 +769,7 @@ class FacilityDischargedPatientViewSet(GenericViewSet, mixins.ListModelMixin): "created_date", "modified_date", "review_time", - "last_consultation__current_bed__bed__name", + "last_discharge_consultation__current_bed__bed__name", "date_declared_positive", ] diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index c243c7a10b..c40d57639b 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -1,6 +1,6 @@ from enum import Enum -from django.utils.timezone import now +from django.utils.timezone import now, timedelta from rest_framework import status from rest_framework.test import APITestCase @@ -728,6 +728,156 @@ def test_filter_by_has_consents(self): self.assertEqual(res.json()["count"], 3) +class DischargePatientFilterTestCase(TestUtils, APITestCase): + @classmethod + def setUpTestData(cls): + cls.state = cls.create_state() + cls.district = cls.create_district(cls.state) + cls.local_body = cls.create_local_body(cls.district) + cls.super_user = cls.create_super_user("su", cls.district) + cls.facility = cls.create_facility(cls.super_user, cls.district, cls.local_body) + cls.user = cls.create_user( + "user", cls.district, user_type=15, home_facility=cls.facility + ) + cls.location = cls.create_asset_location(cls.facility) + + cls.iso_bed = cls.create_bed(cls.facility, cls.location, bed_type=1, name="ISO") + cls.icu_bed = cls.create_bed(cls.facility, cls.location, bed_type=2, name="ICU") + cls.oxy_bed = cls.create_bed(cls.facility, cls.location, bed_type=6, name="OXY") + cls.nor_bed = cls.create_bed(cls.facility, cls.location, bed_type=7, name="NOR") + + cls.patient_iso = cls.create_patient(cls.district, cls.facility) + cls.patient_icu = cls.create_patient(cls.district, cls.facility) + cls.patient_oxy = cls.create_patient(cls.district, cls.facility) + cls.patient_nor = cls.create_patient(cls.district, cls.facility) + cls.patient_nb = cls.create_patient(cls.district, cls.facility) + + cls.consultation_iso = cls.create_consultation( + patient=cls.patient_iso, + facility=cls.facility, + discharge_date=now(), + ) + cls.consultation_icu = cls.create_consultation( + patient=cls.patient_icu, + facility=cls.facility, + discharge_date=now(), + ) + cls.consultation_oxy = cls.create_consultation( + patient=cls.patient_oxy, + facility=cls.facility, + discharge_date=now(), + ) + cls.consultation_nor = cls.create_consultation( + patient=cls.patient_nor, + facility=cls.facility, + discharge_date=now(), + ) + + cls.consultation_nb = cls.create_consultation( + patient=cls.patient_nb, + facility=cls.facility, + discharge_date=now(), + ) + + cls.consultation_bed_iso = cls.create_consultation_bed( + cls.consultation_iso, + cls.iso_bed, + end_date=now(), + ) + cls.consultation_bed_icu = cls.create_consultation_bed( + cls.consultation_icu, + cls.icu_bed, + end_date=now(), + ) + cls.consultation_bed_oxy = cls.create_consultation_bed( + cls.consultation_oxy, + cls.oxy_bed, + end_date=now(), + ) + cls.consultation_bed_nor = cls.create_consultation_bed( + cls.consultation_nor, + cls.nor_bed, + end_date=now(), + ) + + def get_base_url(self) -> str: + return ( + "/api/v1/facility/" + + str(self.facility.external_id) + + "/discharged_patients/" + ) + + def test_filter_by_admitted_to_bed(self): + self.client.force_authenticate(user=self.user) + choices = ["1", "2", "6", "7", "None"] + + res = self.client.get( + self.get_base_url(), + {"last_consultation_admitted_bed_type_list": ",".join([choices[0]])}, + ) + + self.assertContains(res, self.patient_iso.external_id) + self.assertNotContains(res, self.patient_icu.external_id) + self.assertNotContains(res, self.patient_oxy.external_id) + self.assertNotContains(res, self.patient_nor.external_id) + self.assertNotContains(res, self.patient_nb.external_id) + + res = self.client.get( + self.get_base_url(), + {"last_consultation_admitted_bed_type_list": ",".join(choices[1:3])}, + ) + + self.assertNotContains(res, self.patient_iso.external_id) + self.assertContains(res, self.patient_icu.external_id) + self.assertContains(res, self.patient_oxy.external_id) + self.assertNotContains(res, self.patient_nor.external_id) + self.assertNotContains(res, self.patient_nb.external_id) + + res = self.client.get( + self.get_base_url(), + {"last_consultation_admitted_bed_type_list": ",".join(choices)}, + ) + + self.assertContains(res, self.patient_iso.external_id) + self.assertContains(res, self.patient_icu.external_id) + self.assertContains(res, self.patient_oxy.external_id) + self.assertContains(res, self.patient_nor.external_id) + self.assertContains(res, self.patient_nb.external_id) + + res = self.client.get( + self.get_base_url(), + {"last_consultation_admitted_bed_type_list": ",".join(choices[3:])}, + ) + + self.assertNotContains(res, self.patient_iso.external_id) + self.assertNotContains(res, self.patient_icu.external_id) + self.assertNotContains(res, self.patient_oxy.external_id) + self.assertContains(res, self.patient_nor.external_id) + self.assertContains(res, self.patient_nb.external_id) + + # if patient is readmitted to another bed type, only the latest admission should be considered + + def test_admitted_to_bed_after_readmission(self): + self.client.force_authenticate(user=self.user) + self.create_consultation_bed( + self.consultation_icu, self.iso_bed, end_date=now() + timedelta(days=1) + ) + + res = self.client.get( + self.get_base_url(), + {"last_consultation_admitted_bed_type_list": "1"}, + ) + + self.assertContains(res, self.patient_icu.external_id) + + res = self.client.get( + self.get_base_url(), + {"last_consultation_admitted_bed_type_list": "2"}, + ) + + self.assertNotContains(res, self.patient_icu.external_id) + + class PatientTransferTestCase(TestUtils, APITestCase): @classmethod def setUpTestData(cls): From 97e715db2521894456af7264289d49452130b481 Mon Sep 17 00:00:00 2001 From: Khavin Shankar Date: Mon, 23 Sep 2024 14:06:58 +0530 Subject: [PATCH 35/46] HCX Plug (#2399) * seperated hcx into a plug * added HCX_CERT_URL env for hcx plug * updated hcx plug package name * updated hcx plug repo url * enabled loading static data from plugs * removed env declaration in plug config * fixed linting errors * added hcx staging config for local and testing * fixed tests --------- Co-authored-by: Aakash Singh --- care/facility/api/serializers/patient.py | 34 - .../management/commands/load_redis_index.py | 18 +- care/facility/tasks/redis_index.py | 17 +- .../facility/tests/sample_reports/sample2.png | Bin 317643 -> 264282 bytes care/facility/tests/test_pdf_generation.py | 1 - .../utils/reports/discharge_summary.py | 3 - care/hcx/__init__.py | 0 care/hcx/api/serializers/claim.py | 98 -- care/hcx/api/serializers/communication.py | 41 - care/hcx/api/serializers/gateway.py | 38 - care/hcx/api/serializers/policy.py | 67 - care/hcx/api/viewsets/claim.py | 49 - care/hcx/api/viewsets/communication.py | 46 - care/hcx/api/viewsets/gateway.py | 368 ----- care/hcx/api/viewsets/listener.py | 124 -- care/hcx/api/viewsets/policy.py | 44 - care/hcx/apps.py | 7 - care/hcx/migrations/0001_initial_squashed.py | 309 ----- ...im_id_alter_claim_items_alter_policy_id.py | 56 - care/hcx/migrations/0007_communication.py | 100 -- .../migrations/0008_merge_20230617_1253.py | 12 - ...nication_content_alter_communication_id.py | 48 - care/hcx/migrations/__init__.py | 0 care/hcx/migrations_old/0001_initial.py | 116 -- care/hcx/migrations_old/0002_claim.py | 163 --- .../migrations_old/0003_auto_20230217_1901.py | 51 - .../migrations_old/0004_auto_20230222_2012.py | 47 - .../migrations_old/0005_auto_20230222_2217.py | 43 - .../migrations_old/0006_auto_20230323_1208.py | 34 - care/hcx/migrations_old/__init__.py | 0 care/hcx/models/base.py | 62 - care/hcx/models/claim.py | 53 - care/hcx/models/communication.py | 24 - care/hcx/models/json_schema/claim.py | 17 - care/hcx/models/json_schema/communication.py | 15 - care/hcx/models/policy.py | 44 - care/hcx/static_data/__init__.py | 0 care/hcx/static_data/pmjy_packages.py | 46 - care/hcx/utils/fhir.py | 1189 ----------------- care/hcx/utils/hcx/__init__.py | 137 -- care/hcx/utils/hcx/operations.py | 21 - ...patient_discharge_summary_pdf_template.typ | 18 - care/utils/queryset/communications.py | 22 - care/utils/tests/test_utils.py | 26 - config/api_router.py | 10 - config/settings/base.py | 15 - config/urls.py | 27 - docker/.local.env | 10 + docker/.prebuilt.env | 10 + plug_config.py | 10 +- 50 files changed, 60 insertions(+), 3630 deletions(-) delete mode 100644 care/hcx/__init__.py delete mode 100644 care/hcx/api/serializers/claim.py delete mode 100644 care/hcx/api/serializers/communication.py delete mode 100644 care/hcx/api/serializers/gateway.py delete mode 100644 care/hcx/api/serializers/policy.py delete mode 100644 care/hcx/api/viewsets/claim.py delete mode 100644 care/hcx/api/viewsets/communication.py delete mode 100644 care/hcx/api/viewsets/gateway.py delete mode 100644 care/hcx/api/viewsets/listener.py delete mode 100644 care/hcx/api/viewsets/policy.py delete mode 100644 care/hcx/apps.py delete mode 100644 care/hcx/migrations/0001_initial_squashed.py delete mode 100644 care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py delete mode 100644 care/hcx/migrations/0007_communication.py delete mode 100644 care/hcx/migrations/0008_merge_20230617_1253.py delete mode 100644 care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py delete mode 100644 care/hcx/migrations/__init__.py delete mode 100644 care/hcx/migrations_old/0001_initial.py delete mode 100644 care/hcx/migrations_old/0002_claim.py delete mode 100644 care/hcx/migrations_old/0003_auto_20230217_1901.py delete mode 100644 care/hcx/migrations_old/0004_auto_20230222_2012.py delete mode 100644 care/hcx/migrations_old/0005_auto_20230222_2217.py delete mode 100644 care/hcx/migrations_old/0006_auto_20230323_1208.py delete mode 100644 care/hcx/migrations_old/__init__.py delete mode 100644 care/hcx/models/base.py delete mode 100644 care/hcx/models/claim.py delete mode 100644 care/hcx/models/communication.py delete mode 100644 care/hcx/models/json_schema/claim.py delete mode 100644 care/hcx/models/json_schema/communication.py delete mode 100644 care/hcx/models/policy.py delete mode 100644 care/hcx/static_data/__init__.py delete mode 100644 care/hcx/static_data/pmjy_packages.py delete mode 100644 care/hcx/utils/fhir.py delete mode 100644 care/hcx/utils/hcx/__init__.py delete mode 100644 care/hcx/utils/hcx/operations.py delete mode 100644 care/utils/queryset/communications.py diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index b3a88b21e3..7214be8d77 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -2,7 +2,6 @@ from django.conf import settings from django.db import transaction -from django.db.models import Q from django.utils.timezone import make_aware, now from rest_framework import serializers @@ -37,8 +36,6 @@ ) from care.facility.models.patient_consultation import PatientConsultation from care.facility.models.patient_external_test import PatientExternalTest -from care.hcx.models.claim import Claim -from care.hcx.models.policy import Policy from care.users.api.serializers.lsg import ( DistrictSerializer, LocalBodySerializer, @@ -94,37 +91,6 @@ class PatientListSerializer(serializers.ModelSerializer): assigned_to_object = UserBaseMinimumSerializer(source="assigned_to", read_only=True) - # HCX - has_eligible_policy = serializers.SerializerMethodField( - "get_has_eligible_policy", read_only=True - ) - - def get_has_eligible_policy(self, patient): - eligible_policies = Policy.objects.filter( - (Q(error_text="") | Q(error_text=None)), - outcome="complete", - patient=patient.id, - ) - return bool(len(eligible_policies)) - - approved_claim_amount = serializers.SerializerMethodField( - "get_approved_claim_amount", read_only=True - ) - - def get_approved_claim_amount(self, patient): - if patient.last_consultation is not None: - claim = ( - Claim.objects.filter( - Q(error_text="") | Q(error_text=None), - consultation__external_id=patient.last_consultation.external_id, - outcome="complete", - total_claim_amount__isnull=False, - ) - .order_by("-modified_date") - .first() - ) - return claim.total_claim_amount if claim is not None else None - class Meta: model = PatientRegistration exclude = ( diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index ed09b4d2ff..da31525d34 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -1,9 +1,11 @@ +from importlib import import_module + from django.core.cache import cache from django.core.management import BaseCommand from care.facility.static_data.icd11 import load_icd11_diagnosis from care.facility.static_data.medibase import load_medibase_medicines -from care.hcx.static_data.pmjy_packages import load_pmjy_packages +from plug_config import manager class Command(BaseCommand): @@ -23,6 +25,18 @@ def handle(self, *args, **options): load_icd11_diagnosis() load_medibase_medicines() - load_pmjy_packages() + + for plug in manager.plugs: + try: + module_path = f"{plug.name}.static_data" + module = import_module(module_path) + + load_static_data = getattr(module, "load_static_data", None) + if load_static_data: + load_static_data() + except ModuleNotFoundError: + print(f"Module {module_path} not found") + except Exception as e: + print(f"Error loading static data for {plug.name}: {e}") cache.delete("redis_index_loading") diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index 68bb5c6f59..0dba4797e7 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -1,3 +1,4 @@ +from importlib import import_module from logging import Logger from celery import shared_task @@ -6,8 +7,8 @@ from care.facility.static_data.icd11 import load_icd11_diagnosis from care.facility.static_data.medibase import load_medibase_medicines -from care.hcx.static_data.pmjy_packages import load_pmjy_packages from care.utils.static_data.models.base import index_exists +from plug_config import manager logger: Logger = get_task_logger(__name__) @@ -26,7 +27,19 @@ def load_redis_index(): load_icd11_diagnosis() load_medibase_medicines() - load_pmjy_packages() + + for plug in manager.plugs: + try: + module_path = f"{plug.name}.static_data" + module = import_module(module_path) + + load_static_data = getattr(module, "load_static_data", None) + if load_static_data: + load_static_data() + except ModuleNotFoundError: + logger.info(f"Module {module_path} not found") + except Exception as e: + logger.info(f"Error loading static data for {plug.name}: {e}") cache.delete("redis_index_loading") logger.info("Redis Index Loaded") diff --git a/care/facility/tests/sample_reports/sample2.png b/care/facility/tests/sample_reports/sample2.png index e46ee400018cd75312092000e6212fe70b8bfb00..468bf54b45bf7a9525626ad6cb6e359343f21b4d 100644 GIT binary patch delta 167 zcmX@TQTWybfeFe?9Ey#qt*VTzs!Ur|nLRGE|4)8~V)U&is`IP^htGJ2* O2s~Z=T-G@yGywqKaXMZA delta 53060 zcmeFadtB6Y+CKgnP{};7YGtK@WoE7=mMNOZ*jDRyFt*xqR||42U9$wGVi1@QW@cte zX+@sEr`CMR2gzCka$xX?iil;2#=s0`k8=cOhM8f0*L9DTPuu>!eP6$Se}8N*YjOC@ z`*7daeO=dme|~u9ca^&ay)z>0^txpuSB%uG7#X&EWcZ&>=EhIe^cto~>U=)w(E7ag zTXueZ{<9tUhxX)b!@=4150bqfPxs z|9?IHug`-j=jZz;W6JNI@!c$M&WTwczqjwnna^~X=We|9g@UJF`5yoB(?0@{n(P-h z?bsQXXMcT^z1A40HKm!eOsUmV{Vhq|Bh^5ST4f8=+ZrckZz^R(D^l_N-Jr z5!T|S{yR%rZ~XBt$c6Z9M|XrW?W?LMgYyzH8wYtCW4izT(CGIb|L@&5uUfG29|hmc zYU*dU_e*a})6ciM?=U;>@E^(d_uh8R3kB~ze)y4D3;%WM9|ewsNq+U<(;1g``)79T z*ZS`(Yk%1IUQ%~U(!A??S=}}GI)>(l%WetYn4|ieycesxU(`DWtM7jN@d&H?OReuq z!_;Zt?lx%H_+$;~PiBzcy>k zwrd_5{il5sH#|7+uKel}wY%#=3I3OUBscTH#Mb8%-w0MM+H+}Q{kbjmtEO0sm&f?` z#WYW?Z<<~Bv9 z-mSHVYsV|0%3mq+@Q$d@mkzBAr2Nc(P${!&fr`$^vSOU?%)YM=I> zhz$JB*tNrW)SYE^JEpDEtuE5_Ilg)F{+EjC4%xb`wl5M3)0;;`AIUIOWY}>;9PL@v z!HN0y-udM-^(`~QQZDVTSyC}9?RNv>PHZxIH@Wy2resyk&t04wK1jWMP!>&}KK*Gm z&~edbOEu;Pz4>i3%r-g(0g;$4Y9iAu@>#O z4;v0VtlIh4eOc#n^`oQRi=!)^463C#Xm_jbG_@iS-}SL}W1;7*LW|z#^JQAw^kcC$ zNxjP-E^B_+iBq{h?)+mpjoNQZ-Sw5SM02&Ba}D1;TzZYUtH69z{bzD_YjP|1M$w(g z&H5YeP50Yvoy(#<%QAgM>J-&IB`)r{U4KpaUpM)g4y~em@zTz&NXO}l_zydzEG?*M)E5SRD6C4xk+YjG$zFe#5cta#YjUC{v#Q2^XSC~1 z<^CQg?{@fQzir+;+YwodpBAIG>{6|Byw)||WSg&c&QJd`He>wGuHc1XX2&q^s(epx zt9y{vG04y{+c=^6QXiwekC)4@@Pu=}T<@%mdp&qtPWrLAy2e5ISCvPo_6XM+vuBNe z`i*Zai}8IC(_|`1m8%$;e)vA!qUf&wiax5ws;<~nTHW-Ku>rnL5}xAgPmwC}Wa?erhc z@n7ZkeV-I~GSSwNn6slhxU|}xnV`2s=(BwRhRkHo8_9=LXBXDQHQyB3fZ*BF@;q>X zce&OR-c{D7{e#*4$jKh%6{^3&wJoV@n}4PcH|fu1-c800zF|>OU;Op+k2@6X>ihX?SCZ z?%r;Hb#U{{rQ2pU>Up$}G0NIs^LHc#Czv~5G9SwCK9|3w_NAi5RTFCL_Y}JB$=o+8 zGc4AeIlj6&AF0HS&#Us|c7_+3^;2T&ULF*KU8|a(d1!u0 z#-Ptw%|z&8#NVt{^ENH|dX;?k$MNSsE=K0kcr20K&sq(xN59@V;%4N%8r_;2OH;hH zN%1gcs`6Wz_hv1Wv&@`OVqVc_u+HBUU2X2ZWOhzh+o$`#i4F{q04evHw>I;;=~>rA zL_}obXvWJvg$IzkuGb=F9vzft?=kl2;_~~VyZ)SbVRz!{1n+wZb5c{+M|}PAxq&L8 z@DW}+r<#jD`*DnWCOfW6-kY0sCRbx0Vss42#4=4#@sEScquPhpBev!OmAIQ$c*|_Q zcsZ0vB*tfV&vIBIv>&D|-JaG6Y!mpfYF?by?A>BMBImpf7w7G!!~3JVQ|cYV%W2)Qk_f(6Q<3V;dOnCogX7;r9Jd7?`C;Oy9)g*{YA}{CmtHUPz_YH5oV3 zToE2{#su#&`%2A~P0>M}wWKX7N_D=o(*96k`$IUJnY~6OAHkYdqo|=Q^P6v>=|$JbB+ zE>elD%;$M8!F$c2#mzkJd!zlD9DV!8CFX{Pn{19K+umybv1)sNo4vmQ-#*&N+&nY$ z;LK+?RjE}o^oSGdXX;kWw3KB1bT6;CmW;I)<|HpK8N(S@w9s*5p=R3s_P^zK{LSDT zW4!Cidx_g&YvvjCBGGDIbWM}ZUT%`r7@HcG#aqJTv)$?W?uHH|qQ8#KRpLB*BU?XB$ojoPV4a?umOdEx zqWcXlaP#|#=ic}5Oz44A`e#Q>J$tXj%qdf*m~1?!-i?Lc8}aP~g&Z;0-`DGA6lvYb)E#jpM1qD4!LQ*4Iy zZY)wOQ`{G|mJ)GRY3#idyi&OPDGasmH+?mzx<_P4{Vl(2S8sm5a8yu$v z{u?^w)who|>m_EJQo`M{3!aj|&X9Wem)M zdi7$J2j}%(mOO*APi{|evDPtVx~fbz{>jVz=b4K-yErFfXwK5_KGtm|cy1w>l@q@D z;A;_G$XGy{sqrfh{(UbY*YH&dzJDb&a6 zU$`gn9h5P7o&%$n{G9G}i-@zT=j&VNhuQg|2Be87CEBiuE@v5_*gKPhouPx~!x__+ z;aUvLwt3dtELrt!S&C#bU~+xWY~H@+oS-L1OT4dJK|38O549iDm>kKsor|Yvn$$nr_zmY{DFiHVpFf7+81XnG8(IotRJ~;GhAF*mOWX;GvFh&&oxeBPcy%UablyH>$Pj;>IWSD& z**(H<6>jHGNPVg#==KfLvDlfkmLKzUaf&88$NOPU1COI_>+Iyx_nAuOB_~H_Z`m>o z;K9RvuW!7g{xxFw%<_m#1$BhNs@C*M%`AJd} zmQ7{nHc1RpPF&J4Sui7WaBSPe}88BwW`(4AI(HXq1|>(Ue|ZqmjEFtc0`Je z{%%_^UdiuVkbkHR2WYVozl3lJZ4)?jWq_nQKbeOi|32o1ERys~3!5>Z-mStd(>a_>oL2&-5-tZCV=mLH{+dklRD{=&+uV$H|K=Q;ZR1jtn)a@H#j(uXFS zBS(#k0VX>hkZ2P8g6E`^{l-MK#h*r7wcl>Bb>T>f#~7B5MM zsI$j5j0cL0%9caP9H`qm6Bw*5GH8#~6rcNKQ}Ly*YITbkQunRsqkxjayvq#&qCQm48@f$aQ_vbTJ7S?;>0Q>!MJY*WArgf0cHZQ?3a;o4?B5>)Xk3v{pje^v6nnt>)>OZeJdK z>{5~WctDRT^20RU$}~mLNcyIt^dT?eM!N12{Ob0x^yUG$UT$RVqCIPTm#o$$&gKWL z?w(2Zo|#9tXI@=>si6LRfu|(jH~EDEf1Qe8!=$1h9eJ)Q1I`?f-Bgj2+U-xztK~&- zy^`d9#bmSYv9#BK`ZBSx;Ks zrNDi-Qie#k@1!>HM547Nacx8w3Lf?E@dzI8`;d5Yns}fi+t(sv`H{aSDXiRz>RugE zT^)&7W@n%RSVzH-zC2bJZjD_#0^iRkmmFk~`GE1zp0W#jaM7G2Og0|e^wOG)sd=>z zmwxxKgV{-u2!a%=?hVgk*^;k%H{>4)Fzx|}$Gz^$sz}R>kdzvbW4#5XgrRXv+08MB zd3V7m^)6pndTODAbzLrv|%CWZ8{*jUCiwZkt6&@sV47YEMnEK}Ssh1YS zonY;4$K`+5WE0L~LvH#FRyWqJk%d8oMbaV4v&XDOvkE*QQXd4Bg8ZBO!2Kzx4IE7s z%!M~aJ8v@Zgu8vb>)pVkS7p!Cses*#fjT3&TFi0VYV*SmffMSvZ@AJ zOwvel#GT9R>GqLvC<8rot@7Z|sHnR(RVkCy?zs8KH#g`7xDaAEO{{vPs+F`oQbjRQ zVs67>wNK7>O*YxU5p9l3Ul-B!dV;415zIPYU52f6b9~muh^~L*P|QqZuF%1FENAI8 zAgj!Kca)vq@o&kY@2`_9>VITcbX$qV+8B_PFVDOUi0Skbp%RT+zP_-t$vb({B#U4~ zppaLAeD%wH0l@EDWf#MLrIy(ku@v=Th8{;^VD^w?!@IS~s&PwO8{w&r43pK(dJ4(K zpmA^dWYw7$4+W(1P4<12-1MK)z_!Hm+dO-WzA1_@)C{+A`dF6pO=AhCJ$7LzKj!Z>lIV<_R&VOxpOcO{y zJ@deHHN_}^{@TQbYnAIO-YqD&#YSyJ~(&En3Bf3v!IO%1IJ%Wf^0@KsUK-wJCKR4iY7`Q?6B>d6_gjtCW@ z|GmeRVwAAOz+A;lFk81Z!Si~8B}A5x z`#pDZd-}iE1->;+hsPic*o#zOktt!Ia2zCT_{$d)zo9VPi;rZHl!FC9$Hwo>K4Ux zJRWn9Xu# zWSLS`BwDP9ex>jETdOba236$=n1fkLRQuKl$G^5uT@%p-cKMH@4*7&)fp%7?ls84W zCnYwHOnl8(ipnuo;!YnIgLx(~r`yN+1a(UN+Tc}4Tec(2U|5x|ZAp!yed+u6?>}jA zquOIb?`ij^fUQ|OY|0^G`^1X}6W{h_QL;+vcrxkW>P8lkZwy&v(dYY0@+%3QT>jIo zqx6lVJcxWz!k^TnZ>hO=AXs^0d4QVSqT+^LQD+CmHw^L|E%ZNRO|zA{mvpr+_$70s zf;tk$NhEB3cH9XRCKaSE9qVGe>wc**jtU8THzh&^{{SVhvEiqJJ?5|o0c5jGHZuQS zAd{Zf+zo)xyJi(A#Cw%e}>9-Eu_&ETv7Nx->#U)bHKm-Mi1P*Pn8H!xAi-NC0y;t}nJs9)C{h~PTQ{_HD<**$Lh`yKu4!1?S?=n65 z3wBh>ChD~%W?_H`oRzps3lf?J;ipOv5(3xC zZ6IWq8Z-3!xmwiKj!-a3H8PhhEG+%tw}AlNq8;DAB~*K5*~Lm!JlF_UP-Ln`tfE2V z3feRHtf;Tr1N>QMtP0R-vUS!bxAB7Y^t!acRH-R^rGVmh%6$qiMadUO%DVTog7?Y6 zHW6x67wrDy=oP+FLS8D+8qbe~_V2QV0VXcHV*TSz5G%NZB(w>a1d23W-Adft#{WxG z-1)=85|UJUQJRD()BjX&rWhA&1i5{gr~E3}H?ey{eaAEA_gN4(=n?acM+J_WfCm!a z;&X;uo(Ckr1A6*IoxMUaC37%6u5p0L7OsM0^n94(y`E?{%>Lf?xD^!sTS#0Mn^~$Z z`uZFsW&29)X2Ma$aA={S7pOzy{wqW8jM%yZRD@IaI1UZrchKnIHpmJwi)~!rgNSRB zCQSk&C`oNQeddhDBEd<0tI%_`^7YQSrHQWJewIx(@XQx4UDAC1*V}bRpUB(Ckk!X2 zo`8#$Ww*OC3Tl?#AxvP~{Grpo-r4l)j7VFXD#-@+i07^DkD*)`$Y;2xyn69^C>siq z==Qjl(Zqdi6W5M-re?8`bzOwkv)5$%GXBDsp8u)tx;^SFSeTVse*@WHCGOmV35^Yr z>LlfgBmL$4>Ln=7|1Bh|zY>!XlJz2vN`>$^(x)PEK!84I$01htKSxbnwQ}mkIlqj~ z5#eIHxG5@dcDk#1ly-w}YD4b?3vXx4VaoMR%4H@S6%H@r${i}6h}0RBi6vIIP+erI zW9xMz+&DqLDfN&mHpO&(6eDS?1o$WY^!}(r0g2lzw|iKaHPY!wu)6U#_85NHQF>!! z`-Xh)hD_F2ZUg||_)y8Wl=cC_e@(VG^Z4Gq8g;i(RAkekdFkI&*9a}&270^rXU-(9NW_0EdYFZWOrUVoS(KS4MX$8F2_OvA^xYXG zN|^Uu3}Pl*WKgw7rmv~d^|QJq*@H4S*$K3)mqbRh9xK@uJoG_`;r{bIv5J+;ZpI5$iyyfP-x%uvCg0)Hj9{L-$PI z%9^Teef+ug5GCjZ3GLIc`i5ar(plY<{4$XeVz!|cIEvq+3W*TixIl zN<DF!MoHWSj@dAQ94%@4a#s?OadFOplInwO|4>Sj>S`P~W(4l(4 z3$_1G*0exJjf+QsH5YzPQaMe2)}oSu1sc+k>on5&Mg{%rT5xlyj`h10z( z&O^J`ZC$W&QdZa9a6u?w)6@y{hsB+cUmvOvgbfQ>vpy^A`pkbOKX8L2!Xaf1L&8!B zB=`4=RqSM{Rd8aKW$`Uf#IFEWjA_t|CE$YL_ys~*@nbOv-ZcJljjsW<@^o6=D++x+ zAjW2?3l))_scBVLgRd`i-kR*Z)ntR#n~`3WmJustL1fp~$VPHtdVbu43)hI;gmqU; zFQ1$tYzwI4vCn*cKT>iQg2ipP-{yS*JaO4F^3Sb$gyz^7$H17q2Uici3||)6I@LvF z77(iP8>m!4H}(!o1W3Ozo4=@mmEW$a0CS+A{fBeMI=C^sd%S~&BbyApW8ON}EIa${ z`n&^ydhBMtNMCviBUFJFmbETC{Kzd1aYfK+1llU%YuE89=|@x;%f==e4hN`%@R)t` z$}kXBY6Xi*f%-y=#Ed^v*lF+IrAUVz?xd~5f2h-IfI}}S*38p-{c7&*-aVO|8aZqiANB{(?+dVU5g4NAv zb|*CnpTby!@Sh$cpScHKdt@->tFOLN#B?Z}2+886lJ_6i z;h<>6da)Db`yNB(9y=4cDLFph_4&%FZ%ghXjW;OqQmFeK_p zR51$b@V%2mXowV|Xv`$II0 z6xK|9tl`9g!|*z9=yYpZ>#-Z~)b*A)h4murBw5dALS}kZjx+URqUa=qKeZ zHQjhT=N9If=XTFpYy=(dq7O^*Dsg_lauEaRp#wHcXegOV6Bd0jP;zuVqt1$GM$_Y~ zX=&+;YMx0O##*3SEN(FyK3TMT$vnrt(wV{2_q}3f{XmK$LKXURwfDh!LX(IDjMP^?f)`Q|P zB{vgy;*KKIzCES2dmJc{_c91(9$uMw6-|c^jb0%FHP3>imgvAY(GIfP64K^GW?cWX z(N2{=27A+|{=M-`K~eota#c?}^k47&q2SsFA31sd!ZoLE#J4XzBz%c68q9_q?9to) zyl~rlf0*~}tdDp9ap4-N8xfG>PKcVtU>eu)X~OeErwgGWMFo#p`l7GA#Cu|dOogcp zwjalJ2AGxx?|w>1RG#>4r;d`havn6 zj>qBPaRZzFjbS(}37LSZH_;P(g>zVNNJWMrLWJ`dc7Jr?o}0sJ|8)xNCCN3BX&CX( ze!X_qn0235-8}=lDR7FSH*=MrJ|KW*^=OvruB%v5(#+Cbs`6S0s)*}e-bOKo2;>+) z|G1rLQQVhUrq=oTH<)Rqz7xNo&@uQiMGP_b_t^c^&0*Q6ZajJSV#EiyPvhTid-dG6 zBPAb_jE%5JtovAnIb&(H^784faobP6h-lmM3F$i&x z%D#AB>43fb6=y75vv_Da)qAnMk@XbKmy!!_D1BezN~^2CR3`?lw(9&4>#b6X-ocrgAD5r|M(_CCjEqfWduRh?gq}Zl?()G0C#@CVujI}r z^1J>r@5>v+^uvn8MTc11sPO_~CGO4|LsReJAx0Io08IwIf~UrW76+8poRX)VkJ{QF z4QYsSJB8E)0t8K+xQv#ZIY=)}psvL(AgA)?@e2w7@I{~I1!ORJq2T@=>*#2Q2Mw0L z31?6x$h0<51=^`E){maU3l%}ZRoA9?aY(javU+`92*CJb= za%St4_1|25&2W0Gm$Yq3b95ED?_q9@m>Jlikqo?kW&FjFxdZ;7xi%^ThS_xe*$Ct6 z9o^rD?);JaqB{pFF!SlJT0f`t+Ci#+lzNCRO4qB&zNa!_T8=LozCu$URuuh)=0%OF zJt`@QuiW%28eE+y{Ye&dGvdoZ+YwVyH-U6v@=(lOpCX-E22x^=` zywp1Vtc6ljNX==rj?E98$*-EU7B$tlzSy&IeV@B^)T5il1H&|!{;8U~baeMfksD9u zcfb75Xo0U}meUu>NeKm-k>fR_&9`k6+-1NG z99dlrXeyQXtcB0ry5WfMMI`h62$AFzZMqo}ebFrDwn6fB-%eHH7arjZEU{&J#WeW;9m!RGwE7COSuD;<- zlfFQ;xw|H@k|mU#zV`2CrO%PH$aD3{dw)v=pmc&+A%uc zIePOwue=qpl%gQNw6t8%4?+`_c!lI@Q>%w^O{WgO>!aM4uaJ-gWDriXVa2F-T(+t|F&Q%n5Twn^_#_U5^^iB)@P8sL5cx7E1j<< zyI&QX83=drF|89xZ zFYCTw%p>e=mO6usE!GMbLt8;?z1WCKy|9s&*-N%f`c`1q5~CQghY6+xW%HD@wesj} z>bT(zO}2=bPpY_YSZ6ZL&EjOkS4k2pO08r!DI&PzVRrOBd@Fq`pbx-OWQKz~)N0BH zXMHo+L5rwi9N27iKR3t83jR-xl!HCg?W4|8$C4s@Yh>4OvAIfhEFOS#J&N@1Y|2=3 zHr^_12hn%4w}gk11tD|Guz9_;C%Nb4QVb95^C;k39OpkInX-d3-e8ql@&I-AFYnu89Z zRw`s<;OK@d0f$Noh!N$k^(@jF<&)v;UMzMny4RmgOUsqbW`m!m7?;umTzMBB@=R@Q z6Bk{Y#iWNKH+@5H1N+RFKzMqNjq9sq{;)kOq}1Y4o1MF&Ro^b6Qt~o>`WNWkPQQ27 zLO&ZWq?!zgwvdk{=P9fKdbt1n@{WSlfg=={98(8A=?Dgbcib<1( z5%@L~vA3zrz-TFEIh zBa3ff_ft{-*m$dKtgwiSr^DE-$oY}*{CCt1rll7Oq)CVc_0{FHb|cn;gOj#1FL@Cr z20VV*Ya-x%wj=Q`my^jqes*!C!1lESzL`{`ga8UgA-;?xc&bQ0TxV&v3z`4v(V6$}1KLM7#kC zP~fhdtlJZ^&Of2?kb8`BeOO`NC;e724s~FR1~MsebPJa}eG#=J;UNUmyk>xQI3DIf z?HeyWniqGZF3min<`uNqP*P;FSYi}5RtU4LAXb5I(xsu0o?JVDNZg5q&|DC=AwE-a z<6ZWlaVYBs05{dRmr_dV_rWMy2ia<<=)~|D>I*n~Z)79&$e&nl8mq;r$hu3;RyGZz zEu{Lqv>?Ufn+;WJ1>Xt@$g(!v?y1n`5urz*ivWHVu;hQs%TMZ7(M!nxKsPzsd3so6 zgPBGYq3I-t>!DarpL+f=M6}EZ{-WwY$h(EBqOrhq48gH)wh*Fg>d&rGgoULaAV)pI z5aGmcJEMrlN0gF)ctRAg+jbzL{## zh9knHQqPJzLA$dwWhtgtYhMz^-6naRq@c7s`JO=yH)zzODMpnXRSUWC^hM+-k25WQ z`Se(1qVyZ&phk3&%neyx*^<-Ds#hZoL`b7$J+hgWS_zU$%p{^+)U*_-?opH54g|VE zpRD8YXCt^6TOi(qOBa>@8#OvG!0ju}@lApYuq8;%Ga*)GOR-YVLf)MGT9DRwmHuN31O zc9Jw9ufEAfs!OVHX?~E3XPjsQ>?aMgi+stZ4Y!Z^jc2k~wlFsYw#8;LUeaenBq@p| z(I`4u2OmV!ODvGBPctKT&bOKy)V$8`j7{1fL~me>mcRuQ<&qFmWd~xhc}M9FJ0Maz zhM;LYWWt??ydDS1S{Z&>@o-V%%mj0;b^C~eL=GYy9>&%Ozzs#x1R{;u;!S50ESU+H z+P+=8g&j~P8%qa<0;V7;k@|%6qfhAIYwcOyQIk|XEX}kxyp$GSs&)OR&2vrz_ZB0Y z7NR`)+Zwqv8V}P_NU6q}mJVT*>|0J6+{xQ%7{^*fclf@jeQE$=saVMpv1XeimI9+J zC__TZXUu5xmXN39m5>9(TC=Bx23dhhdHKxtA{-Y(aM$SAjE!rrhi97pUZJoAMx8zT zrl`71xw@?s4x5?ZHN`4B640Ngrx}|;WXNFt!0+y#P5MRJh!t^AfVcXSA`nh` z4)pNlChZ&7SK1Qs8K~f{u9faE8vCjOQqhjWPKl935t73!op-9>xWpA3^R?U9l7P=jC52hBNUmu<4C7q;J^Jp?12VLe`NC zOei?IuuN@TxCE**atpmp32N8LcMk;eobPQHob;zg5m0_q&aUuaC?Pi=cDR>C6b(wC zihPYo-{+A{p?%qoRszK?_nv;0$acB7Y%@Vf!eUR-tNuv}>x)We7fe(DpV!RXsnNm1 zFjs_G^$1lM7pp_%X@l-v+@;Vkj!f#O3X#vzkZwb)eTdiHMf#FlJJ`=u)X#~M4|yU6 zkO~Ntp&e`sosKMC|!en_?~!tzXg%QMB`X21Y@uI?U5Q`=!mt>N}3pt1-9>CPZ8&7MOAKU}{; z$o6Fd_gFJOi-{YoWDTcRmqJYGh{-*%P$MDZ+H5*~q~+sDl-1IWM^E0QH0w}BRJ@R@ zf%J!Pb?hlCHp8S3#DCvFu@Xw#haafz^Dn;)FBjCrKoqByi3Vt1Se)10zTL$R0iQ@; zU)3&ZLf4Ec<6dkxRO@<0W}UrHBe{ym%Qa2dZi3_RcQ%-8M-udRv4;nQTigk0YNb-F zWTI%AiZ+4heF1U(Fv5ge_%!0~N&JuvbeNf8=poxqB%?^5f?$b_3)P(sdPcG;F1wvD zI!=r3-28*!RmIOVH`@2t7eOn#UzPh+9qL`Dmx+h1F+?ia>B3-{?<+li!O$PwFG&>= zhnX8iheiR{E3%z!qkhEWdlaIson7V@bg_!pzy4UNfS?AEo{^;zWB}t^fRGQeoDT4TN`^GDV8ZjFx3C7me^#ImI@B1$g7ncM=n7@zvBx9%Ktny2DSQ;N7zZ{b+vhOg+1a0 z?OQJTA0k;p5%%o4TUq}frEiCqaJ>*RBs|okQpWO>-`_)Q|6~h*l5?8`*B14iWrA)PHXzojvu}CK4S61KvD4|C}MtEGhbcqHWMf%yLK+;QirejHCr#H&aUgqJ!bs;^Z_FAY=1|7C31(!I3qe6efSUc!_B>2?Mrx;A%zpVLvZrOZIFBKq2bGeXd*2_01 zaArVtu@x6bSrJ}}jV6kPb)%#T+Ssjb3VCoLWJbC(m_S8cqs$X=&!kDu&YvG4WM2be zh)yp*W3!UZ(~ouYO|Aw0<$96 z3QbS>;pO~b`Hw=t$%kSQ)gEFx!c!a3k^Lhz5Me>@C0S@_Iy78MN46N!J_|m>WQ%(n zQO=O*;_E1>IX zAf^NN$EUI?hxn9C@Bz8**^q>flAxbVUh0HVitEoyera00!83<=7I2Xf)W^Y z&@Y3Bu`fYt^aG7erA$)Acs^p)oa^7b%IYRPo(W^#WJSmj4xh3!j%1oDvW7xf7dszf z^0>%$nXrLoQT1wtM9CqV0yK6WEb-d&fzr2znz8$rv+Q*8WSeZ_s3xen`86r5Swo}j z#qo)!b){!H^u;r0&NS%797lK=W@o;VQa7?));hlZzZB^&Mu<_3ta&qF#0ag|1NXxN z;PydKdYZ2OA=y%1F&lWS4n3_}|>hpnaJ zTt^+Ed<&NVeBSRp&JwmYckc?#t+|Vz(p;y?MZiIDlF0Rw?zcIjY|CD{MS6&Y!Xp1A z#2qRB6yxf_F;0Ob*ruNDT5z}`Pf+==rlq8mA3xq-5#T_&-0qKKzpI3bu9Nx4X~nX- zY0;s%w5dYPaWRR*;qD-zFN}S`1nGukBx@`Axn3NG6RghS%ucfjRJze_J-b<9~LT#8ExW#SatPJ_IYf}u9A}8<-B8ddU_P}!C`#B4HfU= z$)fz02p2u$Z7@xBz955>6t?OAwXjCKv@%cuWu-^%5vG|$J^0J;r-tI+e+YjvM*(&A zkZh6cgd9KrS#Y%2e8_o70T+uADmdHdxqpLV)6NUhVJ^0)Gz^ZBkuo0K{yQGckFSFK zQxRTD69P-INVObf-a7~2&)+}pXHqD#oxSLOLJrs|%}2yt-2Tr|@GG(DoZZ?Oe`qe} zhe=1TIO3oOj#tnv8cqHs)TPg}9d4(&c=M#iukoyjkC?i-gfJqxXE8?YiemTk($X1b zH*B}No*wJPeSB8T?GZmfCL#0>`NC1^R1;EzFq|w6u&mFS*qS-rP50Pujf8ireZyBD z49i})v*Zo-B2hY7T9TVFS*#RqV&>Gl8~o@}8!db!^Er}Cs3^Rs9+Iury{>K7$ zkumV9%1H5ZRNQ$%Xnc8WIH(3^lR}X}Ksvoln&8Qe7(QMj{S;*UW!m1qfxtU+()*$+ zK)vPftzdQ!)nq>N;Dbz9g8lfP6u-)!kdXbt?l21(Z7*-^FGC=v!xZi#kf_Ido^Y{2lgK#Uf5#YcCcP)q( z60sh(?!Op5e3hI=5sAP%s?zDxt6bndn=~NO02GTgB?a61;>7S@BlO_-lx`hqJ|`DZ zjZt_%QhGAibkx*y4>GPRhLclD2Z`*x`aE@Fu}>>M&e>4YEcze$)UeP~&=?U{L|11E zV~7T%ucx2zzqq3Ljcim&=lCh{;QvJaXvD`XRWScn8+ise7sZlF-sjWZu--(ra{!AU zN?M!(A$)MnGiur}62Zc?OTibyfIQ9kc~;uTg`StgoC!EAcHXSOs}Jb@kyMdVMR zH#x8trr~w7#*jHV8qV?<1H=?6YDBzFvdrkQ2)By@OLuL|XTPyBF8O}eF2vf~)!c`F zSr7A((RFOB`k-@%E90W^4>q#N=z;dUs%y97qZd?v$pc`r2{ zbXAT<(5rvTH9g_)oMQnGpFU?hF|i2DIN- zvVlke)I4Oufy*Tw0u)ThwrKdZX-KE<0bck^mtIm}3rJkf$8Yd@0n!&rq-t66Alg)Z zr0!I5cckt}!is;Mrm$yfLJT9^1(T{TvEkPX{Wlf95MO{0ldDgA(3Mb+yh!y*L?62p z6D%w)PhcV;_Yu(66&TM4DktzefEv}d7PyvxQJ5%Eepi>CUhSAhLFBk_4S!5-|KrP$ z%S6Cq!hX}jlwV|8(kHT=P5rV_7{Hb+w3LgIwIaiYDTc3jiB_2#vLyo28>CFa&|6T* zMk0FkvSblbEFvt#S?G-KK|C=tU|WJY4j4Iok5Z5Q3k`1aqBX~Rt<=2;c|VO9hUIUW z%yvu5C`xOB$Q$u2til98*a@jcLya#W{Tk>3)#=41f)S_)%%Tj5d5MuI%uYQ=2Kikc zphXVtN<=3HQ&IYvgGa0qPKup_y2t)s2t84wMIxd8-qy(udsXU1g~msMVku^YpJQM# zaNvsU0|Q2w9TLS4!cY&6No*!>rO}C73xRn^HfoS1802hq|1y-N4O7S+LxT~gd1O1e z8Mtk8EMUCVCnjS~3s-ypI2r9DUKM<5bV%MJr{_d2JfaAeMR$MUfxxeEdsa7VGr`fJ z8dt2o_8+U+D~s6_Y^v-Cq+Hb*cLMbb2nod^t(Tm>Dn2S2cj=-bBf&AvwB~;lYAgo5 z(1+ibzTbfOYe`M^v`B4?l1UqNmUGHsg6w5snFY2e|* zyXZK3M<6mxI%*#=Q`sjbyCy2=HCv2?FTyGvJOBdlrX3YiY`aRKbtw_;6XDnnZ#5sIa@i2av;O?9zuIHG^bLk0Ga*qc zn~g&~{~_8&6gqZ;N4~cKvI;;CXWW5&6ah6-Z(q(S)9NB(<{{bA*+lB^9Wf`9>|7$g z3=SU`OM@bU9!1Z9%&uqPjIi*Ua-7nNbSjheKo~Oqu~$eyq|-{qSc?aj={CahGo4RO zTwN=8@XE@PWr%2jm)&a^M%p7=hJw>VrSTx@4rI@2&ur;t61V#t%BILVA(DowX?V!A z2YqppA}$*_Q~wr!)c+WOMD$?a_~mY2%sa;*w(e5t)a4DjnFE;zw}L4Qh}}^U;4D;b zXjTSwwuo|Z(=wy~DvDAu5c`)fO1LQus_aeLiBv8(F6jazxro_{bU#&lH&-`2&>=Si z>d`%l^q5gmS!|TK!x(KJCTb~KjXavDe+-=tmIF><^chJTFvNm&g}J|bS##U3$puK# zM`*4PkUWjHZb==OrPsflZUcsR1$Ih%A0evJOp7Df+RmmL1E&6uP$cbzvN1D(bSdB` zaEnM;r205_j%tX|g3}_?)x-C3uFNDTF60|R;uLMA*JV?oYhvMpo>G_<#l@#N{77Cc zfxQ^vX;Z)KsKQl>D3=fA&@}NpiTMkjXHI#ObX{9pYZ%9s73qZPO=^ZJ$_fVeV3<1U zykQcMM7N>DAkOz2F}67F1UL%D6Euvm{+7>kFD$roQ(yqf2`~c(#<_(YP*+i zSgV^4DQ;IqkO~8G2tx^kQZ>bwT_4M2=t|2H&~Zm{Q@#)OXNW>?S5FHo4CvjSo{EUs zLKle7bgKx2a{U|2W{puo7LTlNM``SfF;G$v zRdAi`BF7VGJY*xKk;PXB(KoLBzmWT7mY$cB@S@I&v6aY35xWlWa~tm7DA*yT^5-|} zT}}(QOvbyS6b%jDgz~GgYpWEGb5)%0P?7L&u?(j~ zhj(5uB56DX&r*p0b_a;?qEtc^84YFtoSF=vYU>cagNz_rfYcd=kO7KbCuYE(Rpufw zmU3P;+ku!7C-av6QVxi0XN{V9dS%x2C=6Al{8zNn@_^(BHVXqy;kpunIXq6E`0WTtdp ztuVyA^OS~|dl^<0X+4tCK@JPMkuEpe6QT)2%MBfQvm(R{yWIz8%R%9(e&+y&XQ)m6 zMRk|HVi}~%kO+APE-75D00?$omP$GmIffWPb*}W``lj-|Fr?-b^f6)Xq!q&k+-Wki zMaoD1iZDAthqN6l(n>?KRjN^#)NoLgicTeyFX(M>s~B8~Z&3KlEG8K*&a0#GNXZ)- zO~4Ez4RUOV%FDlk#37hrPVApeLz#GL67+X4DV|CTDQJAq>Vozr!j)h&3EUM#7B8B0@*VvyvC(0KSmVl@qQLCFNs z*GI)I6U%JqodaMB6dg7rP2xgCmrRodNZ5#8^jkFcfoQrAvw<+NDoOwaJ`g(miQOJw z*dpsd;F$QKOxvQJRq=?QgY`SC8=Y!(dATPmK=|jcPM^{fHza)Iu;Oph01m>Aqxl?OUzW= zuc>vRnRvfxsB-YS* zL@f-f6(@%Z>rFj{q?tyHZmie`WYeD|oNuw28v?L?Q;d(P|0cn+JoVZU8AAzxLFPA?uakUpfEq@J$l=3{@+W ziH_Z!e92AJSvtkVrp6(8n5EE@{jVIOu3%!bE<)PPNIZ*Kism;mzw)}7t-@53UQx-t za=sZ2-ACZrkoYCWTi%hTkSCp95%JG(bz1c>XGb5TO6ww-qtJsUzH9P1B*j@AI=q@hDZ$3K|!`4^g1PucoKn|m<8NfxSj2S zV&zak=GgXAF_r1@ZJn%g#33UqOglwrfChE^$d}QR#tR_fbsLa%1`CnKv!e*Tjb`^rg6t#S=rQ2-lB{k75s$dh=^;7=asQ;tV9c zDH8rU??Gmjh@bi^FFTvWM+W+c!VGnv4e&B?BO2l=!&V7NHrwDw1=R*rPyt(4(^2<{VN{Zz0bI27aq|P7}*?|6ZatEz4hm$Szvewel%z7uCuTTjk0l9ymdFS&s&}t)Ro(ZRa zxlG+^H>&A|6w^0>bT##^Q7U_LAg+e3ZXeMz5NX zJJk8&K*og6%rM4?(vwp#56m&ZK#GU+jzSZm(2w6!UR82$bo;&R7ZxWVS5`2ebmK`2 z>MP$HYlyb==nL6Qv{W%s!KT`(IdEE{5BhcSR+dA;?&xqVaCzVx^ASTT~$me~_If+PK;!?D9N6|;N427d= z(G(ZInwk7L6~khvmrwwbX)kPOImW*PQ)SsI$&x2HBLVPqf>g+2E3>fGJrzbop*Uq` z-)jWiauXPDsply&DL`(E+yn%kiJXUXL`+`@90(FOFPCZma@yHh!1ONtLzG0{J@Dkr z(%W*sd7Ta8kHIV1{RVx{n3EIg$cKeHUHZ)Unj4HuKy@#jGrV2Zm&nk_dy?2AYIQSM zv(3dqlQE#w+={gh_Et+_wN9ret-(~im}WFgQlkJOZFYpX7uAaw?bAr(RdIJYZD9jO zqH!BSqr>4@@Oc>Ac&$>Q3(AmKf@@Cfu>0i&m=wa$KqZ+>MiapBRxm+=8J=dbQ528rG1L>c38jK!nCZQq>QS zUcm_t3)dhZv#}L(!k~tik=vV#zt8z0GQ^0&1uf_@-avdmFb2U=B92EgOd-zj%PJm5 zJBOrF>4MbC+Dft=WiY%;L;d=QkGEdNVl)tk zBclG+h0D|j0B8Qcr{FFJ>P=)R6Eb-MrcNO1)yoR;0`$j1om;|QKQF6hdBxDIF`*GOMARU{)GBW-CJC@^6;ylq-6n5H zYZt9XZ(bF3RyyXuj(xg&AP)hKwt#JF48$Y6iNob>xDMKLU>mXvM@lEuVM2564ae#d zHM$VokH%?9k+N^;iD_I^Jknh=1_{oP!}%IDWC1EN%<2l6l)js_3NOFK zM6dg$X+xqZx?%q?5GlUUQ3NeXBQt)?np{l!(e(gOxo`dAKG)r}UfVss+mq1!c2P!c zQ6qkrtK@uAKbcj(DXx7^%--C0<2Bbr)#4;oy6)3@o_V^XuovG%+&v?z>+@X263}`A z{G@@E+Tg?8&g$U0pFXMR!dsdWP0`wKG|y{tZ+lylnTtQBtHC|E%O==p2kc#KY<`$; za|i^fLFIQlq8)z+UV>$(-^@VuI~(H8tx9+nualjrJ2;c{`uMu6nrGlP?e3TCIh4FF z_*|O#mBDH-O|2XPLE5E-*R8%Xsrz8Q>risnJb!X(1@xDHq86{o0z>v$Fdr{MjIr8+ zQQ5gVG`y?J`wTvg*8t*`L=8iXct>$}>=#uh^E>}yExRx_5q_%mJGwJ+v)LvFaFn zYJLOk32FM;qSor}=A061*9Gr|3hk=ks{WDvb%kkJ^YvJ-l^2qWml>Cu26=5S*B}j5 z*QMdLT|p-rz4jPS)}j{lZhG;hWLI;owOM`3902{=xGkym`7@I-EV<28f>+387^bNH zm1_CNQGx6AZk!;zO4YlDlLIg=aorxPVSQxqY)->TtuNJ^Zw=JD>b%?XgOjlX0jm#l z5R~$XMwrTuj44H2{p;->+qw;YD@Iuq`oX=SDc_rp$&&|G>dSqOSTz#E8jr*3to0Al zqSJ9|?!9j#Rr{wSeBPey`XqT@ZE4dKP_xdRCNvFao*S%>=fYPMxHq?`AIToPl62`w z&wJIL+vB^Mw9C{TwQz*#FU0D%RCl*0JfHsZfefTB$E!B`TEn+H48P~>yx=2f07|Nu zul}&zx!Tx08(7U47yPEIX?EF$gunqcr>)l5=EG}~^+URiNS@tex$yhV9<*fZpZC9nSRsAAXS1_~f*5jskCcjn3pif840{*DJJZYxM8WY#gNRIO#o|;J>9i*0a@}m8?g!eQi&S^`_q0Nx|=v z8tUpBE}_Z88yD~YSo=nBtkJ*PSe{W@ImK}%$-V*#t9nb!;k#$VIq!{i%qnz!omt}! z4xbrtYdpCH$FU93=a$#IOOHHKc4?0>*Vk`9I4I{B=Xz~$_0#J9e*T11p03Ap^gZ$& zlk+Q9s_hkaTgTdD*9WrS2}=(RyEt@8=tk7!bf1WHz8mel*MB}a7!~71$C>&spS3hM z+EZ_92x!kPKU06EA-D&_Fq~QRL09`!RjEkWY*|ra^-Q$6mqi~N3>gJ4;0SK0ZzwRo zmSHPfX%4K{HmP3}sg<#9^_W!S?ryL4+S*rQz(L%lPZQwWyHHwU&M;n>0!M3LrvLk< zyu(4hQh7#_d$-}-Y(vk;49^JNocgn`B(9$|Mc+`PU%sPT{o01Hnn(UJed^|fz_IGq z5!%iskJ*BdI@endeZcy4$|}AP*!hyVeT|`Mk6}!#NjD<+O#e=|w=>y0uijf%zvaSC zW8UD_kx|}piQXrZ)+GC^wwBL=QMtp55OqeBdPYPY-GjGRVbqf2Z*eCI@lto^z!>l2 zCb&7UvbL#i`?{Q^ldfFZGMi(Ozp*-Sds?1(Tj-VYTCA&`>*~K^U7rl5KAo1i$?YqT zZ0%FlUJ0Lk@DZD5V)BuxX?0b$?j!m09ojQf;7Cc~24<*TZToH#(8QqbRc<#x>_ka0if2cIxjzu40bVo4w>^t3hE+OOhe~4?I6WLTUblyue~Gs_cYUVaG?U^cA)w8!tyO?u<0fr=b=W$%gH5(? ztz(h72j}RI%{}tSB5-_o$f$kcn4-7^?;X1%Qa^6M-f+w(%=4&tbK>K0TPdb4yhUd# zp8i1^K{#IF==!|-wHM$oivFY79d`m#X%{49U0w5Z#vNc*8cK#;w88&hZP^vm*{BS~ zzo4jy^1rh(#dxgwNW#*aw?CTS+I|jmMc3!I-w(XI9}f&a`}R2RgK-xdaxmT}>g;q? z^+b3v+HttPqe#uk#T9BTsp|ygdA@xClwoht(nAGBU+gYDh@ONJ^Aq{b=ho&=+KYeD zFysAgY>w{q{^-4_m5<~(n@8y3G}aHUsfxlNTL*z@!9r3Ll2aP5aNo^NBCJ_~NK?r|qt4Nd)z z!%-JompQ)r67rWR!#y?5|8CB6==L>nU(Tw&Jlw^o?D?jpV=&y~xs3TMeg98$UmMrt zdG;MdrPbQ3waQwRw6&HxYtzDOmAi0t&$@4!?R@dFz{o9x4!?O?jZLJ1! zy`0B!{ND~-S*%6sxKJzc{01auL#3wfz7?NynUHztFG-rs9TjOjw z)H|5$>h43rWPn(HCnrP~AkzBHZ@ww7B#&&zwb%3VCU3adj+gQrY)A6@k+nny&xa-?;3s@W}Va=8Fs5MWlm>`>Odk&>4{! z>ju7(?P{{qCnG%c=}T<~%B+3C!^ z*)1z-PhvDb^3SJ^bA3cWuR-2@A(I>j-@z(Gn!Yi1+>AF6$>GDY@J&#ldA~*kVvf^pL1lk!JVtIsx--}l@;gcX@d5C2b%S~D&NlJE7j^>?YS6`gnPt4cIOci#f8fgVZ523c{%}0E7U4u4PpnvA#oJy}i_s;^^J&(>4CZo% zqum@Tth{MyDA)SRJp-Je-&+zRVWYp{K+rWinffxHJ1qqe#aU_JFy!F*4DH*D;V+E# zDOsMq6%ax4_-m7xTiMLcG}%pf4xJFr8T?lbjo#~9xlpUznZe%B2WniQtizG7%hnCN z+vb^L30DbML8BRb`wcB`SN4!c^C%#Bp0q}Hvm^fe(j;~=Z84Gxo65UuW!|Iw`|126 z2OBwISb?nse3qt5&n#To7PI!B%FkL0X{%(ORr2kaL6#!OsT0K_I)#H5tz{0F0nb^J ztI#yf63CM`9dfE>f9n2IqHLpzEmPU=hccFxgti&Zl~&hfdX`E?YDkiNJGSf9D@Xb? zezEWqKe-2Lt13SY?n%Y=oDb~Rj;%+yF3vA(PTj|A~ zB^I|YUyuv2i0ow&?H5A68hOfS}YGOlQ+&shNL7?rvUQG--7`-@)W6m#!ygs#vgI(;A;~$ z3s=O^pXi@h`TFS<*n~bZ+4OzC=?V0d?8P{6kn8;L+kdJUp8uQfG{zmbfE}C;4NsC{ zhPxkzf37edNVctjZ)uNZzqabb=HtSj25(3QSI;GrP= ze=hyP75qlod(12>AqNk?(-mfHx8`!&EkP`;BmI8`?p@%ZRCqXRq*t*q9mHwR_3Iyy z2gZgDtjHMIqGY>~;Nsn*w)oX5v!7~IVA>S`D6K{-Svo+c+?d`OA1{M@z zlXu9kkticCD-j8+8O+u^ORfM5m2NDylUdFrTl%YfCk=tcB26$+`!FOw+^>J-&d{+B zyWB4;YaH;Z@L%eJe~>xyWV-k|KWkO_$c697^LJ_-<=Qx9=x*2fa3m;=?zUob?|TZ8 zp~w3sGTjZ(5TT*w`{XY8!C5@Zb<^9q#@Hh+>Y=+{hjd0_BEh?Q-ynkYp#e4&zGh#UX#GF*gw#WS#w~r;cPb4 zF0}LWv;J}z9(HKj#KMDO?UmX}F4^3ixz4&$<*Q=7*&W8y_3@BvxK`+67-~ZC$R%o?(jnLkTj{7BsP@9HkWH^+w#y0MMk!zcU5x(D+7Qb=I*>(Tp8+4 z*ez1KRE6;ox1ZIi)Mcu!@5|&nfYg>GD=kIR=mBHzTw8Fp`iKjNlOvuP%tm5X;X{{QuoNycg6%iN9uaWh)$~M;)#WDjb#$XJ3rTA0$3&U-ixr~s_9*(b*glk-Q z_lI}ULPUJ1Fv)uxCH|+N>CJ1Mo8(&p8Y88*F}?c@MPv$;VWCMUf;#vYQ}+9{ zewAa2qIWs*a3q_1OX0hV-XZH{=gB5+kV6vSKt5%Yn$J)_Pp}Zifk9FdzG1);g8aut zTzG)1cQW5+uT`%-Lk0y;DeMm#;0j6QvhZ66EBhW`ddnHTCE$}gPRYZ|UYV=!D72<( z;Om@j7n1uh?zMIbKgJB zXCl&WWw3R6#~jPxG+@!pWRVPw&0dNIC=a{;8v-cxbF|_rgi1qii?51o{8V?1k&d{F z-Kgs9fi_TwU3+BVdE{ls&6&xQpB))eMh<7fn=O(B!nWcEAhbS8H4nq)WC7MuCHZnUL!=5vYC2;OFJ*1o-7_{DAEM&uVH^(K86Pc~KEPeG9_HMD4X~k^7QlMRiFtvX{Q>^`&dN z7ulj(DbPzhsEKg1rQaJ2%`0G1XCV z@F(PT1>&mW8zrHoGVdbURtxKGUBGmY_1p5qO1@rs-XIKfW8Ca>u8@t))50xj^k*V@ zc(S}D&ccP=&6D`>UU^Rt&$K)&Fjm zUW%IT41)VhWN)SMANW~?*}fX_m18|z z_w&Q^kZz*69%quU{+=?#Z_Osvz;T~Q8_s0^?QbaBRGr5mo&oLV?@+I|J!P!>~n0*~C+D+_*?~j)Jc(zeL)?UQ9 zw1G-PWH!MX_vo3c`f-Rz>*^0|Lk1@OXJ*$`uHErzg6}sn_afPc>A1dQ?Qs@YA%8^| zcoLdR#YSsTkg|3q*xIWLnfFzYf%QHQbSKbxOw$nec@XKPj7Jr2Q}Y2Su77>+#$1G7v3s%%!F@~ z|JoXy{y%#K7R`c){Iv&+Ltf2|4DBSszl=ZQgvNz*2Q!4`$%ilY^*t*6A{`~VyAWv_ zIQ7(v%s2ZjyK;K1 z#76%XG?r(lmb0y9!mwS%N&q^`X`kn?S*a4sZttD5lbl8hzL$!Px zJ};1Ob9UM`OC#Hj-PN2c97F22k>74S^Prv?Zgan>^z0LnQ^3@{qGlI_FJkuDJ}6omGwX&wAlf1+O$ zXohmbHax?&Pa6EnXxDk7(-kEG1dBE7H(niJWEYk20cLO2g*H3>FTF}_~ck_&dKe^9JFUm)gH5}@}HXm1i1zlFkRW~7DaFe2QIYqQAWT- zA;oyt<75YtkXWOcmGyyNt5Z2h^IYi@o*?#fQ`3a(ssexP4{vfwzPp=AzO}f*s|y&s#Iv_$vI%6_}G+vOR4&_FmY?Q~g!F2bNK6 zUlEzFXb7Xrl)Wzz%&y)BcEm}$_CY^^w@i^7^8*Usi`>Oy=4&~CLetWUO*J{@XO+>H zTo+l=2gJIox#2^avuW%jv`(i%JV!#ib{8Regz-%>MdP%07^1Tj6V4Do4cf?Ozfr9Z zV8)rq^b|9Q!#ykg48j{5NGn6#>oq!%61kSq28J9RMPO(XM%-y4cN^>qCQrY4szT^h zG=iLB9y5m*44cpL(Gp4wJB=Ji0r%H;DZeRHoQ-d zBy!I7Fr=zBw3}H_Zk_T#Ed=V8v8xX(P(`NGQXa=%Npmk$1fCaZIy1Dh6z+wFIz4l_ z&3)MAGYgr*_r~b6v;*^l>-3FR4%LfH4awE9&X&Wydd63tf21Eo7d9u>XlnG#{g9`7 zF*dHIm&78JT56YAh#^_e!Vg8+MG-eNtZ4ci8|NETOor%iE)=Kal~sgZ?a!S`hbdbuD{J#QF>wrSdv*qQ0ojy1HCAskRn*UvHal#F7A1Pma6 z`;;ti9q_2gf7v`MZF{l0`LkUr=u+)ydOkAnpDJRIMBpsXCs4j^AeAdI2Cf;MT=&_{ zzLb%V#eELhjb#WL<$otKgK6Dr1U|raZ!5R8;5#tNp=XVYkI%SWb4i~wTu^6?-c&wm zuwJctXWOZ4m*X`tI~HRIpegoYN#v5GmNX}S`?%m&(ykjSE{*ex(A@~*T|?g}>*AsK z^+Y5Bd@Ps5=FJ%eNSg+7msY`14FdayVrX1S??}}Z8@4ykwOya24z?b?z{xxip!o}{ z+tkYsd;gT^K{HhYZk~+Wvn2dl8Je@wQM9I2eq%C!!yx3+$^ZKYRv!p70%kT3O_Uo9 zs1iUM_~q)QTJjnhqQI!7LQv`T8r|DFTqSZgn#X@34gJ;(57qlgI19Vd2p_do9Q;MZ z4owMZt2$ip0JQ`E3;*GEdpO3vOxEQH9CiiPQ?@k~5!#N^S^O0Kug2g5wtg)q-XJdE z{804WGzJ`&AoeSh1}KI6IRYBTsQ)_Ia3*J&`LA=_5y{X)nuuOATeQD16B$I_R!g7> zuuNN^78p_ZWK1?`b5ynqTeU-Vk-MdI0Sw^J~ z{=yp#DZBOgm3n8DIT{CF1|T~Cdqd-d+GU0o!oViyv7c7-?!}B~2g$JG{C^x={O0ccn(JxIeL8i|(SI7PD`-Ji z4n3;Gq2ViEXW@d_XK;ucJkD>Fgzn6WPU7y^pa@(wbj8|2sSDo?vOlLlWgNP?FHct^ zTYB)AdAAh&YqwDnedI(HF-bSgFT=*f{0y^@W8N9k-b&N_R6bHGKeyU{DD7T<2p#&P zvVlEd37B$Kw)ww4*uP-U#U4Vz{=^mv%Qo!T zfi2kihT;2xVru56TS78u<3&xqyNg$_lO!WcCC%CJbmm%U%ajhS0AE}tI}``>G4ina zR*89oU&HxlK1`nvWFkdwBAB~+qKL>x>YR`nd&^j#zr%YZ&Z9)P;F)HEie(BHJt}8g zh%zhIO8Ty@s>))cxaB<9&3iqdq`sL9oqS`UF95X*%Sg2Q5sWgeBeI_b< zN;#1dDfl`C5K6nBvM8hHKX8j4hQd4#7>7p-lt1)|H8kBEY_hyhj&soQtf(Uwo0>XG zbWMAtD75 z(pvDaF)~5g5YO@%UYW$bvBMS1IjaLjTzF-q>+^Ky=+v4D-Kh&utM37ce3AXRIXJ zXgHe}IxprQqz&(n%3Pl@vYTkY@{iD#q7RGyj>`k97)J)XSEVg3?$qeD0sti9PWUer+-Pn*a^qi(6Q9Hrlo^69J zk9-gpB*>CEoN3<11;TRdcot?Mh!%35G}+(PJR)^x20Mk|U&Gw>9rQBbbR+|+u3%`@ zkE@wN9$8G(P93gt_;A%_<@s|)1%MRZyp1}psdhe&?4(fy7t*WVzz4d(Z)71K77zVx zVEkcTr1GtT-VBhBH*>Ir7bjiT%}Y8v`Aq{glIY^Bk0_R6ggGJ5-;lbi`S@HWlFb~* zgbR-10e=b$fk%KqT9iiX>qF$lQ9`Qq6wwbJv&=8DFQo(xyotIjFmrHUoFZi|!^`)DJUlu2*?maP=tFGYnze$Gg^@olyucPC|Mp?V zuYG(fJCQ5`6<0e^-k1 z1w%8@d9spbU3kcf=(3UHbXw-ZMe>WKSgYXM^!NI68)^TS^{2^lZqTLO)d2Se z{O3fDBm$OL9Ru`7sCR`Q5)VN%@Vo}U!$YjMjcq7O5PnbaZT27OP7%R>NT^p2K^1s^ zkQ`02=qdRgdNf7IGExe6lervFgXVrR@}4b(s{`Pzx2dDR`U-3i{;IF5ng}H4LahGT zHcb67sMJ5wJB1Hp_Z39G0=u1JrLJL2WH~!ZUozM)EBa64Yep7keMFw(Zi1ci043KE zgkdYuAAlwcCofztEQr1ub*tEh{_6T;;FLncruIT}yeR4Lvn|J}`*QF+4xdW-3u#ek z|I3YZa!t^5n+HlQJCxBrrSquFtAFYJmIh74_` zzROnV>LO$_QB1a}Ilnpy-~zLc9?@NlPV<)+O_xedvlaek?n0R9CyNZ3s`9=#=2jwA zMm)WTsxqNj8V?MC1&CmTVxN$6Fu{ile}qE)aJ+T6_T{`pmG2t9O@p0&1pTK1i|md< z>=dduh8~bPTje{9!naD~F8Qj}&;5O3Dvw@I$0`xU`^ebT6YvF=!?m{pxiAJo*2MaF z@QD+^1Ii7Q^O|PvJI0%=!MjF>ass?jNIM;yarpm-1C)EWv>x6Nb_w49Xpsn%S@c1w z-!eug7eXTTras|Y@C^Bv-LQK`ad1omN7UPC#*K^*x{ znXANt>>e@>O1u$QlQn$P=$l{+ynvOtGs?MoY|36MVgTCh@TDu@>HlSJl16T&^QDO?`{RPQ#}Jobme`wZ-s8CHkz^tS^@lXn=N3%y6DG(Nu@}|D{==pDq;wOo+j^^I$Arz=hD0X?aXw0WMco_%tVw` zw5@gt5eY;A-E;rv%mfZX>YOO|pW?s8=xy^H;`XvSO7r2VyK4iP?I;9K=J5b!*a3e) zC^Q-Fa5cM-aPXV_;8VMSp~_r!5@FEg6{l;T{U1Wv(8`LnhZ|CQo}(QK6%o#ZzzHdu zwYXupUsmML_Jl0XD*l=>IF;gdl!YUMhFWMnTr<*gx7B)kbQ&#J?7yv7l^n#_A1AG7 zio^C!-z#9{p}dbyclp-1&Jy?@$t2-Q(}Na&vZa|Q>D?>k?nQ73S1*iCjhC*lKPA@w zMB-m;fGhYy*%HRamN?EA0%lqeI`*J~5* zK4ZkHJP!k{zsY;d{D6F9p1eU3#iG-x%AufU#K}J7@;@iR0yB&w9^i7@gd3>H$P`C{Ma2L37)`iSVXi=`qpoczY; z5X^d9o_7_)do_`4_R$|YwzZbgVq&{7BS0ks#C2sAK1}Ze0>}3Ad%57stl%4peW6Yj zS8alYubyhkmP-5{k>*08W;*Aa!L_W6e*(E?DUn<%N>BuWNf%ZFd2)Xx9r**+?PAK^ zb#CJ8rG(S`r2kopA4(z{UCxQX-}&-?1@Ni&H_VW2uY0{n4v@BcW0rTR7^r&ha>@;# z2)aCa`=yvfR(`=41+q=Pm_T0F-4z_d0qYVXKaoXCEFTig%2^`+8@x$H-J@DvBC}9H z#Ch%qp@s;$QehmWJt0IThX#gkt|V1>2zAOloq#DVVKWp}dUEF-e}m810uiOc$z0nn zj(ik?O`^$s9t4%VTL7;h3^ikU|Y;1e7Mn*2lbpOT38(?N49qjQ5VHLc~@-tioET9CMvd< zuohuXCHHsN6w>b=HU@7RYmh@1_)@H4nLr|QAXMF+Y?=Y9e4=GIj=k$l8%+3^!+fvM zS&~RPV*=3WO}2G|IZV=)l=mcCTyYf~0o;ACqLrm|ec!G=B9GL{?Z;te%KseO4Vhp( zyO{;g7+zx;cxH{ccPgSAerTg~;4AWp)x}arJxdU|?9@2n8&i%5Dj0#}MD7<3W(o|Jl+wNJei0)W{C_j-flrAig44GZnXcUp0Jb99>IcDlu z;(JXpd`W|7iv$r>tPs5!d9r&Qa(>w;W({$avEDEIMYq;s1(WTVhQJdX{DzvwJ!K+w z5R3ihSZ*!x(9`R?25kybgjRBmiJGA_?=*|Q7}%2ek8NeyR2}SbPu`P&of3FPI zQ`(9=J;?5ZTy0u)OAZ~~hy&vHc{wGwAK7yyH)-EY*a-rDi5vi@cA%^X%1I1c2T z=!MyaFqmUY_)Q8J7J3Po!<45w?k8|@jC&d!^QQJ>^9*{jWLi4geac+oDKFIF3idP8TilJhU zb<)UBWrKTUo6{j;7>B@Meh~bpDoRPSj71<(@#7w`W;lM`dE5F_hj{s3{AexzQguWI%MrQ7p*6d7r^whroTS z(39@o=<;Qmd{x!SlzUM^`{-Q;?T3W21{_tUJt-mINc;h?pT7$4N-evOpQ zGbnUu*CQX5W6uxrI+Z<8>II$Z^H?54cw#D%OJm(3jVD7h#U-q9L2=-0w%Lav7zj_J zJS7#D5=A0$5ci(teZm#1Fnwj)NjwC8)}`_hGRbr;$MmQoJc&Cu7n{#uH*^fxgl6?w zhom9IoQcEMLqPcXp#h;oHRxFzqP22&0@Q&A<=DuJbV$cs1nVP_ zv#T6}?0f?h=Lz1w6yuQ`nM^#At00H~Tq!T5GV_(3BbA5Ebp@u`=0$5N_DS(Lbx~U~ zIqeF~@b~Uh`kv98Ez>+ej!pLEtnieqeR_4H9(w==9YkS|aKYdr2AFnfXJ-j$$Z8oE zSk2X-7pn`ILsg!kvedmsOyGSxkUhGPs1HNve^*fWU5{R?7x4g#u#bU?>WLyBkwDs1 z0({AEHt(aa+9Fx^O#X;C%&sLf5vaS({IDWCh4M27bVyyq+eUz_fLk0CF*ltoD8Bvr zF8V}AT>Khypvkg3qp-QHrwU6-sSF7Yx+ly8)+?3?U4MN18Fkdph$CG%4~)T}65CR5 z9D)PVMG!Ocg5~c)1H+neeD2(nm<~HyRAm|{t)AWgT620>kRrY%^ws0~B?$$!r>We$ znCUJf;#%m0BRR63FsCnvtg&lU#q1oc;=fK0j-{t09h+p4447TzPNgAs-e20>RB@knv4K9|$VyxSy#&Hqa z-e_`Gbbi*33z)e(%qp?C^WLvLLlw<)x#855-o@$No!pIqO7abNqPx!W zoz5!7i!|s5)qhe0W__AbK$$IszI4xip+y;f)HRajYMujjJ+B|Tu&ADgzE$L*b9rp8 zT4H9h)Du9J)oXymlNQO;Uua4+c=sFX5Z8e`2kgOFujJpv>>}=Km4)&u9FW5Ui(5rY z@zL5vS*}U850sG>*PF;xfhsmYsubLdbIc?CuO!UyykZ!cjo~1AMjGvNL3^dsUMV89 z15wpn!~{o$1uT(Z63KFrO6^p2HFGzQ^R+wr|Qxq?JCSIk%QvkU&Kh3bj&Bh zwLGE>g8R{@U2%==> z8ge5LX_22u{V7V}8y8~7AZQPB;Tf_UtK?gxM2PqE%-BP;{$)kLXc+PRdWk}o z9P5)5{qq+*o6_yG^ellU9PRY7lp9EaTAZ#KLxocrOlO<#&uQ{4$0FKSb2{D$j-{A3 zWvB?*$Ew(da%U`Jx5#O@mU4NtGn`@LR|bgbh4HnqI}E{-5(j2ZDV4iYsU6&cO}^)` z7>m*eF7KcV2S`o)NpunH2rPf3YUgz#8Q&of`CuFtJI!!?E|S~BfohIlP0vpi!}|Td zM|3=yhRC5biwo7tFXUJT`*TSjxX|Eviw;TWtl=@EHBO9$99}|sQ3L`@rnrj)z9x28 znql!C);jnBzV*DTh)$yBzY=+9nan@Y^3i1M$^7K+Ahs@8p>wh%r()r=DUdSaBBSm2 z*{HQu-gmOYMKq(N;rb1!y; z^zoF3;FmdEE?aAh7ePag2;IOlkwHN9CtSn;wcHjt zWZRG3UJN~_xWHw_r{8UQo#Swr}K#MVT% z{&wQk-D#O%(C^xSbY?PH zyDPvYuQBseWp)Vj$=pREeh`s3tO+z|?&9!MW5;j)sD7JDYxVb#zVQMkiBG3~HA_0e z?v~cHn>v#rG>yFV-H9Ra zniR1mEZy|SMiPWTyvx%z5EF(Z7;*L9se^rq(@dn~eZMKv4|HLN!au(QNeI8X^Q&1& zo$Kc57|caT;ETNR!rTSTjmyeLj~#eVw_2j5mRT?S<+J)Ggd2LEQAY2k0&vJ79o?HJ zVo9RSyV|xdNqsb_ZzCl(B%v0`h7 zo5<|puQJP_0LLC^)Dtdzh-;}tkTMXO`BYOSf$Xr)55^QF_%gq?5?ia~(N_73pKtzS<8YGg4AmFkR((sn zU962ZmNx)R&tI-ArVPsZ?!8FQgu*kBOST-|Dyd%JLh(fO@`|P~I_v0814iziL2Ud$ z{uD9RL49e{#mR?0CjMYrP@xnnLuke240aOz?B7lRe;~g|h|6Vf*Sv+OiJ2^X9F@ph zsQ^)_*ItVMZO-%L;t|I$#R<(YOK#F@vFVK*cjRm&(JJ{wkhR8iw>pj2GIN-i-gO zny`iWeiTmcPtwM!Y)stJP0!3Hp8DP7_vs}vUCIZLZ-02?yRxV8lFWgJ}VaL^`5D`1> zWf}@FiCLm4OO@~*ho7(@4(mLYebhH6acA4jwaH#GA&E$u25CdSdv)p7XXaCtdEubB zlt^2bz5P(;S0^iLsQYd$RynJ*?d4v)Y*DM9X;XRcl!SK)El|so2dwfY(&wW2 zB}oObJ9pmA@8CjhxdEjZo1xsu-(=Q$E5 z{|J<=(M!-oxC}M--Buh4Y2V`Ap$&jEA42apz@BG>8z|zyk^}6umkusygfnf)+RMvD zO3$yAXD(fO$O0VuJo`2AI*|#~^kl=%8BSudq#~$)P#OdbH?!HBiIrHO2sd!#KglYD z%Zig0OT$FkWRlnt;!ONT8F|Kxr~i;HcwXE^5(g{wXIf(JW7*3&rtYRw3EvAh6ww(p zXiY*!DykAoaD`=>s~POv=q0LZxxB7%qAv(zh3RUhX;xH1)n=hkq3KDZufS-{G&i>s zkIeE>b~W^WtoCdSQnb5>%nW0Go54kzxpNlyr5&d#AZDy&Z_4Wl_vftu{+#s@v5(LP zAYK741iE3v{Ayk&fj9u$sA; zeIg>c2Y^4;dsb=*$)oDbPn1GwaCQ#*@?qPG;TVhl{St(GMRN#!OGhUcK)~^Kg4pEi z@h;O^LZ8rYv8fj`bWyBun+v(aUBmTRljA3#zljNq-q5aG{Et z@S!w>I~0*QzF2wCfxlpxpueXiW)2%YfS3Xj_p~f$T+Pp4BDp{-3OnCLYSr>+c_+jK z2diB&(nyf(xJ$Y>u|j6_w;`sR+PCg|IPgG~ssjPS9T2iyC;VE4ewHcNlcFJq=86n+Y0Cv!c{QgSSE zS@gl`yvvrxDpP+*gq+8r3HnPD#!Q*gk=wAc1Hru}P%%%Cyj=Jq-!89djY$}zH@3#i z8_(TxA{U%~;G_ai&YAT&^2G pW_pa?IDU-q|IvSSPL157%G|k4-R9jW!haS&|I)8c{POpk{x5c}j5+`S diff --git a/care/facility/tests/test_pdf_generation.py b/care/facility/tests/test_pdf_generation.py index c0c7d2fca6..f049185aeb 100644 --- a/care/facility/tests/test_pdf_generation.py +++ b/care/facility/tests/test_pdf_generation.py @@ -143,7 +143,6 @@ def setUpTestData(cls) -> None: suggestion="A", ) cls.create_patient_sample(cls.patient, cls.consultation, cls.facility, cls.user) - cls.create_policy(patient=cls.patient, user=cls.user) cls.create_encounter_symptom(cls.consultation, cls.user) cls.patient_investigation_group = cls.create_patient_investigation_group() cls.patient_investigation = cls.create_patient_investigation( diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index 7dc6f0f0d7..b31ca9762d 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -31,7 +31,6 @@ ConditionVerificationStatus, ) from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id -from care.hcx.models.policy import Policy logger = logging.getLogger(__name__) @@ -117,7 +116,6 @@ def get_discharge_summary_data(consultation: PatientConsultation): samples = PatientSample.objects.filter( patient=consultation.patient, consultation=consultation ) - hcx = Policy.objects.filter(patient=consultation.patient) symptoms = EncounterSymptom.objects.filter( consultation=consultation, onset_date__lt=consultation.encounter_date ).exclude(clinical_impression_status=ClinicalImpressionStatus.ENTERED_IN_ERROR) @@ -181,7 +179,6 @@ def get_discharge_summary_data(consultation: PatientConsultation): return { "patient": consultation.patient, "samples": samples, - "hcx": hcx, "symptoms": symptoms, "admitted_to": admitted_to, "admission_duration": admission_duration, diff --git a/care/hcx/__init__.py b/care/hcx/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/api/serializers/claim.py b/care/hcx/api/serializers/claim.py deleted file mode 100644 index c6466179d2..0000000000 --- a/care/hcx/api/serializers/claim.py +++ /dev/null @@ -1,98 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError -from rest_framework.serializers import ( - CharField, - FloatField, - JSONField, - ModelSerializer, - UUIDField, -) - -from care.facility.api.serializers.patient_consultation import ( - PatientConsultationSerializer, -) -from care.facility.models.patient_consultation import PatientConsultation -from care.hcx.api.serializers.policy import PolicySerializer -from care.hcx.models.base import ( - CLAIM_TYPE_CHOICES, - OUTCOME_CHOICES, - PRIORITY_CHOICES, - STATUS_CHOICES, - USE_CHOICES, -) -from care.hcx.models.claim import Claim -from care.hcx.models.json_schema.claim import ITEMS -from care.hcx.models.policy import Policy -from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.models.validators import JSONFieldSchemaValidator -from config.serializers import ChoiceField - -TIMESTAMP_FIELDS = ( - "created_date", - "modified_date", -) - - -class ClaimSerializer(ModelSerializer): - id = UUIDField(source="external_id", read_only=True) - - consultation = UUIDField(write_only=True, required=True) - consultation_object = PatientConsultationSerializer( - source="consultation", read_only=True - ) - - policy = UUIDField(write_only=True, required=True) - policy_object = PolicySerializer(source="policy", read_only=True) - - items = JSONField(required=False, validators=[JSONFieldSchemaValidator(ITEMS)]) - total_claim_amount = FloatField(required=False) - total_amount_approved = FloatField(required=False) - - use = ChoiceField(choices=USE_CHOICES, default="claim") - status = ChoiceField(choices=STATUS_CHOICES, default="active") - priority = ChoiceField(choices=PRIORITY_CHOICES, default="normal") - type = ChoiceField(choices=CLAIM_TYPE_CHOICES, default="institutional") - - outcome = ChoiceField(choices=OUTCOME_CHOICES, read_only=True) - error_text = CharField(read_only=True) - - created_by = UserBaseMinimumSerializer(read_only=True) - last_modified_by = UserBaseMinimumSerializer(read_only=True) - - class Meta: - model = Claim - exclude = ("deleted", "external_id") - read_only_fields = TIMESTAMP_FIELDS - - def validate(self, attrs): - if "consultation" in attrs and "policy" in attrs: - consultation = get_object_or_404( - PatientConsultation.objects.filter(external_id=attrs["consultation"]) - ) - policy = get_object_or_404( - Policy.objects.filter(external_id=attrs["policy"]) - ) - attrs["consultation"] = consultation - attrs["policy"] = policy - else: - raise ValidationError( - {"consultation": "Field is Required", "policy": "Field is Required"} - ) - - if "total_claim_amount" not in attrs and "items" in attrs: - total_claim_amount = 0.0 - for item in attrs["items"]: - total_claim_amount += item["price"] - - attrs["total_claim_amount"] = total_claim_amount - - return super().validate(attrs) - - def create(self, validated_data): - validated_data["created_by"] = self.context["request"].user - validated_data["last_modified_by"] = self.context["request"].user - return super().create(validated_data) - - def update(self, instance, validated_data): - instance.last_modified_by = self.context["request"].user - return super().update(instance, validated_data) diff --git a/care/hcx/api/serializers/communication.py b/care/hcx/api/serializers/communication.py deleted file mode 100644 index 969654c146..0000000000 --- a/care/hcx/api/serializers/communication.py +++ /dev/null @@ -1,41 +0,0 @@ -from rest_framework.serializers import CharField, JSONField, ModelSerializer, UUIDField - -from care.hcx.api.serializers.claim import ClaimSerializer -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.serializer.external_id_field import ExternalIdSerializerField - -TIMESTAMP_FIELDS = ( - "created_date", - "modified_date", -) - - -class CommunicationSerializer(ModelSerializer): - id = UUIDField(source="external_id", read_only=True) - - claim = ExternalIdSerializerField( - queryset=Claim.objects.all(), write_only=True, required=True - ) - claim_object = ClaimSerializer(source="claim", read_only=True) - - identifier = CharField(required=False) - content = JSONField(required=False) - - created_by = UserBaseMinimumSerializer(read_only=True) - last_modified_by = UserBaseMinimumSerializer(read_only=True) - - class Meta: - model = Communication - exclude = ("deleted", "external_id") - read_only_fields = TIMESTAMP_FIELDS - - def create(self, validated_data): - validated_data["created_by"] = self.context["request"].user - validated_data["last_modified_by"] = self.context["request"].user - return super().create(validated_data) - - def update(self, instance, validated_data): - instance.last_modified_by = self.context["request"].user - return super().update(instance, validated_data) diff --git a/care/hcx/api/serializers/gateway.py b/care/hcx/api/serializers/gateway.py deleted file mode 100644 index 8b36efe03b..0000000000 --- a/care/hcx/api/serializers/gateway.py +++ /dev/null @@ -1,38 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError -from rest_framework.serializers import Serializer, UUIDField - -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.hcx.models.policy import Policy -from care.utils.serializer.external_id_field import ExternalIdSerializerField - - -class CheckEligibilitySerializer(Serializer): - policy = UUIDField(required=True) - - def validate(self, attrs): - if "policy" in attrs: - get_object_or_404(Policy.objects.filter(external_id=attrs["policy"])) - else: - raise ValidationError({"policy": "Field is Required"}) - - return super().validate(attrs) - - -class MakeClaimSerializer(Serializer): - claim = UUIDField(required=True) - - def validate(self, attrs): - if "claim" in attrs: - get_object_or_404(Claim.objects.filter(external_id=attrs["claim"])) - else: - raise ValidationError({"claim": "Field is Required"}) - - return super().validate(attrs) - - -class SendCommunicationSerializer(Serializer): - communication = ExternalIdSerializerField( - queryset=Communication.objects.all(), required=True - ) diff --git a/care/hcx/api/serializers/policy.py b/care/hcx/api/serializers/policy.py deleted file mode 100644 index 9175399edd..0000000000 --- a/care/hcx/api/serializers/policy.py +++ /dev/null @@ -1,67 +0,0 @@ -from django.shortcuts import get_object_or_404 -from rest_framework.exceptions import ValidationError -from rest_framework.serializers import CharField, ModelSerializer, UUIDField - -from care.facility.api.serializers.patient import PatientDetailSerializer -from care.facility.models.patient import PatientRegistration -from care.hcx.models.policy import ( - OUTCOME_CHOICES, - PRIORITY_CHOICES, - PURPOSE_CHOICES, - STATUS_CHOICES, - Policy, -) -from care.users.api.serializers.user import UserBaseMinimumSerializer -from config.serializers import ChoiceField - -TIMESTAMP_FIELDS = ( - "created_date", - "modified_date", -) - - -class PolicySerializer(ModelSerializer): - id = UUIDField(source="external_id", read_only=True) - - patient = UUIDField(write_only=True, required=True) - patient_object = PatientDetailSerializer(source="patient", read_only=True) - - subscriber_id = CharField() - policy_id = CharField() - - insurer_id = CharField(required=False) - insurer_name = CharField(required=False) - - status = ChoiceField(choices=STATUS_CHOICES, default="active") - priority = ChoiceField(choices=PRIORITY_CHOICES, default="normal") - purpose = ChoiceField(choices=PURPOSE_CHOICES, default="benefits") - - outcome = ChoiceField(choices=OUTCOME_CHOICES, read_only=True) - error_text = CharField(read_only=True) - - created_by = UserBaseMinimumSerializer(read_only=True) - last_modified_by = UserBaseMinimumSerializer(read_only=True) - - class Meta: - model = Policy - exclude = ("deleted", "external_id") - read_only_fields = TIMESTAMP_FIELDS - - def validate(self, attrs): - if "patient" in attrs: - patient = get_object_or_404( - PatientRegistration.objects.filter(external_id=attrs["patient"]) - ) - attrs["patient"] = patient - else: - raise ValidationError({"patient": "Field is Required"}) - return super().validate(attrs) - - def create(self, validated_data): - validated_data["created_by"] = self.context["request"].user - validated_data["last_modified_by"] = self.context["request"].user - return super().create(validated_data) - - def update(self, instance, validated_data): - instance.last_modified_by = self.context["request"].user - return super().update(instance, validated_data) diff --git a/care/hcx/api/viewsets/claim.py b/care/hcx/api/viewsets/claim.py deleted file mode 100644 index fe81b615c3..0000000000 --- a/care/hcx/api/viewsets/claim.py +++ /dev/null @@ -1,49 +0,0 @@ -from django_filters import rest_framework as filters -from rest_framework import filters as drf_filters -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import GenericViewSet - -from care.hcx.api.serializers.claim import ClaimSerializer -from care.hcx.models.base import USE_CHOICES -from care.hcx.models.claim import Claim - - -class PolicyFilter(filters.FilterSet): - consultation = filters.UUIDFilter(field_name="consultation__external_id") - policy = filters.UUIDFilter(field_name="policy__external_id") - use = filters.ChoiceFilter(field_name="use", choices=USE_CHOICES) - - -class ClaimViewSet( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - GenericViewSet, -): - queryset = Claim.objects.all().select_related( - "policy", "created_by", "last_modified_by" - ) - permission_classes = (IsAuthenticated,) - serializer_class = ClaimSerializer - lookup_field = "external_id" - search_fields = ["consultation", "policy"] - filter_backends = ( - filters.DjangoFilterBackend, - drf_filters.SearchFilter, - drf_filters.OrderingFilter, - ) - filterset_class = PolicyFilter - ordering_fields = [ - "id", - "created_date", - "modified_date", - ] diff --git a/care/hcx/api/viewsets/communication.py b/care/hcx/api/viewsets/communication.py deleted file mode 100644 index 455460794b..0000000000 --- a/care/hcx/api/viewsets/communication.py +++ /dev/null @@ -1,46 +0,0 @@ -from django_filters import rest_framework as filters -from rest_framework import filters as drf_filters -from rest_framework.mixins import ( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import GenericViewSet - -from care.hcx.api.serializers.communication import CommunicationSerializer -from care.hcx.models.communication import Communication -from care.utils.queryset.communications import get_communications - - -class CommunicationFilter(filters.FilterSet): - claim = filters.UUIDFilter(field_name="claim__external_id") - - -class CommunicationViewSet( - CreateModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - GenericViewSet, -): - queryset = Communication.objects.all() - permission_classes = (IsAuthenticated,) - serializer_class = CommunicationSerializer - lookup_field = "external_id" - search_fields = ["claim"] - filter_backends = ( - filters.DjangoFilterBackend, - drf_filters.SearchFilter, - drf_filters.OrderingFilter, - ) - filterset_class = CommunicationFilter - ordering_fields = [ - "id", - "created_date", - "modified_date", - ] - - def get_queryset(self): - return get_communications(self.request.user) diff --git a/care/hcx/api/viewsets/gateway.py b/care/hcx/api/viewsets/gateway.py deleted file mode 100644 index be621f51bf..0000000000 --- a/care/hcx/api/viewsets/gateway.py +++ /dev/null @@ -1,368 +0,0 @@ -import json -from datetime import datetime -from uuid import uuid4 as uuid - -from django.db.models import Q -from drf_spectacular.utils import extend_schema -from redis_om import FindQuery -from rest_framework import status -from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated -from rest_framework.response import Response -from rest_framework.viewsets import GenericViewSet - -from care.facility.models.file_upload import FileUpload -from care.facility.models.icd11_diagnosis import ConditionVerificationStatus -from care.facility.models.patient_consultation import PatientConsultation -from care.facility.static_data.icd11 import get_icd11_diagnosis_object_by_id -from care.facility.utils.reports.discharge_summary import ( - generate_discharge_report_signed_url, -) -from care.hcx.api.serializers.claim import ClaimSerializer -from care.hcx.api.serializers.communication import CommunicationSerializer -from care.hcx.api.serializers.gateway import ( - CheckEligibilitySerializer, - MakeClaimSerializer, - SendCommunicationSerializer, -) -from care.hcx.api.serializers.policy import PolicySerializer -from care.hcx.models.base import ( - REVERSE_CLAIM_TYPE_CHOICES, - REVERSE_PRIORITY_CHOICES, - REVERSE_PURPOSE_CHOICES, - REVERSE_STATUS_CHOICES, - REVERSE_USE_CHOICES, -) -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.hcx.models.policy import Policy -from care.hcx.static_data.pmjy_packages import PMJYPackage -from care.hcx.utils.fhir import Fhir -from care.hcx.utils.hcx import Hcx -from care.hcx.utils.hcx.operations import HcxOperations -from care.utils.queryset.communications import get_communications -from care.utils.static_data.helpers import query_builder - - -class HcxGatewayViewSet(GenericViewSet): - queryset = Policy.objects.all() - permission_classes = (IsAuthenticated,) - - @extend_schema(tags=["hcx"], request=CheckEligibilitySerializer()) - @action(detail=False, methods=["post"]) - def check_eligibility(self, request): - data = request.data - - serializer = CheckEligibilitySerializer(data=data) - serializer.is_valid(raise_exception=True) - - policy = PolicySerializer(self.queryset.get(external_id=data["policy"])).data - - eligibility_check_fhir_bundle = ( - Fhir().create_coverage_eligibility_request_bundle( - policy["id"], - policy["policy_id"], - policy["patient_object"]["facility_object"]["id"], - policy["patient_object"]["facility_object"]["name"], - "IN000018", - "GICOFINDIA", - "GICOFINDIA", - "GICOFINDIA", - policy["last_modified_by"]["username"], - policy["last_modified_by"]["username"], - "223366009", - "7894561232", - policy["patient_object"]["id"], - policy["patient_object"]["name"], - ( - "male" - if policy["patient_object"]["gender"] == 1 - else ( - "female" if policy["patient_object"]["gender"] == 2 else "other" - ) - ), - policy["subscriber_id"], - policy["policy_id"], - policy["id"], - policy["id"], - policy["id"], - policy["patient_object"]["phone_number"], - REVERSE_STATUS_CHOICES[policy["status"]], - REVERSE_PRIORITY_CHOICES[policy["priority"]], - REVERSE_PURPOSE_CHOICES[policy["purpose"]], - ) - ) - - # if not Fhir().validate_fhir_remote(eligibility_check_fhir_bundle.json())[ - # "valid" - # ]: - # return Response( - # {"message": "Invalid FHIR object"}, status=status.HTTP_400_BAD_REQUEST - # ) - - response = Hcx().generateOutgoingHcxCall( - fhirPayload=json.loads(eligibility_check_fhir_bundle.json()), - operation=HcxOperations.COVERAGE_ELIGIBILITY_CHECK, - recipientCode=policy["insurer_id"], - ) - - return Response(dict(response.get("response")), status=status.HTTP_200_OK) - - @extend_schema(tags=["hcx"], request=MakeClaimSerializer()) - @action(detail=False, methods=["post"]) - def make_claim(self, request): - data = request.data - - serializer = MakeClaimSerializer(data=data) - serializer.is_valid(raise_exception=True) - - claim = ClaimSerializer(Claim.objects.get(external_id=data["claim"])).data - consultation = PatientConsultation.objects.get( - external_id=claim["consultation_object"]["id"] - ) - - procedures = [] - if len(consultation.procedure): - procedures = list( - map( - lambda procedure: { - "id": str(uuid()), - "name": procedure["procedure"], - "performed": ( - procedure["time"] - if "time" in procedure - else procedure["frequency"] - ), - "status": ( - ( - "completed" - if datetime.strptime( - procedure["time"], "%Y-%m-%dT%H:%M" - ) - < datetime.now() - else "preparation" - ) - if "time" in procedure - else "in-progress" - ), - }, - consultation.procedure, - ) - ) - - diagnoses = [] - for diagnosis_id, is_principal in consultation.diagnoses.filter( - verification_status=ConditionVerificationStatus.CONFIRMED - ).values_list("diagnosis_id", "is_principal"): - diagnosis = get_icd11_diagnosis_object_by_id(diagnosis_id) - diagnoses.append( - { - "id": str(uuid()), - "label": diagnosis.label.split(" ", 1)[1], - "code": diagnosis.label.split(" ", 1)[0] or "00", - "type": "principal" if is_principal else "clinical", - } - ) - - previous_claim = ( - Claim.objects.filter( - consultation__external_id=claim["consultation_object"]["id"] - ) - .order_by("-modified_date") - .exclude(external_id=claim["id"]) - .first() - ) - related_claims = [] - if previous_claim: - related_claims.append( - {"id": str(previous_claim.external_id), "type": "prior"} - ) - - docs = list( - map( - lambda file: ( - { - "type": "MB", - "name": file.name, - "url": file.read_signed_url(), - } - ), - FileUpload.objects.filter( - Q(associating_id=claim["consultation_object"]["id"]) - | Q(associating_id=claim["id"]) - ), - ) - ) - - if REVERSE_USE_CHOICES[claim["use"]] == "claim": - discharge_summary_url = generate_discharge_report_signed_url( - claim["policy_object"]["patient_object"]["id"] - ) - docs.append( - { - "type": "DIA", - "name": "Discharge Summary", - "url": discharge_summary_url, - } - ) - - claim_fhir_bundle = Fhir().create_claim_bundle( - claim["id"], - claim["id"], - claim["policy_object"]["patient_object"]["facility_object"]["id"], - claim["policy_object"]["patient_object"]["facility_object"]["name"], - "IN000018", - "GICOFINDIA", - "GICOFINDIA", - "GICOFINDIA", - claim["policy_object"]["patient_object"]["id"], - claim["policy_object"]["patient_object"]["name"], - ( - "male" - if claim["policy_object"]["patient_object"]["gender"] == 1 - else ( - "female" - if claim["policy_object"]["patient_object"]["gender"] == 2 - else "other" - ) - ), - claim["policy_object"]["subscriber_id"], - claim["policy_object"]["policy_id"], - claim["policy_object"]["id"], - claim["id"], - claim["id"], - claim["items"], - claim["policy_object"]["patient_object"]["phone_number"], - REVERSE_USE_CHOICES[claim["use"]], - REVERSE_STATUS_CHOICES[claim["status"]], - REVERSE_CLAIM_TYPE_CHOICES[claim["type"]], - REVERSE_PRIORITY_CHOICES[claim["priority"]], - supporting_info=docs, - related_claims=related_claims, - procedures=procedures, - diagnoses=diagnoses, - ) - - # if not Fhir().validate_fhir_remote(claim_fhir_bundle.json())["valid"]: - # return Response( - # {"message": "Invalid FHIR object"}, - # status=status.HTTP_400_BAD_REQUEST, - # ) - - response = Hcx().generateOutgoingHcxCall( - fhirPayload=json.loads(claim_fhir_bundle.json()), - operation=( - HcxOperations.CLAIM_SUBMIT - if REVERSE_USE_CHOICES[claim["use"]] == "claim" - else HcxOperations.PRE_AUTH_SUBMIT - ), - recipientCode=claim["policy_object"]["insurer_id"], - ) - - return Response(dict(response.get("response")), status=status.HTTP_200_OK) - - @extend_schema(tags=["hcx"], request=SendCommunicationSerializer()) - @action(detail=False, methods=["post"]) - def send_communication(self, request): - data = request.data - - serializer = SendCommunicationSerializer(data=data) - serializer.is_valid(raise_exception=True) - - communication = CommunicationSerializer( - get_communications(self.request.user).get(external_id=data["communication"]) - ).data - - payload = [ - *communication["content"], - *list( - map( - lambda file: ( - { - "type": "url", - "name": file.name, - "data": file.read_signed_url(), - } - ), - FileUpload.objects.filter(associating_id=communication["id"]), - ) - ), - ] - - communication_fhir_bundle = Fhir().create_communication_bundle( - communication["id"], - communication["id"], - communication["id"], - communication["id"], - payload, - [{"type": "Claim", "id": communication["claim_object"]["id"]}], - ) - - if not Fhir().validate_fhir_remote(communication_fhir_bundle.json())["valid"]: - return Response( - Fhir().validate_fhir_remote(communication_fhir_bundle.json())["issues"], - status=status.HTTP_400_BAD_REQUEST, - ) - - response = Hcx().generateOutgoingHcxCall( - fhirPayload=json.loads(communication_fhir_bundle.json()), - operation=HcxOperations.COMMUNICATION_ON_REQUEST, - recipientCode=communication["claim_object"]["policy_object"]["insurer_id"], - correlationId=Communication.objects.filter( - claim__external_id=communication["claim_object"]["id"], created_by=None - ) - .last() - .identifier, - ) - - return Response(dict(response.get("response")), status=status.HTTP_200_OK) - - @extend_schema(tags=["hcx"]) - @action(detail=False, methods=["get"]) - def payors(self, request): - payors = Hcx().searchRegistry("roles", "payor")["participants"] - - result = filter(lambda payor: payor["status"] == "Active", payors) - - if query := request.query_params.get("query"): - query = query.lower() - result = filter( - lambda payor: ( - query in payor["participant_name"].lower() - or query in payor["participant_code"].lower() - ), - result, - ) - - response = list( - map( - lambda payor: { - "name": payor["participant_name"], - "code": payor["participant_code"], - }, - result, - ) - ) - - return Response(response, status=status.HTTP_200_OK) - - def serialize_data(self, objects: list[PMJYPackage]): - return [package.get_representation() for package in objects] - - @extend_schema(tags=["hcx"]) - @action(detail=False, methods=["get"]) - def pmjy_packages(self, request): - try: - limit = min(int(request.query_params.get("limit")), 20) - except (ValueError, TypeError): - limit = 20 - - query = [] - if q := request.query_params.get("query"): - query.append(PMJYPackage.vec % query_builder(q)) - - results = FindQuery(expressions=query, model=PMJYPackage, limit=limit).execute( - exhaust_results=False - ) - - return Response(self.serialize_data(results)) diff --git a/care/hcx/api/viewsets/listener.py b/care/hcx/api/viewsets/listener.py deleted file mode 100644 index fd2f6922e3..0000000000 --- a/care/hcx/api/viewsets/listener.py +++ /dev/null @@ -1,124 +0,0 @@ -import json - -from drf_spectacular.utils import extend_schema -from rest_framework import status -from rest_framework.generics import GenericAPIView -from rest_framework.permissions import AllowAny -from rest_framework.response import Response - -from care.hcx.models.claim import Claim -from care.hcx.models.communication import Communication -from care.hcx.models.policy import Policy -from care.hcx.utils.fhir import Fhir -from care.hcx.utils.hcx import Hcx -from care.utils.notification_handler import send_webpush - - -class CoverageElibilityOnCheckView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_coverage_elibility_check_response(response["payload"]) - - policy = Policy.objects.filter(external_id=data["id"]).first() - policy.outcome = data["outcome"] - policy.error_text = data["error"] - policy.save() - - message = { - "type": "MESSAGE", - "from": "coverageelegibility/on_check", - "message": "success" if not data["error"] else "failed", - } - send_webpush( - username=policy.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class PreAuthOnSubmitView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_claim_response(response["payload"]) - - claim = Claim.objects.filter(external_id=data["id"]).first() - claim.outcome = data["outcome"] - claim.total_amount_approved = data["total_approved"] - claim.error_text = data["error"] - claim.save() - - message = { - "type": "MESSAGE", - "from": "preauth/on_submit", - "message": "success" if not data["error"] else "failed", - } - send_webpush( - username=claim.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class ClaimOnSubmitView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_claim_response(response["payload"]) - - claim = Claim.objects.filter(external_id=data["id"]).first() - claim.outcome = data["outcome"] - claim.total_amount_approved = data["total_approved"] - claim.error_text = data["error"] - claim.save() - - message = { - "type": "MESSAGE", - "from": "preauth/on_submit", - "message": "success" if not data["error"] else "failed", - } - send_webpush( - username=claim.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) - - -class CommunicationRequestView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] - - @extend_schema(tags=["hcx"]) - def post(self, request, *args, **kwargs): - response = Hcx().processIncomingRequest(request.data["payload"]) - data = Fhir().process_communication_request(response["payload"]) - - claim = Claim.objects.filter(external_id__in=data["about"] or []).last() - communication = Communication.objects.create( - claim=claim, - content=data["payload"], - identifier=data[ - "identifier" - ], # TODO: replace identifier with corelation id - ) - - message = { - "type": "MESSAGE", - "from": "communication/request", - "message": f"{communication.external_id}", - } - send_webpush( - username=claim.last_modified_by.username, message=json.dumps(message) - ) - - return Response({}, status=status.HTTP_202_ACCEPTED) diff --git a/care/hcx/api/viewsets/policy.py b/care/hcx/api/viewsets/policy.py deleted file mode 100644 index 18ee6abbf5..0000000000 --- a/care/hcx/api/viewsets/policy.py +++ /dev/null @@ -1,44 +0,0 @@ -from django_filters import rest_framework as filters -from rest_framework import filters as drf_filters -from rest_framework.mixins import ( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, -) -from rest_framework.permissions import IsAuthenticated -from rest_framework.viewsets import GenericViewSet - -from care.hcx.api.serializers.policy import PolicySerializer -from care.hcx.models.policy import Policy - - -class PolicyFilter(filters.FilterSet): - patient = filters.UUIDFilter(field_name="patient__external_id") - - -class PolicyViewSet( - CreateModelMixin, - DestroyModelMixin, - ListModelMixin, - RetrieveModelMixin, - UpdateModelMixin, - GenericViewSet, -): - queryset = Policy.objects.all() - permission_classes = (IsAuthenticated,) - serializer_class = PolicySerializer - lookup_field = "external_id" - search_fields = ["patient"] - filter_backends = ( - filters.DjangoFilterBackend, - drf_filters.SearchFilter, - drf_filters.OrderingFilter, - ) - filterset_class = PolicyFilter - ordering_fields = [ - "id", - "created_date", - "modified_date", - ] diff --git a/care/hcx/apps.py b/care/hcx/apps.py deleted file mode 100644 index 5b4da22bd7..0000000000 --- a/care/hcx/apps.py +++ /dev/null @@ -1,7 +0,0 @@ -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class HcxConfig(AppConfig): - name = "care.hcx" - verbose_name = _("HCX Integration") diff --git a/care/hcx/migrations/0001_initial_squashed.py b/care/hcx/migrations/0001_initial_squashed.py deleted file mode 100644 index 0ed09f9623..0000000000 --- a/care/hcx/migrations/0001_initial_squashed.py +++ /dev/null @@ -1,309 +0,0 @@ -# Generated by Django 2.2.11 on 2023-06-13 10:52 - -import uuid - -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - initial = True - - replaces = [ - ("hcx", "0001_initial"), - ("hcx", "0002_claim"), - ("hcx", "0003_auto_20230217_1901"), - ("hcx", "0004_auto_20230222_2012"), - ("hcx", "0005_auto_20230222_2217"), - ("hcx", "0006_auto_20230323_1208"), - ] - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("facility", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="Policy", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("subscriber_id", models.TextField(blank=True, null=True)), - ("policy_id", models.TextField(blank=True, null=True)), - ("insurer_id", models.TextField(blank=True, null=True)), - ("insurer_name", models.TextField(blank=True, null=True)), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "purpose", - models.CharField( - blank=True, - choices=[ - ("auth-requirements", "Auth Requirements"), - ("benefits", "Benefits"), - ("discovery", "Discovery"), - ("validation", "Validation"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "last_modified_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="policy_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "patient", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientRegistration", - ), - ), - ], - options={ - "abstract": False, - }, - ), - migrations.CreateModel( - name="Claim", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ( - "items", - django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "category": {"type": "string"}, - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ("total_claim_amount", models.FloatField(blank=True, null=True)), - ("total_amount_approved", models.FloatField(blank=True, null=True)), - ( - "use", - models.CharField( - blank=True, - choices=[ - ("claim", "Claim"), - ("preauthorization", "Pre-Authorization"), - ("predetermination", "Pre-Determination"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "type", - models.CharField( - blank=True, - choices=[ - ("institutional", "Institutional"), - ("oral", "Oral"), - ("pharmacy", "Pharmacy"), - ("professional", "Professional"), - ("vision", "Vision"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "consultation", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientConsultation", - ), - ), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "last_modified_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="claim_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "policy", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="hcx.Policy" - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py b/care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py deleted file mode 100644 index 6f39c8479a..0000000000 --- a/care/hcx/migrations/0002_alter_claim_id_alter_claim_items_alter_policy_id.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-14 08:36 - -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0001_initial_squashed"), - ] - - operations = [ - migrations.AlterField( - model_name="claim", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - migrations.AlterField( - model_name="claim", - name="items", - field=models.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "category": {"type": "string"}, - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - migrations.AlterField( - model_name="policy", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] diff --git a/care/hcx/migrations/0007_communication.py b/care/hcx/migrations/0007_communication.py deleted file mode 100644 index ca30d63c5b..0000000000 --- a/care/hcx/migrations/0007_communication.py +++ /dev/null @@ -1,100 +0,0 @@ -# Generated by Django 2.2.11 on 2023-05-10 06:18 - -import uuid - -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ("hcx", "0006_auto_20230323_1208"), - ] - - operations = [ - migrations.CreateModel( - name="Communication", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("identifier", models.TextField(blank=True, null=True)), - ( - "content", - django.contrib.postgres.fields.jsonb.JSONField( - default=list, - null=True, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "data": {"type": "string"}, - "type": {"type": "string"}, - }, - "required": ["type", "data"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ( - "claim", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="hcx.Claim" - ), - ), - ( - "created_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ( - "last_modified_by", - models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="communication_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations/0008_merge_20230617_1253.py b/care/hcx/migrations/0008_merge_20230617_1253.py deleted file mode 100644 index 587f806bca..0000000000 --- a/care/hcx/migrations/0008_merge_20230617_1253.py +++ /dev/null @@ -1,12 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-17 07:23 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0002_alter_claim_id_alter_claim_items_alter_policy_id"), - ("hcx", "0007_communication"), - ] - - operations = [] diff --git a/care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py b/care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py deleted file mode 100644 index 0a43c19cf4..0000000000 --- a/care/hcx/migrations/0009_alter_communication_content_alter_communication_id.py +++ /dev/null @@ -1,48 +0,0 @@ -# Generated by Django 4.2.2 on 2023-06-17 07:23 - -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0008_merge_20230617_1253"), - ] - - operations = [ - migrations.AlterField( - model_name="communication", - name="content", - field=models.JSONField( - default=list, - null=True, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "content": [ - { - "additionalProperties": False, - "properties": { - "data": {"type": "string"}, - "type": {"type": "string"}, - }, - "required": ["type", "data"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - migrations.AlterField( - model_name="communication", - name="id", - field=models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ] diff --git a/care/hcx/migrations/__init__.py b/care/hcx/migrations/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/migrations_old/0001_initial.py b/care/hcx/migrations_old/0001_initial.py deleted file mode 100644 index e3a7bb854f..0000000000 --- a/care/hcx/migrations_old/0001_initial.py +++ /dev/null @@ -1,116 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-14 07:56 - -import uuid - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - initial = True - - dependencies = [ - ("facility", "0335_auto_20230207_1914"), - ] - - operations = [ - migrations.CreateModel( - name="Policy", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ("subscriber_id", models.TextField(blank=True, null=True)), - ("policy_id", models.TextField(blank=True, null=True)), - ("insurer_id", models.TextField(blank=True, null=True)), - ("insurer_name", models.TextField(blank=True, null=True)), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "purpose", - models.CharField( - blank=True, - choices=[ - ("auth-requirements", "Auth Requirements"), - ("benefits", "Benefits"), - ("discovery", "Discovery"), - ("validation", "Validation"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "patient", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientRegistration", - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations_old/0002_claim.py b/care/hcx/migrations_old/0002_claim.py deleted file mode 100644 index d9efc76a87..0000000000 --- a/care/hcx/migrations_old/0002_claim.py +++ /dev/null @@ -1,163 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-15 08:04 - -import uuid - -import django.contrib.postgres.fields.jsonb -import django.db.models.deletion -from django.db import migrations, models - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("facility", "0335_auto_20230207_1914"), - ("hcx", "0001_initial"), - ] - - operations = [ - migrations.CreateModel( - name="Claim", - fields=[ - ( - "id", - models.AutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name="ID", - ), - ), - ( - "external_id", - models.UUIDField(db_index=True, default=uuid.uuid4, unique=True), - ), - ( - "created_date", - models.DateTimeField(auto_now_add=True, db_index=True, null=True), - ), - ( - "modified_date", - models.DateTimeField(auto_now=True, db_index=True, null=True), - ), - ("deleted", models.BooleanField(db_index=True, default=False)), - ( - "procedures", - django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ("total_claim_amount", models.FloatField(blank=True, null=True)), - ("total_amount_approved", models.FloatField(blank=True, null=True)), - ( - "use", - models.CharField( - blank=True, - choices=[ - ("claim", "Claim"), - ("preauthorization", "Pre-Authorization"), - ("predetermination", "Pre-Determination"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "status", - models.CharField( - blank=True, - choices=[ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "priority", - models.CharField( - choices=[ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), - ], - default="normal", - max_length=20, - ), - ), - ( - "type", - models.CharField( - blank=True, - choices=[ - ("institutional", "Institutional"), - ("oral", "Oral"), - ("pharmacy", "Pharmacy"), - ("professional", "Professional"), - ("vision", "Vision"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ( - "outcome", - models.CharField( - blank=True, - choices=[ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), - ], - default=None, - max_length=20, - null=True, - ), - ), - ("error_text", models.TextField(blank=True, null=True)), - ( - "consultation", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to="facility.PatientConsultation", - ), - ), - ( - "policy", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to="hcx.Policy" - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/care/hcx/migrations_old/0003_auto_20230217_1901.py b/care/hcx/migrations_old/0003_auto_20230217_1901.py deleted file mode 100644 index 30fbdade19..0000000000 --- a/care/hcx/migrations_old/0003_auto_20230217_1901.py +++ /dev/null @@ -1,51 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-17 13:31 - -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), - ("hcx", "0002_claim"), - ] - - operations = [ - migrations.AddField( - model_name="claim", - name="created_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="claim", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="policy", - name="created_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AddField( - model_name="policy", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/care/hcx/migrations_old/0004_auto_20230222_2012.py b/care/hcx/migrations_old/0004_auto_20230222_2012.py deleted file mode 100644 index 55de67d61e..0000000000 --- a/care/hcx/migrations_old/0004_auto_20230222_2012.py +++ /dev/null @@ -1,47 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-22 14:42 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0003_auto_20230217_1901"), - ] - - operations = [ - migrations.RemoveField( - model_name="claim", - name="procedures", - ), - migrations.AddField( - model_name="claim", - name="items", - field=django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "category": "string", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ] diff --git a/care/hcx/migrations_old/0005_auto_20230222_2217.py b/care/hcx/migrations_old/0005_auto_20230222_2217.py deleted file mode 100644 index a8638a2015..0000000000 --- a/care/hcx/migrations_old/0005_auto_20230222_2217.py +++ /dev/null @@ -1,43 +0,0 @@ -# Generated by Django 2.2.11 on 2023-02-22 16:47 - -import django.contrib.postgres.fields.jsonb -from django.db import migrations - -import care.utils.models.validators - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0004_auto_20230222_2012"), - ] - - operations = [ - migrations.AlterField( - model_name="claim", - name="items", - field=django.contrib.postgres.fields.jsonb.JSONField( - default=list, - validators=[ - care.utils.models.validators.JSONFieldSchemaValidator( - { - "$schema": "http://json-schema.org/draft-07/schema#", - "items": [ - { - "additionalProperties": False, - "properties": { - "category": {"type": "string"}, - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - }, - "required": ["id", "name", "price"], - "type": "object", - } - ], - "type": "array", - } - ) - ], - ), - ), - ] diff --git a/care/hcx/migrations_old/0006_auto_20230323_1208.py b/care/hcx/migrations_old/0006_auto_20230323_1208.py deleted file mode 100644 index 1b0b8c960f..0000000000 --- a/care/hcx/migrations_old/0006_auto_20230323_1208.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 2.2.11 on 2023-03-23 06:38 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("hcx", "0005_auto_20230222_2217"), - ] - - operations = [ - migrations.AlterField( - model_name="claim", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="claim_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - migrations.AlterField( - model_name="policy", - name="last_modified_by", - field=models.ForeignKey( - null=True, - on_delete=django.db.models.deletion.SET_NULL, - related_name="policy_last_modified_by", - to=settings.AUTH_USER_MODEL, - ), - ), - ] diff --git a/care/hcx/migrations_old/__init__.py b/care/hcx/migrations_old/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/models/base.py b/care/hcx/models/base.py deleted file mode 100644 index dcc43f1270..0000000000 --- a/care/hcx/models/base.py +++ /dev/null @@ -1,62 +0,0 @@ -def reverse_choices(choices): - output = {} - for choice in choices: - output[choice[1]] = choice[0] - return output - - -# http://hl7.org/fhir/fm-status -STATUS_CHOICES = [ - ("active", "Active"), - ("cancelled", "Cancelled"), - ("draft", "Draft"), - ("entered-in-error", "Entered in Error"), -] -REVERSE_STATUS_CHOICES = reverse_choices(STATUS_CHOICES) - - -# http://terminology.hl7.org/CodeSystem/processpriority -PRIORITY_CHOICES = [ - ("stat", "Immediate"), - ("normal", "Normal"), - ("deferred", "Deferred"), -] -REVERSE_PRIORITY_CHOICES = reverse_choices(PRIORITY_CHOICES) - - -# http://hl7.org/fhir/eligibilityrequest-purpose -PURPOSE_CHOICES = [ - ("auth-requirements", "Auth Requirements"), - ("benefits", "Benefits"), - ("discovery", "Discovery"), - ("validation", "Validation"), -] -REVERSE_PURPOSE_CHOICES = reverse_choices(PURPOSE_CHOICES) - - -# http://hl7.org/fhir/remittance-outcome -OUTCOME_CHOICES = [ - ("queued", "Queued"), - ("complete", "Processing Complete"), - ("error", "Error"), - ("partial", "Partial Processing"), -] -REVERSE_OUTCOME_CHOICES = reverse_choices(OUTCOME_CHOICES) - -# http://hl7.org/fhir/claim-use -USE_CHOICES = [ - ("claim", "Claim"), - ("preauthorization", "Pre-Authorization"), - ("predetermination", "Pre-Determination"), -] -REVERSE_USE_CHOICES = reverse_choices(USE_CHOICES) - -# http://hl7.org/fhir/claim-use -CLAIM_TYPE_CHOICES = [ - ("institutional", "Institutional"), - ("oral", "Oral"), - ("pharmacy", "Pharmacy"), - ("professional", "Professional"), - ("vision", "Vision"), -] -REVERSE_CLAIM_TYPE_CHOICES = reverse_choices(CLAIM_TYPE_CHOICES) diff --git a/care/hcx/models/claim.py b/care/hcx/models/claim.py deleted file mode 100644 index 6334e258c0..0000000000 --- a/care/hcx/models/claim.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import models -from django.db.models import JSONField - -from care.facility.models.patient import PatientConsultation -from care.hcx.models.base import ( - CLAIM_TYPE_CHOICES, - OUTCOME_CHOICES, - PRIORITY_CHOICES, - STATUS_CHOICES, - USE_CHOICES, -) -from care.hcx.models.json_schema.claim import ITEMS -from care.hcx.models.policy import Policy -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Claim(BaseModel): - consultation = models.ForeignKey(PatientConsultation, on_delete=models.CASCADE) - policy = models.ForeignKey( - Policy, on_delete=models.CASCADE - ) # cascade - check it with Gigin - - items = JSONField(default=list, validators=[JSONFieldSchemaValidator(ITEMS)]) - total_claim_amount = models.FloatField(blank=True, null=True) - total_amount_approved = models.FloatField(blank=True, null=True) - - use = models.CharField( - choices=USE_CHOICES, max_length=20, default=None, blank=True, null=True - ) - status = models.CharField( - choices=STATUS_CHOICES, max_length=20, default=None, blank=True, null=True - ) - priority = models.CharField( - choices=PRIORITY_CHOICES, max_length=20, default="normal" - ) - type = models.CharField( - choices=CLAIM_TYPE_CHOICES, max_length=20, default=None, blank=True, null=True - ) - - outcome = models.CharField( - choices=OUTCOME_CHOICES, max_length=20, default=None, blank=True, null=True - ) - error_text = models.TextField(null=True, blank=True) - - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - last_modified_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="claim_last_modified_by", - ) diff --git a/care/hcx/models/communication.py b/care/hcx/models/communication.py deleted file mode 100644 index c0a8b2e6f2..0000000000 --- a/care/hcx/models/communication.py +++ /dev/null @@ -1,24 +0,0 @@ -from django.db import models - -from care.hcx.models.claim import Claim -from care.hcx.models.json_schema.communication import CONTENT -from care.users.models import User -from care.utils.models.base import BaseModel -from care.utils.models.validators import JSONFieldSchemaValidator - - -class Communication(BaseModel): - identifier = models.TextField(null=True, blank=True) - claim = models.ForeignKey(Claim, on_delete=models.CASCADE) - - content = models.JSONField( - default=list, validators=[JSONFieldSchemaValidator(CONTENT)], null=True - ) - - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - last_modified_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="communication_last_modified_by", - ) diff --git a/care/hcx/models/json_schema/claim.py b/care/hcx/models/json_schema/claim.py deleted file mode 100644 index 091a843336..0000000000 --- a/care/hcx/models/json_schema/claim.py +++ /dev/null @@ -1,17 +0,0 @@ -ITEMS = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "items": [ - { - "type": "object", - "properties": { - "id": {"type": "string"}, - "name": {"type": "string"}, - "price": {"type": "number"}, - "category": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["id", "name", "price"], - } - ], -} diff --git a/care/hcx/models/json_schema/communication.py b/care/hcx/models/json_schema/communication.py deleted file mode 100644 index c120e477ec..0000000000 --- a/care/hcx/models/json_schema/communication.py +++ /dev/null @@ -1,15 +0,0 @@ -CONTENT = { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "array", - "content": [ - { - "type": "object", - "properties": { - "type": {"type": "string"}, - "data": {"type": "string"}, - }, - "additionalProperties": False, - "required": ["type", "data"], - } - ], -} diff --git a/care/hcx/models/policy.py b/care/hcx/models/policy.py deleted file mode 100644 index 1ee33474cf..0000000000 --- a/care/hcx/models/policy.py +++ /dev/null @@ -1,44 +0,0 @@ -from django.db import models - -from care.facility.models.patient import PatientRegistration -from care.hcx.models.base import ( - OUTCOME_CHOICES, - PRIORITY_CHOICES, - PURPOSE_CHOICES, - STATUS_CHOICES, -) -from care.users.models import User -from care.utils.models.base import BaseModel - - -class Policy(BaseModel): - patient = models.ForeignKey(PatientRegistration, on_delete=models.CASCADE) - - subscriber_id = models.TextField(null=True, blank=True) - policy_id = models.TextField(null=True, blank=True) - - insurer_id = models.TextField(null=True, blank=True) - insurer_name = models.TextField(null=True, blank=True) - - status = models.CharField( - choices=STATUS_CHOICES, max_length=20, default=None, blank=True, null=True - ) - priority = models.CharField( - choices=PRIORITY_CHOICES, max_length=20, default="normal" - ) - purpose = models.CharField( - choices=PURPOSE_CHOICES, max_length=20, default=None, blank=True, null=True - ) - - outcome = models.CharField( - choices=OUTCOME_CHOICES, max_length=20, default=None, blank=True, null=True - ) - error_text = models.TextField(null=True, blank=True) - - created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True) - last_modified_by = models.ForeignKey( - User, - on_delete=models.SET_NULL, - null=True, - related_name="policy_last_modified_by", - ) diff --git a/care/hcx/static_data/__init__.py b/care/hcx/static_data/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/care/hcx/static_data/pmjy_packages.py b/care/hcx/static_data/pmjy_packages.py deleted file mode 100644 index 6c036f31ef..0000000000 --- a/care/hcx/static_data/pmjy_packages.py +++ /dev/null @@ -1,46 +0,0 @@ -import json -from typing import TypedDict - -from redis_om import Field, Migrator - -from care.utils.static_data.models.base import BaseRedisModel - - -class PMJYPackageObject(TypedDict): - code: str - name: str - price: str - package_name: str - - -class PMJYPackage(BaseRedisModel): - code: str = Field(primary_key=True) - name: str - price: str - package_name: str - vec: str = Field(index=True, full_text_search=True) - - def get_representation(self) -> PMJYPackageObject: - return { - "code": self.code, - "name": self.name, - "price": self.price, - "package_name": self.package_name, - } - - -def load_pmjy_packages(): - print("Loading PMJY Packages into the redis cache...", end="", flush=True) - with open("data/pmjy_packages.json") as f: - pmjy_packages = json.load(f) - for package in pmjy_packages: - PMJYPackage( - code=package["procedure_code"], - name=package["procedure_label"], - price=package["procedure_price"], - package_name=package["package_name"], - vec=f"{package['procedure_label']} {package['package_name']}", - ).save() - - Migrator().run() - print("Done") diff --git a/care/hcx/utils/fhir.py b/care/hcx/utils/fhir.py deleted file mode 100644 index 31524a39ee..0000000000 --- a/care/hcx/utils/fhir.py +++ /dev/null @@ -1,1189 +0,0 @@ -from datetime import UTC, datetime -from functools import reduce -from typing import Literal, TypedDict - -import requests -from fhir.resources import ( - annotation, - attachment, - bundle, - claim, - claimresponse, - codeableconcept, - coding, - communication, - communicationrequest, - condition, - coverage, - coverageeligibilityrequest, - coverageeligibilityresponse, - domainresource, - identifier, - meta, - organization, - patient, - period, - practitionerrole, - procedure, - reference, -) - -from config.settings.base import CURRENT_DOMAIN - - -class PROFILE: - patient = "https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient" - organization = "https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization" - coverage = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-Coverage.html" - coverage_eligibility_request = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-CoverageEligibilityRequest.html" - claim = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-Claim.html" - claim_bundle = ( - "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-ClaimRequestBundle.html" - ) - coverage_eligibility_request_bundle = "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-CoverageEligibilityRequestBundle.html" - practitioner_role = ( - "https://nrces.in/ndhm/fhir/r4/StructureDefinition/PractitionerRole" - ) - procedure = "http://hl7.org/fhir/R4/procedure.html" - condition = "https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition" - communication = ( - "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-Communication.html" - ) - communication_bundle = ( - "https://ig.hcxprotocol.io/v0.7.1/StructureDefinition-CommunicationBundle.html" - ) - - -class SYSTEM: - codes = "http://terminology.hl7.org/CodeSystem/v2-0203" - patient_identifier = "http://gicofIndia.com/beneficiaries" - provider_identifier = "http://abdm.gov.in/facilities" - insurer_identifier = "http://irdai.gov.in/insurers" - coverage_identifier = "https://www.gicofIndia.in/policies" - coverage_relationship = ( - "http://terminology.hl7.org/CodeSystem/subscriber-relationship" - ) - priority = "http://terminology.hl7.org/CodeSystem/processpriority" - claim_identifier = CURRENT_DOMAIN - claim_type = "http://terminology.hl7.org/CodeSystem/claim-type" - claim_payee_type = "http://terminology.hl7.org/CodeSystem/payeetype" - claim_item = "https://pmjay.gov.in/hbp-package-code" - claim_bundle_identifier = "https://www.tmh.in/bundle" - coverage_eligibility_request_bundle_identifier = "https://www.tmh.in/bundle" - practitioner_speciality = "http://snomed.info/sct" - claim_supporting_info_category = ( - "http://hcxprotocol.io/codes/claim-supporting-info-categories" - ) - related_claim_relationship = ( - "http://terminology.hl7.org/CodeSystem/ex-relatedclaimrelationship" - ) - procedure_status = "http://hl7.org/fhir/event-status" - condition = "http://snomed.info/sct" - diagnosis_type = "http://terminology.hl7.org/CodeSystem/ex-diagnosistype" - claim_item_category = "https://irdai.gov.in/benefit-billing-group-code" - claim_item_category_pmjy = "https://pmjay.gov.in/benefit-billing-group-code" - communication_identifier = "http://www.providerco.com/communication" - communication_bundle_identifier = "https://www.tmh.in/bundle" - - -PRACTIONER_SPECIALITY = { - "223366009": "Healthcare professional", - "1421009": "Specialized surgeon", - "3430008": "Radiation therapist", - "3842006": "Chiropractor", - "4162009": "Dental assistant", - "5275007": "Auxiliary nurse", - "6816002": "Specialized nurse", - "6868009": "Hospital administrator", - "8724009": "Plastic surgeon", - "11661002": "Neuropathologist", - "11911009": "Nephrologist", - "11935004": "Obstetrician", - "13580004": "School dental assistant", - "14698002": "Medical microbiologist", - "17561000": "Cardiologist", - "18803008": "Dermatologist", - "18850004": "Laboratory hematologist", - "19244007": "Gerodontist", - "20145008": "Removable prosthodontist", - "21365001": "Specialized dentist", - "21450003": "Neuropsychiatrist", - "22515006": "Medical assistant", - "22731001": "Orthopedic surgeon", - "22983004": "Thoracic surgeon", - "23278007": "Community health physician", - "24430003": "Physical medicine specialist", - "24590004": "Urologist", - "25961008": "Electroencephalography specialist", - "26042002": "Dental hygienist", - "26369006": "Public health nurse", - "28229004": "Optometrist", - "28411006": "Neonatologist", - "28544002": "Medical biochemist", - "36682004": "Physiotherapist", - "37154003": "Periodontist", - "37504001": "Orthodontist", - "39677007": "Internal medicine specialist", - "40127002": "Dietitian (general)", - "40204001": "Hematologist", - "40570005": "Interpreter", - "41672002": "Respiratory disease specialist", - "41904004": "Medical X-ray technician", - "43702002": "Occupational health nurse", - "44652006": "Pharmaceutical assistant", - "45419001": "Masseur", - "45440000": "Rheumatologist", - "45544007": "Neurosurgeon", - "45956004": "Sanitarian", - "46255001": "Pharmacist", - "48740002": "Philologist", - "49203003": "Dispensing optician", - "49993003": "Oral surgeon", - "50149000": "Endodontist", - "54503009": "Faith healer", - "56397003": "Neurologist", - "56466003": "Public health physician", - "56542007": "Medical record administrator", - "56545009": "Cardiovascular surgeon", - "57654006": "Fixed prosthodontist", - "59058001": "General physician", - "59169001": "Orthopedic technician", - "59317003": "Dental prosthesis maker and repairer", - "59944000": "Psychologist", - "60008001": "Public health nutritionist", - "61207006": "Medical pathologist", - "61246008": "Laboratory medicine specialist", - "61345009": "Otorhinolaryngologist", - "61894003": "Endocrinologist", - "62247001": "Family medicine specialist", - "63098009": "Clinical immunologist", - "66476003": "Oral pathologist", - "66862007": "Radiologist", - "68867008": "Public health dentist", - "68950000": "Prosthodontist", - "69280009": "Specialized physician", - "71838004": "Gastroenterologist", - "73265009": "Nursing aid", - "75271001": "Professional midwife", - "76166008": "Practical aid (pharmacy)", - "76231001": "Osteopath", - "76899008": "Infectious disease specialist", - "78703002": "General surgeon", - "78729002": "Diagnostic radiologist", - "79898004": "Auxiliary midwife", - "80409005": "Translator", - "80546007": "Occupational therapist", - "80584001": "Psychiatrist", - "80933006": "Nuclear medicine specialist", - "81464008": "Clinical pathologist", - "82296001": "Pediatrician", - "83273008": "Anatomic pathologist", - "83685006": "Gynecologist", - "85733003": "General pathologist", - "88189002": "Anesthesiologist", - "90201008": "Pedodontist", - "90655003": "Geriatrics specialist", - "106289002": "Dentist", - "106291005": "Dietician AND/OR public health nutritionist", - "106292003": "Professional nurse", - "106293008": "Nursing personnel", - "106294002": "Midwifery personnel", - "307988006": "Medical technician", - "159036002": "ECG technician", - "159037006": "EEG technician", - "159039009": "AT - Audiology technician", - "159041005": "Trainee medical technician", - "721942007": "Cardiovascular perfusionist (occupation)", - "878786001": "Operating room technician (occupation)", - "878787005": "Anesthesia technician", -} - - -class IClaimItem(TypedDict): - id: str - name: str - price: float - - -class IClaimProcedure(TypedDict): - id: str - name: str - performed: str - status: Literal[ - "preparation", - "in-progress", - "not-done", - "on-hold", - "stopped", - "completed", - "entered-in-error", - "unknown", - ] - - -class IClaimDiagnosis(TypedDict): - id: str - label: str - code: str - type: Literal[ - "admitting", - "clinical", - "differential", - "discharge", - "laboratory", - "nursing", - "prenatal", - "principal", - "radiology", - "remote", - "retrospective", - "self", - ] - - -class IClaimSupportingInfo(TypedDict): - type: str - url: str - name: str - - -class IRelatedClaim(TypedDict): - id: str - type: Literal["prior", "associated"] - - -FHIR_VALIDATION_URL = "https://staging-hcx.swasth.app/hapi-fhir/fhir/Bundle/$validate" - - -class Fhir: - def get_reference_url(self, resource: domainresource.DomainResource): - return f"{resource.resource_type}/{resource.id}" - - def create_patient_profile( - self, id: str, name: str, gender: str, phone: str, identifier_value: str - ): - return patient.Patient( - id=id, - meta=meta.Meta(profile=[PROFILE.patient]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="SN", - display="Subscriber Number", - ) - ] - ), - system=SYSTEM.patient_identifier, - value=identifier_value, - ) - ], - name=[{"text": name}], - gender=gender, - telecom=[{"system": "phone", "use": "mobile", "value": phone}], - ) - - def create_provider_profile(self, id: str, name: str, identifier_value: str): - return organization.Organization( - id=id, - meta=meta.Meta(profile=[PROFILE.organization]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="AC", - display=name, - ) - ] - ), - system=SYSTEM.provider_identifier, - value=identifier_value, - ) - ], - name=name, - ) - - def create_insurer_profile(self, id: str, name: str, identifier_value: str): - return organization.Organization( - id=id, - meta=meta.Meta(profile=[PROFILE.organization]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="AC", - display=name, - ) - ] - ), - system=SYSTEM.insurer_identifier, - value=identifier_value, - ) - ], - name=name, - ) - - def create_practitioner_role_profile( - self, id, identifier_value, speciality: str, phone: str - ): - return practitionerrole.PractitionerRole( - id=id, - meta=meta.Meta(profile=[PROFILE.practitioner_role]), - identifier=[ - identifier.Identifier( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.codes, - code="NP", - display="Nurse practitioner number", - ) - ] - ), - value=identifier_value, - ) - ], - specialty=[ - codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.practitioner_speciality, - code=speciality, - display=PRACTIONER_SPECIALITY[speciality], - ) - ] - ) - ], - telecom=[{"system": "phone", "value": phone}], - ) - - def create_coverage_profile( - self, - id: str, - identifier_value: str, - subscriber_id: str, - patient: patient.Patient, - insurer: organization.Organization, - status="active", - relationship="self", - ): - return coverage.Coverage( - id=id, - meta=meta.Meta(profile=[PROFILE.coverage]), - identifier=[ - identifier.Identifier( - system=SYSTEM.coverage_identifier, value=identifier_value - ) - ], - status=status, - subscriber=reference.Reference(reference=self.get_reference_url(patient)), - subscriberId=subscriber_id, - beneficiary=reference.Reference(reference=self.get_reference_url(patient)), - relationship=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.coverage_relationship, - code=relationship, - ) - ] - ), - payor=[reference.Reference(reference=self.get_reference_url(insurer))], - ) - - def create_coverage_eligibility_request_profile( - self, - id: str, - identifier_value: str, - patient: patient.Patient, - enterer: practitionerrole.PractitionerRole, - provider: organization.Organization, - insurer: organization.Organization, - coverage: coverage.Coverage, - priority="normal", - status="active", - purpose="validation", - service_period_start=datetime.now().astimezone(tz=UTC), - service_period_end=datetime.now().astimezone(tz=UTC), - ): - return coverageeligibilityrequest.CoverageEligibilityRequest( - id=id, - meta=meta.Meta(profile=[PROFILE.coverage_eligibility_request]), - identifier=[identifier.Identifier(value=identifier_value)], - status=status, - priority=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.priority, - code=priority, - ) - ] - ), - purpose=[purpose], - patient=reference.Reference(reference=self.get_reference_url(patient)), - servicedPeriod=period.Period( - start=service_period_start, - end=service_period_end, - ), - created=datetime.now().astimezone(tz=UTC), - enterer=reference.Reference(reference=self.get_reference_url(enterer)), - provider=reference.Reference(reference=self.get_reference_url(provider)), - insurer=reference.Reference(reference=self.get_reference_url(insurer)), - insurance=[ - coverageeligibilityrequest.CoverageEligibilityRequestInsurance( - coverage=reference.Reference( - reference=self.get_reference_url(coverage) - ) - ) - ], - ) - - def create_claim_profile( - self, - id: str, - identifier_value: str, - items: list[IClaimItem], - patient: patient.Patient, - provider: organization.Organization, - insurer: organization.Organization, - coverage: coverage.Coverage, - use="claim", - status="active", - type="institutional", - priority="normal", - claim_payee_type="provider", - supporting_info=[], - related_claims=[], - procedures=[], - diagnoses=[], - ): - return claim.Claim( - id=id, - meta=meta.Meta( - profile=[PROFILE.claim], - ), - identifier=[ - identifier.Identifier( - system=SYSTEM.claim_identifier, value=identifier_value - ) - ], - status=status, - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_type, - code=type, - ) - ] - ), - use=use, - related=list( - map( - lambda related_claim: ( - claim.ClaimRelated( - id=related_claim["id"], - relationship=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.related_claim_relationship, - code=related_claim["type"], - ) - ] - ), - claim=reference.Reference( - reference=f'Claim/{related_claim["id"]}' - ), - ) - ), - related_claims, - ) - ), - patient=reference.Reference(reference=self.get_reference_url(patient)), - created=datetime.now().astimezone(tz=UTC), - insurer=reference.Reference(reference=self.get_reference_url(insurer)), - provider=reference.Reference(reference=self.get_reference_url(provider)), - priority=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.priority, - code=priority, - ) - ] - ), - payee=claim.ClaimPayee( - type=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_payee_type, - code=claim_payee_type, - ) - ] - ), - party=reference.Reference(reference=self.get_reference_url(provider)), - ), - careTeam=[ - claim.ClaimCareTeam( - sequence=1, - provider=reference.Reference( - reference=self.get_reference_url(provider) - ), - ) - ], - insurance=[ - claim.ClaimInsurance( - sequence=1, - focal=True, - coverage=reference.Reference( - reference=self.get_reference_url(coverage) - ), - ) - ], - item=list( - map( - lambda item, i: ( - claim.ClaimItem( - sequence=i, - productOrService=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_item, - code=item["id"], - display=item["name"], - ) - ] - ), - unitPrice={"value": item["price"], "currency": "INR"}, - category=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=( - SYSTEM.claim_item_category_pmjy - if item["category"] == "HBP" - else SYSTEM.claim_item_category - ), - code=item["category"], - ) - ] - ), - ) - ), - items, - range(1, len(items) + 1), - ) - ), - supportingInfo=list( - map( - lambda info, i: ( - claim.ClaimSupportingInfo( - sequence=i, - category=codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.claim_supporting_info_category, - code=info["type"], - ) - ] - ), - valueAttachment=attachment.Attachment( - url=info["url"], - title=info["name"], - ), - ) - ), - supporting_info, - range(1, len(supporting_info) + 1), - ) - ), - procedure=list( - map( - lambda procedure, i: ( - claim.ClaimProcedure( - sequence=i, - procedureReference=reference.Reference( - reference=self.get_reference_url(procedure) - ), - ) - ), - procedures, - range(1, len(procedures) + 1), - ) - ), - diagnosis=list( - map( - lambda diagnosis, i: ( - claim.ClaimDiagnosis( - sequence=i, - diagnosisReference=reference.Reference( - reference=self.get_reference_url(diagnosis["profile"]) - ), - type=[ - codeableconcept.CodeableConcept( - coding=[ - coding.Coding( - system=SYSTEM.diagnosis_type, - code=diagnosis["type"], - ) - ] - ) - ], - ) - ), - diagnoses, - range(1, len(diagnoses) + 1), - ) - ), - ) - - def create_procedure_profile( - self, - id, - name, - patient, - provider, - status="unknown", - performed=None, - ): - return procedure.Procedure( - id=id, - # meta=meta.Meta( - # profile=[PROFILE.procedure], - # ), - status=status, - note=[annotation.Annotation(text=name)], - subject=reference.Reference(reference=self.get_reference_url(patient)), - performer=[ - procedure.ProcedurePerformer( - actor=reference.Reference( - reference=self.get_reference_url(provider) - ) - ) - ], - performedString=performed, - ) - - def create_condition_profile(self, id, code, label, patient): - return condition.Condition( - id=id, - # meta=meta.Meta(profile=[PROFILE.condition]), - code=codeableconcept.CodeableConcept( - coding=[ - coding.Coding(system=SYSTEM.condition, code=code, display=label) - ] - ), - subject=reference.Reference(reference=self.get_reference_url(patient)), - ) - - def create_coverage_eligibility_request_bundle( - self, - id: str, - identifier_value: str, - provider_id: str, - provider_name: str, - provider_identifier_value: str, - insurer_id: str, - insurer_name: str, - insurer_identifier_value: str, - enterer_id: str, - enterer_identifier_value: str, - enterer_speciality: str, - enterer_phone: str, - patient_id: str, - pateint_name: str, - patient_gender: Literal["male", "female", "other", "unknown"], - subscriber_id: str, - policy_id: str, - coverage_id: str, - eligibility_request_id: str, - eligibility_request_identifier_value: str, - patient_phone: str, - status="active", - priority="normal", - purpose="validation", - service_period_start=datetime.now().astimezone(tz=UTC), - service_period_end=datetime.now().astimezone(tz=UTC), - last_upadted=datetime.now().astimezone(tz=UTC), - ): - provider = self.create_provider_profile( - provider_id, provider_name, provider_identifier_value - ) - insurer = self.create_insurer_profile( - insurer_id, insurer_name, insurer_identifier_value - ) - patient = self.create_patient_profile( - patient_id, pateint_name, patient_gender, patient_phone, subscriber_id - ) - enterer = self.create_practitioner_role_profile( - enterer_id, enterer_identifier_value, enterer_speciality, enterer_phone - ) - coverage = self.create_coverage_profile( - coverage_id, - policy_id, - subscriber_id, - patient, - insurer, - status, - ) - coverage_eligibility_request = self.create_coverage_eligibility_request_profile( - eligibility_request_id, - eligibility_request_identifier_value, - patient, - enterer, - provider, - insurer, - coverage, - priority, - status, - purpose, - service_period_start, - service_period_end, - ) - - return bundle.Bundle( - id=id, - meta=meta.Meta( - lastUpdated=last_upadted, - profile=[PROFILE.coverage_eligibility_request_bundle], - ), - identifier=identifier.Identifier( - system=SYSTEM.coverage_eligibility_request_bundle_identifier, - value=identifier_value, - ), - type="collection", - timestamp=datetime.now().astimezone(tz=UTC), - entry=[ - bundle.BundleEntry( - fullUrl=self.get_reference_url(coverage_eligibility_request), - resource=coverage_eligibility_request, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(provider), - resource=provider, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(insurer), - resource=insurer, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(patient), - resource=patient, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(coverage), - resource=coverage, - ), - ], - ) - - def create_claim_bundle( - self, - id: str, - identifier_value: str, - provider_id: str, - provider_name: str, - provider_identifier_value: str, - insurer_id: str, - insurer_name: str, - insurer_identifier_value: str, - patient_id: str, - pateint_name: str, - patient_gender: Literal["male", "female", "other", "unknown"], - subscriber_id: str, - policy_id: str, - coverage_id: str, - claim_id: str, - claim_identifier_value: str, - items: list[IClaimItem], - patient_phone: str, - use="claim", - status="active", - type="institutional", - priority="normal", - claim_payee_type="provider", - last_updated=datetime.now().astimezone(tz=UTC), - supporting_info=[], - related_claims=[], - procedures=[], - diagnoses=[], - ): - provider = self.create_provider_profile( - provider_id, provider_name, provider_identifier_value - ) - insurer = self.create_insurer_profile( - insurer_id, insurer_name, insurer_identifier_value - ) - patient = self.create_patient_profile( - patient_id, pateint_name, patient_gender, patient_phone, subscriber_id - ) - coverage = self.create_coverage_profile( - coverage_id, - policy_id, - subscriber_id, - patient, - insurer, - status, - ) - - procedures = list( - map( - lambda procedure: self.create_procedure_profile( - procedure["id"], - procedure["name"], - patient, - provider, - procedure["status"], - procedure["performed"], - ), - procedures, - ) - ) - - diagnoses = list( - map( - lambda diagnosis: { - "profile": self.create_condition_profile( - diagnosis["id"], diagnosis["code"], diagnosis["label"], patient - ), - "type": diagnosis["type"], - }, - diagnoses, - ) - ) - - claim = self.create_claim_profile( - claim_id, - claim_identifier_value, - items, - patient, - provider, - insurer, - coverage, - use, - status, - type, - priority, - claim_payee_type, - supporting_info=supporting_info, - related_claims=related_claims, - procedures=procedures, - diagnoses=diagnoses, - ) - - return bundle.Bundle( - id=id, - meta=meta.Meta( - lastUpdated=last_updated, - profile=[PROFILE.claim_bundle], - ), - identifier=identifier.Identifier( - system=SYSTEM.claim_bundle_identifier, - value=identifier_value, - ), - type="collection", - timestamp=datetime.now().astimezone(tz=UTC), - entry=[ - bundle.BundleEntry( - fullUrl=self.get_reference_url(claim), - resource=claim, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(provider), - resource=provider, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(insurer), - resource=insurer, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(patient), - resource=patient, - ), - bundle.BundleEntry( - fullUrl=self.get_reference_url(coverage), - resource=coverage, - ), - *list( - map( - lambda procedure: bundle.BundleEntry( - fullUrl=self.get_reference_url(procedure), - resource=procedure, - ), - procedures, - ) - ), - *list( - map( - lambda diagnosis: bundle.BundleEntry( - fullUrl=self.get_reference_url(diagnosis["profile"]), - resource=diagnosis["profile"], - ), - diagnoses, - ) - ), - ], - ) - - def create_communication_profile( - self, - id: str, - identifier_value: str, - payload: list, - about: list, - last_updated=datetime.now().astimezone(tz=UTC), - ): - return communication.Communication( - id=id, - identifier=[ - identifier.Identifier( - system=SYSTEM.communication_identifier, value=identifier_value - ) - ], - meta=meta.Meta(lastUpdated=last_updated, profile=[PROFILE.communication]), - status="completed", - about=list( - map( - lambda ref: (reference.Reference(type=ref["type"], id=ref["id"])), - about, - ) - ), - payload=list( - map( - lambda content: ( - communication.CommunicationPayload( - contentString=( - content["data"] if content["type"] == "text" else None - ), - contentAttachment=( - attachment.Attachment( - url=content["data"], - title=content["name"] if content["name"] else None, - ) - if content["type"] == "url" - else None - ), - ) - ), - payload, - ) - ), - ) - - def create_communication_bundle( - self, - id: str, - identifier_value: str, - communication_id: str, - communication_identifier_value: str, - payload: list, - about: list, - last_updated=datetime.now().astimezone(tz=UTC), - ): - communication_profile = self.create_communication_profile( - communication_id, - communication_identifier_value, - payload, - about, - last_updated, - ) - - return bundle.Bundle( - id=id, - meta=meta.Meta( - lastUpdated=last_updated, - profile=[PROFILE.communication_bundle], - ), - identifier=identifier.Identifier( - system=SYSTEM.communication_bundle_identifier, - value=identifier_value, - ), - type="collection", - timestamp=datetime.now().astimezone(tz=UTC), - entry=[ - bundle.BundleEntry( - fullUrl=self.get_reference_url(communication_profile), - resource=communication_profile, - ), - ], - ) - - def process_coverage_elibility_check_response(self, response): - coverage_eligibility_check_bundle = bundle.Bundle(**response) - - coverage_eligibility_check_response = ( - coverageeligibilityresponse.CoverageEligibilityResponse( - **list( - filter( - lambda entry: isinstance( - entry.resource, - coverageeligibilityresponse.CoverageEligibilityResponse, - ), - coverage_eligibility_check_bundle.entry, - ) - )[0].resource.dict() - ) - ) - coverage_request = coverage.Coverage( - **list( - filter( - lambda entry: isinstance(entry.resource, coverage.Coverage), - coverage_eligibility_check_bundle.entry, - ) - )[0].resource.dict() - ) - - def get_errors_from_coding(codings): - return "; ".join( - list(map(lambda coding: f"{coding.code}: {coding.display}", codings)) - ) - - return { - "id": coverage_request.id, - "outcome": coverage_eligibility_check_response.outcome, - "error": ", ".join( - list( - map( - lambda error: get_errors_from_coding(error.code.coding), - coverage_eligibility_check_response.error or [], - ) - ) - ), - } - - def process_claim_response(self, response): - claim_bundle = bundle.Bundle(**response) - - claim_response = claimresponse.ClaimResponse( - **list( - filter( - lambda entry: isinstance( - entry.resource, claimresponse.ClaimResponse - ), - claim_bundle.entry, - ) - )[0].resource.dict() - ) - - def get_errors_from_coding(codings): - return "; ".join( - list(map(lambda coding: f"{coding.code}: {coding.display}", codings)) - ) - - return { - "id": claim_bundle.id, - "total_approved": reduce( - lambda price, acc: price + acc, - map( - lambda claim_response_total: float( - claim_response_total.amount.value - ), - claim_response.total, - ), - 0.0, - ), - "outcome": claim_response.outcome, - "error": ", ".join( - list( - map( - lambda error: get_errors_from_coding(error.code.coding), - claim_response.error or [], - ) - ) - ), - } - - def process_communication_request(self, request): - communication_request = communicationrequest.CommunicationRequest(**request) - - data = { - "identifier": communication_request.id - or communication_request.identifier[0].value, - "status": communication_request.status, - "priority": communication_request.priority, - "about": None, - "based_on": None, - "payload": None, - } - - if communication_request.about: - data["about"] = [] - for object in communication_request.about: - about = reference.Reference(**object.dict()) - if about.identifier: - id = identifier.Identifier(about.identifier).value - data["about"].append(id) - continue - - if about.reference: - id = about.reference.split("/")[-1] - data["about"].append(id) - continue - - if communication_request.basedOn: - data["based_on"] = [] - for object in communication_request.basedOn: - based_on = reference.Reference(**object.dict()) - if based_on.identifier: - id = identifier.Identifier(based_on.identifier).value - data["based_on"].append(id) - continue - - if based_on.reference: - id = based_on.reference.split("/")[-1] - data["based_on"].append(id) - continue - - if communication_request.payload: - data["payload"] = [] - for object in communication_request.payload: - payload = communicationrequest.CommunicationRequestPayload( - **object.dict() - ) - - if payload.contentString: - data["payload"].append( - {"type": "text", "data": payload.contentString} - ) - continue - - if payload.contentAttachment: - content = attachment.Attachment(payload.contentAttachment) - if content.data: - data["payload"].append( - { - "type": content.contentType or "text", - "data": content.data, - } - ) - elif content.url: - data["payload"].append({"type": "url", "data": content.url}) - - return data - - def validate_fhir_local(self, fhir_payload, type="bundle"): - try: - if type == "bundle": - bundle.Bundle(**fhir_payload) - except Exception as e: - return {"valid": False, "issues": [e]} - - return {"valid": True, "issues": None} - - def validate_fhir_remote(self, fhir_payload): - headers = {"Content-Type": "application/json"} - response = requests.request( - "POST", FHIR_VALIDATION_URL, headers=headers, data=fhir_payload - ).json() - - issues = response["issue"] if "issue" in response else [] - valid = True - - for issue in issues: - if issue["severity"] == "error": - valid = False - break - - return {"valid": valid, "issues": issues} diff --git a/care/hcx/utils/hcx/__init__.py b/care/hcx/utils/hcx/__init__.py deleted file mode 100644 index 17207ff45d..0000000000 --- a/care/hcx/utils/hcx/__init__.py +++ /dev/null @@ -1,137 +0,0 @@ -import datetime -import json -import uuid -from urllib.parse import urlencode - -import requests -from django.conf import settings -from jwcrypto import jwe, jwk - - -class Hcx: - def __init__( - self, - protocolBasePath=settings.HCX_PROTOCOL_BASE_PATH, - participantCode=settings.HCX_PARTICIPANT_CODE, - authBasePath=settings.HCX_AUTH_BASE_PATH, - username=settings.HCX_USERNAME, - password=settings.HCX_PASSWORD, - encryptionPrivateKeyURL=settings.HCX_ENCRYPTION_PRIVATE_KEY_URL, - igUrl=settings.HCX_IG_URL, - ): - self.protocolBasePath = protocolBasePath - self.participantCode = participantCode - self.authBasePath = authBasePath - self.username = username - self.password = password - self.encryptionPrivateKeyURL = encryptionPrivateKeyURL - self.igUrl = igUrl - - def generateHcxToken(self): - url = self.authBasePath - - payload = { - "client_id": "registry-frontend", - "username": self.username, - "password": self.password, - "grant_type": "password", - } - payload_urlencoded = urlencode(payload) - headers = {"content-type": "application/x-www-form-urlencoded"} - - response = requests.request( - "POST", url, headers=headers, data=payload_urlencoded - ) - y = json.loads(response.text) - return y["access_token"] - - def searchRegistry(self, searchField, searchValue): - url = self.protocolBasePath + "/participant/search" - access_token = self.generateHcxToken() - payload = json.dumps({"filters": {searchField: {"eq": searchValue}}}) - headers = { - "Authorization": "Bearer " + access_token, - "Content-Type": "application/json", - } - - response = requests.request("POST", url, headers=headers, data=payload) - return dict(json.loads(response.text)) - - def createHeaders(self, recipientCode=None, correlationId=None): - # creating HCX headers - # getting sender code - # regsitry_user = self.searchRegistry("primary_email", self.username) - hcx_headers = { - "alg": "RSA-OAEP", - "enc": "A256GCM", - "x-hcx-recipient_code": recipientCode, - "x-hcx-timestamp": datetime.datetime.now() - .astimezone() - .replace(microsecond=0) - .isoformat(), - "x-hcx-sender_code": self.participantCode, - "x-hcx-correlation_id": ( - correlationId if correlationId else str(uuid.uuid4()) - ), - # "x-hcx-workflow_id": str(uuid.uuid4()), - "x-hcx-api_call_id": str(uuid.uuid4()), - # "x-hcx-status": "response.complete", - } - return hcx_headers - - def encryptJWE(self, recipientCode=None, fhirPayload=None, correlationId=None): - if recipientCode is None: - raise ValueError("Recipient code can not be empty, must be a string") - if type(fhirPayload) is not dict: - raise ValueError("Fhir paylaod must be a dictionary") - regsitry_data = self.searchRegistry( - searchField="participant_code", searchValue=recipientCode - ) - public_cert = requests.get(regsitry_data["participants"][0]["encryption_cert"]) - key = jwk.JWK.from_pem(public_cert.text.encode("utf-8")) - headers = self.createHeaders(recipientCode, correlationId) - jwePayload = jwe.JWE( - str(json.dumps(fhirPayload)), - recipient=key, - protected=json.dumps(headers), - ) - enc = jwePayload.serialize(compact=True) - return enc - - def decryptJWE(self, encryptedString): - private_key = requests.get(self.encryptionPrivateKeyURL) - privateKey = jwk.JWK.from_pem(private_key.text.encode("utf-8")) - jwetoken = jwe.JWE() - jwetoken.deserialize(encryptedString, key=privateKey) - return { - "headers": dict(json.loads(jwetoken.payload.decode("utf-8"))), - "payload": dict(json.loads(jwetoken.payload.decode("utf-8"))), - } - - def makeHcxApiCall(self, operation, encryptedString): - url = "".join(self.protocolBasePath + operation.value) - print("making the API call to url " + url) - access_token = self.generateHcxToken() - payload = json.dumps({"payload": encryptedString}) - headers = { - "Authorization": "Bearer " + access_token, - "Content-Type": "application/json", - } - response = requests.request("POST", url, headers=headers, data=payload) - return dict(json.loads(response.text)) - - def generateOutgoingHcxCall( - self, fhirPayload, operation, recipientCode, correlationId=None - ): - encryptedString = self.encryptJWE( - recipientCode=recipientCode, - fhirPayload=fhirPayload, - correlationId=correlationId, - ) - response = self.makeHcxApiCall( - operation=operation, encryptedString=encryptedString - ) - return {"payload": encryptedString, "response": response} - - def processIncomingRequest(self, encryptedString): - return self.decryptJWE(encryptedString=encryptedString) diff --git a/care/hcx/utils/hcx/operations.py b/care/hcx/utils/hcx/operations.py deleted file mode 100644 index 8e2615064a..0000000000 --- a/care/hcx/utils/hcx/operations.py +++ /dev/null @@ -1,21 +0,0 @@ -import enum - - -class HcxOperations(enum.Enum): - COVERAGE_ELIGIBILITY_CHECK = "/coverageeligibility/check" - COVERAGE_ELIGIBILITY_ON_CHECK = "/coverageeligibility/on_check" - PRE_AUTH_SUBMIT = "/preauth/submit" - PRE_AUTH_ON_SUBMIT = "/preauth/on_submit" - CLAIM_SUBMIT = "/claim/submit" - CLAIM_ON_SUBMIT = "/claim/on_submit" - PAYMENT_NOTICE_REQUEST = "/paymentnotice/request" - PAYMENT_NOTICE_ON_REQUEST = "/paymentnotice/on_request" - HCX_STATUS = "/hcx/status" - HCX_ON_STATUS = "/hcx/on_status" - COMMUNICATION_REQUEST = "/communication/request" - COMMUNICATION_ON_REQUEST = "/communication/on_request" - PREDETERMINATION_SUBMIT = "/predetermination/submit" - PREDETERMINATION_ON_SUBMIT = "/predetermination/on_submit" - - def __str__(self): - return "%s" % self.value diff --git a/care/templates/reports/patient_discharge_summary_pdf_template.typ b/care/templates/reports/patient_discharge_summary_pdf_template.typ index b24c8402e4..c24079b6dc 100644 --- a/care/templates/reports/patient_discharge_summary_pdf_template.typ +++ b/care/templates/reports/patient_discharge_summary_pdf_template.typ @@ -380,24 +380,6 @@ - {% endif %}]] -{% if hcx %} - #align(center, [#line(length: 40%, stroke: mygray,)]) - - #align(left, text(14pt,weight: "bold")[=== Health Insurance Details]) - - #table( - columns: (1fr, 1fr, 1fr, 1fr), - inset: 10pt, - align: horizon, - table.header( - [*INSURER NAME*], [*ISSUER ID*], [*MEMBER ID*], [*POLICY ID*], - ), - {% for policy in hcx %} - "{{policy.insurer_name|format_empty_data }}", "{{policy.insurer_id|format_empty_data }}", "{{policy.subscriber_id }}", "{{policy.policy_id }}", - {% endfor %} - ) -{% endif %} - {% if files %} #align(center, [#line(length: 40%, stroke: mygray,)]) diff --git a/care/utils/queryset/communications.py b/care/utils/queryset/communications.py deleted file mode 100644 index 81ea679f8c..0000000000 --- a/care/utils/queryset/communications.py +++ /dev/null @@ -1,22 +0,0 @@ -from care.hcx.models.communication import Communication -from care.users.models import User -from care.utils.cache.cache_allowed_facilities import get_accessible_facilities - - -def get_communications(user): - queryset = Communication.objects.all() - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(claim__policy__patient__facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter( - claim__policy__patient__facility__district=user.district - ) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter( - claim__policy__patient__facility__id__in=allowed_facilities - ) - - return queryset diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 1f668b7585..707f8b8308 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -51,7 +51,6 @@ PatientCodeStatusType, PatientConsent, ) -from care.hcx.models.policy import Policy from care.users.models import District, State fake = Faker() @@ -570,31 +569,6 @@ def create_patient_sample( sample.save() return sample - @classmethod - def get_policy_data(cls, patient, user) -> dict: - return { - "patient": patient, - "subscriber_id": "sample_subscriber_id", - "policy_id": "sample_policy_id", - "insurer_id": "sample_insurer_id", - "insurer_name": "Sample Insurer", - "status": "active", - "priority": "normal", - "purpose": "discovery", - "outcome": "complete", - "error_text": "No errors", - "created_by": user, - "last_modified_by": user, - } - - @classmethod - def create_policy( - cls, patient: PatientRegistration, user: User, **kwargs - ) -> Policy: - data = cls.get_policy_data(patient, user) - data.update(**kwargs) - return Policy.objects.create(**data) - @classmethod def get_encounter_symptom_data(cls, consultation, user) -> dict: return { diff --git a/config/api_router.py b/config/api_router.py index dd79b1c122..bab899767c 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -94,10 +94,6 @@ TestsSummaryViewSet, TriageSummaryViewSet, ) -from care.hcx.api.viewsets.claim import ClaimViewSet -from care.hcx.api.viewsets.communication import CommunicationViewSet -from care.hcx.api.viewsets.gateway import HcxGatewayViewSet -from care.hcx.api.viewsets.policy import PolicyViewSet from care.users.api.viewsets.lsg import ( DistrictViewSet, LocalBodyViewSet, @@ -306,12 +302,6 @@ router.register("medibase", MedibaseViewSet, basename="medibase") -# HCX -router.register("hcx/policy", PolicyViewSet, basename="hcx-policy") -router.register("hcx/claim", ClaimViewSet, basename="hcx-claim") -router.register("hcx/communication", CommunicationViewSet, basename="hcx-communication") -router.register("hcx", HcxGatewayViewSet) - # Public endpoints router.register("public/asset", AssetPublicViewSet, basename="public-asset") router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") diff --git a/config/settings/base.py b/config/settings/base.py index 07c5c5e1aa..1908b24390 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -125,7 +125,6 @@ "care.abdm", "care.users", "care.audit_log", - "care.hcx", ] PLUGIN_APPS = manager.get_apps() @@ -645,20 +644,6 @@ IS_PRODUCTION = False -# HCX -HCX_PROTOCOL_BASE_PATH = env( - "HCX_PROTOCOL_BASE_PATH", default="http://staging-hcx.swasth.app/api/v0.7" -) -HCX_AUTH_BASE_PATH = env( - "HCX_AUTH_BASE_PATH", - default="https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token", -) -HCX_PARTICIPANT_CODE = env("HCX_PARTICIPANT_CODE", default="") -HCX_USERNAME = env("HCX_USERNAME", default="") -HCX_PASSWORD = env("HCX_PASSWORD", default="") -HCX_ENCRYPTION_PRIVATE_KEY_URL = env("HCX_ENCRYPTION_PRIVATE_KEY_URL", default="") -HCX_IG_URL = env("HCX_IG_URL", default="https://ig.hcxprotocol.io/v0.7.1") - PLAUSIBLE_HOST = env("PLAUSIBLE_HOST", default="") PLAUSIBLE_SITE_ID = env("PLAUSIBLE_SITE_ID", default="") PLAUSIBLE_AUTH_TOKEN = env("PLAUSIBLE_AUTH_TOKEN", default="") diff --git a/config/urls.py b/config/urls.py index 832f8168a4..d06994121e 100644 --- a/config/urls.py +++ b/config/urls.py @@ -14,12 +14,6 @@ from care.facility.api.viewsets.patient_consultation import ( dev_preview_discharge_summary, ) -from care.hcx.api.viewsets.listener import ( - ClaimOnSubmitView, - CommunicationRequestView, - CoverageElibilityOnCheckView, - PreAuthOnSubmitView, -) from care.users.api.viewsets.change_password import ChangePasswordView from care.users.reset_password_views import ( ResetPasswordCheck, @@ -72,27 +66,6 @@ name="change_password_view", ), path("api/v1/", include(api_router.urlpatterns)), - # Hcx Listeners - path( - "coverageeligibility/on_check", - CoverageElibilityOnCheckView.as_view(), - name="hcx_coverage_eligibility_on_check", - ), - path( - "preauth/on_submit", - PreAuthOnSubmitView.as_view(), - name="hcx_pre_auth_on_submit", - ), - path( - "claim/on_submit", - ClaimOnSubmitView.as_view(), - name="hcx_claim_on_submit", - ), - path( - "communication/request", - CommunicationRequestView.as_view(), - name="hcx_communication_on_request", - ), # Health check urls path("middleware/verify", MiddlewareAuthenticationVerifyView.as_view()), path("middleware/verify-asset", MiddlewareAssetAuthenticationVerifyView.as_view()), diff --git a/docker/.local.env b/docker/.local.env index ccfaef5fba..b00327fc9b 100644 --- a/docker/.local.env +++ b/docker/.local.env @@ -16,3 +16,13 @@ BUCKET_ENDPOINT=http://localstack:4566 BUCKET_EXTERNAL_ENDPOINT=http://localhost:4566 FILE_UPLOAD_BUCKET=patient-bucket FACILITY_S3_BUCKET=facility-bucket + +# HCX Sandbox Config for local and testing +HCX_AUTH_BASE_PATH=https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token +HCX_ENCRYPTION_PRIVATE_KEY_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-private-key.pem +HCX_IG_URL=https://ig.hcxprotocol.io/v0.7.1 +HCX_PARTICIPANT_CODE=qwertyreboot.gmail@swasth-hcx-staging +HCX_PASSWORD=Opensaber@123 +HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 +HCX_USERNAME=qwertyreboot@gmail.com +HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem diff --git a/docker/.prebuilt.env b/docker/.prebuilt.env index 02267439a7..8bcc36312e 100644 --- a/docker/.prebuilt.env +++ b/docker/.prebuilt.env @@ -29,3 +29,13 @@ DJANGO_SECURE_HSTS_PRELOAD=False DJANGO_SECURE_HSTS_INCLUDE_SUBDOMAINS=False DJANGO_SECURE_SSL_REDIRECT=False DJANGO_SECURE_CONTENT_TYPE_NOSNIFF=False + +# HCX Sandbox Config for local and testing +HCX_AUTH_BASE_PATH=https://staging-hcx.swasth.app/auth/realms/swasth-health-claim-exchange/protocol/openid-connect/token +HCX_ENCRYPTION_PRIVATE_KEY_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-private-key.pem +HCX_IG_URL=https://ig.hcxprotocol.io/v0.7.1 +HCX_PARTICIPANT_CODE=qwertyreboot.gmail@swasth-hcx-staging +HCX_PASSWORD=Opensaber@123 +HCX_PROTOCOL_BASE_PATH=http://staging-hcx.swasth.app/api/v0.7 +HCX_USERNAME=qwertyreboot@gmail.com +HCX_CERT_URL=https://raw.githubusercontent.com/Swasth-Digital-Health-Foundation/hcx-platform/main/demo-app/server/resources/keys/x509-self-signed-certificate.pem diff --git a/plug_config.py b/plug_config.py index a99af83fc5..c177c62cde 100644 --- a/plug_config.py +++ b/plug_config.py @@ -1,5 +1,13 @@ from plugs.manager import PlugManager +from plugs.plug import Plug -plugs = [] +hcx_plugin = Plug( + name="hcx", + package_name="git+https://github.com/ohcnetwork/care_hcx.git", + version="@main", + configs={}, +) + +plugs = [hcx_plugin] manager = PlugManager(plugs) From 084033acc301af8369cb537d166ba86812be5cf7 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 23 Sep 2024 15:44:38 +0530 Subject: [PATCH 36/46] fix aws deploys (#2488) --- aws/backend.json | 4 ++++ aws/celery.json | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/aws/backend.json b/aws/backend.json index 3f109e46c2..2514b63d86 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -271,6 +271,10 @@ "valueFrom": "/care/backend/HCX_IG_URL", "name": "HCX_IG_URL" }, + { + "valueFrom": "/care/backend/HCX_CERT_URL", + "name": "HCX_CERT_URL" + }, { "valueFrom": "/care/backend/ABDM_CLIENT_ID", "name": "ABDM_CLIENT_ID" diff --git a/aws/celery.json b/aws/celery.json index 0c4e574bd4..efb6182134 100644 --- a/aws/celery.json +++ b/aws/celery.json @@ -256,6 +256,10 @@ "valueFrom": "/care/backend/HCX_IG_URL", "name": "HCX_IG_URL" }, + { + "valueFrom": "/care/backend/HCX_CERT_URL", + "name": "HCX_CERT_URL" + }, { "valueFrom": "/care/backend/PLAUSIBLE_HOST", "name": "PLAUSIBLE_HOST" @@ -533,6 +537,10 @@ "valueFrom": "/care/backend/HCX_IG_URL", "name": "HCX_IG_URL" }, + { + "valueFrom": "/care/backend/HCX_CERT_URL", + "name": "HCX_CERT_URL" + }, { "valueFrom": "/care/backend/PLAUSIBLE_HOST", "name": "PLAUSIBLE_HOST" From 9038b931e3e97873b9d92f80b768cb763edbdd69 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 23 Sep 2024 17:18:16 +0530 Subject: [PATCH 37/46] fix facility capacity exports (#2489) fix facility capacity uploads --- care/facility/models/facility.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index d2a1db7360..fcc2408114 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -79,7 +79,7 @@ class FacilityFeature(models.IntegerChoices): ROOM_TYPES.extend(BASE_ROOM_TYPES) -REVERSE_ROOM_TYPES = reverse_choices(ROOM_TYPES) +REVERSE_ROOM_TYPES = reverse_choices(RoomType.choices) REVERSE_FEATURE_CHOICES = reverse_choices(FEATURE_CHOICES) FACILITY_TYPES = [ From 981682272b22e5670da80c0a3777fb2a95ab2be5 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 23 Sep 2024 19:36:07 +0530 Subject: [PATCH 38/46] disable aspect ratio validation for images (#2487) * disable aspect ratio validation for images * fix --------- Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/utils/models/validators.py | 4 +++- care/utils/tests/test_image_validator.py | 12 ------------ 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index bf394d9087..1e50d905ea 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -248,6 +248,9 @@ def __init__( self.aspect_ratio_str = ", ".join( f"{ratio.numerator}:{ratio.denominator}" for ratio in self.aspect_ratio ) + else: + self.aspect_ratio = None + self.aspect_ratio_str = None def __call__(self, value: UploadedFile) -> None: with Image.open(value.file) as image: @@ -326,7 +329,6 @@ def _humanize_bytes(self, size: int) -> str: min_height=400, max_width=1024, max_height=1024, - aspect_ratio=[1 / 1], min_size=1024, # 1 KB max_size=1024 * 1024 * 2, # 2 MB ) diff --git a/care/utils/tests/test_image_validator.py b/care/utils/tests/test_image_validator.py index 1c8d2656f0..dccdfb5165 100644 --- a/care/utils/tests/test_image_validator.py +++ b/care/utils/tests/test_image_validator.py @@ -60,15 +60,3 @@ def test_invalid_image_too_large(self): "Image size is greater than the maximum allowed size of 2 MB.", ], ) - - def test_invalid_image_aspect_ratio(self): - image = Image.new("RGB", (400, 800)) - file = io.BytesIO() - image.save(file, format="JPEG") - test_file = UploadedFile(file, "test.jpg", "image/jpeg", 2048) - with self.assertRaises(ValidationError) as cm: - self.cover_image_validator(test_file) - self.assertEqual( - cm.exception.messages, - ["Image aspect ratio must be one of the following: 1:1."], - ) From 58ad8abcf7dbc1700468f389c7f9ffbf91be2320 Mon Sep 17 00:00:00 2001 From: Dhruv Goyal Date: Mon, 23 Sep 2024 19:42:33 +0530 Subject: [PATCH 39/46] Set default permission class to IsAuthenticated & made required changes (#2268) Set default permission class to IsAuthenticated & made required changes (#2268) --------- Co-authored-by: Aakash Singh --- care/abdm/api/viewsets/abha_number.py | 2 -- care/abdm/api/viewsets/auth.py | 11 ----------- care/abdm/api/viewsets/consent.py | 3 --- care/abdm/api/viewsets/health_information.py | 3 --- care/abdm/api/viewsets/healthid.py | 2 -- care/abdm/api/viewsets/hip.py | 2 -- care/abdm/api/viewsets/monitoring.py | 5 ++--- care/abdm/api/viewsets/patients.py | 3 --- care/abdm/api/viewsets/status.py | 3 --- care/facility/api/viewsets/__init__.py | 3 --- care/facility/api/viewsets/ambulance.py | 2 -- care/facility/api/viewsets/asset.py | 8 ++++---- care/facility/api/viewsets/daily_round.py | 5 +---- care/facility/api/viewsets/events.py | 3 --- care/facility/api/viewsets/facility.py | 6 ++---- care/facility/api/viewsets/facility_capacity.py | 5 +---- care/facility/api/viewsets/facility_users.py | 2 -- care/facility/api/viewsets/file_upload.py | 2 +- care/facility/api/viewsets/hospital_doctor.py | 6 +----- care/facility/api/viewsets/icd.py | 2 -- care/facility/api/viewsets/inventory.py | 1 - care/facility/api/viewsets/notification.py | 3 +-- care/facility/api/viewsets/open_id.py | 3 +-- care/facility/api/viewsets/patient.py | 6 +----- care/facility/api/viewsets/patient_investigation.py | 5 ----- care/facility/api/viewsets/patient_otp.py | 4 ++-- care/facility/api/viewsets/patient_otp_data.py | 2 -- care/facility/api/viewsets/patient_sample.py | 5 +---- care/facility/api/viewsets/prescription.py | 1 - care/facility/api/viewsets/resources.py | 3 --- care/users/api/viewsets/change_password.py | 2 -- care/users/api/viewsets/users.py | 5 +---- care/users/api/viewsets/userskill.py | 2 +- config/health_views.py | 3 --- config/settings/base.py | 3 +++ 35 files changed, 23 insertions(+), 103 deletions(-) diff --git a/care/abdm/api/viewsets/abha_number.py b/care/abdm/api/viewsets/abha_number.py index eae53df9c5..2e94f2aae6 100644 --- a/care/abdm/api/viewsets/abha_number.py +++ b/care/abdm/api/viewsets/abha_number.py @@ -2,7 +2,6 @@ from django.http import Http404 from rest_framework.decorators import action from rest_framework.mixins import RetrieveModelMixin -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -19,7 +18,6 @@ class AbhaNumberViewSet( serializer_class = AbhaNumberSerializer model = AbhaNumber queryset = AbhaNumber.objects.all() - permission_classes = (IsAuthenticated,) def get_object(self): id = self.kwargs.get("pk") diff --git a/care/abdm/api/viewsets/auth.py b/care/abdm/api/viewsets/auth.py index ad1d48beea..b63b484583 100644 --- a/care/abdm/api/viewsets/auth.py +++ b/care/abdm/api/viewsets/auth.py @@ -5,7 +5,6 @@ from django.core.cache import cache from rest_framework import status from rest_framework.generics import GenericAPIView, get_object_or_404 -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from care.abdm.utils.api_call import AbdmGateway @@ -19,7 +18,6 @@ class OnFetchView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -31,7 +29,6 @@ def post(self, request, *args, **kwargs): class OnInitView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -43,7 +40,6 @@ def post(self, request, *args, **kwargs): class OnConfirmView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -76,7 +72,6 @@ def post(self, request, *args, **kwargs): class AuthNotifyView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -94,7 +89,6 @@ def post(self, request, *args, **kwargs): class OnAddContextsView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -102,7 +96,6 @@ def post(self, request, *args, **kwargs): class DiscoverView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -171,7 +164,6 @@ def post(self, request, *args, **kwargs): class LinkInitView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -191,7 +183,6 @@ def post(self, request, *args, **kwargs): class LinkConfirmView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -225,7 +216,6 @@ def post(self, request, *args, **kwargs): class NotifyView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -243,7 +233,6 @@ def post(self, request, *args, **kwargs): class RequestDataView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): diff --git a/care/abdm/api/viewsets/consent.py b/care/abdm/api/viewsets/consent.py index 383f3fde3c..da6fc0ac4f 100644 --- a/care/abdm/api/viewsets/consent.py +++ b/care/abdm/api/viewsets/consent.py @@ -4,7 +4,6 @@ from rest_framework import status from rest_framework.decorators import action from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -43,7 +42,6 @@ class ConsentViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin): serializer_class = ConsentRequestSerializer model = ConsentRequest queryset = ConsentRequest.objects.all() - permission_classes = (IsAuthenticated,) filter_backends = (filters.DjangoFilterBackend,) filterset_class = ConsentRequestFilter @@ -130,7 +128,6 @@ def fetch(self, request, pk): class ConsentCallbackViewSet(GenericViewSet): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def consent_request__on_init(self, request): diff --git a/care/abdm/api/viewsets/health_information.py b/care/abdm/api/viewsets/health_information.py index 2128fbc137..98a2825276 100644 --- a/care/abdm/api/viewsets/health_information.py +++ b/care/abdm/api/viewsets/health_information.py @@ -4,7 +4,6 @@ from django.db.models import Q from rest_framework import status from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -20,7 +19,6 @@ class HealthInformationViewSet(GenericViewSet): - permission_classes = (IsAuthenticated,) def retrieve(self, request, pk): files = FileUpload.objects.filter( @@ -83,7 +81,6 @@ def request(self, request, pk): class HealthInformationCallbackViewSet(GenericViewSet): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def health_information__hiu__on_request(self, request): diff --git a/care/abdm/api/viewsets/healthid.py b/care/abdm/api/viewsets/healthid.py index e435c1614f..347f1a01b2 100644 --- a/care/abdm/api/viewsets/healthid.py +++ b/care/abdm/api/viewsets/healthid.py @@ -8,7 +8,6 @@ from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.mixins import CreateModelMixin -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -40,7 +39,6 @@ class ABDMHealthIDViewSet(GenericViewSet, CreateModelMixin): base_name = "healthid" model = AbhaNumber - permission_classes = (IsAuthenticated,) @extend_schema( operation_id="generate_aadhaar_otp", diff --git a/care/abdm/api/viewsets/hip.py b/care/abdm/api/viewsets/hip.py index 2de9b9fca8..aa6abb5b1e 100644 --- a/care/abdm/api/viewsets/hip.py +++ b/care/abdm/api/viewsets/hip.py @@ -3,7 +3,6 @@ from rest_framework import status from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -16,7 +15,6 @@ class HipViewSet(GenericViewSet): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def get_linking_token(self, data): diff --git a/care/abdm/api/viewsets/monitoring.py b/care/abdm/api/viewsets/monitoring.py index 62b38360ce..b1ee830398 100644 --- a/care/abdm/api/viewsets/monitoring.py +++ b/care/abdm/api/viewsets/monitoring.py @@ -2,13 +2,12 @@ from rest_framework import status from rest_framework.generics import GenericAPIView -from rest_framework.permissions import AllowAny from rest_framework.response import Response class HeartbeatView(GenericAPIView): - permission_classes = (AllowAny,) - authentication_classes = [] + permission_classes = () + authentication_classes = () def get(self, request, *args, **kwargs): return Response( diff --git a/care/abdm/api/viewsets/patients.py b/care/abdm/api/viewsets/patients.py index 267679d48d..e29a72487f 100644 --- a/care/abdm/api/viewsets/patients.py +++ b/care/abdm/api/viewsets/patients.py @@ -4,7 +4,6 @@ from django.db.models import Q from rest_framework import status from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -17,7 +16,6 @@ class PatientsViewSet(GenericViewSet): - permission_classes = (IsAuthenticated,) @action(detail=False, methods=["POST"]) def find(self, request): @@ -57,7 +55,6 @@ def find(self, request): class PatientsCallbackViewSet(GenericViewSet): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def patients__on_find(self, request): diff --git a/care/abdm/api/viewsets/status.py b/care/abdm/api/viewsets/status.py index 8c126ec7ef..72913c847a 100644 --- a/care/abdm/api/viewsets/status.py +++ b/care/abdm/api/viewsets/status.py @@ -1,6 +1,5 @@ from rest_framework import status from rest_framework.generics import GenericAPIView -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from care.abdm.models import AbhaNumber @@ -10,7 +9,6 @@ class NotifyView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): @@ -29,7 +27,6 @@ def post(self, request, *args, **kwargs): class SMSOnNotifyView(GenericAPIView): - permission_classes = (IsAuthenticated,) authentication_classes = [ABDMAuthentication] def post(self, request, *args, **kwargs): diff --git a/care/facility/api/viewsets/__init__.py b/care/facility/api/viewsets/__init__.py index e9eb119910..40ee4c5499 100644 --- a/care/facility/api/viewsets/__init__.py +++ b/care/facility/api/viewsets/__init__.py @@ -4,7 +4,6 @@ RetrieveModelMixin, UpdateModelMixin, ) -from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet from care.facility.api.viewsets.mixins.access import UserAccessMixin @@ -19,5 +18,3 @@ class FacilityBaseViewset( GenericViewSet, ): """Base class for all endpoints related to Faclity model.""" - - permission_classes = (IsAuthenticated,) diff --git a/care/facility/api/viewsets/ambulance.py b/care/facility/api/viewsets/ambulance.py index dd5e5d991f..a2dbe3de95 100644 --- a/care/facility/api/viewsets/ambulance.py +++ b/care/facility/api/viewsets/ambulance.py @@ -9,7 +9,6 @@ RetrieveModelMixin, UpdateModelMixin, ) -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -49,7 +48,6 @@ class AmbulanceViewSet( DestroyModelMixin, GenericViewSet, ): - permission_classes = (IsAuthenticated,) serializer_class = AmbulanceSerializer queryset = Ambulance.objects.filter(deleted=False).select_related( "primary_district", "secondary_district", "third_district" diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index cf6ff5790d..26b9d35b34 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -189,6 +189,8 @@ class AssetPublicViewSet(GenericViewSet): queryset = Asset.objects.all() serializer_class = AssetPublicSerializer lookup_field = "external_id" + permission_classes = () + authentication_classes = () def retrieve(self, request, *args, **kwargs): key = "asset:" + kwargs["external_id"] @@ -207,6 +209,8 @@ class AssetPublicQRViewSet(GenericViewSet): queryset = Asset.objects.all() serializer_class = AssetPublicSerializer lookup_field = "qr_code_id" + permission_classes = () + authentication_classes = () def retrieve(self, request, *args, **kwargs): qr_code_id = kwargs["qr_code_id"] @@ -227,7 +231,6 @@ def retrieve(self, request, *args, **kwargs): class AvailabilityViewSet(ListModelMixin, RetrieveModelMixin, GenericViewSet): queryset = AvailabilityRecord.objects.all() serializer_class = AvailabilityRecordSerializer - permission_classes = (IsAuthenticated,) def get_queryset(self): facility_queryset = get_facility_queryset(self.request.user) @@ -423,7 +426,6 @@ def operate_assets(self, request, *args, **kwargs): class AssetRetrieveConfigViewSet(ListModelMixin, GenericViewSet): queryset = Asset.objects.all() authentication_classes = [MiddlewareAuthentication] - permission_classes = [IsAuthenticated] serializer_class = AssetConfigSerializer @extend_schema( @@ -550,8 +552,6 @@ class AssetServiceViewSet( ) serializer_class = AssetServiceSerializer - permission_classes = (IsAuthenticated,) - lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) diff --git a/care/facility/api/viewsets/daily_round.py b/care/facility/api/viewsets/daily_round.py index 1423b7e50e..612c451437 100644 --- a/care/facility/api/viewsets/daily_round.py +++ b/care/facility/api/viewsets/daily_round.py @@ -41,10 +41,7 @@ class DailyRoundsViewSet( GenericViewSet, ): serializer_class = DailyRoundSerializer - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) queryset = DailyRound.objects.all().select_related("created_by", "last_edited_by") lookup_field = "external_id" filterset_class = DailyRoundFilterSet diff --git a/care/facility/api/viewsets/events.py b/care/facility/api/viewsets/events.py index 32b81d3ab4..8dc1678b4d 100644 --- a/care/facility/api/viewsets/events.py +++ b/care/facility/api/viewsets/events.py @@ -4,7 +4,6 @@ from django_filters import rest_framework as filters from drf_spectacular.utils import extend_schema from rest_framework.decorators import action -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.serializers import BaseSerializer from rest_framework.viewsets import ReadOnlyModelViewSet @@ -21,7 +20,6 @@ class EventTypeViewSet(ReadOnlyModelViewSet): serializer_class = EventTypeSerializer queryset = EventType.objects.filter(is_active=True) - permission_classes = (IsAuthenticated,) def get_serializer_class(self) -> type[BaseSerializer]: if self.action == "roots": @@ -68,7 +66,6 @@ class PatientConsultationEventViewSet(ReadOnlyModelViewSet): queryset = PatientConsultationEvent.objects.all().select_related( "event_type", "caused_by" ) - permission_classes = (IsAuthenticated,) filter_backends = (filters.DjangoFilterBackend,) filterset_class = PatientConsultationEventFilterSet # lookup_field = "external_id" diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index 7b9d64c068..222f0591fd 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -79,10 +79,7 @@ class FacilityViewSet( queryset = Facility.objects.all().select_related( "ward", "local_body", "district", "state" ) - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) filter_backends = ( FacilityQSPermissions, filters.DjangoFilterBackend, @@ -178,6 +175,7 @@ class AllFacilityViewSet( mixins.ListModelMixin, viewsets.GenericViewSet, ): + permission_classes = () queryset = Facility.objects.all().select_related("local_body", "district", "state") serializer_class = FacilityBasicInfoSerializer filter_backends = (filters.DjangoFilterBackend, drf_filters.SearchFilter) diff --git a/care/facility/api/viewsets/facility_capacity.py b/care/facility/api/viewsets/facility_capacity.py index cfb92ef8b7..dd18d13f62 100644 --- a/care/facility/api/viewsets/facility_capacity.py +++ b/care/facility/api/viewsets/facility_capacity.py @@ -18,10 +18,7 @@ class FacilityCapacityViewSet(FacilityBaseViewset, ListModelMixin): lookup_field = "external_id" serializer_class = FacilityCapacitySerializer queryset = FacilityCapacity.objects.filter(facility__deleted=False) - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) def get_queryset(self): user = self.request.user diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py index 578f326849..5d3cb79629 100644 --- a/care/facility/api/viewsets/facility_users.py +++ b/care/facility/api/viewsets/facility_users.py @@ -4,7 +4,6 @@ from rest_framework import filters as drf_filters from rest_framework import mixins from rest_framework.exceptions import ValidationError -from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet from care.facility.models.facility import Facility @@ -28,7 +27,6 @@ class FacilityUserViewSet(GenericViewSet, mixins.ListModelMixin): serializer_class = UserAssignedSerializer filterset_class = UserFilter queryset = User.objects.all() - permission_classes = [IsAuthenticated] filter_backends = [ filters.DjangoFilterBackend, drf_filters.SearchFilter, diff --git a/care/facility/api/viewsets/file_upload.py b/care/facility/api/viewsets/file_upload.py index 2f9ad882c5..5e784fafb6 100644 --- a/care/facility/api/viewsets/file_upload.py +++ b/care/facility/api/viewsets/file_upload.py @@ -59,7 +59,7 @@ class FileUploadViewSet( queryset = ( FileUpload.objects.all().select_related("uploaded_by").order_by("-created_date") ) - permission_classes = [IsAuthenticated, FileUploadPermission] + permission_classes = (IsAuthenticated, FileUploadPermission) lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) filterset_class = FileUploadFilter diff --git a/care/facility/api/viewsets/hospital_doctor.py b/care/facility/api/viewsets/hospital_doctor.py index e2c1297d3e..6139634739 100644 --- a/care/facility/api/viewsets/hospital_doctor.py +++ b/care/facility/api/viewsets/hospital_doctor.py @@ -13,11 +13,7 @@ class HospitalDoctorViewSet(FacilityBaseViewset, ListModelMixin): serializer_class = HospitalDoctorSerializer queryset = HospitalDoctors.objects.filter(facility__deleted=False) - - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) def get_queryset(self): user = self.request.user diff --git a/care/facility/api/viewsets/icd.py b/care/facility/api/viewsets/icd.py index d836de3b5f..e18d86de23 100644 --- a/care/facility/api/viewsets/icd.py +++ b/care/facility/api/viewsets/icd.py @@ -1,6 +1,5 @@ from django.http import Http404 from redis_om import FindQuery -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ViewSet @@ -9,7 +8,6 @@ class ICDViewSet(ViewSet): - permission_classes = (IsAuthenticated,) def serialize_data(self, objects: list[ICD11]): return [diagnosis.get_representation() for diagnosis in objects] diff --git a/care/facility/api/viewsets/inventory.py b/care/facility/api/viewsets/inventory.py index 3453c6c3b4..e50d353635 100644 --- a/care/facility/api/viewsets/inventory.py +++ b/care/facility/api/viewsets/inventory.py @@ -52,7 +52,6 @@ class FacilityInventoryItemViewSet( .prefetch_related("allowed_units", "tags") .all() ) - permission_classes = (IsAuthenticated,) filter_backends = (filters.DjangoFilterBackend,) filterset_class = FacilityInventoryFilter diff --git a/care/facility/api/viewsets/notification.py b/care/facility/api/viewsets/notification.py index 183e873a3f..213f1324e9 100644 --- a/care/facility/api/viewsets/notification.py +++ b/care/facility/api/viewsets/notification.py @@ -6,7 +6,7 @@ from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.generics import get_object_or_404 from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin -from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly +from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response from rest_framework.serializers import CharField, UUIDField from rest_framework.viewsets import GenericViewSet @@ -38,7 +38,6 @@ class NotificationViewSet( .order_by("-created_date") ) serializer_class = NotificationSerializer - permission_classes = [IsAuthenticated] lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) filterset_class = NotificationFilter diff --git a/care/facility/api/viewsets/open_id.py b/care/facility/api/viewsets/open_id.py index 0f2cd2f910..cb0f186bd8 100644 --- a/care/facility/api/viewsets/open_id.py +++ b/care/facility/api/viewsets/open_id.py @@ -2,7 +2,6 @@ from django.utils.decorators import method_decorator from django.views.decorators.cache import cache_page from rest_framework.generics import GenericAPIView -from rest_framework.permissions import AllowAny from rest_framework.response import Response @@ -12,7 +11,7 @@ class PublicJWKsView(GenericAPIView): """ authentication_classes = () - permission_classes = (AllowAny,) + permission_classes = () @method_decorator(cache_page(60 * 60 * 24)) def get(self, *args, **kwargs): diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index f7cb1a05e0..043c1046ab 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -789,10 +789,7 @@ class FacilityPatientStatsHistoryFilterSet(filters.FilterSet): class FacilityPatientStatsHistoryViewSet(viewsets.ModelViewSet): lookup_field = "external_id" - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) queryset = FacilityPatientStatsHistory.objects.filter( facility__deleted=False ).order_by("-entry_date") @@ -958,7 +955,6 @@ class PatientNotesEditViewSet( queryset = PatientNotesEdit.objects.all().order_by("-edited_date") lookup_field = "external_id" serializer_class = PatientNotesEditSerializer - permission_classes = (IsAuthenticated,) def get_queryset(self): user = self.request.user diff --git a/care/facility/api/viewsets/patient_investigation.py b/care/facility/api/viewsets/patient_investigation.py index bb9682abff..07350e4a41 100644 --- a/care/facility/api/viewsets/patient_investigation.py +++ b/care/facility/api/viewsets/patient_investigation.py @@ -10,7 +10,6 @@ from rest_framework.decorators import action from rest_framework.exceptions import ValidationError from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from care.facility.api.serializers.patient_investigation import ( @@ -60,7 +59,6 @@ class InvestigationGroupViewset( serializer_class = PatientInvestigationGroupSerializer queryset = PatientInvestigationGroup.objects.all() lookup_field = "external_id" - permission_classes = (IsAuthenticated,) filterset_class = InvestigationGroupFilter filter_backends = (filters.DjangoFilterBackend,) @@ -76,7 +74,6 @@ class PatientInvestigationViewSet( serializer_class = PatientInvestigationSerializer queryset = PatientInvestigation.objects.all().prefetch_related("groups") lookup_field = "external_id" - permission_classes = (IsAuthenticated,) filterset_class = PatientInvestigationFilter filter_backends = (filters.DjangoFilterBackend,) pagination_class = InvestigationResultsSetPagination @@ -101,7 +98,6 @@ class PatientInvestigationSummaryViewSet( serializer_class = InvestigationValueSerializer queryset = InvestigationValue.objects.select_related("consultation").all() lookup_field = "external_id" - permission_classes = (IsAuthenticated,) filterset_class = PatientInvestigationFilter filter_backends = (filters.DjangoFilterBackend,) pagination_class = InvestigationSummaryResultsSetPagination @@ -157,7 +153,6 @@ class InvestigationValueViewSet( serializer_class = InvestigationValueSerializer queryset = InvestigationValue.objects.select_related("consultation").all() lookup_field = "external_id" - permission_classes = (IsAuthenticated,) filterset_class = PatientInvestigationFilter filter_backends = (filters.DjangoFilterBackend,) pagination_class = InvestigationValueSetPagination diff --git a/care/facility/api/viewsets/patient_otp.py b/care/facility/api/viewsets/patient_otp.py index af365533ce..ca88d83793 100644 --- a/care/facility/api/viewsets/patient_otp.py +++ b/care/facility/api/viewsets/patient_otp.py @@ -5,7 +5,6 @@ from rest_framework import mixins from rest_framework.decorators import action from rest_framework.exceptions import ValidationError -from rest_framework.permissions import AllowAny from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -20,7 +19,8 @@ class PatientMobileOTPViewSet( mixins.CreateModelMixin, GenericViewSet, ): - permission_classes = (AllowAny,) + authentication_classes = () + permission_classes = () serializer_class = PatientMobileOTPSerializer queryset = PatientMobileOTP.objects.all() diff --git a/care/facility/api/viewsets/patient_otp_data.py b/care/facility/api/viewsets/patient_otp_data.py index a3c0302788..76647ae5c5 100644 --- a/care/facility/api/viewsets/patient_otp_data.py +++ b/care/facility/api/viewsets/patient_otp_data.py @@ -1,5 +1,4 @@ from rest_framework.mixins import ListModelMixin, RetrieveModelMixin -from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.patient import ( @@ -14,7 +13,6 @@ class OTPPatientDataViewSet(RetrieveModelMixin, ListModelMixin, GenericViewSet): authentication_classes = (JWTTokenPatientAuthentication,) lookup_field = "external_id" - permission_classes = (IsAuthenticated,) queryset = PatientRegistration.objects.all() serializer_class = PatientDetailSerializer diff --git a/care/facility/api/viewsets/patient_sample.py b/care/facility/api/viewsets/patient_sample.py index d6b5f9d3b3..0c698c95a8 100644 --- a/care/facility/api/viewsets/patient_sample.py +++ b/care/facility/api/viewsets/patient_sample.py @@ -76,10 +76,7 @@ class PatientSampleViewSet( ) .order_by("-id") ) - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) filter_backends = ( PatientSampleFilterBackend, filters.DjangoFilterBackend, diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index a551dd639f..fe8914098c 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -184,7 +184,6 @@ def administer(self, request, *args, **kwargs): class MedibaseViewSet(ViewSet): - permission_classes = (IsAuthenticated,) def serialize_data(self, objects: list[MedibaseMedicine]): return [medicine.get_representation() for medicine in objects] diff --git a/care/facility/api/viewsets/resources.py b/care/facility/api/viewsets/resources.py index 3f26fce431..711f7fcac2 100644 --- a/care/facility/api/viewsets/resources.py +++ b/care/facility/api/viewsets/resources.py @@ -116,7 +116,6 @@ class ResourceRequestViewSet( "emergency", "priority", ] - permission_classes = (IsAuthenticated, DRYPermissions) filter_backends = ( filters.DjangoFilterBackend, @@ -150,8 +149,6 @@ class ResourceRequestCommentViewSet( lookup_field = "external_id" queryset = ResourceRequestComment.objects.all().order_by("-created_date") - permission_classes = (IsAuthenticated,) - def get_queryset(self): queryset = self.queryset.filter( request__external_id=self.kwargs.get("resource_external_id") diff --git a/care/users/api/viewsets/change_password.py b/care/users/api/viewsets/change_password.py index 806eb0a414..805bebddd9 100644 --- a/care/users/api/viewsets/change_password.py +++ b/care/users/api/viewsets/change_password.py @@ -2,7 +2,6 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import serializers, status from rest_framework.generics import UpdateAPIView -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response User = get_user_model() @@ -28,7 +27,6 @@ class ChangePasswordView(UpdateAPIView): serializer_class = ChangePasswordSerializer model = User - permission_classes = (IsAuthenticated,) def update(self, request, *args, **kwargs): self.object = self.request.user diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index 937a8d3d00..9deec755de 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -114,10 +114,7 @@ class UserViewSet( queryset = queryset.filter(Q(asset__isnull=True)) lookup_field = "username" lookup_value_regex = "[^/]+" - permission_classes = ( - IsAuthenticated, - DRYPermissions, - ) + permission_classes = (IsAuthenticated, DRYPermissions) filter_backends = ( filters.DjangoFilterBackend, rest_framework_filters.OrderingFilter, diff --git a/care/users/api/viewsets/userskill.py b/care/users/api/viewsets/userskill.py index 59c689396b..358f8c56d5 100644 --- a/care/users/api/viewsets/userskill.py +++ b/care/users/api/viewsets/userskill.py @@ -38,7 +38,7 @@ class UserSkillViewSet( serializer_class = UserSkillSerializer queryset = UserSkill.objects.all() lookup_field = "external_id" - permission_classes = [UserSkillPermission] + permission_classes = (UserSkillPermission,) def get_queryset(self): username = self.kwargs["users_username"] diff --git a/config/health_views.py b/config/health_views.py index 4b1febf74f..99628d9c3c 100644 --- a/config/health_views.py +++ b/config/health_views.py @@ -1,4 +1,3 @@ -from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView @@ -11,7 +10,6 @@ class MiddlewareAuthenticationVerifyView(APIView): authentication_classes = [MiddlewareAuthentication] - permission_classes = [IsAuthenticated] def get(self, request): return Response(UserBaseMinimumSerializer(request.user).data) @@ -19,7 +17,6 @@ def get(self, request): class MiddlewareAssetAuthenticationVerifyView(APIView): authentication_classes = [MiddlewareAssetAuthentication] - permission_classes = [IsAuthenticated] def get(self, request): return Response(UserBaseMinimumSerializer(request.user).data) diff --git a/config/settings/base.py b/config/settings/base.py index 1908b24390..4c439790b6 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -362,6 +362,9 @@ "config.authentication.CustomBasicAuthentication", "rest_framework.authentication.SessionAuthentication", ), + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.IsAuthenticated", + ], "DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.LimitOffsetPagination", "PAGE_SIZE": 14, "SEARCH_PARAM": "search_text", From 1bc9f898a314c06bb52eb6a72af0d6878595c328 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Mon, 23 Sep 2024 20:56:07 +0530 Subject: [PATCH 40/46] Allow installing plugs from env (#2473) Allow installing plugs from env (#2473) --- docker/prod.Dockerfile | 2 ++ install_plugins.py | 15 +++++++++++++++ plugs/__init__.py | 0 plugs/manager.py | 28 ++++++++++++++++++---------- plugs/plug.py | 38 +++++++++++++++++++++++++++++--------- 5 files changed, 64 insertions(+), 19 deletions(-) create mode 100644 plugs/__init__.py diff --git a/docker/prod.Dockerfile b/docker/prod.Dockerfile index 82e17739ae..877478c3a9 100644 --- a/docker/prod.Dockerfile +++ b/docker/prod.Dockerfile @@ -5,9 +5,11 @@ ARG TYPST_VERSION=0.11.0 ARG BUILD_ENVIRONMENT="production" ARG APP_VERSION="unknown" +ARG ADDITIONAL_PLUGS="" ENV BUILD_ENVIRONMENT=$BUILD_ENVIRONMENT ENV APP_VERSION=$APP_VERSION +ENV ADDITIONAL_PLUGS=$ADDITIONAL_PLUGS ENV PYTHONUNBUFFERED=1 ENV PYTHONDONTWRITEBYTECODE=1 ENV PATH=/venv/bin:$PATH diff --git a/install_plugins.py b/install_plugins.py index 8324ff795b..b320f0caaf 100644 --- a/install_plugins.py +++ b/install_plugins.py @@ -1,3 +1,18 @@ +import json +import logging +import os + from plug_config import manager +from plugs.plug import Plug + +logger = logging.getLogger(__name__) + + +if ADDITIONAL_PLUGS := os.getenv("ADDITIONAL_PLUGS"): + try: + for plug in json.loads(ADDITIONAL_PLUGS): + manager.add_plug(Plug(**plug)) + except json.JSONDecodeError: + logger.error("ADDITIONAL_PLUGS is not a valid JSON") manager.install() diff --git a/plugs/__init__.py b/plugs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/plugs/manager.py b/plugs/manager.py index c9216b31e6..dfdac9fa93 100644 --- a/plugs/manager.py +++ b/plugs/manager.py @@ -2,28 +2,36 @@ import sys from collections import defaultdict +from plugs.plug import Plug + class PlugManager: """ Manager to manage plugs in care """ - def __init__(self, plugs): - self.plugs = plugs + def __init__(self, plugs: list[Plug]): + self.plugs: list[Plug] = plugs - def install(self): - packages = [x.package_name + x.version for x in self.plugs] + def install(self) -> None: + packages: list[str] = [f"{x.package_name}{x.version}" for x in self.plugs] if packages: - subprocess.check_call( - [sys.executable, "-m", "pip", "install", " ".join(packages)] - ) + subprocess.check_call([sys.executable, "-m", "pip", "install", *packages]) # noqa: S603 + + def add_plug(self, plug: Plug) -> None: + if not isinstance(plug, Plug): + msg = "plug must be an instance of Plug" + raise ValueError(msg) + self.plugs.append(plug) - def get_apps(self): + def get_apps(self) -> list[str]: return [plug.name for plug in self.plugs] - def get_config(self): - configs = defaultdict(dict) + def get_config(self) -> defaultdict[str, dict]: + configs: defaultdict[str, dict] = defaultdict(dict) for plug in self.plugs: + if plug.configs is None: + continue for key, value in plug.configs.items(): configs[plug.name][key] = value return configs diff --git a/plugs/plug.py b/plugs/plug.py index 043dfe5063..c8db40e314 100644 --- a/plugs/plug.py +++ b/plugs/plug.py @@ -1,10 +1,30 @@ +from dataclasses import _MISSING_TYPE, dataclass, field, fields + + +@dataclass(slots=True) class Plug: - """ - Abstraction of a plugin - """ - - def __init__(self, name, package_name, version, configs): - self.name = name - self.package_name = package_name - self.version = version - self.configs = configs + name: str + package_name: str + version: str = field(default="@main") + configs: dict = field(default_factory=dict) + + def __post_init__(self): + for _field in fields(self): + if ( + not isinstance(_field.default, _MISSING_TYPE) + and getattr(self, _field.name) is None + ): + setattr(self, _field.name, _field.default) + + if not isinstance(self.name, str): + msg = "name must be a string" + raise ValueError(msg) + if not isinstance(self.package_name, str): + msg = "package_name must be a string" + raise ValueError(msg) + if not isinstance(self.version, str): + msg = "version must be a string" + raise ValueError(msg) + if not isinstance(self.configs, dict): + msg = "configs must be a dictionary" + raise ValueError(msg) From d3ad91dd7a6d036e96bf20bd1b23639f94997b07 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 23 Sep 2024 23:04:22 +0530 Subject: [PATCH 41/46] CORS allow cloudflare pages preview deployments (#2490) --- aws/backend.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aws/backend.json b/aws/backend.json index 2514b63d86..fcacb36194 100644 --- a/aws/backend.json +++ b/aws/backend.json @@ -39,7 +39,7 @@ }, { "name": "CORS_ALLOWED_ORIGIN_REGEXES", - "value": "[\"^https://[a-zA-Z0-9-]+--care-ohc\\\\.netlify\\\\.app$\"]" + "value": "[\"^https://[a-zA-Z0-9-]+--care-ohc\\\\.netlify\\\\.app$\", \"^https://[a-zA-Z0-9-]+\\\\.care-fe\\\\.pages\\\\.dev$\"]" }, { "name": "CURRENT_DOMAIN", From 7478b9a5594b698230e1f99a7e380884b70c80b8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Sep 2024 11:16:54 +0530 Subject: [PATCH 42/46] Bump the boto group with 2 updates (#2493) 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.35.24 to 1.35.25 - [Release notes](https://github.com/boto/boto3/releases) - [Commits](https://github.com/boto/boto3/compare/1.35.24...1.35.25) Updates `boto3-stubs` from 1.35.24 to 1.35.25 - [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 | 27 ++++++++++++++------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/Pipfile b/Pipfile index 1917b7dc31..c469da3047 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] argon2-cffi = "==23.1.0" authlib = "==1.3.2" -boto3 = "==1.35.24" +boto3 = "==1.35.25" celery = "==5.4.0" django = "==5.1.1" django-environ = "==0.11.2" @@ -45,7 +45,7 @@ whitenoise = "==6.7.0" [dev-packages] black = "==24.8.0" -boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.24" } +boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.25" } coverage = "==7.6.1" debugpy = "==1.8.5" django-coverage-plugin = "==3.1.0" diff --git a/Pipfile.lock b/Pipfile.lock index ec55e71efb..be658a3a2b 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "20d6dbc4d87c784ecfc03228f4a162a9583de48d46cdc79fd71028f6d04244c9" + "sha256": "41ec4e61d7dcec07332f048933e1c9699217a4b73422a8630121eedd2f6cffa4" }, "pipfile-spec": 6, "requires": { @@ -208,20 +208,20 @@ }, "boto3": { "hashes": [ - "sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b", - "sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40" + "sha256:5df4e2cbe3409db07d3a0d8d63d5220ce3202a78206ad87afdbb41519b26ce45", + "sha256:b1cfad301184cdd44dfd4805187ccab12de8dd28dd12a11a5cfdace17918c6de" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "botocore": { "hashes": [ - "sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b", - "sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3" + "sha256:76c5706b2c6533000603ae8683a297c887abbbaf6ee31e1b2e2863b74b2989bc", + "sha256:e58d60260abf10ccc4417967923117c9902a6a0cff9fddb6ea7ff42dc1bd4630" ], "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "celery": { "hashes": [ @@ -1835,11 +1835,12 @@ "s3" ], "hashes": [ - "sha256:329a9944a75bedd9b343230b03d37c94158e60e7b7a9553e03be9a8f327cd41c", - "sha256:f96c814f26f2ab8fcd42811b7208105f43657fe15ffa82be2cdc7734160e28c4" + "sha256:55dc1e9b9a6c8456d18bd6747ecf30283d84da4c05d321e2233413b009e2a711", + "sha256:cece5d8ed36a5c587bfdcb97a1262678023f1a43c0aad54eeab9f389aefa99ec" ], + "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "botocore": { "hashes": [ @@ -1851,11 +1852,11 @@ }, "botocore-stubs": { "hashes": [ - "sha256:052eead5808ef138988089b6de6786f6271fc7f395ee0d4395e96e06ab04573c", - "sha256:20ca0d0c77215b599ab79e365f2bf5f909a49f4c5440bbbbb08175971a3de442" + "sha256:6e721054da8f4d57d6c272fb7f1f5464c28453e14f98811915c0a4a38ae0b456", + "sha256:ce69bb8e20845cf9d626dc94b84639e314e513cab34c9c7de5193da912a3c1f6" ], "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "certifi": { "hashes": [ From 663bf30587bd4a0d87917c837fbf933add152071 Mon Sep 17 00:00:00 2001 From: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> Date: Tue, 24 Sep 2024 23:24:28 +0530 Subject: [PATCH 43/46] Fix all linting issues in care (#2483) Fix all linting issues in care (#2483) --------- Co-authored-by: Aakash Singh --- .github/workflows/linter.yml | 23 +- Pipfile | 3 - Pipfile.lock | 311 +++++++----------- care/abdm/api/serializers/abha_number.py | 2 +- care/audit_log/helpers.py | 21 +- care/audit_log/middleware.py | 18 +- care/audit_log/receivers.py | 25 +- care/facility/admin.py | 22 +- care/facility/api/serializers/ambulance.py | 12 +- care/facility/api/serializers/asset.py | 39 +-- care/facility/api/serializers/bed.py | 32 +- .../api/serializers/consultation_diagnosis.py | 19 +- care/facility/api/serializers/daily_round.py | 49 +-- .../api/serializers/encounter_symptom.py | 12 +- care/facility/api/serializers/facility.py | 24 +- .../api/serializers/facility_capacity.py | 4 +- care/facility/api/serializers/file_upload.py | 102 +++--- .../api/serializers/hospital_doctor.py | 4 +- care/facility/api/serializers/inventory.py | 17 +- care/facility/api/serializers/notification.py | 2 +- care/facility/api/serializers/patient.py | 70 ++-- .../api/serializers/patient_consultation.py | 181 +++++----- .../api/serializers/patient_external_test.py | 24 +- care/facility/api/serializers/patient_icmr.py | 2 +- .../api/serializers/patient_investigation.py | 23 +- care/facility/api/serializers/patient_otp.py | 35 +- .../api/serializers/patient_sample.py | 17 +- care/facility/api/serializers/prescription.py | 38 +-- care/facility/api/serializers/resources.py | 25 +- care/facility/api/serializers/shifting.py | 43 +-- care/facility/api/viewsets/ambulance.py | 2 +- care/facility/api/viewsets/asset.py | 48 +-- care/facility/api/viewsets/bed.py | 2 +- care/facility/api/viewsets/daily_round.py | 5 +- care/facility/api/viewsets/events.py | 2 - care/facility/api/viewsets/facility.py | 2 +- .../api/viewsets/facility_capacity.py | 4 +- care/facility/api/viewsets/facility_users.py | 4 +- care/facility/api/viewsets/file_upload.py | 16 +- care/facility/api/viewsets/icd.py | 1 - care/facility/api/viewsets/inventory.py | 47 +-- care/facility/api/viewsets/mixins/access.py | 2 +- care/facility/api/viewsets/mixins/history.py | 2 +- care/facility/api/viewsets/notification.py | 2 +- care/facility/api/viewsets/patient.py | 117 +++---- .../api/viewsets/patient_consultation.py | 21 +- .../api/viewsets/patient_external_test.py | 37 +-- .../api/viewsets/patient_investigation.py | 18 +- care/facility/api/viewsets/patient_otp.py | 12 +- .../facility/api/viewsets/patient_otp_data.py | 3 +- care/facility/api/viewsets/patient_sample.py | 6 +- care/facility/api/viewsets/prescription.py | 5 +- care/facility/api/viewsets/resources.py | 4 +- care/facility/api/viewsets/shifting.py | 63 ++-- care/facility/api/viewsets/summary.py | 55 ---- care/facility/events/handler.py | 9 +- .../commands/add_daily_round_consultation.py | 9 +- .../commands/clean_patient_phone_numbers.py | 4 +- .../management/commands/generate_jwks.py | 2 +- .../management/commands/load_dummy_data.py | 7 +- .../management/commands/load_event_types.py | 5 +- .../commands/load_icd11_diagnoses_data.py | 18 +- .../commands/load_medicines_data.py | 12 +- .../management/commands/load_redis_index.py | 8 +- .../management/commands/port_patient_wards.py | 2 +- .../facility/management/commands/summarize.py | 12 +- .../commands/sync_external_test_patient.py | 4 +- .../management/commands/sync_patient_age.py | 4 +- care/facility/models/ambulance.py | 13 - care/facility/models/asset.py | 6 +- care/facility/models/daily_round.py | 13 +- care/facility/models/encounter_symptom.py | 3 +- care/facility/models/events.py | 27 +- care/facility/models/facility.py | 17 +- care/facility/models/file_upload.py | 22 +- .../models/json_schema}/__init__.py | 0 .../models/mixins/permissions/patient.py | 18 +- care/facility/models/patient.py | 62 ++-- care/facility/models/patient_consultation.py | 26 +- care/facility/models/patient_external_test.py | 1 - care/facility/models/patient_icmr.py | 55 +--- care/facility/models/shifting.py | 4 +- care/facility/models/summary.py | 9 + .../models}/tests/__init__.py | 0 care/facility/signals/asset_updates.py | 2 +- care/facility/static_data/icd11.py | 8 +- care/facility/static_data/medibase.py | 7 +- care/facility/tasks/__init__.py | 20 +- care/facility/tasks/asset_monitor.py | 16 +- care/facility/tasks/discharge_summary.py | 16 +- care/facility/tasks/location_monitor.py | 13 +- care/facility/tasks/plausible_stats.py | 12 +- care/facility/tasks/push_asset_config.py | 12 +- care/facility/tasks/redis_index.py | 6 +- care/facility/tasks/summarisation.py | 33 +- care/facility/templatetags/__init__.py | 0 .../templatetags/data_formatting_tags.py | 4 +- care/facility/templatetags/filters.py | 3 +- .../templatetags/prescription_tags.py | 3 +- care/facility/tests/test_asset_public_api.py | 6 +- .../tests/test_asset_service_history_api.py | 6 +- .../test_medicine_administrations_api.py | 12 +- care/facility/tests/test_middleware_auth.py | 4 +- .../test_patient_and_consultation_access.py | 4 +- care/facility/tests/test_patient_api.py | 46 +-- .../tests/test_patient_consultation_api.py | 2 +- .../tests/test_patient_daily_rounds_api.py | 8 +- care/facility/tests/test_pdf_generation.py | 122 +++---- care/facility/utils/icd/__init__.py | 0 care/facility/utils/icd/scraper.py | 42 ++- care/facility/utils/reports/__init__.py | 0 .../utils/reports/discharge_summary.py | 25 +- care/facility/utils/summarization/__init__.py | 0 .../utils/summarization/district/__init__.py | 0 .../district/patient_summary.py | 12 +- .../facility_capacity.py | 18 +- .../patient_summary.py | 12 +- .../tests_summary.py | 0 .../triage_summary.py | 0 care/users/admin.py | 8 +- care/users/api/serializers/user.py | 73 ++-- care/users/api/serializers/userskill.py | 2 +- care/users/api/viewsets/users.py | 34 +- care/users/management/commands/load_data.py | 5 +- .../management/commands/load_lsg_data.py | 19 +- .../management/commands/load_skill_data.py | 2 +- .../management/commands/load_state_data.py | 6 +- .../management/commands/load_ward_data.py | 15 +- .../commands/populate_investigations.py | 7 +- care/users/management/commands/seed_data.py | 4 +- care/users/models.py | 5 +- care/users/reset_password_views.py | 2 +- care/users/signals.py | 8 +- care/users/tests/test_models.py | 30 -- care/utils/assetintegration/base.py | 53 ++- care/utils/assetintegration/hl7monitor.py | 4 +- care/utils/assetintegration/onvif.py | 4 +- care/utils/assetintegration/ventilator.py | 4 +- care/utils/cache/__init__.py | 0 care/utils/csp/__init__.py | 0 care/utils/csp/config.py | 9 +- care/utils/event_utils.py | 14 +- care/utils/exceptions.py | 2 +- care/utils/file_uploads/cover_image.py | 6 +- care/utils/filters/multiselect.py | 3 +- care/utils/jwks/generate_jwk.py | 4 +- care/utils/jwks/token_generator.py | 5 +- care/utils/lock.py | 2 +- care/utils/models/base.py | 26 +- care/utils/models/validators.py | 56 ++-- care/utils/notification_handler.py | 44 +-- care/utils/queryset/__init__.py | 0 care/utils/registries/feature_flag.py | 18 +- care/utils/serializers/__init__.py | 0 .../fields.py} | 26 +- .../history_serializer.py | 0 care/utils/sms/__init__.py | 0 care/utils/sms/{sendSMS.py => send_sms.py} | 8 +- care/utils/tests/test_feature_flags.py | 9 +- care/utils/tests/test_utils.py | 86 +++-- care/utils/ulid/models.py | 4 +- care/utils/ulid/ulid.py | 74 +++-- care/utils/validation/integer_validation.py | 12 - config/adminlogin.py | 3 +- config/api_router.py | 5 +- config/asgi.py | 41 --- config/auth_views.py | 5 +- config/authentication.py | 21 +- config/health_views.py | 4 +- config/{middleware.py => middlewares.py} | 2 +- config/ratelimit.py | 33 +- config/serializers.py | 25 -- config/settings/base.py | 19 +- config/settings/local.py | 2 +- config/urls.py | 5 +- config/utils.py | 2 - config/validators.py | 69 ---- config/views.py | 4 +- config/websocket.py | 13 - config/wsgi.py | 6 +- docs/conf.py | 10 +- manage.py | 6 +- merge_production_dotenvs_in_dotenv.py | 30 -- pyproject.toml | 69 ++-- 184 files changed, 1573 insertions(+), 2019 deletions(-) rename care/{utils/serializer => facility/models/json_schema}/__init__.py (100%) rename care/{utils/serializer => facility/models}/tests/__init__.py (100%) create mode 100644 care/facility/templatetags/__init__.py create mode 100644 care/facility/utils/icd/__init__.py create mode 100644 care/facility/utils/reports/__init__.py create mode 100644 care/facility/utils/summarization/__init__.py create mode 100644 care/facility/utils/summarization/district/__init__.py rename care/facility/utils/{summarisation => summarization}/district/patient_summary.py (92%) rename care/facility/utils/{summarisation => summarization}/facility_capacity.py (85%) rename care/facility/utils/{summarisation => summarization}/patient_summary.py (92%) rename care/facility/utils/{summarisation => summarization}/tests_summary.py (100%) rename care/facility/utils/{summarisation => summarization}/triage_summary.py (100%) create mode 100644 care/utils/cache/__init__.py create mode 100644 care/utils/csp/__init__.py create mode 100644 care/utils/queryset/__init__.py create mode 100644 care/utils/serializers/__init__.py rename care/utils/{serializer/external_id_field.py => serializers/fields.py} (52%) rename care/utils/{serializer => serializers}/history_serializer.py (100%) create mode 100644 care/utils/sms/__init__.py rename care/utils/sms/{sendSMS.py => send_sms.py} (76%) delete mode 100644 care/utils/validation/integer_validation.py delete mode 100644 config/asgi.py rename config/{middleware.py => middlewares.py} (82%) delete mode 100644 config/serializers.py delete mode 100644 config/utils.py delete mode 100644 config/validators.py delete mode 100644 config/websocket.py delete mode 100644 merge_production_dotenvs_in_dotenv.py diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index b7c108dc2f..cbf322bd10 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -7,27 +7,14 @@ on: - staging merge_group: -permissions: { } - jobs: lint: - name: Lint Code Base runs-on: ubuntu-latest - steps: - - name: Checkout Code - uses: actions/checkout@v4 - - - name: Ruff check - uses: chartboost/ruff-action@491342200cdd1cf4d5132a30ddc546b3b5bc531b + - uses: actions/checkout@v4 with: - version: 0.6.7 - args: "check" - changed-files: "true" - - - name: Ruff format - uses: chartboost/ruff-action@491342200cdd1cf4d5132a30ddc546b3b5bc531b + fetch-depth: 0 + - uses: actions/setup-python@v3 + - uses: pre-commit/action@v3.0.1 with: - version: 0.6.7 - args: "format" - changed-files: "true" + extra_args: --color=always --from-ref ${{ github.event.pull_request.base.sha }} --to-ref ${{ github.event.pull_request.head.sha }} diff --git a/Pipfile b/Pipfile index c469da3047..28d27192b8 100644 --- a/Pipfile +++ b/Pipfile @@ -44,7 +44,6 @@ sentry-sdk = "==2.14.0" whitenoise = "==6.7.0" [dev-packages] -black = "==24.8.0" boto3-stubs = { extras = ["s3", "boto3"], version = "==1.35.25" } coverage = "==7.6.1" debugpy = "==1.8.5" @@ -53,10 +52,8 @@ django-extensions = "==3.2.3" django-silk = "==5.2.0" djangorestframework-stubs = "==3.15.1" factory-boy = "==3.3.1" -flake8 = "==7.1.1" freezegun = "==1.5.1" ipython = "==8.27.0" -isort = "==5.13.2" mypy = "==1.11.2" pre-commit = "==3.8.0" requests-mock = "==1.12.1" diff --git a/Pipfile.lock b/Pipfile.lock index be658a3a2b..b43bb3e4f6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "41ec4e61d7dcec07332f048933e1c9699217a4b73422a8630121eedd2f6cffa4" + "sha256": "f249cd5eb0e4b4e2f285fe4b6d0a9b3a125e26ab0521692f3ad0ab5e332fc450" }, "pipfile-spec": 6, "requires": { @@ -470,7 +470,6 @@ "sha256:f98bf604c82c416bc829e490c700ca1553eafdf2912a91e23a79d97d9801372a", "sha256:fba1007b3ef89946dbbb515aeeb41e30203b004f0b4b00e5e16078b518563289" ], - "index": "pypi", "markers": "python_version >= '3.7'", "version": "==43.0.1" }, @@ -1616,11 +1615,11 @@ }, "tzdata": { "hashes": [ - "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd", - "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252" + "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc", + "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd" ], "markers": "python_version >= '2'", - "version": "==2024.1" + "version": "==2024.2" }, "unicodecsv": { "hashes": [ @@ -1670,101 +1669,101 @@ }, "yarl": { "hashes": [ - "sha256:01a8697ec24f17c349c4f655763c4db70eebc56a5f82995e5e26e837c6eb0e49", - "sha256:02da8759b47d964f9173c8675710720b468aa1c1693be0c9c64abb9d8d9a4867", - "sha256:04293941646647b3bfb1719d1d11ff1028e9c30199509a844da3c0f5919dc520", - "sha256:067b961853c8e62725ff2893226fef3d0da060656a9827f3f520fb1d19b2b68a", - "sha256:077da604852be488c9a05a524068cdae1e972b7dc02438161c32420fb4ec5e14", - "sha256:09696438cb43ea6f9492ef237761b043f9179f455f405279e609f2bc9100212a", - "sha256:0b8486f322d8f6a38539136a22c55f94d269addb24db5cb6f61adc61eabc9d93", - "sha256:0ea9682124fc062e3d931c6911934a678cb28453f957ddccf51f568c2f2b5e05", - "sha256:0f351fa31234699d6084ff98283cb1e852270fe9e250a3b3bf7804eb493bd937", - "sha256:14438dfc5015661f75f85bc5adad0743678eefee266ff0c9a8e32969d5d69f74", - "sha256:15061ce6584ece023457fb8b7a7a69ec40bf7114d781a8c4f5dcd68e28b5c53b", - "sha256:15439f3c5c72686b6c3ff235279630d08936ace67d0fe5c8d5bbc3ef06f5a420", - "sha256:17b5a386d0d36fb828e2fb3ef08c8829c1ebf977eef88e5367d1c8c94b454639", - "sha256:18ac56c9dd70941ecad42b5a906820824ca72ff84ad6fa18db33c2537ae2e089", - "sha256:1bb2d9e212fb7449b8fb73bc461b51eaa17cc8430b4a87d87be7b25052d92f53", - "sha256:1e969fa4c1e0b1a391f3fcbcb9ec31e84440253325b534519be0d28f4b6b533e", - "sha256:1fa2e7a406fbd45b61b4433e3aa254a2c3e14c4b3186f6e952d08a730807fa0c", - "sha256:2164cd9725092761fed26f299e3f276bb4b537ca58e6ff6b252eae9631b5c96e", - "sha256:21a7c12321436b066c11ec19c7e3cb9aec18884fe0d5b25d03d756a9e654edfe", - "sha256:238a21849dd7554cb4d25a14ffbfa0ef380bb7ba201f45b144a14454a72ffa5a", - "sha256:250e888fa62d73e721f3041e3a9abf427788a1934b426b45e1b92f62c1f68366", - "sha256:25861303e0be76b60fddc1250ec5986c42f0a5c0c50ff57cc30b1be199c00e63", - "sha256:267b24f891e74eccbdff42241c5fb4f974de2d6271dcc7d7e0c9ae1079a560d9", - "sha256:27fcb271a41b746bd0e2a92182df507e1c204759f460ff784ca614e12dd85145", - "sha256:2909fa3a7d249ef64eeb2faa04b7957e34fefb6ec9966506312349ed8a7e77bf", - "sha256:3257978c870728a52dcce8c2902bf01f6c53b65094b457bf87b2644ee6238ddc", - "sha256:327c724b01b8641a1bf1ab3b232fb638706e50f76c0b5bf16051ab65c868fac5", - "sha256:3de5292f9f0ee285e6bd168b2a77b2a00d74cbcfa420ed078456d3023d2f6dff", - "sha256:3fce4da3703ee6048ad4138fe74619c50874afe98b1ad87b2698ef95bf92c96d", - "sha256:3ff6b1617aa39279fe18a76c8d165469c48b159931d9b48239065767ee455b2b", - "sha256:400cd42185f92de559d29eeb529e71d80dfbd2f45c36844914a4a34297ca6f00", - "sha256:4179522dc0305c3fc9782549175c8e8849252fefeb077c92a73889ccbcd508ad", - "sha256:4307d9a3417eea87715c9736d050c83e8c1904e9b7aada6ce61b46361b733d92", - "sha256:476e20c433b356e16e9a141449f25161e6b69984fb4cdbd7cd4bd54c17844998", - "sha256:489fa8bde4f1244ad6c5f6d11bb33e09cf0d1d0367edb197619c3e3fc06f3d91", - "sha256:48a28bed68ab8fb7e380775f0029a079f08a17799cb3387a65d14ace16c12e2b", - "sha256:48dfd117ab93f0129084577a07287376cc69c08138694396f305636e229caa1a", - "sha256:4973eac1e2ff63cf187073cd4e1f1148dcd119314ab79b88e1b3fad74a18c9d5", - "sha256:498442e3af2a860a663baa14fbf23fb04b0dd758039c0e7c8f91cb9279799bff", - "sha256:501c503eed2bb306638ccb60c174f856cc3246c861829ff40eaa80e2f0330367", - "sha256:504cf0d4c5e4579a51261d6091267f9fd997ef58558c4ffa7a3e1460bd2336fa", - "sha256:61a5f2c14d0a1adfdd82258f756b23a550c13ba4c86c84106be4c111a3a4e413", - "sha256:637c7ddb585a62d4469f843dac221f23eec3cbad31693b23abbc2c366ad41ff4", - "sha256:66b63c504d2ca43bf7221a1f72fbe981ff56ecb39004c70a94485d13e37ebf45", - "sha256:67459cf8cf31da0e2cbdb4b040507e535d25cfbb1604ca76396a3a66b8ba37a6", - "sha256:688654f8507464745ab563b041d1fb7dab5d9912ca6b06e61d1c4708366832f5", - "sha256:6907daa4b9d7a688063ed098c472f96e8181733c525e03e866fb5db480a424df", - "sha256:69721b8effdb588cb055cc22f7c5105ca6fdaa5aeb3ea09021d517882c4a904c", - "sha256:6d23754b9939cbab02c63434776df1170e43b09c6a517585c7ce2b3d449b7318", - "sha256:7175a87ab8f7fbde37160a15e58e138ba3b2b0e05492d7351314a250d61b1591", - "sha256:72bf26f66456baa0584eff63e44545c9f0eaed9b73cb6601b647c91f14c11f38", - "sha256:74db2ef03b442276d25951749a803ddb6e270d02dda1d1c556f6ae595a0d76a8", - "sha256:750f656832d7d3cb0c76be137ee79405cc17e792f31e0a01eee390e383b2936e", - "sha256:75e0ae31fb5ccab6eda09ba1494e87eb226dcbd2372dae96b87800e1dcc98804", - "sha256:768ecc550096b028754ea28bf90fde071c379c62c43afa574edc6f33ee5daaec", - "sha256:7d51324a04fc4b0e097ff8a153e9276c2593106a811704025bbc1d6916f45ca6", - "sha256:7e975a2211952a8a083d1b9d9ba26472981ae338e720b419eb50535de3c02870", - "sha256:8215f6f21394d1f46e222abeb06316e77ef328d628f593502d8fc2a9117bde83", - "sha256:8258c86f47e080a258993eed877d579c71da7bda26af86ce6c2d2d072c11320d", - "sha256:8418c053aeb236b20b0ab8fa6bacfc2feaaf7d4683dd96528610989c99723d5f", - "sha256:87f020d010ba80a247c4abc335fc13421037800ca20b42af5ae40e5fd75e7909", - "sha256:884eab2ce97cbaf89f264372eae58388862c33c4f551c15680dd80f53c89a269", - "sha256:8a336eaa7ee7e87cdece3cedb395c9657d227bfceb6781295cf56abcd3386a26", - "sha256:8aef1b64da41d18026632d99a06b3fefe1d08e85dd81d849fa7c96301ed22f1b", - "sha256:8aef97ba1dd2138112890ef848e17d8526fe80b21f743b4ee65947ea184f07a2", - "sha256:8ed653638ef669e0efc6fe2acb792275cb419bf9cb5c5049399f3556995f23c7", - "sha256:9361628f28f48dcf8b2f528420d4d68102f593f9c2e592bfc842f5fb337e44fd", - "sha256:946eedc12895873891aaceb39bceb484b4977f70373e0122da483f6c38faaa68", - "sha256:94d0caaa912bfcdc702a4204cd5e2bb01eb917fc4f5ea2315aa23962549561b0", - "sha256:964a428132227edff96d6f3cf261573cb0f1a60c9a764ce28cda9525f18f7786", - "sha256:999bfee0a5b7385a0af5ffb606393509cfde70ecca4f01c36985be6d33e336da", - "sha256:a08ea567c16f140af8ddc7cb58e27e9138a1386e3e6e53982abaa6f2377b38cc", - "sha256:a28b70c9e2213de425d9cba5ab2e7f7a1c8ca23a99c4b5159bf77b9c31251447", - "sha256:a34e1e30f1774fa35d37202bbeae62423e9a79d78d0874e5556a593479fdf239", - "sha256:a4264515f9117be204935cd230fb2a052dd3792789cc94c101c535d349b3dab0", - "sha256:a7915ea49b0c113641dc4d9338efa9bd66b6a9a485ffe75b9907e8573ca94b84", - "sha256:aac44097d838dda26526cffb63bdd8737a2dbdf5f2c68efb72ad83aec6673c7e", - "sha256:b91044952da03b6f95fdba398d7993dd983b64d3c31c358a4c89e3c19b6f7aef", - "sha256:ba444bdd4caa2a94456ef67a2f383710928820dd0117aae6650a4d17029fa25e", - "sha256:c2dc4250fe94d8cd864d66018f8344d4af50e3758e9d725e94fecfa27588ff82", - "sha256:c35f493b867912f6fda721a59cc7c4766d382040bdf1ddaeeaa7fa4d072f4675", - "sha256:c92261eb2ad367629dc437536463dc934030c9e7caca861cc51990fe6c565f26", - "sha256:ce928c9c6409c79e10f39604a7e214b3cb69552952fbda8d836c052832e6a979", - "sha256:d95b52fbef190ca87d8c42f49e314eace4fc52070f3dfa5f87a6594b0c1c6e46", - "sha256:dae7bd0daeb33aa3e79e72877d3d51052e8b19c9025ecf0374f542ea8ec120e4", - "sha256:e286580b6511aac7c3268a78cdb861ec739d3e5a2a53b4809faef6b49778eaff", - "sha256:e4b53f73077e839b3f89c992223f15b1d2ab314bdbdf502afdc7bb18e95eae27", - "sha256:e8f63904df26d1a66aabc141bfd258bf738b9bc7bc6bdef22713b4f5ef789a4c", - "sha256:f3a6d90cab0bdf07df8f176eae3a07127daafcf7457b997b2bf46776da2c7eb7", - "sha256:f41fa79114a1d2eddb5eea7b912d6160508f57440bd302ce96eaa384914cd265", - "sha256:f46f81501160c28d0c0b7333b4f7be8983dbbc161983b6fb814024d1b4952f79", - "sha256:f61db3b7e870914dbd9434b560075e0366771eecbe6d2b5561f5bc7485f39efd" - ], - "markers": "python_version >= '3.8'", - "version": "==1.11.1" + "sha256:0103c52f8dfe5d573c856322149ddcd6d28f51b4d4a3ee5c4b3c1b0a05c3d034", + "sha256:01549468858b87d36f967c97d02e6e54106f444aeb947ed76f8f71f85ed07cec", + "sha256:0274b1b7a9c9c32b7bf250583e673ff99fb9fccb389215841e2652d9982de740", + "sha256:0ac33d22b2604b020569a82d5f8a03ba637ba42cc1adf31f616af70baf81710b", + "sha256:0d0a5e87bc48d76dfcfc16295201e9812d5f33d55b4a0b7cad1025b92bf8b91b", + "sha256:10b690cd78cbaca2f96a7462f303fdd2b596d3978b49892e4b05a7567c591572", + "sha256:126309c0f52a2219b3d1048aca00766429a1346596b186d51d9fa5d2070b7b13", + "sha256:15871130439ad10abb25a4631120d60391aa762b85fcab971411e556247210a0", + "sha256:17d4dc4ff47893a06737b8788ed2ba2f5ac4e8bb40281c8603920f7d011d5bdd", + "sha256:18c2a7757561f05439c243f517dbbb174cadfae3a72dee4ae7c693f5b336570f", + "sha256:1d4017e78fb22bc797c089b746230ad78ecd3cdb215bc0bd61cb72b5867da57e", + "sha256:1f50a37aeeb5179d293465e522fd686080928c4d89e0ff215e1f963405ec4def", + "sha256:20d817c0893191b2ab0ba30b45b77761e8dfec30a029b7c7063055ca71157f84", + "sha256:22839d1d1eab9e4b427828a88a22beb86f67c14d8ff81175505f1cc8493f3500", + "sha256:22dda2799c8d39041d731e02bf7690f0ef34f1691d9ac9dfcb98dd1e94c8b058", + "sha256:2376d8cf506dffd0e5f2391025ae8675b09711016656590cb03b55894161fcfa", + "sha256:24197ba3114cc85ddd4091e19b2ddc62650f2e4a899e51b074dfd52d56cf8c72", + "sha256:24416bb5e221e29ddf8aac5b97e94e635ca2c5be44a1617ad6fe32556df44294", + "sha256:2631c9d7386bd2d4ce24ecc6ebf9ae90b3efd713d588d90504eaa77fec4dba01", + "sha256:28389a68981676bf74e2e199fe42f35d1aa27a9c98e3a03e6f58d2d3d054afe1", + "sha256:2aee7594d2c2221c717a8e394bbed4740029df4c0211ceb0f04815686e99c795", + "sha256:2e430ac432f969ef21770645743611c1618362309e3ad7cab45acd1ad1a540ff", + "sha256:2e912b282466444023610e4498e3795c10e7cfd641744524876239fcf01d538d", + "sha256:30ffc046ebddccb3c4cac72c1a3e1bc343492336f3ca86d24672e90ccc5e788a", + "sha256:319c206e83e46ec2421b25b300c8482b6fe8a018baca246be308c736d9dab267", + "sha256:326b8a079a9afcac0575971e56dabdf7abb2ea89a893e6949b77adfeb058b50e", + "sha256:36ee0115b9edca904153a66bb74a9ff1ce38caff015de94eadfb9ba8e6ecd317", + "sha256:3e26e64f42bce5ddf9002092b2c37b13071c2e6413d5c05f9fa9de58ed2f7749", + "sha256:4ea99e64b2ad2635e0f0597b63f5ea6c374791ff2fa81cdd4bad8ed9f047f56f", + "sha256:501a1576716032cc6d48c7c47bcdc42d682273415a8f2908e7e72cb4625801f3", + "sha256:54c8cee662b5f8c30ad7eedfc26123f845f007798e4ff1001d9528fe959fd23c", + "sha256:595bbcdbfc4a9c6989d7489dca8510cba053ff46b16c84ffd95ac8e90711d419", + "sha256:5b860055199aec8d6fe4dcee3c5196ce506ca198a50aab0059ffd26e8e815828", + "sha256:5c667b383529520b8dd6bd496fc318678320cb2a6062fdfe6d3618da6b8790f6", + "sha256:5fb475a4cdde582c9528bb412b98f899680492daaba318231e96f1a0a1bb0d53", + "sha256:607d12f0901f6419a8adceb139847c42c83864b85371f58270e42753f9780fa6", + "sha256:64c5b0f2b937fe40d0967516eee5504b23cb247b8b7ffeba7213a467d9646fdc", + "sha256:664380c7ed524a280b6a2d5d9126389c3e96cd6e88986cdb42ca72baa27421d6", + "sha256:6af871f70cfd5b528bd322c65793b5fd5659858cdfaa35fbe563fb99b667ed1f", + "sha256:6c89894cc6f6ddd993813e79244b36b215c14f65f9e4f1660b1f2ba9e5594b95", + "sha256:6dee0496d5f1a8f57f0f28a16f81a2033fc057a2cf9cd710742d11828f8c80e2", + "sha256:6e9a9f50892153bad5046c2a6df153224aa6f0573a5a8ab44fc54a1e886f6e21", + "sha256:712ba8722c0699daf186de089ddc4677651eb9875ed7447b2ad50697522cbdd9", + "sha256:717f185086bb9d817d4537dd18d5df5d657598cd00e6fc22e4d54d84de266c1d", + "sha256:71978ba778948760cff528235c951ea0ef7a4f9c84ac5a49975f8540f76c3f73", + "sha256:71af3766bb46738d12cc288d9b8de7ef6f79c31fd62757e2b8a505fe3680b27f", + "sha256:73a183042ae0918c82ce2df38c3db2409b0eeae88e3afdfc80fb67471a95b33b", + "sha256:7564525a4673fde53dee7d4c307a961c0951918f0b8c7f09b2c9e02067cf6504", + "sha256:76a59d1b63de859398bc7764c860a769499511463c1232155061fe0147f13e01", + "sha256:7e9905fc2dc1319e4c39837b906a024cf71b1261cc66b0cd89678f779c0c61f5", + "sha256:8112f640a4f7e7bf59f7cabf0d47a29b8977528c521d73a64d5cc9e99e48a174", + "sha256:835010cc17d0020e7931d39e487d72c8e01c98e669b6896a8b8c9aa8ca69a949", + "sha256:838dde2cb570cfbb4cab8a876a0974e8b90973ea40b3ac27a79b8a74c8a2db15", + "sha256:8d31dd0245d88cf7239e96e8f2a99f815b06e458a5854150f8e6f0e61618d41b", + "sha256:96b34830bd6825ca0220bf005ea99ac83eb9ce51301ddb882dcf613ae6cd95fb", + "sha256:96c8ff1e1dd680e38af0887927cab407a4e51d84a5f02ae3d6eb87233036c763", + "sha256:9a7ee79183f0b17dcede8b6723e7da2ded529cf159a878214be9a5d3098f5b1e", + "sha256:a3e2aff8b822ab0e0bdbed9f50494b3a35629c4b9488ae391659973a37a9f53f", + "sha256:a4f3ab9eb8ab2d585ece959c48d234f7b39ac0ca1954a34d8b8e58a52064bdb3", + "sha256:a8b54949267bd5704324397efe9fbb6aa306466dee067550964e994d309db5f1", + "sha256:a96198d5d26f40557d986c1253bfe0e02d18c9d9b93cf389daf1a3c9f7c755fa", + "sha256:aebbd47df77190ada603157f0b3670d578c110c31746ecc5875c394fdcc59a99", + "sha256:af1107299cef049ad00a93df4809517be432283a0847bcae48343ebe5ea340dc", + "sha256:b63465b53baeaf2122a337d4ab57d6bbdd09fcadceb17a974cfa8a0300ad9c67", + "sha256:ba1c779b45a399cc25f511c681016626f69e51e45b9d350d7581998722825af9", + "sha256:bce00f3b1f7f644faae89677ca68645ed5365f1c7f874fdd5ebf730a69640d38", + "sha256:bfdf419bf5d3644f94cd7052954fc233522f5a1b371fc0b00219ebd9c14d5798", + "sha256:c1caa5763d1770216596e0a71b5567f27aac28c95992110212c108ec74589a48", + "sha256:c3e4e1f7b08d1ec6b685ccd3e2d762219c550164fbf524498532e39f9413436e", + "sha256:c85ab016e96a975afbdb9d49ca90f3bca9920ef27c64300843fe91c3d59d8d20", + "sha256:c924deab8105f86980983eced740433fb7554a7f66db73991affa4eda99d5402", + "sha256:d4f818f6371970d6a5d1e42878389bbfb69dcde631e4bbac5ec1cb11158565ca", + "sha256:d920401941cb898ef089422e889759dd403309eb370d0e54f1bdf6ca07fef603", + "sha256:da045bd1147d12bd43fb032296640a7cc17a7f2eaba67495988362e99db24fd2", + "sha256:dc3192a81ecd5ff954cecd690327badd5a84d00b877e1573f7c9097ce13e5bfb", + "sha256:ddae504cfb556fe220efae65e35be63cd11e3c314b202723fc2119ce19f0ca2e", + "sha256:de4544b1fb29cf14870c4e2b8a897c0242449f5dcebd3e0366aa0aa3cf58a23a", + "sha256:dea360778e0668a7ad25d7727d03364de8a45bfd5d808f81253516b9f2217765", + "sha256:e2254fe137c4a360b0a13173a56444f756252c9283ba4d267ca8e9081cd140ea", + "sha256:e64f0421892a207d3780903085c1b04efeb53b16803b23d947de5a7261b71355", + "sha256:e97a29b37830ba1262d8dfd48ddb5b28ad4d3ebecc5d93a9c7591d98641ec737", + "sha256:eacbcf30efaca7dc5cb264228ffecdb95fdb1e715b1ec937c0ce6b734161e0c8", + "sha256:eee5ff934b0c9f4537ff9596169d56cab1890918004791a7a06b879b3ba2a7ef", + "sha256:eff6bac402719c14e17efe845d6b98593c56c843aca6def72080fbede755fd1f", + "sha256:f10954b233d4df5cc3137ffa5ced97f8894152df817e5d149bf05a0ef2ab8134", + "sha256:f23bb1a7a6e8e8b612a164fdd08e683bcc16c76f928d6dbb7bdbee2374fbfee6", + "sha256:f494c01b28645c431239863cb17af8b8d15b93b0d697a0320d5dd34cd9d7c2fa", + "sha256:f6a071d2c3d39b4104f94fc08ab349e9b19b951ad4b8e3b6d7ea92d6ef7ccaf8", + "sha256:f736f54565f8dd7e3ab664fef2bc461d7593a389a7f28d4904af8d55a91bd55f", + "sha256:f8981a94a27ac520a398302afb74ae2c0be1c3d2d215c75c582186a006c9e7b0", + "sha256:fd24996e12e1ba7c397c44be75ca299da14cde34d74bc5508cce233676cc68d0", + "sha256:ff54340fc1129e8e181827e2234af3ff659b4f17d9bbe77f43bc19e6577fadec" + ], + "markers": "python_version >= '3.8'", + "version": "==1.12.1" } }, "develop": { @@ -1791,43 +1790,14 @@ "markers": "python_version >= '3.8'", "version": "==2.3.1" }, - "black": { - "hashes": [ - "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6", - "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e", - "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f", - "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018", - "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e", - "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd", - "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4", - "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed", - "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2", - "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42", - "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af", - "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb", - "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368", - "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb", - "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af", - "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed", - "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47", - "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2", - "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a", - "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c", - "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920", - "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==24.8.0" - }, "boto3": { "hashes": [ - "sha256:97fcc1a14cbc759e4ba9535ced703a99fcf652c9c4b8dfcd06f292c80551684b", - "sha256:be7807f30f26d6c0057e45cfd09dad5968e664488bf4f9138d0bb7a0f6d8ed40" + "sha256:5df4e2cbe3409db07d3a0d8d63d5220ce3202a78206ad87afdbb41519b26ce45", + "sha256:b1cfad301184cdd44dfd4805187ccab12de8dd28dd12a11a5cfdace17918c6de" ], "index": "pypi", "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "boto3-stubs": { "extras": [ @@ -1838,17 +1808,16 @@ "sha256:55dc1e9b9a6c8456d18bd6747ecf30283d84da4c05d321e2233413b009e2a711", "sha256:cece5d8ed36a5c587bfdcb97a1262678023f1a43c0aad54eeab9f389aefa99ec" ], - "index": "pypi", "markers": "python_version >= '3.8'", "version": "==1.35.25" }, "botocore": { "hashes": [ - "sha256:1e59b0f14f4890c4f70bd6a58a634b9464bed1c4c6171f87c8795d974ade614b", - "sha256:eb9ccc068255cc3d24c36693fda6aec7786db05ae6c2b13bcba66dce6a13e2e3" + "sha256:76c5706b2c6533000603ae8683a297c887abbbaf6ee31e1b2e2863b74b2989bc", + "sha256:e58d60260abf10ccc4417967923117c9902a6a0cff9fddb6ea7ff42dc1bd4630" ], "markers": "python_version >= '3.8'", - "version": "==1.35.24" + "version": "==1.35.25" }, "botocore-stubs": { "hashes": [ @@ -1970,14 +1939,6 @@ "markers": "python_full_version >= '3.7.0'", "version": "==3.3.2" }, - "click": { - "hashes": [ - "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", - "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de" - ], - "markers": "python_version >= '3.7'", - "version": "==8.1.7" - }, "coverage": { "hashes": [ "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca", @@ -2138,19 +2099,19 @@ }, "django-stubs": { "hashes": [ - "sha256:78e3764488fdfd2695f12502136548ec22f8d4b1780541a835042b8238d11514", - "sha256:c2502f5ecbae50c68f9a86d52b5b2447d8648fd205036dad0ccb41e19a445927" + "sha256:86128c228b65e6c9a85e5dc56eb1c6f41125917dae0e21e6cfecdf1b27e630c5", + "sha256:b98d49a80aa4adf1433a97407102d068de26c739c405431d93faad96dd282c40" ], "markers": "python_version >= '3.8'", - "version": "==5.0.4" + "version": "==5.1.0" }, "django-stubs-ext": { "hashes": [ - "sha256:85da065224204774208be29c7d02b4482d5a69218a728465c2fbe41725fdc819", - "sha256:910cbaff3d1e8e806a5c27d5ddd4088535aae8371ea921b7fd680fdfa5f14e30" + "sha256:a455fc222c90b30b29ad8c53319559f5b54a99b4197205ddbb385aede03b395d", + "sha256:ed7d51c0b731651879fc75f331fb0806d98b67bfab464e96e2724db6b46ef926" ], "markers": "python_version >= '3.8'", - "version": "==5.0.4" + "version": "==5.1.0" }, "djangorestframework-stubs": { "hashes": [ @@ -2194,15 +2155,6 @@ "markers": "python_version >= '3.8'", "version": "==3.16.1" }, - "flake8": { - "hashes": [ - "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", - "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.1'", - "version": "==7.1.1" - }, "freezegun": { "hashes": [ "sha256:b29dedfcda6d5e8e083ce71b2b542753ad48cfec44037b3fc79702e2980a89e9", @@ -2245,15 +2197,6 @@ "markers": "python_version >= '3.10'", "version": "==8.27.0" }, - "isort": { - "hashes": [ - "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109", - "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6" - ], - "index": "pypi", - "markers": "python_full_version >= '3.8.0'", - "version": "==5.13.2" - }, "jedi": { "hashes": [ "sha256:cf0496f3651bc65d7174ac1b7d043eff454892c708a87d1b683e57b569927ffd", @@ -2344,14 +2287,6 @@ "markers": "python_version >= '3.8'", "version": "==0.1.7" }, - "mccabe": { - "hashes": [ - "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", - "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" - ], - "markers": "python_version >= '3.6'", - "version": "==0.7.0" - }, "mypy": { "hashes": [ "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", @@ -2409,14 +2344,6 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'", "version": "==1.9.1" }, - "packaging": { - "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" - ], - "markers": "python_version >= '3.8'", - "version": "==24.1" - }, "parso": { "hashes": [ "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", @@ -2425,14 +2352,6 @@ "markers": "python_version >= '3.6'", "version": "==0.8.4" }, - "pathspec": { - "hashes": [ - "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", - "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712" - ], - "markers": "python_version >= '3.8'", - "version": "==0.12.1" - }, "pexpect": { "hashes": [ "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", @@ -2488,14 +2407,6 @@ "markers": "python_version >= '3.8'", "version": "==2.12.1" }, - "pyflakes": { - "hashes": [ - "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", - "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a" - ], - "markers": "python_version >= '3.8'", - "version": "==3.2.0" - }, "pygments": { "hashes": [ "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", diff --git a/care/abdm/api/serializers/abha_number.py b/care/abdm/api/serializers/abha_number.py index 33af8c3c18..c166d57228 100644 --- a/care/abdm/api/serializers/abha_number.py +++ b/care/abdm/api/serializers/abha_number.py @@ -4,7 +4,7 @@ from care.abdm.models import AbhaNumber from care.facility.api.serializers.patient import PatientDetailSerializer from care.facility.models import PatientRegistration -from care.utils.serializer.external_id_field import ExternalIdSerializerField +from care.utils.serializers.fields import ExternalIdSerializerField class AbhaNumberSerializer(serializers.ModelSerializer): diff --git a/care/audit_log/helpers.py b/care/audit_log/helpers.py index 04e89814bc..42252ffaa6 100644 --- a/care/audit_log/helpers.py +++ b/care/audit_log/helpers.py @@ -1,3 +1,4 @@ +# ruff: noqa: SLF001 import re from fnmatch import fnmatch from functools import lru_cache @@ -14,7 +15,7 @@ def remove_non_member_fields(d: dict): def instance_finder(v): return isinstance( v, - (list, dict, set), + list | dict | set, ) @@ -41,10 +42,9 @@ class Search(NamedTuple): def _make_search(item): splits = item.split(":") - if len(splits) == 2: + if len(splits) == 2: # noqa: PLR2004 return Search(type=splits[0], value=splits[1]) - else: - return Search(type="plain", value=splits[0]) + return Search(type="plain", value=splits[0]) def candidate_in_scope( @@ -62,7 +62,7 @@ def candidate_in_scope( search_candidate = candidate if is_application: splits = candidate.split(".") - if len(splits) == 2: + if len(splits) == 2: # noqa: PLR2004 app_label, model_name = splits search_candidate = app_label @@ -91,12 +91,11 @@ def exclude_model(model_name): ): return True - if candidate_in_scope( - model_name, settings.AUDIT_LOG["models"]["exclude"]["models"] - ): - return True - - return False + return bool( + candidate_in_scope( + model_name, settings.AUDIT_LOG["models"]["exclude"]["models"] + ) + ) class MetaDataContainer(dict): diff --git a/care/audit_log/middleware.py b/care/audit_log/middleware.py index 35f23efd4b..f2c27d376b 100644 --- a/care/audit_log/middleware.py +++ b/care/audit_log/middleware.py @@ -15,6 +15,7 @@ class RequestInformation(NamedTuple): response: HttpResponse | None exception: Exception | None + logger = logging.getLogger(__name__) @@ -50,7 +51,7 @@ def save(request, response=None, exception=None): if not dal_request_id: dal_request_id = ( f"{request.method.lower()}::" - f"{md5(request.path.lower().encode('utf-8')).hexdigest()}::" + f"{md5(request.path.lower().encode('utf-8')).hexdigest()}::" # noqa: S324 f"{uuid.uuid4().hex}" ) request.dal_request_id = dal_request_id @@ -69,8 +70,7 @@ def get_current_user(): environ = RequestInformation(*AuditLogMiddleware.thread.__dal__) if isinstance(environ.request.user, AnonymousUser): return None - else: - return environ.request.user + return environ.request.user @staticmethod def get_current_request(): @@ -85,14 +85,14 @@ def __call__(self, request: HttpRequest): response: HttpResponse = self.get_response(request) self.save(request, response) - if request.user: - current_user_str = f"{request.user.id}|{request.user}" - else: - current_user_str = None + current_user_str = f"{request.user.id}|{request.user}" if request.user else None logger.info( - f"{request.method} {request.path} {response.status_code} " - f"User:[{current_user_str}]" + "%s %s %s User:[%s]", + request.method, + request.path, + response.status_code, + current_user_str, ) return response diff --git a/care/audit_log/receivers.py b/care/audit_log/receivers.py index 9b812c42a0..95c28d1abb 100644 --- a/care/audit_log/receivers.py +++ b/care/audit_log/receivers.py @@ -1,3 +1,4 @@ +# ruff: noqa: SLF001 import json import logging from typing import NamedTuple @@ -22,6 +23,7 @@ logger = logging.getLogger(__name__) + class Event(NamedTuple): model: str actor: AbstractUser @@ -42,7 +44,7 @@ def pre_save_signal(sender, instance, **kwargs) -> None: model_name = get_model_name(instance) if exclude_model(model_name): - logger.debug(f"{model_name} ignored as per settings") + logger.debug("%s ignored as per settings", model_name) return get_or_create_meta(instance) @@ -61,8 +63,9 @@ def pre_save_signal(sender, instance, **kwargs) -> None: changes = {} if operation not in {Operation.INSERT, Operation.DELETE}: - old, new = remove_non_member_fields(pre.__dict__), remove_non_member_fields( - instance.__dict__ + old, new = ( + remove_non_member_fields(pre.__dict__), + remove_non_member_fields(instance.__dict__), ) try: @@ -107,7 +110,7 @@ def _post_processor(instance, event: Event | None, operation: Operation): model_name = get_model_name(instance) if not event and operation != Operation.DELETE: - logger.debug(f"Event not received for {operation}. Ignoring.") + logger.debug("Event not received for %s. Ignoring.", operation) return try: @@ -118,11 +121,17 @@ def _post_processor(instance, event: Event | None, operation: Operation): else: changes = json.dumps(event.changes if event else {}, cls=LogJsonEncoder) except Exception: - logger.warning(f"Failed to log {event}", exc_info=True) + logger.warning("Failed to log %s", event, exc_info=True) return logger.info( - f"AUDIT_LOG::{request_id}|{actor}|{operation.value}|{model_name}|ID:{instance.pk}|{changes}" + "AUDIT_LOG::%s|%s|%s|%s|ID:%s|%s", + request_id, + actor, + operation.value, + model_name, + instance.pk, + changes, ) @@ -137,7 +146,7 @@ def post_save_signal(sender, instance, created, update_fields: frozenset, **kwar model_name = get_model_name(instance) if exclude_model(model_name): - logger.debug(f"Ignoring {model_name}.") + logger.debug("Ignoring %s.", model_name) return operation = Operation.INSERT if created else Operation.UPDATE @@ -158,7 +167,7 @@ def post_delete_signal(sender, instance, **kwargs) -> None: model_name = get_model_name(instance) if exclude_model(model_name): - logger.debug(f"Ignoring {model_name}.") + logger.debug("Ignoring %s.", model_name) return event = instance._meta.dal.event diff --git a/care/facility/admin.py b/care/facility/admin.py index 55a9bdff65..779a313acb 100644 --- a/care/facility/admin.py +++ b/care/facility/admin.py @@ -55,7 +55,7 @@ class DistrictFilter(SimpleListFilter): def lookups(self, request, model_admin): district = Facility.objects.values_list("district__name", flat=True) - return list(map(lambda x: (x, x), set(district))) + return [(x, x) for x in set(district)] def queryset(self, request, queryset): if self.value() is None: @@ -63,22 +63,6 @@ def queryset(self, request, queryset): return queryset.filter(district__name=self.value()) -# class LocalBodyFilter(SimpleListFilter): -# """Local body filter""" - -# title = "Local body" -# parameter_name = "local_body" - -# def lookups(self, request, model_admin): -# local_body = Facility.objects.values_list("local_body__name", flat=True) -# return list(map(lambda x: (x, x), set(local_body))) - -# def queryset(self, request, queryset): -# if self.value() is None: -# return queryset -# return queryset.filter(local_body__name=self.value()) - - class StateFilter(SimpleListFilter): """State filter""" @@ -87,7 +71,7 @@ class StateFilter(SimpleListFilter): def lookups(self, request, model_admin): state = Facility.objects.values_list("state__name", flat=True) - return list(map(lambda x: (x, x), set(state))) + return [(x, x) for x in set(state)] def queryset(self, request, queryset): if self.value() is None: @@ -222,7 +206,7 @@ class FacilityFeatureFlagForm(forms.ModelForm): ) class Meta: - fields = "__all__" + fields = ("flag", "facility") model = FacilityFlag form = FacilityFeatureFlagForm diff --git a/care/facility/api/serializers/ambulance.py b/care/facility/api/serializers/ambulance.py index 1668922158..7cdbf13922 100644 --- a/care/facility/api/serializers/ambulance.py +++ b/care/facility/api/serializers/ambulance.py @@ -10,7 +10,7 @@ class AmbulanceDriverSerializer(serializers.ModelSerializer): class Meta: model = AmbulanceDriver - exclude = TIMESTAMP_FIELDS + ("ambulance",) + exclude = (*TIMESTAMP_FIELDS, "ambulance") class AmbulanceSerializer(serializers.ModelSerializer): @@ -36,9 +36,8 @@ class Meta: def validate(self, obj): validated = super().validate(obj) if not validated.get("price_per_km") and not validated.get("has_free_service"): - raise ValidationError( - "The ambulance must provide a price or be marked as free" - ) + msg = "The ambulance must provide a price or be marked as free" + raise ValidationError(msg) return validated def create(self, validated_data): @@ -46,7 +45,7 @@ def create(self, validated_data): drivers = validated_data.pop("drivers", []) validated_data.pop("created_by", None) - ambulance = super(AmbulanceSerializer, self).create(validated_data) + ambulance = super().create(validated_data) for d in drivers: d["ambulance"] = ambulance @@ -55,8 +54,7 @@ def create(self, validated_data): def update(self, instance, validated_data): validated_data.pop("drivers", []) - ambulance = super(AmbulanceSerializer, self).update(instance, validated_data) - return ambulance + return super().update(instance, validated_data) class DeleteDriverSerializer(serializers.Serializer): diff --git a/care/facility/api/serializers/asset.py b/care/facility/api/serializers/asset.py index c1543d73c4..f403361e1a 100644 --- a/care/facility/api/serializers/asset.py +++ b/care/facility/api/serializers/asset.py @@ -1,5 +1,3 @@ -from datetime import datetime - from django.core.cache import cache from django.db import models, transaction from django.db.models import F, Value @@ -38,9 +36,9 @@ from care.utils.assetintegration.hl7monitor import HL7MonitorAsset from care.utils.assetintegration.onvif import OnvifAsset from care.utils.assetintegration.ventilator import VentilatorAsset +from care.utils.models.validators import MiddlewareDomainAddressValidator from care.utils.queryset.facility import get_facility_queryset -from config.serializers import ChoiceField -from config.validators import MiddlewareDomainAddressValidator +from care.utils.serializers.fields import ChoiceField class AssetLocationSerializer(ModelSerializer): @@ -125,9 +123,7 @@ def update(self, instance, validated_data): ) edit.save() - updated_instance = super().update(instance, validated_data) - - return updated_instance + return super().update(instance, validated_data) @extend_schema_field( @@ -159,10 +155,7 @@ class AssetSerializer(ModelSerializer): class Meta: model = Asset exclude = ("deleted", "external_id", "current_location") - read_only_fields = TIMESTAMP_FIELDS + ( - "resolved_middleware", - "latest_status", - ) + read_only_fields = (*TIMESTAMP_FIELDS, "resolved_middleware", "latest_status") def validate_qr_code_id(self, value): value = value or None # treat empty string as null @@ -195,15 +188,14 @@ def validate(self, attrs): ): del attrs["warranty_amc_end_of_validity"] - elif warranty_amc_end_of_validity < datetime.now().date(): - raise ValidationError( - "Warranty/AMC end of validity cannot be in the past" - ) + elif warranty_amc_end_of_validity < now().date(): + msg = "Warranty/AMC end of validity cannot be in the past" + raise ValidationError(msg) # validate that last serviced date is not in the future - if attrs.get("last_serviced_on"): - if attrs["last_serviced_on"] > datetime.now().date(): - raise ValidationError("Last serviced on cannot be in the future") + if attrs.get("last_serviced_on") and attrs["last_serviced_on"] > now().date(): + msg = "Last serviced on cannot be in the future" + raise ValidationError(msg) # only allow setting asset class on creation (or updation if asset class is not set) if ( @@ -250,9 +242,8 @@ def validate(self, attrs): .first() ) if asset_using_ip: - raise ValidationError( - f"IP Address {ip_address} is already in use by {asset_using_ip.name} asset" - ) + msg = f"IP Address {ip_address} is already in use by {asset_using_ip.name} asset" + raise ValidationError(msg) return super().validate(attrs) @@ -361,7 +352,7 @@ def to_representation(self, instance: Asset): data["ip_address"] = instance.meta.get("local_ip_address") if camera_access_key := instance.meta.get("camera_access_key"): values = camera_access_key.split(":") - if len(values) == 3: + if len(values) == 3: # noqa: PLR2004 data["username"], data["password"], data["access_key"] = values return data @@ -416,7 +407,7 @@ class Meta: class AssetActionSerializer(Serializer): - def actionChoices(): + def action_choices(): actions = [ OnvifAsset.OnvifActions, HL7MonitorAsset.HL7MonitorActions, @@ -428,7 +419,7 @@ def actionChoices(): return choices type = ChoiceField( - choices=actionChoices(), + choices=action_choices(), required=True, ) data = JSONField(required=False) diff --git a/care/facility/api/serializers/bed.py b/care/facility/api/serializers/bed.py index ea66efd499..41597e186d 100644 --- a/care/facility/api/serializers/bed.py +++ b/care/facility/api/serializers/bed.py @@ -30,8 +30,7 @@ from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.queryset.consultation import get_consultation_queryset from care.utils.queryset.facility import get_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class BedSerializer(ModelSerializer): @@ -51,8 +50,10 @@ def validate_name(self, value): return value.strip() if value else value def validate_number_of_beds(self, value): - if value > 100: - raise ValidationError("Cannot create more than 100 beds at once.") + max_beds = 100 + if value > max_beds: + msg = f"Cannot create more than {max_beds} beds at once." + raise ValidationError(msg) return value class Meta: @@ -152,10 +153,11 @@ def get_patient(self, obj): ).first() if patient: return PatientListSerializer(patient).data + return None class Meta: model = AssetBed - exclude = ("external_id", "id") + TIMESTAMP_FIELDS + exclude = ("external_id", "id", *TIMESTAMP_FIELDS) class ConsultationBedSerializer(ModelSerializer): @@ -179,7 +181,7 @@ class Meta: exclude = ("deleted", "external_id") read_only_fields = TIMESTAMP_FIELDS - def validate(self, attrs): + def validate(self, attrs): # noqa: PLR0912 if "consultation" not in attrs: raise ValidationError({"consultation": "This field is required."}) if "bed" not in attrs: @@ -192,7 +194,8 @@ def validate(self, attrs): facilities = get_facility_queryset(user) if not facilities.filter(id=bed.facility_id).exists(): - raise ValidationError("You do not have access to this facility") + msg = "You do not have access to this facility" + raise ValidationError(msg) permitted_consultations = get_consultation_queryset(user).select_related( "patient" @@ -205,17 +208,20 @@ def validate(self, attrs): or consultation.discharge_date or consultation.death_datetime ): - raise ValidationError("Patient not active") + msg = "Patient not active" + raise ValidationError(msg) # bed validations if consultation.facility_id != bed.facility_id: - raise ValidationError("Consultation and bed are not in the same facility") + msg = "Consultation and bed are not in the same facility" + raise ValidationError(msg) if ( ConsultationBed.objects.filter(bed=bed, end_date__isnull=True) .exclude(consultation=consultation) .exists() ): - raise ValidationError("Bed is already in use") + msg = "Bed is already in use" + raise ValidationError(msg) # check whether the same set of bed and assets are already assigned current_consultation_bed = consultation.current_bed @@ -230,7 +236,8 @@ def validate(self, attrs): ) == set(attrs.get("assets", [])) ): - raise ValidationError("These set of bed and assets are already assigned") + msg = "These set of bed and assets are already assigned" + raise ValidationError(msg) # date validations # note: end_date is for setting end date on current instance @@ -323,10 +330,11 @@ def create(self, validated_data) -> ConsultationBed: ) not_found_assets = set(assets_ids) - set(assets) if not_found_assets: - raise ValidationError( + msg = ( "Some assets are not available - " f"{' ,'.join([str(x) for x in not_found_assets])}" ) + raise ValidationError(msg) obj: ConsultationBed = super().create(validated_data) if assets_ids: asset_objects = Asset.objects.filter(external_id__in=assets_ids).only( diff --git a/care/facility/api/serializers/consultation_diagnosis.py b/care/facility/api/serializers/consultation_diagnosis.py index be0e64676f..a6ccd6cd4c 100644 --- a/care/facility/api/serializers/consultation_diagnosis.py +++ b/care/facility/api/serializers/consultation_diagnosis.py @@ -14,7 +14,8 @@ class ConsultationCreateDiagnosisSerializer(serializers.ModelSerializer): def validate_verification_status(self, value): if value in INACTIVE_CONDITION_VERIFICATION_STATUSES: - raise serializers.ValidationError("Verification status not allowed") + msg = "Verification status not allowed" + raise serializers.ValidationError(msg) return value class Meta: @@ -54,7 +55,8 @@ def get_consultation_external_id(self): def validate_diagnosis(self, value): if self.instance and value != self.instance.diagnosis: - raise serializers.ValidationError("Diagnosis cannot be changed") + msg = "Diagnosis cannot be changed" + raise serializers.ValidationError(msg) if ( not self.instance @@ -63,15 +65,15 @@ def validate_diagnosis(self, value): diagnosis=value, ).exists() ): - raise serializers.ValidationError( - "Diagnosis already exists for consultation" - ) + msg = "Diagnosis already exists for consultation" + raise serializers.ValidationError(msg) return value def validate_verification_status(self, value): if not self.instance and value in INACTIVE_CONDITION_VERIFICATION_STATUSES: - raise serializers.ValidationError("Verification status not allowed") + msg = "Verification status not allowed" + raise serializers.ValidationError(msg) return value def validate_is_principal(self, value): @@ -87,9 +89,8 @@ def validate_is_principal(self, value): qs = qs.exclude(id=self.instance.id) if qs.exists(): - raise serializers.ValidationError( - "Consultation already has a principal diagnosis. Unset the existing principal diagnosis first." - ) + msg = "Consultation already has a principal diagnosis. Unset the existing principal diagnosis first." + raise serializers.ValidationError(msg) return value diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 1f4639ec0d..92d06c3c5c 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -1,4 +1,5 @@ from datetime import timedelta +from typing import TYPE_CHECKING from django.db import transaction from django.utils import timezone @@ -16,11 +17,13 @@ from care.facility.models.daily_round import DailyRound from care.facility.models.notification import Notification from care.facility.models.patient_base import SuggestionChoices -from care.facility.models.patient_consultation import PatientConsultation from care.users.api.serializers.user import UserBaseMinimumSerializer from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField + +if TYPE_CHECKING: + from care.facility.models.patient_consultation import PatientConsultation class DailyRoundSerializer(serializers.ModelSerializer): @@ -102,8 +105,6 @@ class DailyRoundSerializer(serializers.ModelSerializer): last_edited_by = UserBaseMinimumSerializer(read_only=True) created_by = UserBaseMinimumSerializer(read_only=True) - # bed_object = BedSerializer(read_only=True) - class Meta: model = DailyRound read_only_fields = ( @@ -120,7 +121,8 @@ def validate_bp(self, value): if value is not None: sys, dia = value.get("systolic"), value.get("diastolic") if sys is not None and dia is not None and sys < dia: - raise ValidationError("Systolic must be greater than diastolic") + msg = "Systolic must be greater than diastolic" + raise ValidationError(msg) return value def update(self, instance, validated_data): @@ -295,24 +297,22 @@ def validate(self, attrs): {"consultation": ["Discharged Consultation data cannot be updated"]} ) - if "action" in validated: - if validated["action"] == PatientRegistration.ActionEnum.REVIEW: - if "consultation__review_interval" not in validated: - raise ValidationError( - { - "review_interval": [ - "This field is required as the patient has been requested Review." - ] - } - ) - if validated["consultation__review_interval"] <= 0: - raise ValidationError( - { - "review_interval": [ - "This field value is must be greater than 0." - ] - } - ) + if ( + "action" in validated + and validated["action"] == PatientRegistration.ActionEnum.REVIEW + ): + if "consultation__review_interval" not in validated: + raise ValidationError( + { + "review_interval": [ + "This field is required as the patient has been requested Review." + ] + } + ) + if validated["consultation__review_interval"] <= 0: + raise ValidationError( + {"review_interval": ["This field value is must be greater than 0."]} + ) if "bed" in validated: external_id = validated.pop("bed")["external_id"] @@ -327,5 +327,6 @@ def validate(self, attrs): def validate_taken_at(self, value): if value and value > timezone.now(): - raise serializers.ValidationError("Cannot create an update in the future") + msg = "Cannot create an update in the future" + raise serializers.ValidationError(msg) return value diff --git a/care/facility/api/serializers/encounter_symptom.py b/care/facility/api/serializers/encounter_symptom.py index 858ab7f9c8..d669dd0aab 100644 --- a/care/facility/api/serializers/encounter_symptom.py +++ b/care/facility/api/serializers/encounter_symptom.py @@ -33,7 +33,8 @@ class Meta: def validate_onset_date(self, value): if value and value > now(): - raise serializers.ValidationError("Onset date cannot be in the future") + msg = "Onset date cannot be in the future" + raise serializers.ValidationError(msg) return value def validate(self, attrs): @@ -49,11 +50,10 @@ def validate(self, attrs): if self.instance else validated_data.get("onset_date") ) - if cure_date := validated_data.get("cure_date"): - if cure_date < onset_date: - raise serializers.ValidationError( - {"cure_date": "Cure date should be after onset date"} - ) + if validated_data.get("cure_date") and validated_data["cure_date"] < onset_date: + raise serializers.ValidationError( + {"cure_date": "Cure date should be after onset date"} + ) if validated_data.get("symptom") != Symptom.OTHERS and validated_data.get( "other_symptom" diff --git a/care/facility/api/serializers/facility.py b/care/facility/api/serializers/facility.py index 92dea0870a..bd42b9bba4 100644 --- a/care/facility/api/serializers/facility.py +++ b/care/facility/api/serializers/facility.py @@ -14,12 +14,11 @@ ) from care.utils.file_uploads.cover_image import upload_cover_image from care.utils.models.validators import ( + MiddlewareDomainAddressValidator, cover_image_validator, custom_image_extension_validator, ) -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField -from config.validators import MiddlewareDomainAddressValidator +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField User = get_user_model() @@ -97,12 +96,7 @@ class FacilitySerializer(FacilityBasicInfoSerializer): """Serializer for facility.models.Facility.""" facility_type = ChoiceField(choices=FACILITY_TYPES) - # A valid location => { - # "latitude": 49.8782482189424, - # "longitude": 24.452545489 - # } read_cover_image_url = serializers.URLField(read_only=True) - # location = PointField(required=False) features = serializers.ListField( child=serializers.ChoiceField(choices=FEATURE_CHOICES), required=False, @@ -155,7 +149,8 @@ class Meta: def validate_middleware_address(self, value): if not value: - raise serializers.ValidationError("Middleware Address is required") + msg = "Middleware Address is required" + raise serializers.ValidationError(msg) value = value.strip() if not value: return value @@ -166,9 +161,8 @@ def validate_middleware_address(self, value): def validate_features(self, value): if len(value) != len(set(value)): - raise serializers.ValidationError( - "Features should not contain duplicate values." - ) + msg = "Features should not contain duplicate values." + raise serializers.ValidationError(msg) return value def create(self, validated_data): @@ -211,12 +205,14 @@ def validate_spoke(self, spoke: Facility): hub: Facility = self.context["facility"] if hub == spoke: - raise serializers.ValidationError("Cannot set a facility as it's own spoke") + msg = "Cannot set a facility as it's own spoke" + raise serializers.ValidationError(msg) if FacilityHubSpoke.objects.filter( Q(hub=hub, spoke=spoke) | Q(hub=spoke, spoke=hub) ).first(): - raise serializers.ValidationError("Facility is already a spoke/hub") + msg = "Facility is already a spoke/hub" + raise serializers.ValidationError(msg) return spoke diff --git a/care/facility/api/serializers/facility_capacity.py b/care/facility/api/serializers/facility_capacity.py index 4db943c69c..983dbe1a94 100644 --- a/care/facility/api/serializers/facility_capacity.py +++ b/care/facility/api/serializers/facility_capacity.py @@ -2,7 +2,7 @@ from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.models import FacilityCapacity, RoomType -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class FacilityCapacitySerializer(serializers.ModelSerializer): @@ -44,4 +44,4 @@ def __init__(self, model, *args, **kwargs): super().__init__() class Meta: - exclude = TIMESTAMP_FIELDS + ("facility",) + exclude = (*TIMESTAMP_FIELDS, "facility") diff --git a/care/facility/api/serializers/file_upload.py b/care/facility/api/serializers/file_upload.py index 6afc7d8434..9f451aaf73 100644 --- a/care/facility/api/serializers/file_upload.py +++ b/care/facility/api/serializers/file_upload.py @@ -16,10 +16,10 @@ from care.users.api.serializers.user import UserBaseMinimumSerializer from care.users.models import User from care.utils.notification_handler import NotificationGenerator -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField -def check_permissions(file_type, associating_id, user, action="create"): +def check_permissions(file_type, associating_id, user, action="create"): # noqa: PLR0911, PLR0912 try: if file_type == FileUpload.FileType.PATIENT.value: patient = PatientRegistration.objects.get(external_id=associating_id) @@ -27,17 +27,19 @@ def check_permissions(file_type, associating_id, user, action="create"): raise serializers.ValidationError( {"patient": "Cannot upload file for a discharged patient."} ) - if patient.assigned_to: - if user == patient.assigned_to: - return patient.id - if patient.last_consultation: - if patient.last_consultation.assigned_to: - if user == patient.last_consultation.assigned_to: - return patient.id + if patient.assigned_to and user == patient.assigned_to: + return patient.id + if ( + patient.last_consultation + and patient.last_consultation.assigned_to + and user == patient.last_consultation.assigned_to + ): + return patient.id if not has_facility_permission(user, patient.facility): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return patient.id - elif file_type == FileUpload.FileType.CONSULTATION.value: + if file_type == FileUpload.FileType.CONSULTATION.value: consultation = PatientConsultation.objects.get(external_id=associating_id) if consultation.discharge_date and not action == "read": raise serializers.ValidationError( @@ -45,19 +47,21 @@ def check_permissions(file_type, associating_id, user, action="create"): "consultation": "Cannot upload file for a discharged consultation." } ) - if consultation.patient.assigned_to: - if user == consultation.patient.assigned_to: - return consultation.id - if consultation.assigned_to: - if user == consultation.assigned_to: - return consultation.id + if ( + consultation.patient.assigned_to + and user == consultation.patient.assigned_to + ): + return consultation.id + if consultation.assigned_to and user == consultation.assigned_to: + return consultation.id if not ( has_facility_permission(user, consultation.patient.facility) or has_facility_permission(user, consultation.facility) ): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return consultation.id - elif file_type == FileUpload.FileType.CONSENT_RECORD.value: + if file_type == FileUpload.FileType.CONSENT_RECORD.value: consultation = PatientConsent.objects.get( external_id=associating_id ).consultation @@ -68,14 +72,14 @@ def check_permissions(file_type, associating_id, user, action="create"): } ) if ( - user == consultation.assigned_to - or user == consultation.patient.assigned_to + user in (consultation.assigned_to, 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: + msg = "No Permission" + raise Exception(msg) + if file_type == FileUpload.FileType.DISCHARGE_SUMMARY.value: consultation = PatientConsultation.objects.get(external_id=associating_id) if ( consultation.patient.assigned_to @@ -88,36 +92,39 @@ def check_permissions(file_type, associating_id, user, action="create"): has_facility_permission(user, consultation.patient.facility) or has_facility_permission(user, consultation.facility) ): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return consultation.external_id - elif file_type == FileUpload.FileType.SAMPLE_MANAGEMENT.value: + if file_type == FileUpload.FileType.SAMPLE_MANAGEMENT.value: sample = PatientSample.objects.get(external_id=associating_id) patient = sample.patient - if patient.assigned_to: - if user == patient.assigned_to: - return sample.id - if sample.consultation: - if sample.consultation.assigned_to: - if user == sample.consultation.assigned_to: - return sample.id - if sample.testing_facility: - if has_facility_permission( - user, - Facility.objects.get( - external_id=sample.testing_facility.external_id - ), - ): - return sample.id + if patient.assigned_to and user == patient.assigned_to: + return sample.id + if ( + sample.consultation + and sample.consultation.assigned_to + and user == sample.consultation.assigned_to + ): + return sample.id + if sample.testing_facility and has_facility_permission( + user, + Facility.objects.get(external_id=sample.testing_facility.external_id), + ): + return sample.id if not has_facility_permission(user, patient.facility): - raise Exception("No Permission") + msg = "No Permission" + raise Exception(msg) return sample.id - elif file_type == FileUpload.FileType.CLAIM.value or file_type == FileUpload.FileType.COMMUNICATION.value: + if file_type in ( + FileUpload.FileType.CLAIM.value, + FileUpload.FileType.COMMUNICATION.value, + ): return associating_id - else: - raise Exception("Undefined File Type") + msg = "Undefined File Type" + raise Exception(msg) - except Exception: - raise serializers.ValidationError({"permission": "denied"}) + except Exception as e: + raise serializers.ValidationError({"permission": "denied"}) from e class FileUploadCreateSerializer(serializers.ModelSerializer): @@ -248,7 +255,8 @@ def update(self, instance, validated_data): def validate(self, attrs): validated = super().validate(attrs) if validated.get("is_archived") and not validated.get("archive_reason"): - raise ValidationError("Archive reason must be specified.") + msg = "Archive reason must be specified." + raise ValidationError(msg) return validated diff --git a/care/facility/api/serializers/hospital_doctor.py b/care/facility/api/serializers/hospital_doctor.py index 540a46a38d..e53901c7bf 100644 --- a/care/facility/api/serializers/hospital_doctor.py +++ b/care/facility/api/serializers/hospital_doctor.py @@ -2,7 +2,7 @@ from care.facility.api.serializers import TIMESTAMP_FIELDS from care.facility.models import DOCTOR_TYPES, HospitalDoctors -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class HospitalDoctorSerializer(serializers.ModelSerializer): @@ -15,4 +15,4 @@ class Meta: "id", "area_text", ) - exclude = TIMESTAMP_FIELDS + ("facility", "external_id") + exclude = (*TIMESTAMP_FIELDS, "facility", "external_id") diff --git a/care/facility/api/serializers/inventory.py b/care/facility/api/serializers/inventory.py index e035137a5c..2172e19e24 100644 --- a/care/facility/api/serializers/inventory.py +++ b/care/facility/api/serializers/inventory.py @@ -68,10 +68,10 @@ def create(self, validated_data): try: item.allowed_units.get(id=unit.id) - except FacilityInventoryUnit.DoesNotExist: + except FacilityInventoryUnit.DoesNotExist as e: raise serializers.ValidationError( {"unit": ["Item cannot be measured with unit"]} - ) + ) from e multiplier = 1 @@ -80,10 +80,10 @@ def create(self, validated_data): multiplier = FacilityInventoryUnitConverter.objects.get( from_unit=unit, to_unit=item.default_unit ).multiplier - except FacilityInventoryUnitConverter.DoesNotExist: + except FacilityInventoryUnitConverter.DoesNotExist as e: raise serializers.ValidationError( {"item": ["Please Ask Admin to Add Conversion Metrics"]} - ) + ) from e validated_data["created_by"] = self.context["request"].user @@ -197,10 +197,10 @@ def create(self, validated_data): try: instance = super().create(validated_data) - except IntegrityError: + except IntegrityError as e: raise serializers.ValidationError( {"item": ["Item min quantity already set"]} - ) + ) from e try: summary_obj = FacilityInventorySummary.objects.get( @@ -214,9 +214,8 @@ def create(self, validated_data): return instance def update(self, instance, validated_data): - if "item" in validated_data: - if instance.item != validated_data["item"]: - raise serializers.ValidationError({"item": ["Item cannot be Changed"]}) + if "item" in validated_data and instance.item != validated_data["item"]: + raise serializers.ValidationError({"item": ["Item cannot be Changed"]}) item = validated_data["item"] diff --git a/care/facility/api/serializers/notification.py b/care/facility/api/serializers/notification.py index 7d7d424f87..e0fcd2b535 100644 --- a/care/facility/api/serializers/notification.py +++ b/care/facility/api/serializers/notification.py @@ -2,7 +2,7 @@ from care.facility.models.notification import Notification from care.users.api.serializers.user import UserBaseMinimumSerializer -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class NotificationSerializer(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient.py b/care/facility/api/serializers/patient.py index 7214be8d77..926e9b21bb 100644 --- a/care/facility/api/serializers/patient.py +++ b/care/facility/api/serializers/patient.py @@ -1,8 +1,6 @@ -import datetime - from django.conf import settings from django.db import transaction -from django.utils.timezone import make_aware, now +from django.utils.timezone import now from rest_framework import serializers from care.facility.api.serializers import TIMESTAMP_FIELDS @@ -46,8 +44,7 @@ from care.users.models import User from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class PatientMetaInfoSerializer(serializers.ModelSerializer): @@ -102,7 +99,7 @@ class Meta: "allergies", "external_id", ) - read_only = TIMESTAMP_FIELDS + ("death_datetime",) + read_only = (*TIMESTAMP_FIELDS, "death_datetime") class PatientContactDetailsSerializer(serializers.ModelSerializer): @@ -144,9 +141,6 @@ class MedicalHistorySerializer(serializers.Serializer): last_consultation = PatientConsultationSerializer(read_only=True) facility_object = FacilitySerializer(source="facility", read_only=True) - # nearest_facility_object = FacilitySerializer( - # source="nearest_facility", read_only=True - # ) source = ChoiceField( choices=PatientRegistration.SourceChoices, @@ -170,7 +164,7 @@ class MedicalHistorySerializer(serializers.Serializer): last_edited = UserBaseMinimumSerializer(read_only=True) created_by = UserBaseMinimumSerializer(read_only=True) vaccine_name = serializers.ChoiceField( - choices=PatientRegistration.vaccineChoices, required=False, allow_null=True + choices=PatientRegistration.VaccineChoices, required=False, allow_null=True ) assigned_to_object = UserBaseMinimumSerializer(source="assigned_to", read_only=True) @@ -189,24 +183,14 @@ class Meta: "external_id", ) include = ("contacted_patients",) - read_only = TIMESTAMP_FIELDS + ( + read_only = ( + *TIMESTAMP_FIELDS, "last_edited", "created_by", "is_active", "death_datetime", ) - # def get_last_consultation(self, obj): - # last_consultation = PatientConsultation.objects.filter(patient=obj).last() - # if not last_consultation: - # return None - # return PatientConsultationSerializer(last_consultation).data - - # def validate_facility(self, value): - # if value is not None and Facility.objects.filter(external_id=value).first() is None: - # raise serializers.ValidationError("facility not found") - # return value - def validate_countries_travelled(self, value): if not value: value = [] @@ -216,12 +200,14 @@ def validate_countries_travelled(self, value): def validate_date_of_birth(self, value): if value and value > now().date(): - raise serializers.ValidationError("Enter a valid DOB such that age > 0") + msg = "Enter a valid DOB such that age > 0" + raise serializers.ValidationError(msg) return value def validate_year_of_birth(self, value): if value and value > now().year: - raise serializers.ValidationError("Enter a valid year of birth") + msg = "Enter a valid year of birth" + raise serializers.ValidationError(msg) return value def validate(self, attrs): @@ -239,9 +225,11 @@ def validate(self, attrs): if validated.get("is_vaccinated"): if validated.get("number_of_doses") == 0: - raise serializers.ValidationError("Number of doses cannot be 0") + msg = "Number of doses cannot be 0" + raise serializers.ValidationError(msg) if validated.get("vaccine_name") is None: - raise serializers.ValidationError("Vaccine name cannot be null") + msg = "Vaccine name cannot be null" + raise serializers.ValidationError(msg) return validated @@ -276,9 +264,8 @@ def create(self, validated_data): # Authorisation checks end - if "srf_id" in validated_data: - if validated_data["srf_id"]: - self.check_external_entry(validated_data["srf_id"]) + if validated_data.get("srf_id"): + self.check_external_entry(validated_data["srf_id"]) validated_data["created_by"] = self.context["request"].user patient = super().create(validated_data) @@ -325,9 +312,11 @@ def update(self, instance, validated_data): external_id=external_id ).id - if "srf_id" in validated_data: - if instance.srf_id != validated_data["srf_id"]: - self.check_external_entry(validated_data["srf_id"]) + if ( + "srf_id" in validated_data + and instance.srf_id != validated_data["srf_id"] + ): + self.check_external_entry(validated_data["srf_id"]) patient = super().update(instance, validated_data) Disease.objects.filter(patient=patient).update(deleted=True) @@ -371,9 +360,7 @@ def update(self, instance, validated_data): class FacilityPatientStatsHistorySerializer(serializers.ModelSerializer): id = serializers.CharField(source="external_id", read_only=True) - entry_date = serializers.DateField( - default=make_aware(datetime.datetime.today()).date() - ) + entry_date = serializers.DateField(default=lambda: now().date()) facility = ExternalIdSerializerField( queryset=Facility.objects.all(), read_only=True ) @@ -430,7 +417,8 @@ class Meta: def validate_year_of_birth(self, value): if self.instance and self.instance.year_of_birth != value: - raise serializers.ValidationError("Year of birth does not match") + msg = "Year of birth does not match" + raise serializers.ValidationError(msg) return value def create(self, validated_data): @@ -531,13 +519,11 @@ def create(self, validated_data): if validated_data.get("reply_to"): reply_to_note = validated_data["reply_to"] if reply_to_note.thread != validated_data["thread"]: - raise serializers.ValidationError( - "Reply to note should be in the same thread" - ) + msg = "Reply to note should be in the same thread" + raise serializers.ValidationError(msg) if reply_to_note.consultation != validated_data.get("consultation"): - raise serializers.ValidationError( - "Reply to note should be in the same consultation" - ) + msg = "Reply to note should be in the same consultation" + raise serializers.ValidationError(msg) user = self.context["request"].user note = validated_data.get("note") diff --git a/care/facility/api/serializers/patient_consultation.py b/care/facility/api/serializers/patient_consultation.py index 98c81d68df..25405b6f7e 100644 --- a/care/facility/api/serializers/patient_consultation.py +++ b/care/facility/api/serializers/patient_consultation.py @@ -66,8 +66,7 @@ from care.utils.lock import Lock from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_home_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField MIN_ENCOUNTER_DATE = make_aware(settings.MIN_ENCOUNTER_DATE) @@ -196,7 +195,8 @@ def _lock_key(self, patient_id): class Meta: model = PatientConsultation - read_only_fields = TIMESTAMP_FIELDS + ( + read_only_fields = ( + *TIMESTAMP_FIELDS, "last_updated_by_telemedicine", "discharge_date", "last_edited_by", @@ -227,10 +227,9 @@ def update(self, instance, validated_data): raise ValidationError( {"consultation": ["Discharged Consultation data cannot be updated"]} ) - else: - instance.medico_legal_case = validated_data.pop("medico_legal_case") - instance.save() - return instance + instance.medico_legal_case = validated_data.pop("medico_legal_case") + instance.save() + return instance if instance.suggestion == SuggestionChoices.OP: instance.discharge_date = localtime(now()) @@ -259,9 +258,12 @@ def update(self, instance, validated_data): self.context["request"].user == instance.assigned_to ) - if "is_kasp" in validated_data: - if validated_data["is_kasp"] and (not instance.is_kasp): - validated_data["kasp_enabled_date"] = localtime(now()) + if ( + "is_kasp" in validated_data + and validated_data["is_kasp"] + and (not instance.is_kasp) + ): + validated_data["kasp_enabled_date"] = localtime(now()) _temp = instance.assigned_to @@ -275,18 +277,21 @@ def update(self, instance, validated_data): old=old_instance, ) - if "assigned_to" in validated_data: - if validated_data["assigned_to"] != _temp and validated_data["assigned_to"]: - NotificationGenerator( - event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, - caused_by=self.context["request"].user, - caused_object=instance, - facility=instance.patient.facility, - notification_mediums=[ - Notification.Medium.SYSTEM, - Notification.Medium.WHATSAPP, - ], - ).generate() + if ( + "assigned_to" in validated_data + and validated_data["assigned_to"] != _temp + and validated_data["assigned_to"] + ): + NotificationGenerator( + event=Notification.Event.PATIENT_CONSULTATION_ASSIGNMENT, + caused_by=self.context["request"].user, + caused_object=instance, + facility=instance.patient.facility, + notification_mediums=[ + Notification.Medium.SYSTEM, + Notification.Medium.WHATSAPP, + ], + ).generate() NotificationGenerator( event=Notification.Event.PATIENT_CONSULTATION_UPDATED, @@ -297,7 +302,7 @@ def update(self, instance, validated_data): return consultation - def create(self, validated_data): + def create(self, validated_data): # noqa: PLR0915 PLR0912 if route_to_facility := validated_data.get("route_to_facility"): if route_to_facility == RouteToFacility.OUTPATIENT: validated_data["icu_admission_date"] = None @@ -387,9 +392,8 @@ def create(self, validated_data): {"consultation": "Exists please Edit Existing Consultation"} ) - if "is_kasp" in validated_data: - if validated_data["is_kasp"]: - validated_data["kasp_enabled_date"] = now() + if validated_data.get("is_kasp"): + validated_data["kasp_enabled_date"] = now() bed = validated_data.pop("bed", None) @@ -496,15 +500,18 @@ def create(self, validated_data): def validate_create_diagnoses(self, value): # Reject if create_diagnoses is present for edits if self.instance and value: - raise ValidationError("Bulk create diagnoses is not allowed on update") + msg = "Bulk create diagnoses is not allowed on update" + raise ValidationError(msg) # Reject if no diagnoses are provided if len(value) == 0: - raise ValidationError("Atleast one diagnosis is required") + msg = "Atleast one diagnosis is required" + raise ValidationError(msg) # Reject if duplicate diagnoses are provided - if len(value) != len(set([obj["diagnosis"].id for obj in value])): - raise ValidationError("Duplicate diagnoses are not allowed") + if len(value) != len({obj["diagnosis"].id for obj in value}): + msg = "Duplicate diagnoses are not allowed" + raise ValidationError(msg) principal_diagnosis, confirmed_diagnoses = None, [] for obj in value: @@ -514,9 +521,8 @@ def validate_create_diagnoses(self, value): # Reject if there are more than one principal diagnosis if obj["is_principal"]: if principal_diagnosis: - raise ValidationError( - "Only one diagnosis can be set as principal diagnosis" - ) + msg = "Only one diagnosis can be set as principal diagnosis" + raise ValidationError(msg) principal_diagnosis = obj # Reject if principal diagnosis is not one of confirmed diagnosis (if it is present) @@ -526,15 +532,15 @@ def validate_create_diagnoses(self, value): and principal_diagnosis["verification_status"] != ConditionVerificationStatus.CONFIRMED ): - raise ValidationError( - "Only confirmed diagnosis can be set as principal diagnosis if it is present" - ) + msg = "Only confirmed diagnosis can be set as principal diagnosis if it is present" + raise ValidationError(msg) return value def validate_create_symptoms(self, value): if self.instance: - raise ValidationError("Bulk create symptoms is not allowed on update") + msg = "Bulk create symptoms is not allowed on update" + raise ValidationError(msg) counter: set[int | str] = set() for obj in value: @@ -550,7 +556,8 @@ def validate_create_symptoms(self, value): item: str = other_symptom.strip().lower() if item in counter: # Reject if duplicate symptoms are provided - raise ValidationError("Duplicate symptoms are not allowed") + msg = "Duplicate symptoms are not allowed" + raise ValidationError(msg) if not obj.get("cure_date"): # skip duplicate symptom check for ones that has cure date counter.add(item) @@ -593,7 +600,7 @@ def validate_patient_no(self, value): return None return value.strip() - def validate(self, attrs): + def validate(self, attrs): # noqa: PLR0912 validated = super().validate(attrs) # TODO Add Bed Authorisation Validation @@ -621,8 +628,9 @@ def validate(self, attrs): ] } ) - if not treating_physician.user_type == User.TYPE_VALUE_MAP["Doctor"]: - raise ValidationError("Only Doctors can verify a Consultation") + if treating_physician.user_type != User.TYPE_VALUE_MAP["Doctor"]: + msg = "Only Doctors can verify a Consultation" + raise ValidationError(msg) facility = ( self.instance @@ -631,53 +639,48 @@ def validate(self, attrs): ) # Check if the Doctor is associated with the Facility (.facilities) if not treating_physician.facilities.filter(id=facility.id).exists(): - raise ValidationError( - "The treating doctor is no longer linked to this facility. Please update the respective field in the form before proceeding." - ) + msg = "The treating doctor is no longer linked to this facility. Please update the respective field in the form before proceeding." + raise ValidationError(msg) if ( treating_physician.home_facility and treating_physician.home_facility != facility + ): + msg = "Home Facility of the Doctor must be the same as the Consultation Facility" + raise ValidationError(msg) + + if "suggestion" in validated and validated["suggestion"] is SuggestionChoices.R: + if not validated.get("referred_to") and not validated.get( + "referred_to_external" ): raise ValidationError( - "Home Facility of the Doctor must be the same as the Consultation Facility" + { + "referred_to": [ + f"This field is required as the suggestion is {SuggestionChoices.R}." + ] + } ) + if validated.get("referred_to_external"): + validated["referred_to"] = None + elif validated.get("referred_to"): + validated["referred_to_external"] = None - if "suggestion" in validated: - if validated["suggestion"] is SuggestionChoices.R: - if not validated.get("referred_to") and not validated.get( - "referred_to_external" - ): - raise ValidationError( - { - "referred_to": [ - f"This field is required as the suggestion is {SuggestionChoices.R}." - ] - } - ) - if validated.get("referred_to_external"): - validated["referred_to"] = None - elif validated.get("referred_to"): - validated["referred_to_external"] = None - - if "action" in validated: - if validated["action"] == PatientRegistration.ActionEnum.REVIEW: - if "review_interval" not in validated: - raise ValidationError( - { - "review_interval": [ - "This field is required as the patient has been requested Review." - ] - } - ) - if validated["review_interval"] <= 0: - raise ValidationError( - { - "review_interval": [ - "This field value is must be greater than 0." - ] - } - ) + if ( + "action" in validated + and validated["action"] == PatientRegistration.ActionEnum.REVIEW + ): + if "review_interval" not in validated: + raise ValidationError( + { + "review_interval": [ + "This field is required as the patient has been requested Review." + ] + } + ) + if validated["review_interval"] <= 0: + raise ValidationError( + {"review_interval": ["This field value is must be greater than 0."]} + ) if not self.instance and "create_diagnoses" not in validated: raise ValidationError({"create_diagnoses": ["This field is required."]}) @@ -894,9 +897,8 @@ def get_files(self, obj): def validate_patient_code_status(self, value): if value == PatientCodeStatusType.NOT_SPECIFIED: - raise ValidationError( - "Specify a correct Patient Code Status for the Consent" - ) + msg = "Specify a correct Patient Code Status for the Consent" + raise ValidationError(msg) return value def validate(self, attrs): @@ -905,9 +907,8 @@ def validate(self, attrs): user.user_type < User.TYPE_VALUE_MAP["DistrictAdmin"] and self.context["consultation"].facility_id != user.home_facility_id ): - raise ValidationError( - "Only Home Facility Staff can create consent for a Consultation" - ) + msg = "Only Home Facility Staff can create consent for a Consultation" + raise ValidationError(msg) if ( attrs.get("type", None) @@ -936,9 +937,9 @@ def validate(self, attrs): ) return attrs - def clear_existing_records(self, consultation, type, user, self_id=None): + def clear_existing_records(self, consultation, _type, user, self_id=None): consents = PatientConsent.objects.filter( - consultation=consultation, type=type + consultation=consultation, type=_type ).exclude(id=self_id) archived_date = timezone.now() @@ -962,7 +963,7 @@ def create(self, validated_data): with transaction.atomic(): self.clear_existing_records( consultation=self.context["consultation"], - type=validated_data["type"], + _type=validated_data["type"], user=self.context["request"].user, ) validated_data["consultation"] = self.context["consultation"] @@ -973,7 +974,7 @@ def update(self, instance, validated_data): with transaction.atomic(): self.clear_existing_records( consultation=instance.consultation, - type=instance.type, + _type=instance.type, user=self.context["request"].user, self_id=instance.id, ) diff --git a/care/facility/api/serializers/patient_external_test.py b/care/facility/api/serializers/patient_external_test.py index 677c6b2e74..526dfd235e 100644 --- a/care/facility/api/serializers/patient_external_test.py +++ b/care/facility/api/serializers/patient_external_test.py @@ -23,13 +23,7 @@ class PatientExternalTestSerializer(serializers.ModelSerializer): ) result_date = serializers.DateField(input_formats=["%Y-%m-%d"], required=False) - def validate_empty_values(self, data, *args, **kwargs): - # if "is_repeat" in data: - # is_repeat = data["is_repeat"] - # if is_repeat.lower() == "yes": - # data["is_repeat"] = True - # else: - # data["is_repeat"] = False + def validate_empty_values(self, data, *args, **kwargs): # noqa: PLR0912 district_obj = None if "district" in data: district = data["district"] @@ -76,8 +70,10 @@ def validate_empty_values(self, data, *args, **kwargs): if "ward" in data and local_body_obj: try: int(data["ward"]) - except Exception: - raise ValidationError({"ward": ["Ward must be an integer value"]}) + except Exception as e: + raise ValidationError( + {"ward": ["Ward must be an integer value"]} + ) from e if data["ward"]: ward_obj = Ward.objects.filter( number=data["ward"], local_body=local_body_obj @@ -92,11 +88,13 @@ def validate_empty_values(self, data, *args, **kwargs): return super().validate_empty_values(data, *args, **kwargs) def create(self, validated_data): - if "srf_id" in validated_data: - if PatientRegistration.objects.filter( + if ( + "srf_id" in validated_data + and PatientRegistration.objects.filter( srf_id__iexact=validated_data["srf_id"] - ).exists(): - validated_data["patient_created"] = True + ).exists() + ): + validated_data["patient_created"] = True return super().create(validated_data) class Meta: diff --git a/care/facility/api/serializers/patient_icmr.py b/care/facility/api/serializers/patient_icmr.py index b90b5645dc..0e6ffb320d 100644 --- a/care/facility/api/serializers/patient_icmr.py +++ b/care/facility/api/serializers/patient_icmr.py @@ -7,7 +7,7 @@ PatientSampleICMR, ) from care.users.models import GENDER_CHOICES -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField class ICMRPersonalDetails(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient_investigation.py b/care/facility/api/serializers/patient_investigation.py index 65ab93e614..0c1ecc0664 100644 --- a/care/facility/api/serializers/patient_investigation.py +++ b/care/facility/api/serializers/patient_investigation.py @@ -15,7 +15,7 @@ class PatientInvestigationGroupSerializer(serializers.ModelSerializer): class Meta: model = PatientInvestigationGroup - exclude = TIMESTAMP_FIELDS + ("id",) + exclude = (*TIMESTAMP_FIELDS, "id") class PatientInvestigationSerializer(serializers.ModelSerializer): @@ -23,13 +23,13 @@ class PatientInvestigationSerializer(serializers.ModelSerializer): class Meta: model = PatientInvestigation - exclude = TIMESTAMP_FIELDS + ("id",) + exclude = (*TIMESTAMP_FIELDS, "id") class MinimalPatientInvestigationSerializer(serializers.ModelSerializer): class Meta: model = PatientInvestigation - exclude = TIMESTAMP_FIELDS + ("id", "groups") + exclude = (*TIMESTAMP_FIELDS, "id", "groups") class PatientInvestigationSessionSerializer(serializers.ModelSerializer): @@ -38,7 +38,7 @@ class PatientInvestigationSessionSerializer(serializers.ModelSerializer): class Meta: model = InvestigationSession - exclude = TIMESTAMP_FIELDS + ("external_id", "id") + exclude = (*TIMESTAMP_FIELDS, "external_id", "id") class InvestigationValueSerializer(serializers.ModelSerializer): @@ -58,13 +58,14 @@ class InvestigationValueSerializer(serializers.ModelSerializer): class Meta: model = InvestigationValue - read_only_fields = TIMESTAMP_FIELDS + ( + read_only_fields = ( + *TIMESTAMP_FIELDS, "session_id", "investigation", "consultation", "session", ) - exclude = TIMESTAMP_FIELDS + ("external_id",) + exclude = (*TIMESTAMP_FIELDS, "external_id") def update(self, instance, validated_data): if instance.consultation.discharge_date: @@ -72,14 +73,6 @@ def update(self, instance, validated_data): {"consultation": ["Discharged Consultation data cannot be updated"]} ) - # Removed since it might flood messages - # NotificationGenerator( - # event=Notification.Event.INVESTIGATION_UPDATED, - # caused_by=self.context["request"].user, - # caused_object=instance, - # facility=instance.consultation.patient.facility, - # ).generate() - return super().update(instance, validated_data) @@ -87,7 +80,7 @@ class InvestigationValueCreateSerializer(serializers.ModelSerializer): class Meta: model = InvestigationValue read_only_fields = TIMESTAMP_FIELDS - exclude = TIMESTAMP_FIELDS + ("external_id",) + exclude = (*TIMESTAMP_FIELDS, "external_id") class ValueSerializer(serializers.ModelSerializer): diff --git a/care/facility/api/serializers/patient_otp.py b/care/facility/api/serializers/patient_otp.py index 7457ac8454..6da20280df 100644 --- a/care/facility/api/serializers/patient_otp.py +++ b/care/facility/api/serializers/patient_otp.py @@ -1,4 +1,4 @@ -import random +import secrets import string from datetime import timedelta @@ -8,30 +8,16 @@ from rest_framework.exceptions import ValidationError from care.facility.models.patient import PatientMobileOTP -from care.utils.sms.sendSMS import sendSMS +from care.utils.sms.send_sms import send_sms def rand_pass(size): if not settings.USE_SMS: return "45612" - generate_pass = "".join( - [random.choice(string.ascii_uppercase + string.digits) for n in range(size)] - ) - - return generate_pass - -def send_sms(otp, phone_number): - if settings.USE_SMS: - sendSMS( - phone_number, - ( - f"Open Healthcare Network Patient Management System Login, OTP is {otp} . " - "Please do not share this Confidential Login Token with anyone else" - ), - ) - else: - print(otp, phone_number) + return "".join( + secrets.choice(string.ascii_uppercase + string.digits) for _ in range(size) + ) class PatientMobileOTPSerializer(serializers.ModelSerializer): @@ -56,7 +42,16 @@ def create(self, validated_data): otp_obj = super().create(validated_data) otp = rand_pass(settings.OTP_LENGTH) - send_sms(otp, otp_obj.phone_number) + if settings.USE_SMS: + send_sms( + otp_obj.phone_number, + ( + f"Open Healthcare Network Patient Management System Login, OTP is {otp} . " + "Please do not share this Confidential Login Token with anyone else" + ), + ) + elif settings.DEBUG: + print(otp, otp_obj.phone_number) # noqa: T201 otp_obj.otp = otp otp_obj.save() diff --git a/care/facility/api/serializers/patient_sample.py b/care/facility/api/serializers/patient_sample.py index 261d4f9ca3..e6e3a407c0 100644 --- a/care/facility/api/serializers/patient_sample.py +++ b/care/facility/api/serializers/patient_sample.py @@ -12,8 +12,7 @@ PatientSampleFlow, ) from care.users.api.serializers.user import UserBaseMinimumSerializer -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class PatientSampleFlowSerializer(serializers.ModelSerializer): @@ -79,7 +78,8 @@ class PatientSampleSerializer(serializers.ModelSerializer): class Meta: model = PatientSample - read_only_fields = TIMESTAMP_FIELDS + ( + read_only_fields = ( + *TIMESTAMP_FIELDS, "id", "facility", "last_edited_by", @@ -91,7 +91,7 @@ def create(self, validated_data): validated_data.pop("status", None) validated_data.pop("result", None) - sample = super(PatientSampleSerializer, self).create(validated_data) + sample = super().create(validated_data) sample.created_by = self.context["request"].user sample.last_edited_by = self.context["request"].user sample.save() @@ -119,8 +119,8 @@ def update(self, instance, validated_data): validated_data["status"] = PatientSample.SAMPLE_TEST_FLOW_MAP[ "COMPLETED" ] - except KeyError: - raise ValidationError({"status": ["is required"]}) + except KeyError as e: + raise ValidationError({"status": ["is required"]}) from e valid_choices = PatientSample.SAMPLE_FLOW_RULES[ PatientSample.SAMPLE_TEST_FLOW_CHOICES[instance.status - 1][1] ] @@ -134,7 +134,10 @@ def update(self, instance, validated_data): ) if choice == "COMPLETED" and not validated_data.get("result"): raise ValidationError({"result": ["is required as the test is complete"]}) - if choice == "COMPLETED" and instance.result != 3: + if ( + choice == "COMPLETED" + and instance.result != PatientSample.SAMPLE_TEST_RESULT_MAP["AWAITING"] + ): raise ValidationError( {"result": ["cannot change result for completed test."]} ) diff --git a/care/facility/api/serializers/prescription.py b/care/facility/api/serializers/prescription.py index f94d3cccff..71cd5c3544 100644 --- a/care/facility/api/serializers/prescription.py +++ b/care/facility/api/serializers/prescription.py @@ -32,13 +32,11 @@ class MedicineAdministrationSerializer(serializers.ModelSerializer): def validate_administered_date(self, value): if value > timezone.now(): - raise serializers.ValidationError( - "Administered Date cannot be in the future." - ) + msg = "Administered Date cannot be in the future." + raise serializers.ValidationError(msg) if self.context["prescription"].created_date > value: - raise serializers.ValidationError( - "Administered Date cannot be before Prescription Date." - ) + msg = "Administered Date cannot be before Prescription Date." + raise serializers.ValidationError(msg) return value def validate(self, attrs): @@ -50,9 +48,7 @@ def validate(self, attrs): raise serializers.ValidationError( {"dosage": "Dosage is required for titrated prescriptions."} ) - elif ( - self.context["prescription"].dosage_type != PrescriptionDosageType.TITRATED - ): + if self.context["prescription"].dosage_type != PrescriptionDosageType.TITRATED: attrs.pop("dosage", None) return super().validate(attrs) @@ -107,22 +103,24 @@ def validate(self, attrs): MedibaseMedicine, external_id=attrs["medicine"] ) - if not self.instance: - if Prescription.objects.filter( + if ( + not self.instance + and Prescription.objects.filter( consultation__external_id=self.context["request"].parser_context[ "kwargs" ]["consultation_external_id"], medicine=attrs["medicine"], discontinued=False, - ).exists(): - raise serializers.ValidationError( - { - "medicine": ( - "This medicine is already prescribed to this patient. " - "Please discontinue the existing prescription to prescribe again." - ) - } - ) + ).exists() + ): + raise serializers.ValidationError( + { + "medicine": ( + "This medicine is already prescribed to this patient. " + "Please discontinue the existing prescription to prescribe again." + ) + } + ) if not attrs.get("base_dosage"): raise serializers.ValidationError( diff --git a/care/facility/api/serializers/resources.py b/care/facility/api/serializers/resources.py index efc5df3294..3a6b920db6 100644 --- a/care/facility/api/serializers/resources.py +++ b/care/facility/api/serializers/resources.py @@ -13,8 +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 +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField def inverse_choices(choices): @@ -86,6 +85,7 @@ def __init__(self, instance=None, **kwargs): super().__init__(instance=instance, **kwargs) def update(self, instance, validated_data): + # ruff: noqa: N806 better to refactor this LIMITED_RECIEVING_STATUS_ = [] LIMITED_RECIEVING_STATUS = [ REVERSE_REQUEST_STATUS_CHOICES[x] for x in LIMITED_RECIEVING_STATUS_ @@ -101,18 +101,21 @@ def update(self, instance, validated_data): LIMITED_REQUEST_STATUS = [ REVERSE_REQUEST_STATUS_CHOICES[x] for x in LIMITED_REQUEST_STATUS_ ] - # LIMITED_ORGIN_STATUS = [] user = self.context["request"].user if "status" in validated_data: if validated_data["status"] in LIMITED_RECIEVING_STATUS: - if instance.assigned_facility: - if not has_facility_permission(user, instance.assigned_facility): - raise ValidationError({"status": ["Permission Denied"]}) - elif validated_data["status"] in LIMITED_REQUEST_STATUS: - if not has_facility_permission(user, instance.approving_facility): + if instance.assigned_facility and not has_facility_permission( + user, instance.assigned_facility + ): raise ValidationError({"status": ["Permission Denied"]}) + elif validated_data[ + "status" + ] in LIMITED_REQUEST_STATUS and not has_facility_permission( + user, instance.approving_facility + ): + raise ValidationError({"status": ["Permission Denied"]}) # Dont allow editing origin or patient if "origin_facility" in validated_data: @@ -120,9 +123,7 @@ def update(self, instance, validated_data): instance.last_edited_by = self.context["request"].user - new_instance = super().update(instance, validated_data) - - return new_instance + return super().update(instance, validated_data) def create(self, validated_data): # Do Validity checks for each of these data @@ -158,4 +159,4 @@ def create(self, validated_data): class Meta: model = ResourceRequestComment exclude = ("deleted", "request", "external_id") - read_only_fields = TIMESTAMP_FIELDS + ("created_by",) + read_only_fields = (*TIMESTAMP_FIELDS, "created_by") diff --git a/care/facility/api/serializers/shifting.py b/care/facility/api/serializers/shifting.py index 6c55f56d7a..9d6c91f688 100644 --- a/care/facility/api/serializers/shifting.py +++ b/care/facility/api/serializers/shifting.py @@ -33,8 +33,7 @@ from care.users.api.serializers.lsg import StateSerializer from care.users.api.serializers.user import UserBaseMinimumSerializer from care.utils.notification_handler import NotificationGenerator -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField def inverse_choices(choices): @@ -44,7 +43,9 @@ def inverse_choices(choices): return output -REVERSE_SHIFTING_STATUS_CHOICES = inverse_choices(SHIFTING_STATUS_CHOICES) +REVERSE_SHIFTING_STATUS_CHOICES: dict[str, int] = inverse_choices( + SHIFTING_STATUS_CHOICES +) def has_facility_permission(user, facility): @@ -240,14 +241,17 @@ def __init__(self, instance=None, **kwargs): def validate_shifting_approving_facility(self, value): if not settings.PEACETIME_MODE and not value: - raise ValidationError("Shifting Approving Facility is required") + msg = "Shifting Approving Facility is required" + raise ValidationError(msg) return value - def update(self, instance, validated_data): + def update(self, instance, validated_data): # noqa: PLR0912 if instance.status == REVERSE_SHIFTING_STATUS_CHOICES["CANCELLED"]: - raise ValidationError("Permission Denied, Shifting request was cancelled.") - elif instance.status == REVERSE_SHIFTING_STATUS_CHOICES["COMPLETED"]: - raise ValidationError("Permission Denied, Shifting request was completed.") + msg = "Permission Denied, Shifting request was cancelled." + raise ValidationError(msg) + if instance.status == REVERSE_SHIFTING_STATUS_CHOICES["COMPLETED"]: + msg = "Permission Denied, Shifting request was completed." + raise ValidationError(msg) # Dont allow editing origin or patient validated_data.pop("origin_facility") @@ -289,11 +293,15 @@ def update(self, instance, validated_data): raise ValidationError({"status": ["Permission Denied"]}) elif ( - status in self.LIMITED_RECIEVING_STATUS - and instance.assigned_facility - and not has_facility_permission(user, instance.assigned_facility) - ) or status in self.LIMITED_SHIFTING_STATUS and not has_facility_permission( - user, instance.shifting_approving_facility + ( + status in self.LIMITED_RECIEVING_STATUS + and instance.assigned_facility + and not has_facility_permission(user, instance.assigned_facility) + ) + or status in self.LIMITED_SHIFTING_STATUS + and not has_facility_permission( + user, instance.shifting_approving_facility + ) ): raise ValidationError({"status": ["Permission Denied"]}) @@ -346,7 +354,8 @@ def update(self, instance, validated_data): "status" in validated_data and new_instance.shifting_approving_facility is not None and validated_data["status"] != old_status - and validated_data["status"] == 40 + and validated_data["status"] + == REVERSE_SHIFTING_STATUS_CHOICES["DESTINATION APPROVED"] ): NotificationGenerator( event=Notification.Event.SHIFTING_UPDATED, @@ -569,8 +578,4 @@ def create(self, validated_data): class Meta: model = ShiftingRequestComment exclude = ("deleted", "request") - read_only_fields = TIMESTAMP_FIELDS + ( - "created_by", - "external_id", - "id", - ) + read_only_fields = (*TIMESTAMP_FIELDS, "created_by", "external_id", "id") diff --git a/care/facility/api/viewsets/ambulance.py b/care/facility/api/viewsets/ambulance.py index a2dbe3de95..49387d2bce 100644 --- a/care/facility/api/viewsets/ambulance.py +++ b/care/facility/api/viewsets/ambulance.py @@ -58,7 +58,7 @@ class AmbulanceViewSet( def get_serializer_class(self): if self.action == "add_driver": return AmbulanceDriverSerializer - elif self.action == "remove_driver": + if self.action == "remove_driver": return DeleteDriverSerializer return AmbulanceSerializer diff --git a/care/facility/api/viewsets/asset.py b/care/facility/api/viewsets/asset.py index 26b9d35b34..15dd00e2aa 100644 --- a/care/facility/api/viewsets/asset.py +++ b/care/facility/api/viewsets/asset.py @@ -1,4 +1,6 @@ +import logging import re +from typing import TYPE_CHECKING from django.conf import settings from django.core.cache import cache @@ -58,13 +60,18 @@ ) from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses -from care.utils.assetintegration.base import BaseAssetIntegration from care.utils.cache.cache_allowed_facilities import get_accessible_facilities from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices from care.utils.queryset.asset_location import get_asset_location_queryset from care.utils.queryset.facility import get_facility_queryset from config.authentication import MiddlewareAuthentication +if TYPE_CHECKING: + from care.utils.assetintegration.base import BaseAssetIntegration + +logger = logging.getLogger(__name__) + + inverse_asset_type = inverse_choices(AssetTypeChoices) inverse_asset_status = inverse_choices(StatusChoices) @@ -131,9 +138,11 @@ def perform_create(self, serializer): def destroy(self, request, *args, **kwargs): instance = self.get_object() if instance.bed_set.filter(deleted=False).count(): - raise ValidationError("Cannot delete a Location with associated Beds") + msg = "Cannot delete a Location with associated Beds" + raise ValidationError(msg) if instance.asset_set.filter(deleted=False).count(): - raise ValidationError("Cannot delete a Location with associated Assets") + msg = "Cannot delete a Location with associated Assets" + raise ValidationError(msg) return super().destroy(request, *args, **kwargs) @@ -243,11 +252,9 @@ def get_queryset(self): content_type__model="asset", object_external_id=self.kwargs["asset_external_id"], ) - else: - raise exceptions.PermissionDenied( - "You do not have access to this asset's availability records" - ) - elif "asset_location_external_id" in self.kwargs: + msg = "You do not have access to this asset's availability records" + raise exceptions.PermissionDenied(msg) + if "asset_location_external_id" in self.kwargs: asset_location = get_object_or_404( AssetLocation, external_id=self.kwargs["asset_location_external_id"] ) @@ -256,14 +263,10 @@ def get_queryset(self): content_type__model="assetlocation", object_external_id=self.kwargs["asset_location_external_id"], ) - else: - raise exceptions.PermissionDenied( - "You do not have access to this asset location's availability records" - ) - else: - raise exceptions.ValidationError( - "Either asset_external_id or asset_location_external_id is required" - ) + msg = "You do not have access to this asset location's availability records" + raise exceptions.PermissionDenied(msg) + msg = "Either asset_external_id or asset_location_external_id is required" + raise exceptions.ValidationError(msg) class AssetViewSet( @@ -302,7 +305,7 @@ def get_queryset(self): queryset = queryset.filter( current_location__facility__id__in=allowed_facilities ) - queryset = queryset.annotate( + return queryset.annotate( latest_status=Subquery( AvailabilityRecord.objects.filter( content_type__model="asset", @@ -312,7 +315,6 @@ def get_queryset(self): .values("status")[:1] ) ) - return queryset def list(self, request, *args, **kwargs): if settings.CSV_REQUEST_PARAMETER in request.GET: @@ -323,16 +325,14 @@ def list(self, request, *args, **kwargs): queryset, field_header_map=mapping, field_serializer_map=pretty_mapping ) - return super(AssetViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) def destroy(self, request, *args, **kwargs): user = self.request.user if user.user_type >= User.TYPE_VALUE_MAP["DistrictAdmin"]: return super().destroy(request, *args, **kwargs) - else: - raise exceptions.AuthenticationFailed( - "Only District Admin and above can delete assets" - ) + msg = "Only District Admin and above can delete assets" + raise exceptions.AuthenticationFailed(msg) @extend_schema( responses={200: UserDefaultAssetLocationSerializer()}, tags=["asset"] @@ -416,7 +416,7 @@ def operate_assets(self, request, *args, **kwargs): ) except Exception as e: - print(f"error: {e}") + logger.info("Failed to operate asset: %s", e) return Response( {"message": "Internal Server Error"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR, diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 2f994f108d..336b5f83c2 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -131,7 +131,7 @@ def create(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs): if request.user.user_type < User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - raise PermissionDenied() + raise PermissionDenied instance = self.get_object() if instance.is_occupied: raise DRFValidationError( diff --git a/care/facility/api/viewsets/daily_round.py b/care/facility/api/viewsets/daily_round.py index 612c451437..9786ffb312 100644 --- a/care/facility/api/viewsets/daily_round.py +++ b/care/facility/api/viewsets/daily_round.py @@ -14,7 +14,7 @@ from care.facility.models.daily_round import DailyRound from care.utils.queryset.consultation import get_consultation_queryset -DailyRoundAttributes = [f.name for f in DailyRound._meta.get_fields()] +DailyRoundAttributes = [f.name for f in DailyRound._meta.get_fields()] # noqa: SLF001 class DailyRoundFilterSet(filters.FilterSet): @@ -98,9 +98,6 @@ def analyse(self, request, **kwargs): page = request.data.get("page", 1) - # to_time = datetime.now() - timedelta(days=((page - 1) * self.DEFAULT_LOOKUP_DAYS)) - # from_time = to_time - timedelta(days=self.DEFAULT_LOOKUP_DAYS) - consultation = get_object_or_404( get_consultation_queryset(request.user).filter( external_id=self.kwargs["consultation_external_id"] diff --git a/care/facility/api/viewsets/events.py b/care/facility/api/viewsets/events.py index 8dc1678b4d..b1fedf92e4 100644 --- a/care/facility/api/viewsets/events.py +++ b/care/facility/api/viewsets/events.py @@ -68,8 +68,6 @@ class PatientConsultationEventViewSet(ReadOnlyModelViewSet): ) filter_backends = (filters.DjangoFilterBackend,) filterset_class = PatientConsultationEventFilterSet - # lookup_field = "external_id" - # lookup_url_kwarg = "external_id" def get_consultation_obj(self): return get_object_or_404( diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index 222f0591fd..dd8221c0cb 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -144,7 +144,7 @@ def list(self, request, *args, **kwargs): queryset, field_header_map=mapping, field_serializer_map=pretty_mapping ) - return super(FacilityViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema(tags=["facility"]) @method_decorator(parser_classes([MultiPartParser])) diff --git a/care/facility/api/viewsets/facility_capacity.py b/care/facility/api/viewsets/facility_capacity.py index dd18d13f62..6bc28fce13 100644 --- a/care/facility/api/viewsets/facility_capacity.py +++ b/care/facility/api/viewsets/facility_capacity.py @@ -27,9 +27,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) diff --git a/care/facility/api/viewsets/facility_users.py b/care/facility/api/viewsets/facility_users.py index 5d3cb79629..fb2cf25916 100644 --- a/care/facility/api/viewsets/facility_users.py +++ b/care/facility/api/viewsets/facility_users.py @@ -47,5 +47,5 @@ def get_queryset(self): queryset=UserSkill.objects.filter(skill__deleted=False), ), ) - except Facility.DoesNotExist: - raise ValidationError({"Facility": "Facility not found"}) + except Facility.DoesNotExist as e: + raise ValidationError({"Facility": "Facility not found"}) from e diff --git a/care/facility/api/viewsets/file_upload.py b/care/facility/api/viewsets/file_upload.py index 5e784fafb6..bcf98282f0 100644 --- a/care/facility/api/viewsets/file_upload.py +++ b/care/facility/api/viewsets/file_upload.py @@ -37,11 +37,10 @@ def has_permission(self, request, view) -> bool: "PATIENT", "CONSULTATION", ) - else: - return request.data.get("file_type") not in ( - "PATIENT", - "CONSULTATION", - ) + return request.data.get("file_type") not in ( + "PATIENT", + "CONSULTATION", + ) return True def has_object_permission(self, request, view, obj) -> bool: @@ -67,12 +66,11 @@ class FileUploadViewSet( def get_serializer_class(self): if self.action == "retrieve": return FileUploadRetrieveSerializer - elif self.action == "list": + if self.action == "list": return FileUploadListSerializer - elif self.action == "create": + if self.action == "create": return FileUploadCreateSerializer - else: - return FileUploadUpdateSerializer + return FileUploadUpdateSerializer def get_queryset(self): if "file_type" not in self.request.GET: diff --git a/care/facility/api/viewsets/icd.py b/care/facility/api/viewsets/icd.py index e18d86de23..064707ec71 100644 --- a/care/facility/api/viewsets/icd.py +++ b/care/facility/api/viewsets/icd.py @@ -8,7 +8,6 @@ class ICDViewSet(ViewSet): - def serialize_data(self, objects: list[ICD11]): return [diagnosis.get_representation() for diagnosis in objects] diff --git a/care/facility/api/viewsets/inventory.py b/care/facility/api/viewsets/inventory.py index e50d353635..20c87650e8 100644 --- a/care/facility/api/viewsets/inventory.py +++ b/care/facility/api/viewsets/inventory.py @@ -33,7 +33,17 @@ ) from care.users.models import User from care.utils.queryset.facility import get_facility_queryset -from care.utils.validation.integer_validation import check_integer + + +def check_integer(vals): + if not isinstance(vals, list): + vals = [vals] + for i in range(len(vals)): + try: + vals[i] = int(vals[i]) + except Exception as e: + raise ValidationError({"value": "Integer Required"}) from e + return vals class FacilityInventoryFilter(filters.FilterSet): @@ -87,9 +97,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -171,9 +181,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -215,9 +225,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -225,26 +235,3 @@ def get_object(self): return get_object_or_404( self.get_queryset(), external_id=self.kwargs.get("external_id") ) - - -# class FacilityInventoryBurnRateFilter(filters.FilterSet): -# name = filters.CharFilter(field_name="facility__name", lookup_expr="icontains") -# item = filters.NumberFilter(field_name="item_id") - - -# class FacilityInventoryBurnRateViewSet( -# UserAccessMixin, ListModelMixin, RetrieveModelMixin, GenericViewSet, -# ): -# queryset = FacilityInventoryBurnRate.objects.select_related( -# "item", "item__default_unit", "facility__district" -# ).all() -# filter_backends = (filters.DjangoFilterBackend,) -# filterset_class = FacilityInventoryBurnRateFilter -# permission_classes = (IsAuthenticated, DRYPermissions) -# serializer_class = FacilityInventoryBurnRateSerializer - -# def filter_queryset(self, queryset): -# queryset = super().filter_queryset(queryset) -# if self.kwargs.get("facility_external_id"): -# queryset = queryset.filter(facility__external_id=self.kwargs.get("facility_external_id")) -# return self.filter_by_user_scope(queryset) diff --git a/care/facility/api/viewsets/mixins/access.py b/care/facility/api/viewsets/mixins/access.py index 09f25f6d18..314c36c360 100644 --- a/care/facility/api/viewsets/mixins/access.py +++ b/care/facility/api/viewsets/mixins/access.py @@ -53,7 +53,7 @@ class AssetUserAccessMixin: asset_permissions = (DRYAssetPermissions,) def get_authenticators(self): - return [MiddlewareAssetAuthentication()] + super().get_authenticators() + return [MiddlewareAssetAuthentication(), *super().get_authenticators()] def get_permissions(self): """ diff --git a/care/facility/api/viewsets/mixins/history.py b/care/facility/api/viewsets/mixins/history.py index 2727e3f125..77143a3551 100644 --- a/care/facility/api/viewsets/mixins/history.py +++ b/care/facility/api/viewsets/mixins/history.py @@ -1,6 +1,6 @@ from rest_framework.decorators import action -from care.utils.serializer.history_serializer import ModelHistorySerializer +from care.utils.serializers.history_serializer import ModelHistorySerializer class HistoryMixin: diff --git a/care/facility/api/viewsets/notification.py b/care/facility/api/viewsets/notification.py index 213f1324e9..a51097f8e2 100644 --- a/care/facility/api/viewsets/notification.py +++ b/care/facility/api/viewsets/notification.py @@ -68,7 +68,7 @@ def public_key(self, request, *args, **kwargs): def notify(self, request, *args, **kwargs): user = request.user if user.user_type < User.TYPE_VALUE_MAP["Doctor"]: - raise PermissionDenied() + raise PermissionDenied if "facility" not in request.data or request.data["facility"] == "": raise ValidationError({"facility": "is required"}) if "message" not in request.data or request.data["message"] == "": diff --git a/care/facility/api/viewsets/patient.py b/care/facility/api/viewsets/patient.py index 043c1046ab..72731cd6e2 100644 --- a/care/facility/api/viewsets/patient.py +++ b/care/facility/api/viewsets/patient.py @@ -1,4 +1,3 @@ -import datetime import json from json import JSONDecodeError @@ -97,7 +96,6 @@ class PatientFilterSet(filters.FilterSet): - last_consultation_field = "last_consultation" source = filters.ChoiceFilter(choices=PatientRegistration.SourceChoices) @@ -356,7 +354,7 @@ class PatientCustomOrderingFilter(BaseFilterBackend): def filter_queryset(self, request, queryset, view): ordering = request.query_params.get("ordering", "") - if ordering == "category_severity" or ordering == "-category_severity": + if ordering in ("category_severity", "-category_severity"): category_ordering = { category: index + 1 for index, (category, _) in enumerate(CATEGORY_CHOICES) @@ -507,12 +505,11 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return PatientListSerializer - elif self.action == "icmr_sample": + if self.action == "icmr_sample": return PatientICMRSerializer - elif self.action == "transfer": + if self.action == "transfer": return PatientTransferSerializer - else: - return self.serializer_class + return self.serializer_class def filter_queryset(self, queryset: QuerySet) -> QuerySet: if self.action == "list" and settings.CSV_REQUEST_PARAMETER in self.request.GET: @@ -586,7 +583,7 @@ def list(self, request, *args, **kwargs): field_serializer_map=PatientRegistration.CSV_MAKE_PRETTY, ) - return super(PatientViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema(tags=["patient"]) @action(detail=True, methods=["POST"]) @@ -805,9 +802,9 @@ def get_queryset(self): ) if user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter(facility__state=user.state) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter(facility__district=user.district) return queryset.filter(facility__users__id__exact=user.id) @@ -836,9 +833,7 @@ def list(self, request, *args, **kwargs): - entry_date_before: date in YYYY-MM-DD format, inclusive of this date """ - return super(FacilityPatientStatsHistoryViewSet, self).list( - request, *args, **kwargs - ) + return super().list(request, *args, **kwargs) class PatientSearchSetPagination(PageNumberPagination): @@ -864,62 +859,58 @@ class PatientSearchViewSet(ListModelMixin, GenericViewSet): def get_queryset(self): if self.action != "list": - return super(PatientSearchViewSet, self).get_queryset() + return super().get_queryset() + serializer = PatientSearchSerializer( + data=self.request.query_params, partial=True + ) + serializer.is_valid(raise_exception=True) + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + search_keys = [ + "date_of_birth", + "year_of_birth", + "phone_number", + "name", + "age", + ] else: - serializer = PatientSearchSerializer( - data=self.request.query_params, partial=True + search_keys = [ + "date_of_birth", + "year_of_birth", + "phone_number", + "age", + ] + search_fields = { + key: serializer.validated_data[key] + for key in search_keys + if serializer.validated_data.get(key) + } + if not search_fields: + raise serializers.ValidationError( + { + "detail": [ + f"None of the search keys provided. Available: {', '.join(search_keys)}" + ] + } ) - serializer.is_valid(raise_exception=True) - if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - search_keys = [ - "date_of_birth", - "year_of_birth", - "phone_number", - "name", - "age", - ] - else: - search_keys = [ - "date_of_birth", - "year_of_birth", - "phone_number", - "age", - ] - search_fields = { - key: serializer.validated_data[key] - for key in search_keys - if serializer.validated_data.get(key) - } - if not search_fields: - raise serializers.ValidationError( - { - "detail": [ - f"None of the search keys provided. Available: {', '.join(search_keys)}" - ] - } - ) - - # if not self.request.user.is_superuser: - # search_fields["state_id"] = self.request.user.state_id - if "age" in search_fields: - age = search_fields.pop("age") - year_of_birth = datetime.datetime.now().year - age - search_fields["age__gte"] = year_of_birth - 5 - search_fields["age__lte"] = year_of_birth + 5 + if "age" in search_fields: + age = search_fields.pop("age") + year_of_birth = timezone.now().year - age + search_fields["age__gte"] = year_of_birth - 5 + search_fields["age__lte"] = year_of_birth + 5 - name = search_fields.pop("name", None) + name = search_fields.pop("name", None) - queryset = self.queryset.filter(**search_fields) + queryset = self.queryset.filter(**search_fields) - if name: - queryset = ( - queryset.annotate(similarity=TrigramSimilarity("name", name)) - .filter(similarity__gt=0.2) - .order_by("-similarity") - ) + if name: + queryset = ( + queryset.annotate(similarity=TrigramSimilarity("name", name)) + .filter(similarity__gt=0.2) + .order_by("-similarity") + ) - return queryset + return queryset @extend_schema(tags=["patient"]) def list(self, request, *args, **kwargs): @@ -939,7 +930,7 @@ def list(self, request, *args, **kwargs): `Eg: api/v1/patient/search/?year_of_birth=1992&phone_number=%2B917795937091` """ - return super(PatientSearchViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) class PatientNotesFilterSet(filters.FilterSet): diff --git a/care/facility/api/viewsets/patient_consultation.py b/care/facility/api/viewsets/patient_consultation.py index b166971dc9..8bd7145df9 100644 --- a/care/facility/api/viewsets/patient_consultation.py +++ b/care/facility/api/viewsets/patient_consultation.py @@ -70,12 +70,11 @@ class PatientConsultationViewSet( def get_serializer_class(self): if self.action == "patient_from_asset": return PatientConsultationIDSerializer - elif self.action == "discharge_patient": + if self.action == "discharge_patient": return PatientConsultationDischargeSerializer - elif self.action == "email_discharge_summary": + if self.action == "email_discharge_summary": return EmailDischargeSummarySerializer - else: - return self.serializer_class + return self.serializer_class def get_permissions(self): if self.action == "patient_from_asset": @@ -97,11 +96,11 @@ def get_queryset(self): ) if self.request.user.is_superuser: return self.queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return self.queryset.filter( patient__facility__state=self.request.user.state ) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return self.queryset.filter( patient__facility__district=self.request.user.district ) @@ -304,14 +303,12 @@ def dev_preview_discharge_summary(request, consultation_id): with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file: discharge_summary.generate_discharge_summary_pdf(data, tmp_file) + tmp_file.seek(0) - with open(tmp_file.name, "rb") as pdf_file: - pdf_content = pdf_file.read() - - response = HttpResponse(pdf_content, content_type="application/pdf") - response["Content-Disposition"] = 'inline; filename="discharge_summary.pdf"' + response = HttpResponse(tmp_file, content_type="application/pdf") + response["Content-Disposition"] = 'inline; filename="discharge_summary.pdf"' - return response + return response class PatientConsentViewSet( diff --git a/care/facility/api/viewsets/patient_external_test.py b/care/facility/api/viewsets/patient_external_test.py index f5d983a9ed..e1d86cc311 100644 --- a/care/facility/api/viewsets/patient_external_test.py +++ b/care/facility/api/viewsets/patient_external_test.py @@ -28,9 +28,9 @@ from care.users.models import User -def prettyerrors(errors): +def pretty_errors(errors): pretty_errors = defaultdict(list) - for attribute in PatientExternalTest.HEADER_CSV_MAPPING.keys(): + for attribute in PatientExternalTest.HEADER_CSV_MAPPING: if attribute in errors: for error in errors.get(attribute, ""): pretty_errors[attribute].append(str(error)) @@ -46,8 +46,7 @@ def filter(self, qs, value): self.field_name + "__in": values, self.field_name + "__isnull": False, } - qs = qs.filter(**_filter) - return qs + return qs.filter(**_filter) class PatientExternalTestFilter(filters.FilterSet): @@ -113,22 +112,20 @@ def get_queryset(self): return queryset def get_serializer_class(self): - if self.action == "update" or self.action == "partial_update": + if self.action in ("update", "partial_update"): return PatientExternalTestUpdateSerializer return super().get_serializer_class() def destroy(self, request, *args, **kwargs): if self.request.user.user_type < User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - raise PermissionDenied() + raise PermissionDenied return super().destroy(request, *args, **kwargs) def check_upload_permission(self): - if ( + return bool( self.request.user.is_superuser is True or self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - ): - return True - return False + ) def list(self, request, *args, **kwargs): if settings.CSV_REQUEST_PARAMETER in request.GET: @@ -140,18 +137,14 @@ def list(self, request, *args, **kwargs): field_header_map=mapping, field_serializer_map=pretty_mapping, ) - return super(PatientExternalTestViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) @extend_schema(tags=["external_result"]) @action(methods=["POST"], detail=False) def bulk_upsert(self, request, *args, **kwargs): if not self.check_upload_permission(): - raise PermissionDenied("Permission to Endpoint Denied") - # if len(request.FILES.keys()) != 1: - # raise ValidationError({"file": "Upload 1 File at a time"}) - # csv_file = request.FILES[list(request.FILES.keys())[0]] - # csv_file.seek(0) - # reader = csv.DictReader(io.StringIO(csv_file.read().decode("utf-8-sig"))) + msg = "Permission to Endpoint Denied" + raise PermissionDenied(msg) if "sample_tests" not in request.data: raise ValidationError({"sample_tests": "No Data was provided"}) if not isinstance(request.data["sample_tests"], list): @@ -163,18 +156,16 @@ def bulk_upsert(self, request, *args, **kwargs): raise ValidationError({"Error": "User must belong to same district"}) errors = [] - counter = 0 ser_objects = [] invalid = False for sample in request.data["sample_tests"]: - counter += 1 - serialiser_obj = PatientExternalTestSerializer(data=sample) - valid = serialiser_obj.is_valid() - current_error = prettyerrors(serialiser_obj._errors) + serializer = PatientExternalTestSerializer(data=sample) + valid = serializer.is_valid() + current_error = pretty_errors(serializer._errors) # noqa: SLF001 if current_error and (not valid): errors.append(current_error) invalid = True - ser_objects.append(serialiser_obj) + ser_objects.append(serializer) if invalid: return Response(errors, status=status.HTTP_400_BAD_REQUEST) for ser_object in ser_objects: diff --git a/care/facility/api/viewsets/patient_investigation.py b/care/facility/api/viewsets/patient_investigation.py index 07350e4a41..8a572de500 100644 --- a/care/facility/api/viewsets/patient_investigation.py +++ b/care/facility/api/viewsets/patient_investigation.py @@ -44,8 +44,7 @@ def filter(self, qs, value): if not value: return qs - qs = qs.filter(groups__external_id=value) - return qs + return qs.filter(groups__external_id=value) class PatientInvestigationFilter(filters.FilterSet): @@ -115,8 +114,7 @@ def get_queryset(self): queryset.filter(investigation__external_id__in=investigations.split(",")) .order_by("-session__created_date") .distinct("session__created_date")[ - (session_page - 1) - * self.SESSION_PER_PAGE : (session_page) + (session_page - 1) * self.SESSION_PER_PAGE : (session_page) * self.SESSION_PER_PAGE ] ) @@ -125,11 +123,11 @@ def get_queryset(self): queryset = queryset.filter(session_id__in=sessions.values("session_id")) if self.request.user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter( consultation__patient__facility__state=self.request.user.state ) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter( consultation__patient__facility__district=self.request.user.district ) @@ -168,11 +166,11 @@ def get_queryset(self): ) if self.request.user.is_superuser: return queryset - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: return queryset.filter( consultation__patient__facility__state=self.request.user.state ) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: return queryset.filter( consultation__patient__facility__district=self.request.user.district ) @@ -206,8 +204,8 @@ def get_sessions(self, request, *args, **kwargs): responses={204: "Operation successful"}, tags=["investigation"], ) - @action(detail=False, methods=["PUT"]) - def batchUpdate(self, request, *args, **kwargs): + @action(detail=False, methods=["PUT"], url_path="batchUpdate") + def batch_update(self, request, *args, **kwargs): if "investigations" not in request.data: return Response( {"investigation": "is required"}, diff --git a/care/facility/api/viewsets/patient_otp.py b/care/facility/api/viewsets/patient_otp.py index ca88d83793..bd857fcf7f 100644 --- a/care/facility/api/viewsets/patient_otp.py +++ b/care/facility/api/viewsets/patient_otp.py @@ -1,5 +1,3 @@ -from re import error - from django.conf import settings from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework import mixins @@ -28,13 +26,16 @@ class PatientMobileOTPViewSet( @action(detail=False, methods=["POST"]) def login(self, request): if "phone_number" not in request.data or "otp" not in request.data: - raise ValidationError("Request Incomplete") + msg = "Request Incomplete" + raise ValidationError(msg) phone_number = request.data["phone_number"] otp = request.data["otp"] try: mobile_validator(phone_number) - except error: - raise ValidationError({"phone_number": "Invalid phone number format"}) + except Exception as e: + raise ValidationError( + {"phone_number": "Invalid phone number format"} + ) from e if len(otp) != settings.OTP_LENGTH: raise ValidationError({"otp": "Invalid OTP"}) @@ -47,7 +48,6 @@ def login(self, request): otp_object.is_used = True otp_object.save() - # return JWT token = PatientToken() token["phone_number"] = phone_number diff --git a/care/facility/api/viewsets/patient_otp_data.py b/care/facility/api/viewsets/patient_otp_data.py index 76647ae5c5..b0f5e6d234 100644 --- a/care/facility/api/viewsets/patient_otp_data.py +++ b/care/facility/api/viewsets/patient_otp_data.py @@ -28,5 +28,4 @@ def get_queryset(self): def get_serializer_class(self): if self.action == "list": return PatientListSerializer - else: - return self.serializer_class + return self.serializer_class diff --git a/care/facility/api/viewsets/patient_sample.py b/care/facility/api/viewsets/patient_sample.py index 0c698c95a8..279fd5fcd5 100644 --- a/care/facility/api/viewsets/patient_sample.py +++ b/care/facility/api/viewsets/patient_sample.py @@ -93,7 +93,7 @@ def get_serializer_class(self): return serializer_class def get_queryset(self): - queryset = super(PatientSampleViewSet, self).get_queryset() + queryset = super().get_queryset() if self.kwargs.get("patient_external_id") is not None: queryset = queryset.filter( patient__external_id=self.kwargs.get("patient_external_id") @@ -118,7 +118,7 @@ def list(self, request, *args, **kwargs): not self.kwargs.get("patient_external_id") and request.user.user_type < User.TYPE_VALUE_MAP["Doctor"] ): - raise PermissionDenied() + raise PermissionDenied if settings.CSV_REQUEST_PARAMETER in request.GET: queryset = ( @@ -131,7 +131,7 @@ def list(self, request, *args, **kwargs): field_header_map=PatientSample.CSV_MAPPING, field_serializer_map=PatientSample.CSV_MAKE_PRETTY, ) - return super(PatientSampleViewSet, self).list(request, *args, **kwargs) + return super().list(request, *args, **kwargs) def perform_create(self, serializer): validated_data = serializer.validated_data diff --git a/care/facility/api/viewsets/prescription.py b/care/facility/api/viewsets/prescription.py index fe8914098c..8e67aa5beb 100644 --- a/care/facility/api/viewsets/prescription.py +++ b/care/facility/api/viewsets/prescription.py @@ -184,7 +184,6 @@ def administer(self, request, *args, **kwargs): class MedibaseViewSet(ViewSet): - def serialize_data(self, objects: list[MedibaseMedicine]): return [medicine.get_representation() for medicine in objects] @@ -195,8 +194,8 @@ def list(self, request): limit = 30 query = [] - if type := request.query_params.get("type"): - query.append(MedibaseMedicine.type == type) + if t := request.query_params.get("type"): + query.append(MedibaseMedicine.type == t) if q := request.query_params.get("query"): query.append( diff --git a/care/facility/api/viewsets/resources.py b/care/facility/api/viewsets/resources.py index 711f7fcac2..2197e7123f 100644 --- a/care/facility/api/viewsets/resources.py +++ b/care/facility/api/viewsets/resources.py @@ -46,7 +46,7 @@ def get_request_queryset(request, queryset): q_objects |= Q(approving_facility__state=request.user.state) q_objects |= Q(assigned_facility__state=request.user.state) return queryset.filter(q_objects) - elif request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: q_objects = Q(origin_facility__district=request.user.district) q_objects |= Q(approving_facility__district=request.user.district) q_objects |= Q(assigned_facility__district=request.user.district) @@ -165,7 +165,7 @@ def get_queryset(self): request__assigned_facility__state=self.request.user.state ) return queryset.filter(q_objects) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: q_objects = Q( request__origin_facility__district=self.request.user.district ) diff --git a/care/facility/api/viewsets/shifting.py b/care/facility/api/viewsets/shifting.py index 3111e70c30..500da9fafb 100644 --- a/care/facility/api/viewsets/shifting.py +++ b/care/facility/api/viewsets/shifting.py @@ -15,6 +15,7 @@ from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.shifting import ( + REVERSE_SHIFTING_STATUS_CHOICES, ShiftingDetailSerializer, ShiftingListSerializer, ShiftingRequestCommentDetailSerializer, @@ -161,33 +162,38 @@ def get_serializer_class(self): @action(detail=True, methods=["POST"]) def transfer(self, request, *args, **kwargs): shifting_obj = self.get_object() - if has_facility_permission( - request.user, shifting_obj.shifting_approving_facility - ) or has_facility_permission(request.user, shifting_obj.assigned_facility): - if shifting_obj.assigned_facility and shifting_obj.status >= 70: - if shifting_obj.patient: - patient = shifting_obj.patient - patient.facility = shifting_obj.assigned_facility - patient.is_active = True - patient.allow_transfer = False - patient.save() - shifting_obj.status = 80 - shifting_obj.save(update_fields=["status"]) - # Discharge from all other active consultations - PatientConsultation.objects.filter( - patient=patient, discharge_date__isnull=True - ).update( - discharge_date=localtime(now()), - new_discharge_reason=NewDischargeReasonEnum.REFERRED, - ) - ConsultationBed.objects.filter( - consultation=patient.last_consultation, - end_date__isnull=True, - ).update(end_date=localtime(now())) + if ( + ( + has_facility_permission( + request.user, shifting_obj.shifting_approving_facility + ) + or has_facility_permission(request.user, shifting_obj.assigned_facility) + ) + and shifting_obj.assigned_facility + and shifting_obj.status + >= REVERSE_SHIFTING_STATUS_CHOICES["TRANSFER IN PROGRESS"] + and shifting_obj.patient + ): + patient = shifting_obj.patient + patient.facility = shifting_obj.assigned_facility + patient.is_active = True + patient.allow_transfer = False + patient.save() + shifting_obj.status = REVERSE_SHIFTING_STATUS_CHOICES["COMPLETED"] + shifting_obj.save(update_fields=["status"]) + # Discharge from all other active consultations + PatientConsultation.objects.filter( + patient=patient, discharge_date__isnull=True + ).update( + discharge_date=localtime(now()), + new_discharge_reason=NewDischargeReasonEnum.REFERRED, + ) + ConsultationBed.objects.filter( + consultation=patient.last_consultation, + end_date__isnull=True, + ).update(end_date=localtime(now())) - return Response( - {"transfer": "completed"}, status=status.HTTP_200_OK - ) + return Response({"transfer": "completed"}, status=status.HTTP_200_OK) return Response( {"error": "Invalid Request"}, status=status.HTTP_400_BAD_REQUEST ) @@ -204,8 +210,7 @@ def list(self, request, *args, **kwargs): field_header_map=ShiftingRequest.CSV_MAPPING, field_serializer_map=ShiftingRequest.CSV_MAKE_PRETTY, ) - response = super().list(request, *args, **kwargs) - return response + return super().list(request, *args, **kwargs) class ShifitngRequestCommentViewSet( @@ -236,7 +241,7 @@ def get_queryset(self): request__assigned_facility__state=self.request.user.state ) return queryset.filter(q_objects) - elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + if self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: q_objects = Q( request__origin_facility__district=self.request.user.district ) diff --git a/care/facility/api/viewsets/summary.py b/care/facility/api/viewsets/summary.py index e223a204c4..fbc60f6bb3 100644 --- a/care/facility/api/viewsets/summary.py +++ b/care/facility/api/viewsets/summary.py @@ -48,17 +48,6 @@ class FacilityCapacitySummaryViewSet( def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - class TriageSummaryViewSet(ListModelMixin, GenericViewSet): lookup_field = "external_id" @@ -71,17 +60,6 @@ class TriageSummaryViewSet(ListModelMixin, GenericViewSet): filter_backends = (filters.DjangoFilterBackend,) filterset_class = FacilitySummaryFilter - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - @extend_schema(tags=["summary"]) @method_decorator(cache_page(60 * 60)) def list(self, request, *args, **kwargs): @@ -99,17 +77,6 @@ class TestsSummaryViewSet(ListModelMixin, GenericViewSet): filter_backends = (filters.DjangoFilterBackend,) filterset_class = FacilitySummaryFilter - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - @extend_schema(tags=["summary"]) @method_decorator(cache_page(60 * 60 * 10)) def list(self, request, *args, **kwargs): @@ -132,17 +99,6 @@ class PatientSummaryViewSet(ListModelMixin, GenericViewSet): def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) - class DistrictSummaryFilter(filters.FilterSet): start_date = filters.DateFilter(field_name="created_date", lookup_expr="gte") @@ -168,14 +124,3 @@ class DistrictPatientSummaryViewSet(ListModelMixin, GenericViewSet): @method_decorator(cache_page(60 * 10)) def list(self, request, *args, **kwargs): return super().list(request, *args, **kwargs) - - # def get_queryset(self): - # user = self.request.user - # queryset = self.queryset - # if user.is_superuser: - # return queryset - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"]: - # return queryset.filter(facility__district=user.district) - # elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateReadOnlyAdmin"]: - # return queryset.filter(facility__state=user.state) - # return queryset.filter(facility__users__id__exact=user.id) diff --git a/care/facility/events/handler.py b/care/facility/events/handler.py index 53c3ffc6ba..a674c39e8e 100644 --- a/care/facility/events/handler.py +++ b/care/facility/events/handler.py @@ -25,7 +25,7 @@ def create_consultation_event_entry( fields: set[str] = ( get_changed_fields(old_instance, object_instance) if old_instance - else {field.name for field in object_instance._meta.fields} + else {field.name for field in object_instance._meta.fields} # noqa: SLF001 ) fields_to_store = fields_to_store & fields if fields_to_store else fields @@ -91,11 +91,10 @@ def create_consultation_events( taken_at = created_date with transaction.atomic(): - if isinstance(objects, (QuerySet, list, tuple)): + if isinstance(objects, QuerySet | list | tuple): if old is not None: - raise ValueError( - "diff is not available when objects is a list or queryset" - ) + msg = "diff is not available when objects is a list or queryset" + raise ValueError(msg) for obj in objects: create_consultation_event_entry( consultation_id, diff --git a/care/facility/management/commands/add_daily_round_consultation.py b/care/facility/management/commands/add_daily_round_consultation.py index 680d01cd95..d6732e1480 100644 --- a/care/facility/management/commands/add_daily_round_consultation.py +++ b/care/facility/management/commands/add_daily_round_consultation.py @@ -11,17 +11,18 @@ class Command(BaseCommand): help = "Populate daily round for consultations" def handle(self, *args, **options): + batch_size = 10000 consultations = list( PatientConsultation.objects.filter( last_daily_round__isnull=True ).values_list("external_id") ) total_count = len(consultations) - print(f"{total_count} Consultations need to be updated") + self.stdout.write(f"{total_count} Consultations need to be updated") i = 0 for consultation_eid in consultations: - if i > 10000 and i % 10000 == 0: - print(f"{i} operations performed") + if i > batch_size and i % batch_size == 0: + self.stdout.write(f"{i} operations performed") i = i + 1 PatientConsultation.objects.filter(external_id=consultation_eid[0]).update( last_daily_round=DailyRound.objects.filter( @@ -30,4 +31,4 @@ def handle(self, *args, **options): .order_by("-created_date") .first() ) - print("Operation Completed") + self.stdout.write("Operation Completed") diff --git a/care/facility/management/commands/clean_patient_phone_numbers.py b/care/facility/management/commands/clean_patient_phone_numbers.py index e95bb23686..199cb79ef9 100644 --- a/care/facility/management/commands/clean_patient_phone_numbers.py +++ b/care/facility/management/commands/clean_patient_phone_numbers.py @@ -33,5 +33,5 @@ def handle(self, *args, **options) -> str | None: except Exception: failed.append({"id": patient.id, "phone_number": patient.phone_number}) - print(f"Completed for {qs.count()} | Failed for {len(failed)}") - print(f"Failed for {json.dumps(failed)}") + self.stdout.write(f"Completed for {qs.count()} | Failed for {len(failed)}") + self.stdout.write(f"Failed for {json.dumps(failed)}") diff --git a/care/facility/management/commands/generate_jwks.py b/care/facility/management/commands/generate_jwks.py index 750f82d46a..19f1a58fcc 100644 --- a/care/facility/management/commands/generate_jwks.py +++ b/care/facility/management/commands/generate_jwks.py @@ -11,4 +11,4 @@ class Command(BaseCommand): help = "Generate JWKS" def handle(self, *args, **options): - print(generate_encoded_jwks()) + self.stdout.write(generate_encoded_jwks()) diff --git a/care/facility/management/commands/load_dummy_data.py b/care/facility/management/commands/load_dummy_data.py index f42ac0ceef..4a0633801c 100644 --- a/care/facility/management/commands/load_dummy_data.py +++ b/care/facility/management/commands/load_dummy_data.py @@ -17,9 +17,8 @@ class Command(BaseCommand): def handle(self, *args, **options): env = os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") if "production" in env or "staging" in env: - raise CommandError( - "This command is not intended to be run in production environment." - ) + msg = "This command is not intended to be run in production environment." + raise CommandError(msg) try: management.call_command("loaddata", self.BASE_URL + "states.json") @@ -32,4 +31,4 @@ def handle(self, *args, **options): ) management.call_command("populate_investigations") except Exception as e: - raise CommandError(e) + raise CommandError(e) from e diff --git a/care/facility/management/commands/load_event_types.py b/care/facility/management/commands/load_event_types.py index 271f510b79..e0999e21aa 100644 --- a/care/facility/management/commands/load_event_types.py +++ b/care/facility/management/commands/load_event_types.py @@ -271,7 +271,10 @@ class Command(BaseCommand): ) def create_objects( - self, types: tuple[EventType, ...], model: str = None, parent: EventType = None + self, + types: tuple[EventType, ...], + model: str | None = None, + parent: EventType = None, ): for event_type in types: model = event_type.get("model", model) diff --git a/care/facility/management/commands/load_icd11_diagnoses_data.py b/care/facility/management/commands/load_icd11_diagnoses_data.py index 1354c40c94..9f3343a370 100644 --- a/care/facility/management/commands/load_icd11_diagnoses_data.py +++ b/care/facility/management/commands/load_icd11_diagnoses_data.py @@ -1,12 +1,18 @@ import json +from typing import TYPE_CHECKING +from django.conf import settings from django.core.management import BaseCommand, CommandError from care.facility.models.icd11_diagnosis import ICD11Diagnosis +if TYPE_CHECKING: + from pathlib import Path + def fetch_data(): - with open("data/icd11.json") as json_file: + icd11_json: Path = settings.BASE_DIR / "data" / "icd11.json" + with icd11_json.open() as json_file: return json.load(json_file) @@ -117,17 +123,17 @@ def my(x): # The following code is never executed as the `icd11.json` file is # pre-sorted and hence the parent is always present before the child. - print("Full-scan for", id, item["label"]) + self.stdout.write("Full-scan for", id, item["label"]) return self.find_roots( - [ + next( icd11_object for icd11_object in self.data if icd11_object["ID"] == item["parentId"] - ][0] + ) ) def handle(self, *args, **options): - print("Loading ICD11 diagnoses data to database...") + self.stdout.write("Loading ICD11 diagnoses data to database...") try: self.data = fetch_data() @@ -162,4 +168,4 @@ def roots(item): ignore_conflicts=True, # Voluntarily set to skip duplicates, so that we can run this command multiple times + existing relations are not affected ) except Exception as e: - raise CommandError(e) + raise CommandError(e) from e diff --git a/care/facility/management/commands/load_medicines_data.py b/care/facility/management/commands/load_medicines_data.py index 8f909a7424..4dbbfb245a 100644 --- a/care/facility/management/commands/load_medicines_data.py +++ b/care/facility/management/commands/load_medicines_data.py @@ -1,9 +1,14 @@ import json +from typing import TYPE_CHECKING +from django.conf import settings from django.core.management import BaseCommand from care.facility.models import MedibaseMedicine +if TYPE_CHECKING: + from pathlib import Path + class Command(BaseCommand): """ @@ -14,11 +19,14 @@ class Command(BaseCommand): help = "Loads Medibase Medicines into the database from medibase.json" def fetch_data(self): - with open("data/medibase.json") as json_file: + medibase_json: Path = settings.BASE_DIR / "data" / "medibase.json" + with medibase_json.open() as json_file: return json.load(json_file) def handle(self, *args, **options): - print("Loading Medibase Medicines into the database from medibase.json") + self.stdout.write( + "Loading Medibase Medicines into the database from medibase.json" + ) medibase_objects = self.fetch_data() MedibaseMedicine.objects.bulk_create( diff --git a/care/facility/management/commands/load_redis_index.py b/care/facility/management/commands/load_redis_index.py index da31525d34..736f482836 100644 --- a/care/facility/management/commands/load_redis_index.py +++ b/care/facility/management/commands/load_redis_index.py @@ -18,10 +18,10 @@ class Command(BaseCommand): def handle(self, *args, **options): if cache.get("redis_index_loading"): - print("Redis Index already loading, skipping") + self.stdout.write("Redis Index already loading, skipping") return - cache.set("redis_index_loading", True, timeout=60 * 5) + cache.set("redis_index_loading", value=True, timeout=60 * 5) load_icd11_diagnosis() load_medibase_medicines() @@ -35,8 +35,8 @@ def handle(self, *args, **options): if load_static_data: load_static_data() except ModuleNotFoundError: - print(f"Module {module_path} not found") + self.stdout.write(f"Module {module_path} not found") except Exception as e: - print(f"Error loading static data for {plug.name}: {e}") + self.stdout.write(f"Error loading static data for {plug.name}: {e}") cache.delete("redis_index_loading") diff --git a/care/facility/management/commands/port_patient_wards.py b/care/facility/management/commands/port_patient_wards.py index bde986cbfb..bf6b558d1f 100644 --- a/care/facility/management/commands/port_patient_wards.py +++ b/care/facility/management/commands/port_patient_wards.py @@ -32,7 +32,7 @@ def handle(self, *args, **options): patient.save() except Exception: failed += 1 - print( + self.stdout.write( str(failed), " failed operations ", str(success), diff --git a/care/facility/management/commands/summarize.py b/care/facility/management/commands/summarize.py index bc77fd5d03..d008e21df5 100644 --- a/care/facility/management/commands/summarize.py +++ b/care/facility/management/commands/summarize.py @@ -1,12 +1,12 @@ from django.core.management.base import BaseCommand -from care.facility.utils.summarisation.district.patient_summary import ( +from care.facility.utils.summarization.district.patient_summary import ( district_patient_summary, ) -from care.facility.utils.summarisation.facility_capacity import ( +from care.facility.utils.summarization.facility_capacity import ( facility_capacity_summary, ) -from care.facility.utils.summarisation.patient_summary import patient_summary +from care.facility.utils.summarization.patient_summary import patient_summary class Command(BaseCommand): @@ -18,8 +18,8 @@ class Command(BaseCommand): def handle(self, *args, **options): patient_summary() - print("Patients Summarised") + self.stdout.write("Patients Summarised") facility_capacity_summary() - print("Capacity Summarised") + self.stdout.write("Capacity Summarised") district_patient_summary() - print("District Wise Patient Summarised") + self.stdout.write("District Wise Patient Summarised") diff --git a/care/facility/management/commands/sync_external_test_patient.py b/care/facility/management/commands/sync_external_test_patient.py index 08be07911d..ad0fe27f55 100644 --- a/care/facility/management/commands/sync_external_test_patient.py +++ b/care/facility/management/commands/sync_external_test_patient.py @@ -12,10 +12,10 @@ class Command(BaseCommand): help = "Sync the patient created flag in external tests" def handle(self, *args, **options): - print("Starting Sync") + self.stdout.write("Starting Sync") for patient in PatientRegistration.objects.all(): if patient.srf_id: PatientExternalTest.objects.filter( srf_id__iexact=patient.srf_id ).update(patient_created=True) - print("Completed Sync") + self.stdout.write("Completed Sync") diff --git a/care/facility/management/commands/sync_patient_age.py b/care/facility/management/commands/sync_patient_age.py index 8b2ff40321..72495ca2d8 100644 --- a/care/facility/management/commands/sync_patient_age.py +++ b/care/facility/management/commands/sync_patient_age.py @@ -19,6 +19,6 @@ def handle(self, *args, **options): except Exception: failed += 1 if failed: - print(f"Failed for {failed} Patient") + self.stdout.write(f"Failed for {failed} Patient") else: - print("Successfully Synced Age") + self.stdout.write("Successfully Synced Age") diff --git a/care/facility/models/ambulance.py b/care/facility/models/ambulance.py index e0b7a07751..90040ddc00 100644 --- a/care/facility/models/ambulance.py +++ b/care/facility/models/ambulance.py @@ -32,10 +32,6 @@ class Ambulance(FacilityBaseModel): ) owner_is_smart_phone = models.BooleanField(default=True) - # primary_district = models.IntegerField(choices=DISTRICT_CHOICES, blank=False) - # secondary_district = models.IntegerField(choices=DISTRICT_CHOICES, blank=True, null=True) - # third_district = models.IntegerField(choices=DISTRICT_CHOICES, blank=True, null=True) - primary_district = models.ForeignKey( District, on_delete=models.PROTECT, @@ -122,15 +118,6 @@ def has_object_update_permission(self, request): ) ) - # class Meta: - # constraints = [ - # models.CheckConstraint( - # name="ambulance_free_or_price", - # check=models.Q(price_per_km__isnull=False) - # | models.Q(has_free_service=True), - # ) - # ] - class AmbulanceDriver(FacilityBaseModel): ambulance = models.ForeignKey(Ambulance, on_delete=models.CASCADE) diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index 7cb027283d..bf2ab5e317 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -62,7 +62,7 @@ class AssetType(enum.Enum): AssetTypeChoices = [(e.value, e.name) for e in AssetType] -AssetClassChoices = [(e.name, e.value._name) for e in AssetClasses] +AssetClassChoices = [(e.name, e.value._name) for e in AssetClasses] # noqa: SLF001 class Status(enum.Enum): @@ -162,6 +162,7 @@ def resolved_middleware(self): "hostname": hostname, "source": "facility", } + return None class Meta: constraints = [ @@ -312,3 +313,6 @@ class AssetServiceEdit(models.Model): class Meta: ordering = ["-edited_on"] + + def __str__(self): + return f"{self.asset_service.asset.name} - {self.serviced_on}" diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 718b75d058..edc11a8954 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -571,7 +571,7 @@ def save(self, *args, **kwargs): if self.output is not None: self.total_output_calculated = sum([x["quantity"] for x in self.output]) - super(DailyRound, self).save(*args, **kwargs) + super().save(*args, **kwargs) @staticmethod def has_read_permission(request): @@ -585,8 +585,8 @@ def has_read_permission(request): return request.user.is_superuser or ( (request.user in consultation.patient.facility.users.all()) or ( - request.user == consultation.assigned_to - or request.user == consultation.patient.assigned_to + request.user + in (consultation.assigned_to, consultation.patient.assigned_to) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] @@ -620,8 +620,11 @@ def has_object_read_permission(self, request): and request.user in self.consultation.patient.facility.users.all() ) or ( - self.consultation.assigned_to == request.user - or request.user == self.consultation.patient.assigned_to + request.user + in ( + self.consultation.assigned_to, + self.consultation.patient.assigned_to, + ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] diff --git a/care/facility/models/encounter_symptom.py b/care/facility/models/encounter_symptom.py index 9412927aff..f7b1d6dd63 100644 --- a/care/facility/models/encounter_symptom.py +++ b/care/facility/models/encounter_symptom.py @@ -83,7 +83,8 @@ class EncounterSymptom(BaseModel, ConsultationRelatedPermissionMixin): def save(self, *args, **kwargs): if self.other_symptom and self.symptom != Symptom.OTHERS: - raise ValueError("Other Symptom should be empty when Symptom is not OTHERS") + msg = "Other Symptom should be empty when Symptom is not OTHERS" + raise ValueError(msg) if self.clinical_impression_status != ClinicalImpressionStatus.ENTERED_IN_ERROR: if self.onset_date and self.cure_date: diff --git a/care/facility/models/events.py b/care/facility/models/events.py index 9b7fbfc56a..7499837138 100644 --- a/care/facility/models/events.py +++ b/care/facility/models/events.py @@ -31,19 +31,19 @@ class EventType(models.Model): created_date = models.DateTimeField(auto_now_add=True) is_active = models.BooleanField(default=True) - def get_descendants(self): - descendants = list(self.children.all()) - for child in self.children.all(): - descendants.extend(child.get_descendants()) - return descendants + def __str__(self) -> str: + return f"{self.model} - {self.name}" def save(self, *args, **kwargs): if self.description is not None and not self.description.strip(): self.description = None return super().save(*args, **kwargs) - def __str__(self) -> str: - return f"{self.model} - {self.name}" + def get_descendants(self): + descendants = list(self.children.all()) + for child in self.children.all(): + descendants.extend(child.get_descendants()) + return descendants class PatientConsultationEvent(models.Model): @@ -68,16 +68,9 @@ class PatientConsultationEvent(models.Model): max_length=10, choices=ChangeType, default=ChangeType.CREATED ) - def __str__(self) -> str: - return f"{self.id} - {self.consultation_id} - {self.event_type} - {self.change_type}" - class Meta: ordering = ["-created_date"] indexes = [models.Index(fields=["consultation", "is_latest"])] - # constraints = [ - # models.UniqueConstraint( - # fields=["consultation", "event_type", "is_latest"], - # condition=models.Q(is_latest=True), - # name="unique_consultation_event_type_is_latest", - # ) - # ] + + def __str__(self) -> str: + return f"{self.id} - {self.consultation_id} - {self.event_type} - {self.change_type}" diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index fcc2408114..e3593871b7 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -90,27 +90,27 @@ class FacilityFeature(models.IntegerChoices): (5, "Hotel"), (6, "Lodge"), (7, "TeleMedicine"), - # (8, "Govt Hospital"), # Change from "Govt Hospital" to "Govt Medical College Hospitals" + # 8, "Govt Hospital" # Change from "Govt Hospital" to "Govt Medical College Hospitals" (9, "Govt Labs"), (10, "Private Labs"), # Use 8xx for Govt owned hospitals and health centres (800, "Primary Health Centres"), - # (801, "24x7 Public Health Centres"), # Change from "24x7 Public Health Centres" to "Primary Health Centres" + # 801, "24x7 Public Health Centres" # Change from "24x7 Public Health Centres" to "Primary Health Centres" (802, "Family Health Centres"), (803, "Community Health Centres"), - # (820, "Urban Primary Health Center"), # Change from "Urban Primary Health Center" to "Primary Health Centres" + # 820, "Urban Primary Health Center" # Change from "Urban Primary Health Center" to "Primary Health Centres" (830, "Taluk Hospitals"), - # (831, "Taluk Headquarters Hospitals"), # Change from "Taluk Headquarters Hospitals" to "Taluk Hospitals" + # 831, "Taluk Headquarters Hospitals" # Change from "Taluk Headquarters Hospitals" to "Taluk Hospitals" (840, "Women and Child Health Centres"), - # (850, "General hospitals"), # Change from "General hospitals" to "District Hospitals" + # 850, "General hospitals" # Change from "General hospitals" to "District Hospitals" (860, "District Hospitals"), (870, "Govt Medical College Hospitals"), (900, "Co-operative hospitals"), (910, "Autonomous healthcare facility"), # Use 9xx for Labs - # (950, "Corona Testing Labs"), # Change from "Corona Testing Labs" to "Govt Labs" + # 950, "Corona Testing Labs" # Change from "Corona Testing Labs" to "Govt Labs" # Use 10xx for Corona Care Center - # (1000, "Corona Care Centre"), # Change from "Corona Care Centre" to "Other" + # 1000, "Corona Care Centre" # Change from "Corona Care Centre" to "Other" (1010, "COVID-19 Domiciliary Care Center"), # Use 11xx for First Line Treatment Centre (1100, "First Line Treatment Centre"), @@ -207,7 +207,8 @@ def check_if_spoke_is_not_ancestor(base_id: int, spoke_id: int): "hub_id", flat=True ) if spoke_id in ancestors_of_base: - raise serializers.ValidationError("This facility is already an ancestor hub") + msg = "This facility is already an ancestor hub" + raise serializers.ValidationError(msg) for ancestor in ancestors_of_base: check_if_spoke_is_not_ancestor(ancestor, spoke_id) diff --git a/care/facility/models/file_upload.py b/care/facility/models/file_upload.py index 4093e15e53..1176044635 100644 --- a/care/facility/models/file_upload.py +++ b/care/facility/models/file_upload.py @@ -46,10 +46,6 @@ class FileCategory(models.TextChoices): class Meta: abstract = True - def delete(self, *args): - self.deleted = True - self.save(update_fields=["deleted"]) - def save(self, *args, **kwargs): if "force_insert" in kwargs or (not self.internal_name): internal_name = str(uuid4()) + str(int(time.time())) @@ -60,6 +56,10 @@ def save(self, *args, **kwargs): self.internal_name = internal_name return super().save(*args, **kwargs) + def delete(self, *args): + self.deleted = True + self.save(update_fields=["deleted"]) + def get_extension(self): parts = self.internal_name.split(".") return f".{parts[-1]}" if len(parts) > 1 else "" @@ -67,7 +67,7 @@ def get_extension(self): def signed_url( self, duration=60 * 60, mime_type=None, bucket_type=BucketType.PATIENT ): - config, bucket_name = get_client_config(bucket_type, True) + config, bucket_name = get_client_config(bucket_type, external=True) s3 = boto3.client("s3", **config) params = { "Bucket": bucket_name, @@ -82,7 +82,7 @@ def signed_url( ) def read_signed_url(self, duration=60 * 60, bucket_type=BucketType.PATIENT): - config, bucket_name = get_client_config(bucket_type, True) + config, bucket_name = get_client_config(bucket_type, external=True) s3 = boto3.client("s3", **config) return s3.generate_presigned_url( "get_object", @@ -128,8 +128,6 @@ class FileUpload(BaseFileUpload): all data will be private and file access will be given on a NEED TO BASIS ONLY """ - # TODO : Periodic tasks that removes files that were never uploaded - class FileType(models.IntegerChoices): OTHER = 0, "OTHER" PATIENT = 1, "PATIENT" @@ -164,6 +162,9 @@ class FileType(models.IntegerChoices): FileTypeChoices = [(x.value, x.name) for x in FileType] FileCategoryChoices = [(x.value, x.name) for x in BaseFileUpload.FileCategory] + def __str__(self): + return f"{self.FileTypeChoices[self.file_type][1]} - {self.name}{' (Archived)' if self.is_archived else ''}" + def save(self, *args, **kwargs): from care.facility.models import PatientConsent @@ -192,7 +193,7 @@ def save(self, *args, **kwargs): ).exclude(pk=self.pk if self.is_archived else None) ) if not new_consent - else models.Value(True) + else models.Value(value=True) ) ) .filter(has_files=True) @@ -203,6 +204,3 @@ def save(self, *args, **kwargs): consultation.save() return super().save(*args, **kwargs) - - def __str__(self): - return f"{self.FileTypeChoices[self.file_type][1]} - {self.name}{' (Archived)' if self.is_archived else ''}" diff --git a/care/utils/serializer/__init__.py b/care/facility/models/json_schema/__init__.py similarity index 100% rename from care/utils/serializer/__init__.py rename to care/facility/models/json_schema/__init__.py diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index b814cde15a..82410aabf3 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -19,9 +19,9 @@ def has_object_read_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = ( - self.last_consultation.assigned_to == request.user - or request.user == self.assigned_to + doctor_allowed = request.user in ( + self.last_consultation.assigned_to, + self.assigned_to, ) return request.user.is_superuser or ( (hasattr(self, "created_by") and request.user == self.created_by) @@ -60,9 +60,9 @@ def has_object_write_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = ( - self.last_consultation.assigned_to == request.user - or request.user == self.assigned_to + doctor_allowed = request.user in ( + self.last_consultation.assigned_to, + self.assigned_to, ) return request.user.is_superuser or ( @@ -99,9 +99,9 @@ def has_object_update_permission(self, request): doctor_allowed = False if self.last_consultation: - doctor_allowed = ( - self.last_consultation.assigned_to == request.user - or request.user == self.assigned_to + doctor_allowed = request.user in ( + self.last_consultation.assigned_to, + self.assigned_to, ) return ( diff --git a/care/facility/models/patient.py b/care/facility/models/patient.py index f635a773d4..4f8d54c971 100644 --- a/care/facility/models/patient.py +++ b/care/facility/models/patient.py @@ -69,7 +69,7 @@ class SourceEnum(enum.Enum): SourceChoices = [(e.value, e.name) for e in SourceEnum] - class vaccineEnum(enum.Enum): + class VaccineEnum(enum.Enum): COVISHIELD = "CoviShield" COVAXIN = "Covaxin" SPUTNIK = "Sputnik" @@ -78,7 +78,7 @@ class vaccineEnum(enum.Enum): JANSSEN = "Janssen" SINOVAC = "Sinovac" - vaccineChoices = [(e.value, e.name) for e in vaccineEnum] + VaccineChoices = [(e.value, e.name) for e in VaccineEnum] class ActionEnum(enum.Enum): NO_ACTION = 10 @@ -116,12 +116,10 @@ class TestTypeEnum(enum.Enum): "PatientMetaInfo", on_delete=models.SET_NULL, null=True ) - # name_old = EncryptedCharField(max_length=200, default="") name = models.CharField(max_length=200, default="") gender = models.IntegerField(choices=GENDER_CHOICES, blank=False) - # phone_number_old = EncryptedCharField(max_length=14, validators=[phone_number_regex], default="") phone_number = models.CharField( max_length=14, validators=[mobile_or_landline_number_validator], default="" ) @@ -130,7 +128,6 @@ class TestTypeEnum(enum.Enum): max_length=14, validators=[mobile_or_landline_number_validator], default="" ) - # address_old = EncryptedTextField(default="") address = models.TextField(default="") permanent_address = models.TextField(default="") @@ -205,7 +202,7 @@ class TestTypeEnum(enum.Enum): blank=True, verbose_name="Already pescribed medication if any", ) - has_SARI = models.BooleanField( + has_SARI = models.BooleanField( # noqa: N815 default=False, verbose_name="Does the Patient Suffer from SARI" ) @@ -383,7 +380,7 @@ class TestTypeEnum(enum.Enum): validators=[MinValueValidator(0), MaxValueValidator(3)], ) vaccine_name = models.CharField( - choices=vaccineChoices, + choices=VaccineChoices, default=None, null=True, blank=False, @@ -487,19 +484,17 @@ def get_age(self) -> str: year_str = f"{delta.years} year{pluralize(delta.years)}" return f"{year_str}" - elif delta.months > 0: + if delta.months > 0: month_str = f"{delta.months} month{pluralize(delta.months)}" day_str = ( f" {delta.days} day{pluralize(delta.days)}" if delta.days > 0 else "" ) return f"{month_str}{day_str}" - elif delta.days > 0: - day_str = f"{delta.days} day{pluralize(delta.days)}" - return day_str + if delta.days > 0: + return f"{delta.days} day{pluralize(delta.days)}" - else: - return "0 days" + return "0 days" def annotate_diagnosis_ids(*args, **kwargs): return ArrayAgg( @@ -552,14 +547,14 @@ def annotate_diagnosis_ids(*args, **kwargs): "last_consultation__discharge_date__time": "Time of discharge", } - def format_as_date(date): - return date.strftime("%d/%m/%Y") + def format_as_date(self): + return self.strftime("%d/%m/%Y") - def format_as_time(time): - return time.strftime("%H:%M") + def format_as_time(self): + return self.strftime("%H:%M") - def format_diagnoses(diagnosis_ids): - diagnoses = get_icd11_diagnoses_objects_by_ids(diagnosis_ids) + def format_diagnoses(self): + diagnoses = get_icd11_diagnoses_objects_by_ids(self) return ", ".join([diagnosis["label"] for diagnosis in diagnoses]) CSV_MAKE_PRETTY = { @@ -645,6 +640,9 @@ class DomesticHealthcareSupport(models.IntegerChoices): ) head_of_household = models.BooleanField(blank=True, null=True) + def __str__(self): + return f"PatientMetaInfo - {self.id}" + class PatientContactDetails(models.Model): class RelationEnum(enum.IntEnum): @@ -660,25 +658,23 @@ class RelationEnum(enum.IntEnum): OTHERS = 10 class ModeOfContactEnum(enum.IntEnum): - # "1. Touched body fluids of the patient (respiratory tract secretions/blood/vomit/saliva/urine/faces)" + # Touched body fluids of the patient (respiratory tract secretions/blood/vomit/saliva/urine/faces) TOUCHED_BODY_FLUIDS = 1 - # "2. Had direct physical contact with the body of the patient - # including physical examination without full precautions." + # Had direct physical contact with the body of the patient including physical examination without full precautions. DIRECT_PHYSICAL_CONTACT = 2 - # "3. Touched or cleaned the linens/clothes/or dishes of the patient" + # Touched or cleaned the linens/clothes/or dishes of the patient CLEANED_USED_ITEMS = 3 - # "4. Lives in the same household as the patient." + # Lives in the same household as the patient. LIVE_IN_SAME_HOUSEHOLD = 4 - # "5. Close contact within 3ft (1m) of the confirmed case without precautions." + # Close contact within 3ft (1m) of the confirmed case without precautions. CLOSE_CONTACT_WITHOUT_PRECAUTION = 5 - # "6. Passenger of the aeroplane with a confirmed COVID -19 passenger for more than 6 hours." + # Passenger of the aeroplane with a confirmed COVID -19 passenger for more than 6 hours. CO_PASSENGER_AEROPLANE = 6 - # "7. Health care workers and other contacts who had full PPE while handling the +ve case" + # Health care workers and other contacts who had full PPE while handling the +ve case HEALTH_CARE_WITH_PPE = 7 - # "8. Shared the same space(same class for school/worked in - # same room/similar and not having a high risk exposure" + # Shared the same space(same class for school/worked in same room/similar and not having a high risk exposure SHARED_SAME_SPACE_WITHOUT_HIGH_EXPOSURE = 8 - # "9. Travel in the same environment (bus/train/Flight) but not having a high-risk exposure as cited above." + # Travel in the same environment (bus/train/Flight) but not having a high-risk exposure as cited above. TRAVELLED_TOGETHER_WITHOUT_HIGH_EXPOSURE = 9 RelationChoices = [(item.value, item.name) for item in RelationEnum] @@ -709,6 +705,9 @@ class ModeOfContactEnum(enum.IntEnum): objects = BaseManager() + def __str__(self): + return f"{self.patient.name} - {self.patient_in_contact.name} - {self.get_relation_with_patient_display()}" + class Disease(models.Model): patient = models.ForeignKey( @@ -833,6 +832,9 @@ class PatientNotesEdit(models.Model): class Meta: ordering = ["-edited_date"] + def __str__(self): + return f"PatientNotesEdit {self.patient_note} - {self.edited_by}" + class PatientAgeFunc(Func): """ diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index bf241779a0..f21d4f6a4d 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -247,15 +247,6 @@ def get_related_consultation(self): ), } - # CSV_DATATYPE_DEFAULT_MAPPING = { - # "encounter_date": (None, models.DateTimeField(),), - # "deprecated_symptoms_onset_date": (None, models.DateTimeField(),), - # "deprecated_symptoms": ("-", models.CharField(),), - # "category": ("-", models.CharField(),), - # "examination_details": ("-", models.CharField(),), - # "suggestion": ("-", models.CharField(),), - # } - def __str__(self): return f"{self.patient.name}<>{self.facility.name}" @@ -271,7 +262,7 @@ def save(self, *args, **kwargs): if self.death_datetime and self.patient.death_datetime != self.death_datetime: self.patient.death_datetime = self.death_datetime self.patient.save(update_fields=["death_datetime"]) - super(PatientConsultation, self).save(*args, **kwargs) + super().save(*args, **kwargs) class Meta: constraints = [ @@ -299,10 +290,7 @@ def has_object_read_permission(self, request): self.patient.facility and request.user in self.patient.facility.users.all() ) - or ( - self.assigned_to == request.user - or request.user == self.patient.assigned_to - ) + or (request.user in (self.assigned_to, self.patient.assigned_to)) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] and ( @@ -352,6 +340,9 @@ class ConsultationClinician(models.Model): on_delete=models.PROTECT, ) + def __str__(self): + return f"ConsultationClinician {self.consultation} - {self.clinician}" + class PatientConsent(BaseModel, ConsultationRelatedPermissionMixin): consultation = models.ForeignKey( @@ -433,8 +424,11 @@ def has_object_read_permission(self, request): and request.user in self.consultation.patient.facility.users.all() ) or ( - self.consultation.assigned_to == request.user - or request.user == self.consultation.patient.assigned_to + request.user + in ( + self.consultation.assigned_to, + self.consultation.patient.assigned_to, + ) ) or ( request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] diff --git a/care/facility/models/patient_external_test.py b/care/facility/models/patient_external_test.py index 297ccbfd63..3d7ebfb292 100644 --- a/care/facility/models/patient_external_test.py +++ b/care/facility/models/patient_external_test.py @@ -79,7 +79,6 @@ class PatientExternalTest(FacilityBaseModel): "result": "Final Result", "sample_collection_date": "Sample Collection Date", "source": "Source", - # "result_date": "", } def __str__(self): diff --git a/care/facility/models/patient_icmr.py b/care/facility/models/patient_icmr.py index e6f06da451..23c78a7f4e 100644 --- a/care/facility/models/patient_icmr.py +++ b/care/facility/models/patient_icmr.py @@ -1,7 +1,6 @@ -import datetime - from dateutil.relativedelta import relativedelta from django.utils import timezone +from django.utils.timezone import now from care.facility.models import ( DISEASE_CHOICES_MAP, @@ -18,35 +17,6 @@ class PatientIcmr(PatientRegistration): class Meta: proxy = True - # @property - # def personal_details(self): - # return self - - # @property - # def specimen_details(self): - # instance = self.patientsample_set.last() - # if instance is not None: - # instance.__class__ = PatientSampleICMR - # return instance - - # @property - # def patient_category(self): - # instance = self.consultations.last() - # if instance: - # instance.__class__ = PatientConsultationICMR - # return instance - - # @property - # def exposure_history(self): - # return self - - # @property - # def medical_conditions(self): - # instance = self.patientsample_set.last() - # if instance is not None: - # instance.__class__ = PatientSampleICMR - # return instance - def get_age_delta(self): start = self.date_of_birth or timezone.datetime(self.year_of_birth, 1, 1).date() end = (self.death_datetime or timezone.now()).date() @@ -78,12 +48,14 @@ def state_name(self): @property def has_travel_to_foreign_last_14_days(self): + unsafe_travel_days = 14 if self.countries_travelled: return len(self.countries_travelled) != 0 and ( self.date_of_return - and (self.date_of_return.date() - datetime.datetime.now().date()).days - <= 14 + and (self.date_of_return.date() - now().date()).days + <= unsafe_travel_days ) + return None @property def travel_end_date(self): @@ -201,6 +173,7 @@ def symptoms(self): def date_of_onset_of_symptoms(self): if symptom := self.consultation.symptoms.first(): return symptom.onset_date.date() + return None class PatientConsultationICMR(PatientConsultation): @@ -208,26 +181,22 @@ class Meta: proxy = True def is_symptomatic(self): - if ( - SYMPTOM_CHOICES[0][0] not in self.symptoms.choices.keys() + return bool( + SYMPTOM_CHOICES[0][0] not in self.symptoms.choices or self.symptoms_onset_date is not None - ): - return True - else: - return False + ) def symptomatic_international_traveller( self, ): + unsafe_travel_days = 14 return bool( self.patient.countries_travelled and len(self.patient.countries_travelled) != 0 and ( self.patient.date_of_return - and ( - self.patient.date_of_return.date() - datetime.datetime.now().date() - ).days - <= 14 + and (self.patient.date_of_return.date() - now().date()).days + <= unsafe_travel_days ) and self.is_symptomatic() ) diff --git a/care/facility/models/shifting.py b/care/facility/models/shifting.py index 2a3dd20315..05d246a8f7 100644 --- a/care/facility/models/shifting.py +++ b/care/facility/models/shifting.py @@ -41,7 +41,9 @@ (40, "SEVERE"), ] -REVERSE_SHIFTING_STATUS_CHOICES = reverse_choices(SHIFTING_STATUS_CHOICES) +REVERSE_SHIFTING_STATUS_CHOICES: dict[int, str] = reverse_choices( + SHIFTING_STATUS_CHOICES +) class ShiftingRequest(FacilityBaseModel): diff --git a/care/facility/models/summary.py b/care/facility/models/summary.py index 5579bc564d..2cd1071bad 100644 --- a/care/facility/models/summary.py +++ b/care/facility/models/summary.py @@ -44,6 +44,9 @@ class Meta: models.Index(fields=["-created_date", "s_type"]), ] + def __str__(self): + return f"FacilityRelatedSummary - {self.facility} - {self.s_type}" + DISTRICT_SUMMARY_CHOICES = (("PatientSummary", "PatientSummary"),) @@ -78,6 +81,9 @@ class Meta: models.Index(fields=["-created_date", "s_type"]), ] + def __str__(self): + return f"DistrictScopedSummary - {self.district} - {self.s_type}" + LSG_SUMMARY_CHOICES = (("PatientSummary", "PatientSummary"),) @@ -109,3 +115,6 @@ class Meta: ), models.Index(fields=["-created_date", "s_type"]), ] + + def __str__(self): + return f"LocalBodyScopedSummary - {self.lsg} - {self.s_type}" diff --git a/care/utils/serializer/tests/__init__.py b/care/facility/models/tests/__init__.py similarity index 100% rename from care/utils/serializer/tests/__init__.py rename to care/facility/models/tests/__init__.py diff --git a/care/facility/signals/asset_updates.py b/care/facility/signals/asset_updates.py index 5d728e0482..2d6ba5c425 100644 --- a/care/facility/signals/asset_updates.py +++ b/care/facility/signals/asset_updates.py @@ -17,7 +17,7 @@ def save_asset_fields_before_update( return if instance.pk: - instance._previous_values = { + instance._previous_values = { # noqa: SLF001 "hostname": instance.resolved_middleware.get("hostname"), } diff --git a/care/facility/static_data/icd11.py b/care/facility/static_data/icd11.py index e591e99ae7..0861a1d2bc 100644 --- a/care/facility/static_data/icd11.py +++ b/care/facility/static_data/icd11.py @@ -1,3 +1,4 @@ +import logging import re from typing import TypedDict @@ -7,6 +8,9 @@ from care.facility.models.icd11_diagnosis import ICD11Diagnosis from care.utils.static_data.models.base import BaseRedisModel +logger = logging.getLogger(__name__) + + DISEASE_CODE_PATTERN = r"^(?:[A-Z]+\d|\d+[A-Z])[A-Z\d.]*\s" @@ -33,7 +37,7 @@ def get_representation(self) -> ICD11Object: def load_icd11_diagnosis(): - print("Loading ICD11 Diagnosis into the redis cache...", end="", flush=True) + logger.info("Loading ICD11 Diagnosis into the redis cache...") icd_objs = ICD11Diagnosis.objects.order_by("id").values_list( "id", "label", "meta_chapter_short" @@ -49,7 +53,7 @@ def load_icd11_diagnosis(): vec=diagnosis[1].replace(".", "\\.", 1), ).save() Migrator().run() - print("Done") + logger.info("ICD11 Diagnosis Loaded") def get_icd11_diagnosis_object_by_id( diff --git a/care/facility/static_data/medibase.py b/care/facility/static_data/medibase.py index 44f8935d18..03afab0e11 100644 --- a/care/facility/static_data/medibase.py +++ b/care/facility/static_data/medibase.py @@ -1,3 +1,4 @@ +import logging from typing import TypedDict from django.core.paginator import Paginator @@ -8,6 +9,8 @@ from care.facility.models.prescription import MedibaseMedicine as MedibaseMedicineModel from care.utils.static_data.models.base import BaseRedisModel +logger = logging.getLogger(__name__) + class MedibaseMedicineObject(TypedDict): id: str @@ -46,7 +49,7 @@ def get_representation(self) -> MedibaseMedicineObject: def load_medibase_medicines(): - print("Loading Medibase Medicines into the redis cache...", end="", flush=True) + logger.info("Loading Medibase Medicines into the redis cache...") medibase_objects = ( MedibaseMedicineModel.objects.order_by("external_id") @@ -87,4 +90,4 @@ def load_medibase_medicines(): vec=f"{medicine[1]} {medicine[3]} {medicine[4]}", ).save() Migrator().run() - print("Done") + logger.info("Medibase Medicines Loaded") diff --git a/care/facility/tasks/__init__.py b/care/facility/tasks/__init__.py index 6dd696c06e..f4e4774935 100644 --- a/care/facility/tasks/__init__.py +++ b/care/facility/tasks/__init__.py @@ -8,11 +8,11 @@ from care.facility.tasks.plausible_stats import capture_goals from care.facility.tasks.redis_index import load_redis_index from care.facility.tasks.summarisation import ( - summarise_district_patient, - summarise_facility_capacity, - summarise_patient, - summarise_tests, - summarise_triage, + summarize_district_patient, + summarize_facility_capacity, + summarize_patient, + summarize_tests, + summarize_triage, ) @@ -26,31 +26,31 @@ def setup_periodic_tasks(sender, **kwargs): if settings.TASK_SUMMARIZE_TRIAGE: sender.add_periodic_task( crontab(hour="*/4", minute="59"), - summarise_triage.s(), + summarize_triage.s(), name="summarise_triage", ) if settings.TASK_SUMMARIZE_TESTS: sender.add_periodic_task( crontab(hour="23", minute="59"), - summarise_tests.s(), + summarize_tests.s(), name="summarise_tests", ) if settings.TASK_SUMMARIZE_FACILITY_CAPACITY: sender.add_periodic_task( crontab(minute="*/5"), - summarise_facility_capacity.s(), + summarize_facility_capacity.s(), name="summarise_facility_capacity", ) if settings.TASK_SUMMARIZE_PATIENT: sender.add_periodic_task( crontab(hour="*/1", minute="59"), - summarise_patient.s(), + summarize_patient.s(), name="summarise_patient", ) if settings.TASK_SUMMARIZE_DISTRICT_PATIENT: sender.add_periodic_task( crontab(hour="*/1", minute="59"), - summarise_district_patient.s(), + summarize_district_patient.s(), name="summarise_district_patient", ) diff --git a/care/facility/tasks/asset_monitor.py b/care/facility/tasks/asset_monitor.py index a977bf1c7b..9c8701618c 100644 --- a/care/facility/tasks/asset_monitor.py +++ b/care/facility/tasks/asset_monitor.py @@ -1,6 +1,6 @@ import logging from datetime import datetime -from typing import Any +from typing import TYPE_CHECKING, Any from celery import shared_task from django.contrib.contenttypes.models import ContentType @@ -9,14 +9,16 @@ from care.facility.models.asset import Asset, AvailabilityRecord, AvailabilityStatus from care.utils.assetintegration.asset_classes import AssetClasses -from care.utils.assetintegration.base import BaseAssetIntegration + +if TYPE_CHECKING: + from care.utils.assetintegration.base import BaseAssetIntegration logger = logging.getLogger(__name__) @shared_task -def check_asset_status(): - logger.info(f"Checking Asset Status: {timezone.now()}") +def check_asset_status(): # noqa: PLR0912 + logger.info("Checking Asset Status: %s", timezone.now()) assets = ( Asset.objects.exclude(Q(asset_class=None) | Q(asset_class="")) @@ -50,7 +52,7 @@ def check_asset_status(): if not resolved_middleware: logger.warning( - f"Asset {asset.external_id} does not have a middleware hostname" + "Asset %s does not have a middleware hostname", asset.external_id ) continue @@ -91,7 +93,7 @@ def check_asset_status(): else: result = asset_class.api_get(asset_class.get_url("devices/status")) except Exception as e: - logger.warning(f"Middleware {resolved_middleware} is down", e) + logger.warning("Middleware %s is down: %s", resolved_middleware, e) # If no status is returned, setting default status as down if not result or "error" in result: @@ -138,4 +140,4 @@ def check_asset_status(): timestamp=status_record.get("time", timezone.now()), ) except Exception as e: - logger.error("Error in Asset Status Check", e) + logger.error("Error in Asset Status Check: %s", e) diff --git a/care/facility/tasks/discharge_summary.py b/care/facility/tasks/discharge_summary.py index bbcdc974c8..8f47a469c8 100644 --- a/care/facility/tasks/discharge_summary.py +++ b/care/facility/tasks/discharge_summary.py @@ -12,7 +12,7 @@ email_discharge_summary, generate_and_upload_discharge_summary, ) -from care.utils.exceptions import CeleryTaskException +from care.utils.exceptions import CeleryTaskError logger: Logger = get_task_logger(__name__) @@ -24,17 +24,17 @@ def generate_discharge_summary_task(consultation_ext_id: str): """ Generate and Upload the Discharge Summary """ - logger.info(f"Generating Discharge Summary for {consultation_ext_id}") + logger.info("Generating Discharge Summary for %s", consultation_ext_id) try: consultation = PatientConsultation.objects.get(external_id=consultation_ext_id) except PatientConsultation.DoesNotExist as e: - raise CeleryTaskException( - f"Consultation {consultation_ext_id} does not exist" - ) from e + msg = f"Consultation {consultation_ext_id} does not exist" + raise CeleryTaskError(msg) from e summary_file = generate_and_upload_discharge_summary(consultation) if not summary_file: - raise CeleryTaskException("Unable to generate discharge summary") + msg = "Unable to generate discharge summary" + raise CeleryTaskError(msg) return summary_file.id @@ -45,11 +45,11 @@ def generate_discharge_summary_task(consultation_ext_id: str): expires=10 * 60, ) def email_discharge_summary_task(file_id: int, emails: Iterable[str]): - logger.info(f"Emailing Discharge Summary {file_id} to {emails}") + logger.info("Emailing Discharge Summary %s to %s", file_id, emails) try: summary = FileUpload.objects.get(id=file_id) except FileUpload.DoesNotExist: - logger.error(f"Summary {file_id} does not exist") + logger.error("Summary %s does not exist", file_id) return False email_discharge_summary(summary, emails) return True diff --git a/care/facility/tasks/location_monitor.py b/care/facility/tasks/location_monitor.py index 6df3aef7d7..5c5b764b42 100644 --- a/care/facility/tasks/location_monitor.py +++ b/care/facility/tasks/location_monitor.py @@ -18,7 +18,7 @@ @shared_task def check_location_status(): location_content_type = ContentType.objects.get_for_model(AssetLocation) - logger.info(f"Checking Location Status: {timezone.now()}") + logger.info("Checking Location Status: %s", timezone.now()) locations = AssetLocation.objects.all() for location in locations: @@ -30,7 +30,8 @@ def check_location_status(): if not resolved_middleware: logger.warning( - f"No middleware hostname resolved for location {location.external_id}" + "No middleware hostname resolved for location %s", + location.external_id, ) continue @@ -54,7 +55,7 @@ def check_location_status(): new_status = AvailabilityStatus.OPERATIONAL except Exception as e: - logger.warning(f"Middleware {resolved_middleware} is down", e) + logger.warning("Middleware %s is down: %s", resolved_middleware, e) # Fetching the last record of the location last_record = ( @@ -74,6 +75,8 @@ def check_location_status(): status=new_status.value, timestamp=timezone.now(), ) - logger.info(f"Location {location.external_id} status: {new_status.value}") + logger.info( + "Location %s status: %s", location.external_id, new_status.value + ) except Exception as e: - logger.error("Error in Location Status Check", e) + logger.error("Error in Location Status Check: %s", e) diff --git a/care/facility/tasks/plausible_stats.py b/care/facility/tasks/plausible_stats.py index fdc897ebb4..c8f4660743 100644 --- a/care/facility/tasks/plausible_stats.py +++ b/care/facility/tasks/plausible_stats.py @@ -99,7 +99,7 @@ def capture_goals(): return today = now().date() yesterday = today - timedelta(days=1) - logger.info(f"Capturing Goals for {yesterday}") + logger.info("Capturing Goals for %s", yesterday) for goal in Goals: try: @@ -121,7 +121,7 @@ def capture_goals(): goal_entry_object.events = goal_data["results"]["events"]["value"] goal_entry_object.save() - logger.info(f"Saved goal entry for {goal_name} on {yesterday}") + logger.info("Saved goal entry for %s on %s", goal_name, yesterday) for property_name in goal.value: goal_property_stats = get_goal_event_stats( @@ -145,7 +145,11 @@ def capture_goals(): property_entry_object.events = property_statistic["events"] property_entry_object.save() logger.info( - f"Saved goal property entry for {goal_name} and property {property_name} on {yesterday}" + "Saved goal property entry for %s and property %s on %s", + goal_name, + property_name, + yesterday, ) + except Exception as e: - logger.error(f"Failed to process goal {goal_name} due to error: {e!s}") + logger.error("Failed to process goal %s due to error: %s", goal_name, e) diff --git a/care/facility/tasks/push_asset_config.py b/care/facility/tasks/push_asset_config.py index acccce370d..6baabe52b8 100644 --- a/care/facility/tasks/push_asset_config.py +++ b/care/facility/tasks/push_asset_config.py @@ -32,10 +32,10 @@ def create_asset_on_middleware(hostname: str, data: dict) -> dict: ) response.raise_for_status() response_json = response.json() - logger.info(f"Pushed Asset Configuration to Middleware: {response_json}") + logger.info("Pushed Asset Configuration to Middleware: %s", response_json) return response_json except Exception as e: - logger.error(f"Error Pushing Asset Configuration to Middleware: {e}") + logger.error("Error Pushing Asset Configuration to Middleware: %s", e) return {"error": str(e)} @@ -48,10 +48,10 @@ def delete_asset_from_middleware(hostname: str, asset_id: str) -> dict: ) response.raise_for_status() response_json = response.json() - logger.info(f"Deleted Asset from Middleware: {response_json}") + logger.info("Deleted Asset from Middleware: %s", response_json) return response_json except Exception as e: - logger.error(f"Error Deleting Asset from Middleware: {e}") + logger.error("Error Deleting Asset from Middleware: %s", e) return {"error": str(e)} @@ -68,10 +68,10 @@ def update_asset_on_middleware(hostname: str, asset_id: str, data: dict) -> dict ) response.raise_for_status() response_json = response.json() - logger.info(f"Updated Asset Configuration on Middleware: {response_json}") + logger.info("Updated Asset Configuration on Middleware: %s", response_json) return response_json except Exception as e: - logger.error(f"Error Updating Asset Configuration on Middleware: {e}") + logger.error("Error Updating Asset Configuration on Middleware: %s", e) return {"error": str(e)} diff --git a/care/facility/tasks/redis_index.py b/care/facility/tasks/redis_index.py index 0dba4797e7..306fc1352c 100644 --- a/care/facility/tasks/redis_index.py +++ b/care/facility/tasks/redis_index.py @@ -19,7 +19,7 @@ def load_redis_index(): logger.info("Redis Index already loading, skipping") return - cache.set("redis_index_loading", True, timeout=60 * 2) + cache.set("redis_index_loading", value=True, timeout=60 * 2) logger.info("Loading Redis Index") if index_exists(): logger.info("Index already exists, skipping") @@ -37,9 +37,9 @@ def load_redis_index(): if load_static_data: load_static_data() except ModuleNotFoundError: - logger.info(f"Module {module_path} not found") + logger.info("Module %s not found", module_path) except Exception as e: - logger.info(f"Error loading static data for {plug.name}: {e}") + logger.info("Error loading static data for %s: %s", plug.name, e) cache.delete("redis_index_loading") logger.info("Redis Index Loaded") diff --git a/care/facility/tasks/summarisation.py b/care/facility/tasks/summarisation.py index 774829df7c..541d032157 100644 --- a/care/facility/tasks/summarisation.py +++ b/care/facility/tasks/summarisation.py @@ -1,41 +1,44 @@ from celery import shared_task +from celery.utils.log import get_task_logger -from care.facility.utils.summarisation.district.patient_summary import ( +from care.facility.utils.summarization.district.patient_summary import ( district_patient_summary, ) -from care.facility.utils.summarisation.facility_capacity import ( +from care.facility.utils.summarization.facility_capacity import ( facility_capacity_summary, ) -from care.facility.utils.summarisation.patient_summary import patient_summary -from care.facility.utils.summarisation.tests_summary import tests_summary -from care.facility.utils.summarisation.triage_summary import triage_summary +from care.facility.utils.summarization.patient_summary import patient_summary +from care.facility.utils.summarization.tests_summary import tests_summary +from care.facility.utils.summarization.triage_summary import triage_summary + +logger = get_task_logger(__name__) @shared_task -def summarise_triage(): +def summarize_triage(): triage_summary() - print("Summarised Triages") + logger.info("Summarized Triages") @shared_task -def summarise_tests(): +def summarize_tests(): tests_summary() - print("Summarised Tests") + logger.info("Summarized Tests") @shared_task -def summarise_facility_capacity(): +def summarize_facility_capacity(): facility_capacity_summary() - print("Summarised Facility Capacities") + logger.info("Summarized Facility Capacities") @shared_task -def summarise_patient(): +def summarize_patient(): patient_summary() - print("Summarised Patients") + logger.info("Summarized Patients") @shared_task -def summarise_district_patient(): +def summarize_district_patient(): district_patient_summary() - print("Summarised District Patients") + logger.info("Summarized District Patients") diff --git a/care/facility/templatetags/__init__.py b/care/facility/templatetags/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/templatetags/data_formatting_tags.py b/care/facility/templatetags/data_formatting_tags.py index a3e4403ce6..5e2a1ce087 100644 --- a/care/facility/templatetags/data_formatting_tags.py +++ b/care/facility/templatetags/data_formatting_tags.py @@ -5,7 +5,7 @@ @register.filter(name="format_empty_data") def format_empty_data(data): - if data is None or data == "" or data == 0.0 or data == []: + if data is None or data in ("", 0.0, []): return "N/A" return data @@ -28,7 +28,7 @@ def convert_to_sentence_case(s): converted_items = [convert_to_sentence_case(item) for item in items] return ", ".join(converted_items) - elif isinstance(data, (list, tuple)): + if isinstance(data, list | tuple): converted_items = [convert_to_sentence_case(item) for item in data] return ", ".join(converted_items) diff --git a/care/facility/templatetags/filters.py b/care/facility/templatetags/filters.py index 045819279c..2a15bc2d55 100644 --- a/care/facility/templatetags/filters.py +++ b/care/facility/templatetags/filters.py @@ -24,11 +24,12 @@ def suggestion_string(suggestion_code: str): def field_name_to_label(value): if value: return value.replace("_", " ").capitalize() + return None @register.filter(expects_localtime=True) def parse_datetime(value): try: - return datetime.strptime(value, "%Y-%m-%dT%H:%M") + return datetime.strptime(value, "%Y-%m-%dT%H:%M") # noqa: DTZ007 except ValueError: return None diff --git a/care/facility/templatetags/prescription_tags.py b/care/facility/templatetags/prescription_tags.py index 2f1b2ecee8..30d9f11a27 100644 --- a/care/facility/templatetags/prescription_tags.py +++ b/care/facility/templatetags/prescription_tags.py @@ -9,5 +9,4 @@ def format_prescription(prescription): return f"{prescription.medicine_name}, titration from {prescription.base_dosage} to {prescription.target_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." if prescription.dosage_type == "PRN": return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}" - else: - return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." + return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days." diff --git a/care/facility/tests/test_asset_public_api.py b/care/facility/tests/test_asset_public_api.py index 12b728c45e..40ccc77ce5 100644 --- a/care/facility/tests/test_asset_public_api.py +++ b/care/facility/tests/test_asset_public_api.py @@ -3,7 +3,7 @@ from rest_framework.test import APITestCase from care.facility.api.serializers.asset import AssetSerializer -from care.utils.tests.test_utils import TestUtils, override_cache +from care.utils.tests.test_utils import OverrideCache, TestUtils class AssetPublicViewSetTestCase(TestUtils, APITestCase): @@ -38,7 +38,7 @@ def test_retrieve_nonexistent_asset_qr_code(self): self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) def test_retrieve_asset_qr_cached(self): - with override_cache(self): + with OverrideCache(self): response = self.client.get( f"/api/v1/public/asset_qr/{self.asset.qr_code_id}/" ) @@ -62,7 +62,7 @@ def test_retrieve_asset_qr_cached(self): self.assertEqual(response.data["name"], updated_data["name"]) def test_retrieve_asset_qr_pre_cached(self): - with override_cache(self): + with OverrideCache(self): serializer = AssetSerializer(self.asset) cache.set(f"asset:qr:{self.asset.qr_code_id}", serializer.data) response = self.client.get( diff --git a/care/facility/tests/test_asset_service_history_api.py b/care/facility/tests/test_asset_service_history_api.py index afa27962d1..d58dd01473 100644 --- a/care/facility/tests/test_asset_service_history_api.py +++ b/care/facility/tests/test_asset_service_history_api.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import timedelta from django.utils.timezone import now from rest_framework import status @@ -20,8 +20,8 @@ def setUpTestData(cls): cls.asset_location = cls.create_asset_location(cls.facility) cls.asset = cls.create_asset(cls.asset_location) cls.user = cls.create_user("staff", cls.district, home_facility=cls.facility) - cls.today = datetime.today().strftime("%Y-%m-%d") - cls.yesterday = (datetime.today() - timedelta(days=1)).strftime("%Y-%m-%d") + cls.today = now().strftime("%Y-%m-%d") + cls.yesterday = (now() - timedelta(days=1)).strftime("%Y-%m-%d") cls.asset_service = AssetService.objects.create( asset=cls.asset, serviced_on=cls.today, diff --git a/care/facility/tests/test_medicine_administrations_api.py b/care/facility/tests/test_medicine_administrations_api.py index bbac0a5274..88fa18adbb 100644 --- a/care/facility/tests/test_medicine_administrations_api.py +++ b/care/facility/tests/test_medicine_administrations_api.py @@ -123,27 +123,21 @@ def test_administer_and_archive(self): f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/" ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) + self.assertTrue(any(administration_id == x["id"] for x in res.data["results"])) # test archived list administrations res = self.client.get( f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=true" ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertTrue( - any([administration_id == x["id"] for x in res.data["results"]]) - ) + self.assertTrue(any(administration_id == x["id"] for x in res.data["results"])) # test archived list administrations res = self.client.get( f"/api/v1/consultation/{prescription.consultation.external_id}/prescription_administration/?archived=false" ) self.assertEqual(res.status_code, status.HTTP_200_OK) - self.assertFalse( - any([administration_id == x["id"] for x in res.data["results"]]) - ) + self.assertFalse(any(administration_id == x["id"] for x in res.data["results"])) def test_administer_in_future(self): prescription = self.normal_prescription diff --git a/care/facility/tests/test_middleware_auth.py b/care/facility/tests/test_middleware_auth.py index bdb9453550..b7e5af1958 100644 --- a/care/facility/tests/test_middleware_auth.py +++ b/care/facility/tests/test_middleware_auth.py @@ -6,7 +6,7 @@ from rest_framework.test import APITestCase from care.utils.jwks.token_generator import generate_jwt -from care.utils.tests.test_utils import TestUtils, override_cache +from care.utils.tests.test_utils import OverrideCache, TestUtils class MiddlewareAuthTestCase(TestUtils, APITestCase): @@ -80,7 +80,7 @@ def test_middleware_authentication_successful(self, mock_get_public_key): response.data["username"], "middleware" + str(self.facility.external_id) ) - @override_cache + @OverrideCache @requests_mock.Mocker() def test_middleware_authentication_cached_successful(self, mock_get_public_key): mock_get_public_key.get( diff --git a/care/facility/tests/test_patient_and_consultation_access.py b/care/facility/tests/test_patient_and_consultation_access.py index b3decb5d63..8d5e9658f0 100644 --- a/care/facility/tests/test_patient_and_consultation_access.py +++ b/care/facility/tests/test_patient_and_consultation_access.py @@ -105,7 +105,7 @@ def test_discharge_patient_ordering_filter(self): self.assertEqual(str(patients_order[i].external_id), response[i]["id"]) # order by modified date - patients_order = patients_order[::-1] + patients_order.reverse() response = self.client.get( f"/api/v1/facility/{self.home_facility.external_id}/discharged_patients/?ordering=modified_date", ) @@ -113,7 +113,7 @@ def test_discharge_patient_ordering_filter(self): for i in range(len(response)): self.assertEqual(str(patients_order[i].external_id), response[i]["id"]) - def test_patient_consultation_access(self): + def test_patient_consultation_access(self): # noqa: PLR0915 # In this test, a patient is admitted to a remote facility and then later admitted to a home facility. # Admit patient to the remote facility diff --git a/care/facility/tests/test_patient_api.py b/care/facility/tests/test_patient_api.py index c40d57639b..01bff0b813 100644 --- a/care/facility/tests/test_patient_api.py +++ b/care/facility/tests/test_patient_api.py @@ -144,9 +144,9 @@ def create_patient_note( def test_patient_notes(self): self.client.force_authenticate(user=self.state_admin) - patientId = self.patient.external_id + patient_id = self.patient.external_id response = self.client.get( - f"/api/v1/patient/{patientId}/notes/", + f"/api/v1/patient/{patient_id}/notes/", { "consultation": self.consultation.external_id, "thread": PatientNoteThreadChoices.DOCTORS, @@ -160,7 +160,7 @@ def test_patient_notes(self): # Test if all notes are from same consultation as requested self.assertEqual( str(self.consultation.external_id), - [note["consultation"] for note in results][0], + next(note["consultation"] for note in results), ) # Test created_by_local_user field if user is not from same facility as patient @@ -278,13 +278,13 @@ def test_patient_note_with_reply(self): self.assertEqual(reply_response.status_code, status.HTTP_400_BAD_REQUEST) def test_patient_note_edit(self): - patientId = self.patient.external_id + patient_id = self.patient.external_id notes_list_response = self.client.get( - f"/api/v1/patient/{patientId}/notes/?consultation={self.consultation.external_id}" + f"/api/v1/patient/{patient_id}/notes/?consultation={self.consultation.external_id}" ) note_data = notes_list_response.json()["results"][0] response = self.client.get( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/edits/" + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/edits/" ) data = response.json()["results"] @@ -296,7 +296,7 @@ def test_patient_note_edit(self): # Test with a different user editing the note than the one who created it self.client.force_authenticate(user=self.state_admin) response = self.client.put( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/", + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/", {"note": new_note_content}, ) self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST) @@ -307,7 +307,7 @@ def test_patient_note_edit(self): # Test with the same user editing the note self.client.force_authenticate(user=self.user2) response = self.client.put( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/", + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/", {"note": new_note_content}, ) @@ -318,7 +318,7 @@ def test_patient_note_edit(self): # Ensure the original note is still present in the edits response = self.client.get( - f"/api/v1/patient/{patientId}/notes/{note_data['id']}/edits/" + f"/api/v1/patient/{patient_id}/notes/{note_data['id']}/edits/" ) data = response.json()["results"] @@ -402,16 +402,16 @@ def test_has_consent(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] - patient_2_response = [ + ) + patient_2_response = next( x for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], @@ -424,11 +424,11 @@ def test_consent_edit(self): self.client.force_authenticate(user=self.user) response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], @@ -452,16 +452,16 @@ def test_has_consents_archived(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] - patient_2_response = [ + ) + patient_2_response = next( x for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], @@ -477,16 +477,16 @@ def test_has_consents_archived(self): response = self.client.get(self.get_base_url()) self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data["count"], 2) - patient_1_response = [ + patient_1_response = next( x for x in response.data["results"] if x["id"] == str(self.patient.external_id) - ][0] - patient_2_response = [ + ) + patient_2_response = next( x for x in response.data["results"] if x["id"] == str(self.patient_2.external_id) - ][0] + ) self.assertEqual( patient_1_response["last_consultation"]["has_consents"], [ConsentType.CONSENT_FOR_ADMISSION], diff --git a/care/facility/tests/test_patient_consultation_api.py b/care/facility/tests/test_patient_consultation_api.py index 0bea4b9847..625206f6a8 100644 --- a/care/facility/tests/test_patient_consultation_api.py +++ b/care/facility/tests/test_patient_consultation_api.py @@ -64,7 +64,7 @@ def get_default_data(self): "onset_date": now(), }, ], - "patient_no": datetime.datetime.now().timestamp(), + "patient_no": now().timestamp(), } def get_url(self, consultation=None): diff --git a/care/facility/tests/test_patient_daily_rounds_api.py b/care/facility/tests/test_patient_daily_rounds_api.py index e636dfc37a..eb4a5cf9c5 100644 --- a/care/facility/tests/test_patient_daily_rounds_api.py +++ b/care/facility/tests/test_patient_daily_rounds_api.py @@ -1,4 +1,3 @@ -import datetime from datetime import timedelta from django.utils import timezone @@ -51,7 +50,7 @@ def setUpTestData(cls) -> None: "rounds_type": "NORMAL", "patient_category": "Comfort", "action": "DISCHARGE_RECOMMENDED", - "taken_at": datetime.datetime.now().isoformat(), + "taken_at": timezone.now().isoformat(), } def get_url(self, external_consultation_id=None): @@ -243,11 +242,6 @@ def test_create_pressure_sore_with_invalid_region(self): "region": "", "length": 1, "width": 1, - # "exudate_amount": "None", - # "tissue_type": "Closed", - # "push_score": 1, - # "scale": 1, - # "description": "Description", } ], ) diff --git a/care/facility/tests/test_pdf_generation.py b/care/facility/tests/test_pdf_generation.py index f049185aeb..8460b5c37b 100644 --- a/care/facility/tests/test_pdf_generation.py +++ b/care/facility/tests/test_pdf_generation.py @@ -1,4 +1,4 @@ -import os +import hashlib import subprocess import tempfile from datetime import date @@ -21,89 +21,65 @@ from care.utils.tests.test_utils import TestUtils -def compare_pngs(png_path1, png_path2): - with Image.open(png_path1) as img1, Image.open(png_path2) as img2: - if img1.mode != img2.mode: +def compare_images(image1_path: Path, image2_path: Path) -> bool: + with Image.open(image1_path) as img1, Image.open(image2_path) as img2: + if img1.mode != img2.mode or img1.size != img2.size: return False - if img1.size != img2.size: - return False + img1_hash = hashlib.sha256(img1.tobytes()).hexdigest() + img2_hash = hashlib.sha256(img2.tobytes()).hexdigest() - img1_data = list(img1.getdata()) - img2_data = list(img2.getdata()) + return img1_hash == img2_hash - if img1_data == img2_data: - return True - else: - return False +def test_compile_typ(data) -> bool: + logo_path = ( + Path(settings.BASE_DIR) / "staticfiles" / "images" / "logos" / "black-logo.svg" + ) + data["logo_path"] = str(logo_path) + content = render_to_string( + "reports/patient_discharge_summary_pdf_template.typ", context=data + ) + + sample_files_dir: Path = ( + settings.BASE_DIR / "care" / "facility" / "tests" / "sample_reports" + ) -def test_compile_typ(data): - sample_file_path = os.path.join( - os.getcwd(), "care", "facility", "tests", "sample_reports", "sample{n}.png" + subprocess.run( # noqa: S603 + [ # noqa: S607 + "typst", + "compile", + "-", + sample_files_dir / "test_output{n}.png", + "--format", + "png", + ], + input=content.encode("utf-8"), + capture_output=True, + check=True, + cwd="/", ) - test_output_file_path = os.path.join( - os.getcwd(), "care", "facility", "tests", "sample_reports", "test_output{n}.png" + + sample_files = sorted(sample_files_dir.glob("sample*.png")) + test_generated_files = sorted(sample_files_dir.glob("test_output*.png")) + + result = all( + compare_images(sample_image, test_output_image) + for sample_image, test_output_image in zip( + sample_files, test_generated_files, strict=True + ) ) - try: - logo_path = ( - Path(settings.BASE_DIR) - / "staticfiles" - / "images" - / "logos" - / "black-logo.svg" - ) - data["logo_path"] = str(logo_path) - content = render_to_string( - "reports/patient_discharge_summary_pdf_template.typ", context=data - ) - subprocess.run( - ["typst", "compile", "-", test_output_file_path, "--format", "png"], - input=content.encode("utf-8"), - capture_output=True, - check=True, - cwd="/", - ) - - number_of_pngs_generated = 2 - # To be updated only if the number of sample png increase in future - - for i in range(1, number_of_pngs_generated + 1): - current_sample_file_path = sample_file_path - current_sample_file_path = str(current_sample_file_path).replace( - "{n}", str(i) - ) - - current_test_output_file_path = test_output_file_path - current_test_output_file_path = str(current_test_output_file_path).replace( - "{n}", str(i) - ) - - if not compare_pngs( - Path(current_sample_file_path), Path(current_test_output_file_path) - ): - return False - return True - except Exception: - return False - finally: - count = 1 - while True: - current_test_output_file_path = test_output_file_path - current_test_output_file_path = current_test_output_file_path.replace( - "{n}", str(count) - ) - if Path(current_test_output_file_path).exists(): - os.remove(Path(current_test_output_file_path)) - else: - break - count += 1 + + for file in test_generated_files: + file.unlink() + + return result class TestTypstInstallation(TestCase): def test_typst_installed(self): try: - subprocess.run(["typst", "--version"], check=True, capture_output=True) + subprocess.run(["typst", "--version"], check=True, capture_output=True) # noqa: S603, S607 typst_installed = True except subprocess.CalledProcessError: typst_installed = False @@ -218,8 +194,8 @@ def test_pdf_generation_success(self): with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as file: compile_typ(file.name, test_data) - self.assertTrue(os.path.exists(file.name)) - self.assertGreater(os.path.getsize(file.name), 0) + self.assertTrue(Path(file.name).exists()) + self.assertGreater(Path(file.name).stat().st_size, 0) def test_pdf_generation(self): data = discharge_summary.get_discharge_summary_data(self.consultation) diff --git a/care/facility/utils/icd/__init__.py b/care/facility/utils/icd/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/icd/scraper.py b/care/facility/utils/icd/scraper.py index 936ac5f4c2..a63c19213d 100644 --- a/care/facility/utils/icd/scraper.py +++ b/care/facility/utils/icd/scraper.py @@ -1,9 +1,17 @@ import json +import logging import time +from typing import TYPE_CHECKING import requests from django.conf import settings +if TYPE_CHECKING: + from pathlib import Path + + +logger = logging.getLogger(__name__) + class ICDScraper: def __init__(self): @@ -11,23 +19,22 @@ def __init__(self): self.child_concept_url = settings.ICD_SCRAPER_CHILD_CONCEPTS_URL self.scraped_concepts = [] self.scraped_concept_dict = {} + self.request_timeout = 10 - def add_query(self, url, query={}): - return ( - url - + "?" - + "&".join(map(lambda k: str(k) + "=" + str(query[k]), query.keys())) - ) + def add_query(self, url, query=None): + if query is None: + query = {} + return url + "?" + "&".join(str(k) + "=" + str(query[k]) for k in query) def get_child_concepts(self, p_concept, p_parent_id): if p_concept["ID"] in self.scraped_concept_dict: - print(f"[-] Skipped duplicate, {p_concept['label']}") + logger.info("[-] Skipped duplicate, %s", p_concept["label"]) return self.scraped_concepts.append({**p_concept, "parentId": p_parent_id}) self.scraped_concept_dict[p_concept["ID"]] = True - print(f"[+] Added {p_concept['label']}") + logger.info("[+] Added %s", p_concept["label"]) if p_concept["isLeaf"]: return @@ -41,12 +48,14 @@ def get_child_concepts(self, p_concept, p_parent_id): "useHtml": "false", "ConceptId": p_concept["ID"], }, - ) + ), + timeout=self.request_timeout, ).json() except Exception as e: - print("[x] Error encountered: ", e) - with open("error.txt", "a") as error_file: - error_file.write(f"{p_concept['label']}\n") + logger.info("[x] Error encountered: %s", e) + error_file: Path = settings.BASE_DIR / "error.txt" + with error_file.open("a") as ef: + ef.write(f"{p_concept['label']}\n") time.sleep(10) concepts = requests.get( @@ -56,7 +65,8 @@ def get_child_concepts(self, p_concept, p_parent_id): "useHtml": "false", "ConceptId": p_concept["ID"], }, - ) + ), + timeout=self.request_timeout, ).json() for concept in concepts: @@ -66,7 +76,8 @@ def scrape(self): self.scraped_concepts = [] self.scraped_concept_dict = {} root_concepts = requests.get( - self.add_query(self.root_concept_url, {"useHtml": "false"}) + self.add_query(self.root_concept_url, {"useHtml": "false"}), + timeout=self.request_timeout, ).json() skip = [ @@ -81,5 +92,6 @@ def scrape(self): self.get_child_concepts(root_concept, None) time.sleep(3) - with open("data.json", "w") as json_file: + data_file: Path = settings.BASE_DIR / "data" / "icd11.json" + with data_file.open("w") as json_file: json.dump(self.scraped_concepts, json_file) diff --git a/care/facility/utils/reports/__init__.py b/care/facility/utils/reports/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/reports/discharge_summary.py b/care/facility/utils/reports/discharge_summary.py index b31ca9762d..a66216f38d 100644 --- a/care/facility/utils/reports/discharge_summary.py +++ b/care/facility/utils/reports/discharge_summary.py @@ -112,7 +112,7 @@ def format_duration(duration): def get_discharge_summary_data(consultation: PatientConsultation): - logger.info(f"fetching discharge summary data for {consultation.external_id}") + logger.info("fetching discharge summary data for %s", consultation.external_id) samples = PatientSample.objects.filter( patient=consultation.patient, consultation=consultation ) @@ -212,8 +212,8 @@ def compile_typ(output_file, data): "reports/patient_discharge_summary_pdf_template.typ", context=data ) - subprocess.run( - [ + subprocess.run( # noqa: S603 + [ # noqa: S607 "typst", "compile", "-", @@ -226,29 +226,32 @@ def compile_typ(output_file, data): ) logging.info( - f"Successfully Compiled Summary pdf for {data['consultation'].external_id}" + "Successfully Compiled Summary pdf for %s", data["consultation"].external_id ) return True except subprocess.CalledProcessError as e: logging.error( - f"Error compiling summary pdf for {data['consultation'].external_id}: {e.stderr.decode('utf-8')}" + "Error compiling summary pdf for %s: %s", + data["consultation"].external_id, + e.stderr.decode("utf-8"), ) return False def generate_discharge_summary_pdf(data, file): logger.info( - f"Generating Discharge Summary pdf for {data['consultation'].external_id}" + "Generating Discharge Summary pdf for %s", data["consultation"].external_id ) compile_typ(output_file=file.name, data=data) logger.info( - f"Successfully Generated Discharge Summary pdf for {data['consultation'].external_id}" + "Successfully Generated Discharge Summary pdf for %s", + data["consultation"].external_id, ) def generate_and_upload_discharge_summary(consultation: PatientConsultation): - logger.info(f"Generating Discharge Summary for {consultation.external_id}") + logger.info("Generating Discharge Summary for %s", consultation.external_id) set_lock(consultation.external_id, 5) try: @@ -267,12 +270,14 @@ def generate_and_upload_discharge_summary(consultation: PatientConsultation): set_lock(consultation.external_id, 50) with tempfile.NamedTemporaryFile(suffix=".pdf") as file: generate_discharge_summary_pdf(data, file) - logger.info(f"Uploading Discharge Summary for {consultation.external_id}") + logger.info("Uploading Discharge Summary for %s", consultation.external_id) summary_file.put_object(file, ContentType="application/pdf") summary_file.upload_completed = True summary_file.save() logger.info( - f"Uploaded Discharge Summary for {consultation.external_id}, file id: {summary_file.id}" + "Uploaded Discharge Summary for %s, file id: %s", + consultation.external_id, + summary_file.id, ) finally: clear_lock(consultation.external_id) diff --git a/care/facility/utils/summarization/__init__.py b/care/facility/utils/summarization/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/summarization/district/__init__.py b/care/facility/utils/summarization/district/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/facility/utils/summarisation/district/patient_summary.py b/care/facility/utils/summarization/district/patient_summary.py similarity index 92% rename from care/facility/utils/summarisation/district/patient_summary.py rename to care/facility/utils/summarization/district/patient_summary.py index 31997109fb..a4f20a8d37 100644 --- a/care/facility/utils/summarisation/district/patient_summary.py +++ b/care/facility/utils/summarization/district/patient_summary.py @@ -43,9 +43,9 @@ def district_patient_summary(): home_quarantine = Q(last_consultation__suggestion="HI") total_patients_home_quarantine = patients.filter(home_quarantine).count() - district_summary[local_body_object.id][ - "total_patients_home_quarantine" - ] = total_patients_home_quarantine + district_summary[local_body_object.id]["total_patients_home_quarantine"] = ( + total_patients_home_quarantine + ) # Apply Date Filters @@ -69,9 +69,9 @@ def district_patient_summary(): district_summary[local_body_object.id][clean_name] = count # Update Anything Extra - district_summary[local_body_object.id][ - "today_patients_home_quarantine" - ] = today_patients_home_quarantine + district_summary[local_body_object.id]["today_patients_home_quarantine"] = ( + today_patients_home_quarantine + ) object_filter = Q(s_type="PatientSummary") & Q( created_date__startswith=now().date() diff --git a/care/facility/utils/summarisation/facility_capacity.py b/care/facility/utils/summarization/facility_capacity.py similarity index 85% rename from care/facility/utils/summarisation/facility_capacity.py rename to care/facility/utils/summarization/facility_capacity.py index a41ef71ec9..44df537cc6 100644 --- a/care/facility/utils/summarisation/facility_capacity.py +++ b/care/facility/utils/summarization/facility_capacity.py @@ -34,9 +34,9 @@ def facility_capacity_summary(): patients_in_facility.filter(is_active=True).count() ) discharge_patients = patients_in_facility.filter(is_active=False) - capacity_summary[facility_obj.id][ - "actual_discharged_patients" - ] = discharge_patients.count() + capacity_summary[facility_obj.id]["actual_discharged_patients"] = ( + discharge_patients.count() + ) capacity_summary[facility_obj.id]["availability"] = [] temp_inventory_summary_obj = {} @@ -53,14 +53,7 @@ def facility_capacity_summary(): created_date__gte=current_date, probable_accident=False, ) - # start_log = log_query.order_by("created_date").first() end_log = log_query.order_by("-created_date").first() - # start_stock = summary_obj.quantity_in_default_unit - # if start_log: - # if start_log.is_incoming: # Add current value to current stock to get correct stock - # start_stock = start_log.current_stock + start_log.quantity_in_default_unit - # else: - # start_stock = start_log.current_stock - start_log.quantity_in_default_unit end_stock = summary_obj.quantity if end_log: end_stock = end_log.current_stock @@ -77,11 +70,6 @@ def facility_capacity_summary(): if temp2: total_added = temp2.get("quantity_in_default_unit__sum", 0) or 0 - # Calculate Start Stock as - # end_stock = start_stock - consumption + addition - # start_stock = end_stock - addition + consumption - # This way the start stock will never veer off course - start_stock = end_stock - total_added + total_consumed if burn_rate: diff --git a/care/facility/utils/summarisation/patient_summary.py b/care/facility/utils/summarization/patient_summary.py similarity index 92% rename from care/facility/utils/summarisation/patient_summary.py rename to care/facility/utils/summarization/patient_summary.py index 3f3ca1a068..f54f79605b 100644 --- a/care/facility/utils/summarisation/patient_summary.py +++ b/care/facility/utils/summarization/patient_summary.py @@ -37,9 +37,9 @@ def patient_summary(): home_quarantine = Q(last_consultation__suggestion="HI") total_patients_home_quarantine = patients.filter(home_quarantine).count() - patient_summary[facility_id][ - "total_patients_home_quarantine" - ] = total_patients_home_quarantine + patient_summary[facility_id]["total_patients_home_quarantine"] = ( + total_patients_home_quarantine + ) # Apply Date Filters @@ -63,9 +63,9 @@ def patient_summary(): patient_summary[facility_id][clean_name] = count # Update Anything Extra - patient_summary[facility_id][ - "today_patients_home_quarantine" - ] = today_patients_home_quarantine + patient_summary[facility_id]["today_patients_home_quarantine"] = ( + today_patients_home_quarantine + ) for i in list(patient_summary.keys()): object_filter = Q(s_type="PatientSummary") & Q( diff --git a/care/facility/utils/summarisation/tests_summary.py b/care/facility/utils/summarization/tests_summary.py similarity index 100% rename from care/facility/utils/summarisation/tests_summary.py rename to care/facility/utils/summarization/tests_summary.py diff --git a/care/facility/utils/summarisation/triage_summary.py b/care/facility/utils/summarization/triage_summary.py similarity index 100% rename from care/facility/utils/summarisation/triage_summary.py rename to care/facility/utils/summarization/triage_summary.py diff --git a/care/users/admin.py b/care/users/admin.py index d8d066e931..ba40369025 100644 --- a/care/users/admin.py +++ b/care/users/admin.py @@ -53,7 +53,8 @@ class UserAdmin(auth_admin.UserAdmin, ExportCsvMixin): ) }, ), - ) + auth_admin.UserAdmin.fieldsets + *auth_admin.UserAdmin.fieldsets, + ) list_display = ["username", "is_superuser"] search_fields = ["first_name", "last_name"] @@ -89,7 +90,10 @@ class UserFlagForm(forms.ModelForm): ) class Meta: - fields = "__all__" + fields = ( + "user", + "flag", + ) model = UserFlag form = UserFlagForm diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index edb72c38b4..8d0211c206 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -18,8 +18,7 @@ custom_image_extension_validator, ) from care.utils.queryset.facility import get_home_facility_queryset -from care.utils.serializer.external_id_field import ExternalIdSerializerField -from config.serializers import ChoiceField +from care.utils.serializers.fields import ChoiceField, ExternalIdSerializerField class SignUpSerializer(serializers.ModelSerializer): @@ -88,6 +87,9 @@ def validate(self, attrs): return validated +MIN_USER_AGE = 16 + + class UserCreateSerializer(SignUpSerializer): password = serializers.CharField(required=False) facilities = serializers.ListSerializer( @@ -120,24 +122,24 @@ class Meta: 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") + if value and now().year - value.year < MIN_USER_AGE: + error = "Age must be greater than 15 years" + raise serializers.ValidationError(error) return value def validate_facilities(self, facility_ids): - if facility_ids: - if ( - len(facility_ids) - != Facility.objects.filter(external_id__in=facility_ids).count() - ): - available_facility_ids = Facility.objects.filter( - external_id__in=facility_ids, - ).values_list("external_id", flat=True) - not_found_ids = list(set(facility_ids) - set(available_facility_ids)) - raise serializers.ValidationError( - f"Some facilities are not available - {', '.join([str(_id) for _id in not_found_ids])}", - ) + if ( + facility_ids + and len(facility_ids) + != Facility.objects.filter(external_id__in=facility_ids).count() + ): + available_facility_ids = Facility.objects.filter( + external_id__in=facility_ids, + ).values_list("external_id", flat=True) + not_found_ids = list(set(facility_ids) - set(available_facility_ids)) + error = f"Some facilities are not available - {', '.join([str(_id) for _id in not_found_ids])}" + raise serializers.ValidationError(error) return facility_ids def validate_ward(self, value): @@ -148,7 +150,8 @@ def validate_ward(self, value): and not self.context["created_by"].user_type >= User.TYPE_VALUE_MAP["LocalBodyAdmin"] ): - raise serializers.ValidationError("Cannot create for a different Ward") + error = "Cannot create for a different Ward" + raise serializers.ValidationError(error) return value def validate_local_body(self, value): @@ -159,9 +162,8 @@ def validate_local_body(self, value): and not self.context["created_by"].user_type >= User.TYPE_VALUE_MAP["DistrictAdmin"] ): - raise serializers.ValidationError( - "Cannot create for a different local body", - ) + error = "Cannot create for a different local body" + raise serializers.ValidationError(error) return value def validate_district(self, value): @@ -172,7 +174,8 @@ def validate_district(self, value): and not self.context["created_by"].user_type >= User.TYPE_VALUE_MAP["StateAdmin"] ): - raise serializers.ValidationError("Cannot create for a different district") + error = "Cannot create for a different district" + raise serializers.ValidationError(error) return value def validate_state(self, value): @@ -181,7 +184,8 @@ def validate_state(self, value): and value != self.context["created_by"].state and not self.context["created_by"].is_superuser ): - raise serializers.ValidationError("Cannot create for a different state") + error = "Cannot create for a different state" + raise serializers.ValidationError(error) return value def validate(self, attrs): @@ -195,15 +199,17 @@ def validate(self, attrs): }, ) - if self.context["created_by"].user_type in User.READ_ONLY_TYPES: - if validated["user_type"] not in User.READ_ONLY_TYPES: - raise exceptions.ValidationError( - { - "user_type": [ - "Read only users can create other read only users only", - ], - }, - ) + if ( + self.context["created_by"].user_type in User.READ_ONLY_TYPES + and validated["user_type"] not in User.READ_ONLY_TYPES + ): + raise exceptions.ValidationError( + { + "user_type": [ + "Read only users can create other read only users only", + ], + }, + ) if ( self.context["created_by"].user_type @@ -346,8 +352,9 @@ 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") + if value and now().year - value.year < MIN_USER_AGE: + error = "Age must be greater than 15 years" + raise serializers.ValidationError(error) return value diff --git a/care/users/api/serializers/userskill.py b/care/users/api/serializers/userskill.py index 0d75904700..20b504315d 100644 --- a/care/users/api/serializers/userskill.py +++ b/care/users/api/serializers/userskill.py @@ -2,7 +2,7 @@ from care.users.api.serializers.skill import SkillSerializer from care.users.models import Skill, UserSkill -from care.utils.serializer.external_id_field import ExternalIdSerializerField +from care.utils.serializers.fields import ExternalIdSerializerField class UserSkillSerializer(ModelSerializer): diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index 9deec755de..3f064d9d3a 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -76,9 +76,8 @@ def get_user_type( field_name, value, ): - if value: - if value in INVERSE_USER_TYPE: - return queryset.filter(user_type=INVERSE_USER_TYPE[value]) + if value and value in INVERSE_USER_TYPE: + return queryset.filter(user_type=INVERSE_USER_TYPE[value]) return queryset user_type = filters.CharFilter(method="get_user_type", field_name="user_type") @@ -123,21 +122,6 @@ class UserViewSet( filterset_class = UserFilterSet ordering_fields = ["id", "date_joined", "last_login"] search_fields = ["first_name", "last_name", "username"] - # last_login - # def get_permissions(self): - # return [ - # DRYPermissions(), - # IsAuthenticated(), - # ] - # if self.request.method == "POST": - # return [ - # DRYPermissions(), - # ] - # else: - # return [ - # IsAuthenticated(), - # DRYPermissions(), - # ] def get_queryset(self): if self.request.user.is_superuser: @@ -172,20 +156,18 @@ def get_queryset(self): def get_object(self) -> User: try: return super().get_object() - except Http404: - raise Http404("User not found") + except Http404 as e: + error = "User not found" + raise Http404(error) from e def get_serializer_class(self): if self.action == "list": return UserListSerializer - elif self.action == "add_user": + if self.action == "add_user": return UserCreateSerializer - # elif self.action == "create": - # return SignUpSerializer - elif self.action == "profile_picture": + if self.action == "profile_picture": return UserImageUploadSerializer - else: - return UserSerializer + return UserSerializer @extend_schema(tags=["users"]) @action(detail=False, methods=["GET"]) diff --git a/care/users/management/commands/load_data.py b/care/users/management/commands/load_data.py index 02a444fae5..53c6f70f4e 100644 --- a/care/users/management/commands/load_data.py +++ b/care/users/management/commands/load_data.py @@ -58,12 +58,11 @@ def handle(self, *args, **options): states = self.valid_states else: if state not in self.valid_states: - print("valid state options are ", self.valid_states) - raise Exception("State not found") + error = "State not found" + raise Exception(error) states = [state] for state in states: current_state_data = self.BASE_URL + state + "/lsg/" - print("Processing Files From", current_state_data) management.call_command("load_lsg_data", current_state_data) management.call_command("load_ward_data", current_state_data) diff --git a/care/users/management/commands/load_lsg_data.py b/care/users/management/commands/load_lsg_data.py index 4135a2c235..1398ce4f8c 100644 --- a/care/users/management/commands/load_lsg_data.py +++ b/care/users/management/commands/load_lsg_data.py @@ -1,6 +1,6 @@ -import glob import json from collections import defaultdict +from pathlib import Path from django.core.management.base import BaseCommand, CommandParser @@ -24,7 +24,7 @@ def handle(self, *args, **options) -> str | None: local_bodies = [] # Creates a map with first char of readable value as key - LOCAL_BODY_CHOICE_MAP = dict([(c[1][0], c[0]) for c in LOCAL_BODY_CHOICES]) + local_body_choice_map = {c[1][0]: c[0] for c in LOCAL_BODY_CHOICES} state = {} district = defaultdict(dict) @@ -34,7 +34,6 @@ def get_state_obj(state_name): return state[state_name] state_obj = State.objects.filter(name=state_name).first() if not state_obj: - print(f"Creating State {state_name}") state_obj = State(name=state_name) state_obj.save() state[state_name] = state_obj @@ -42,16 +41,14 @@ def get_state_obj(state_name): def get_district_obj(district_name, state_name): state_obj = get_state_obj(state_name) - if state_name in district: - if district_name in district[state_name]: - return district[state_name][district_name] + if state_name in district and district_name in district[state_name]: + return district[state_name][district_name] district_obj = District.objects.filter( name=district_name, state=state_obj ).first() if not district_obj: if not district_name: return None - print(f"Creating District {district_name}") district_obj = District(name=district_name, state=state_obj) district_obj.save() district[state_name][district_name] = district_obj @@ -80,7 +77,7 @@ def create_local_bodies(local_body_list): name=lb["name"], district=dist_obj, localbody_code=lb.get("localbody_code"), - body_type=LOCAL_BODY_CHOICE_MAP.get( + body_type=local_body_choice_map.get( (lb.get("localbody_code", " "))[0], LOCAL_BODY_CHOICES[-1][0], ), @@ -92,9 +89,8 @@ def create_local_bodies(local_body_list): # Hence, those records can be ignored using the `ignore_conflicts` flag LocalBody.objects.bulk_create(local_body_objs, ignore_conflicts=True) - for f in sorted(glob.glob(f"{folder}/*.json")): - counter += 1 - with open(f"{f}") as data_f: + for counter, f in enumerate(sorted(Path.glob(f"{folder}/*.json"))): + with Path(f).open() as data_f: data = json.load(data_f) data.pop("wards", None) local_bodies.append(data) @@ -103,7 +99,6 @@ def create_local_bodies(local_body_list): if counter % 1000 == 0: create_local_bodies(local_bodies) local_bodies = [] - print(f"Completed: {counter}") if len(local_bodies) > 0: create_local_bodies(local_bodies) diff --git a/care/users/management/commands/load_skill_data.py b/care/users/management/commands/load_skill_data.py index 3325ba448e..cf4d503734 100644 --- a/care/users/management/commands/load_skill_data.py +++ b/care/users/management/commands/load_skill_data.py @@ -10,7 +10,7 @@ class Command(BaseCommand): help = "Seed Data for Skills" - def handle(self, *args, **options): + def handle(self, *args, **kwargs): self.stdout.write("Seeding Skills Data... ", ending="") skills = [ diff --git a/care/users/management/commands/load_state_data.py b/care/users/management/commands/load_state_data.py index cf2895b915..969675e547 100644 --- a/care/users/management/commands/load_state_data.py +++ b/care/users/management/commands/load_state_data.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from django.core.management import BaseCommand, CommandParser @@ -22,13 +23,12 @@ def handle(self, *args, **options): json_file_path = options["json_file_path"] data = [] - with open(json_file_path) as json_file: + with Path(json_file_path).open() as json_file: data = json.load(json_file) for item in data: state_name = item["state"].strip() if state_name.lower() in states_to_ignore: - print(f"Skipping {state_name}") continue districts = [d.strip() for d in item["districts"].split(",")] @@ -36,10 +36,8 @@ def handle(self, *args, **options): state, is_created = State.objects.get_or_create( name__iexact=state_name, defaults={"name": state_name} ) - print(f"{'Created' if is_created else 'Retrieved'} {state_name}") for d in districts: _, is_created = District.objects.get_or_create( state=state, name__iexact=d, defaults={"name": d} ) - print(f"{'Created' if is_created else 'Retrieved'} {state_name}") diff --git a/care/users/management/commands/load_ward_data.py b/care/users/management/commands/load_ward_data.py index c89cf47c8b..9b14dd2bba 100644 --- a/care/users/management/commands/load_ward_data.py +++ b/care/users/management/commands/load_ward_data.py @@ -1,5 +1,6 @@ -import glob import json +import logging +from pathlib import Path from django.core.management.base import BaseCommand, CommandParser from django.db import IntegrityError @@ -43,7 +44,7 @@ def get_ward_name(ward): district_map = {d.name: d for d in districts} # Creates a map with first char of readable value as key - LOCAL_BODY_CHOICE_MAP = dict([(c[1][0], c[0]) for c in LOCAL_BODY_CHOICES]) + local_body_choice_map = {c[1][0]: c[0] for c in LOCAL_BODY_CHOICES} def get_local_body(lb): if not lb["district"]: @@ -52,18 +53,18 @@ def get_local_body(lb): name=lb["name"], district=district_map[lb["district"]], localbody_code=lb.get("localbody_code"), - body_type=LOCAL_BODY_CHOICE_MAP.get( + body_type=local_body_choice_map.get( (lb.get("localbody_code", " "))[0], LOCAL_BODY_CHOICES[-1][0], ), ).first() - for f in sorted(glob.glob(f"{folder}/*.json")): - with open(f"{f}") as data_f: + for f in sorted(Path.glob(f"{folder}/*.json")): + with Path(f).open() as data_f: data = json.load(data_f) wards = data.pop("wards", None) if wards is None: - print("Ward Data not Found ") + logging.info("Ward Data not Found ") if data.get("district") is not None: local_body = get_local_body(data) if not local_body: @@ -80,4 +81,4 @@ def get_local_body(lb): obj.save() except IntegrityError: pass - print("Processed ", str(counter), " wards") + logging.info("Processed %s wards", str(counter)) diff --git a/care/users/management/commands/populate_investigations.py b/care/users/management/commands/populate_investigations.py index a802c66522..c64a46c1ae 100644 --- a/care/users/management/commands/populate_investigations.py +++ b/care/users/management/commands/populate_investigations.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from django.core.management import BaseCommand from django.db import transaction @@ -8,10 +9,10 @@ PatientInvestigationGroup, ) -with open("data/investigations.json") as investigations_data: +with Path("data/investigations.json").open() as investigations_data: investigations = json.load(investigations_data) -with open("data/investigation_groups.json") as investigation_groups_data: +with Path("data/investigation_groups.json").open() as investigation_groups_data: investigation_groups = json.load(investigation_groups_data) @@ -22,7 +23,7 @@ class Command(BaseCommand): help = "Seed Data for Investigations" - def handle(self, *args, **options): + def handle(self, *args, **kwargs): investigation_group_dict = {} investigation_groups_to_create = [ diff --git a/care/users/management/commands/seed_data.py b/care/users/management/commands/seed_data.py index 0229caca9e..4bd59b3c02 100644 --- a/care/users/management/commands/seed_data.py +++ b/care/users/management/commands/seed_data.py @@ -15,9 +15,7 @@ class Command(BaseCommand): help = "Seed Data for Inventory" - def handle(self, *args, **options): - print("Creating Units for Inventory as well as their conversion rates") - + def handle(self, *args, **kwargs): # Inventory Unit items, _ = FacilityInventoryUnit.objects.get_or_create(name="Items") diff --git a/care/users/models.py b/care/users/models.py index 2a701a090e..6e8585878d 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -147,7 +147,7 @@ def create_superuser(self, username, email, password, **extra_fields): 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") + raise Exception district = None extra_fields["district"] = district @@ -439,6 +439,9 @@ class UserFacilityAllocation(models.Model): start_date = models.DateTimeField(default=now) end_date = models.DateTimeField(null=True, blank=True) + def __str__(self): + return self.facility.name + class UserFlag(BaseFlag): user = models.ForeignKey(User, on_delete=models.CASCADE, null=False, blank=False) diff --git a/care/users/reset_password_views.py b/care/users/reset_password_views.py index b3bda5db83..89f67ae087 100644 --- a/care/users/reset_password_views.py +++ b/care/users/reset_password_views.py @@ -170,7 +170,7 @@ def post(self, request, *args, **kwargs): ) except ValidationError as e: # raise a validation error for the serializer - raise exceptions.ValidationError({"password": e.messages}) + raise exceptions.ValidationError({"password": e.messages}) from e reset_password_token.user.set_password(password) reset_password_token.user.save() diff --git a/care/users/signals.py b/care/users/signals.py index aba1418aff..f341ed3185 100644 --- a/care/users/signals.py +++ b/care/users/signals.py @@ -18,12 +18,6 @@ def password_reset_token_created( """ Handles password reset tokens When a token is created, an e-mail needs to be sent to the user - :param sender: View Class that sent the signal - :param instance: View Instance that sent the signal - :param reset_password_token: Token Model Object - :param args: - :param kwargs: - :return: """ # send an e-mail to the user context = { @@ -57,7 +51,7 @@ def save_fields_before_update(sender, instance, raw, using, update_fields, **kwa fields_to_save &= set(update_fields) if fields_to_save: with contextlib.suppress(IndexError): - instance._previous_values = instance.__class__._base_manager.filter( + instance._previous_values = instance.__class__._base_manager.filter( # noqa SLF001 pk=instance.pk ).values(*fields_to_save)[0] diff --git a/care/users/tests/test_models.py b/care/users/tests/test_models.py index e2f45d650b..4f06b9878a 100644 --- a/care/users/tests/test_models.py +++ b/care/users/tests/test_models.py @@ -15,12 +15,6 @@ def setUpTestData(cls): name="ghatak", description="corona virus specialist" ) - def test_max_length_name(self): - """Test max length for name is 255""" - skill = self.skill - max_length = skill._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the name is returned while printing the object""" skill = self.skill @@ -35,12 +29,6 @@ def setUpTestData(cls): """ cls.state = State.objects.create(name="kerala") - def test_max_length_name(self): - """Test max length for name is 255""" - state = self.state - max_length = state._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the correct format is returned while printing the object""" state = self.state @@ -56,12 +44,6 @@ def setUpTestData(cls): state = State.objects.create(name="uttar pradesh") cls.district = District.objects.create(state=state, name="name") - def test_max_length_name(self): - """Test max length for name is 255""" - district = self.district - max_length = district._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the correct format is returned while printing the object""" district = self.district @@ -83,12 +65,6 @@ def setUpTestData(cls): district=district, name="blabla", body_type=1 ) - def test_max_length_name(self): - """Test max length for name is 255""" - local_body = self.local_body - max_length = local_body._meta.get_field("name").max_length - self.assertEqual(max_length, 255) - def test_object_name(self): """Test that the correct format is returned while printing the object""" local_body = self.local_body @@ -114,9 +90,3 @@ def setUpTestData(cls): gender=1, date_of_birth=date(2005, 1, 1), ) - - def test_max_length_phone_number(self): - """Test maximum length for phone number is 14""" - user = self.user - max_length = user._meta.get_field("phone_number").max_length - self.assertEqual(max_length, 14) diff --git a/care/utils/assetintegration/base.py b/care/utils/assetintegration/base.py index 3a430dcad0..334bcecfa5 100644 --- a/care/utils/assetintegration/base.py +++ b/care/utils/assetintegration/base.py @@ -2,6 +2,7 @@ import requests from django.conf import settings +from rest_framework import status from rest_framework.exceptions import APIException from care.utils.jwks.token_generator import generate_jwt @@ -33,40 +34,30 @@ def get_headers(self): "Accept": "application/json", } - def api_post(self, url, data=None): - req = requests.post( - url, - json=data, - headers=self.get_headers(), - timeout=self.timeout, - ) + def _validate_response(self, response: requests.Response): try: - response = req.json() - if req.status_code >= 400: - raise APIException(response, req.status_code) - return response + if response.status_code >= status.HTTP_400_BAD_REQUEST: + raise APIException(response.text, response.status_code) + return response.json() - except requests.Timeout: - raise APIException({"error": "Request Timeout"}, 504) + except requests.Timeout as e: + raise APIException({"error": "Request Timeout"}, 504) from e - except json.decoder.JSONDecodeError: - raise APIException({"error": "Invalid Response"}, req.status_code) + except json.decoder.JSONDecodeError as e: + raise APIException( + {"error": "Invalid Response"}, response.status_code + ) from e - def api_get(self, url, data=None): - req = requests.get( - url, - params=data, - headers=self.get_headers(), - timeout=self.timeout, + def api_post(self, url, data=None): + return self._validate_response( + requests.post( + url, json=data, headers=self.get_headers(), timeout=self.timeout + ) ) - try: - if req.status_code >= 400: - raise APIException(req.text, req.status_code) - response = req.json() - return response - - except requests.Timeout: - raise APIException({"error": "Request Timeout"}, 504) - except json.decoder.JSONDecodeError: - raise APIException({"error": "Invalid Response"}, req.status_code) + def api_get(self, url, data=None): + return self._validate_response( + requests.get( + url, params=data, headers=self.get_headers(), timeout=self.timeout + ) + ) diff --git a/care/utils/assetintegration/hl7monitor.py b/care/utils/assetintegration/hl7monitor.py index fffe61c963..abd14247d3 100644 --- a/care/utils/assetintegration/hl7monitor.py +++ b/care/utils/assetintegration/hl7monitor.py @@ -17,8 +17,8 @@ def __init__(self, meta): super().__init__(meta) except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args) - ) + {key: f"{key} not found in asset metadata" for key in e.args} + ) from e def handle_action(self, action): action_type = action["type"] diff --git a/care/utils/assetintegration/onvif.py b/care/utils/assetintegration/onvif.py index 6b26053f0b..815994855e 100644 --- a/care/utils/assetintegration/onvif.py +++ b/care/utils/assetintegration/onvif.py @@ -24,8 +24,8 @@ def __init__(self, meta): self.access_key = self.meta["camera_access_key"].split(":")[2] except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args) - ) + {key: f"{key} not found in asset metadata" for key in e.args} + ) from e def handle_action(self, action): action_type = action["type"] diff --git a/care/utils/assetintegration/ventilator.py b/care/utils/assetintegration/ventilator.py index a74ec0deb0..23a5280960 100644 --- a/care/utils/assetintegration/ventilator.py +++ b/care/utils/assetintegration/ventilator.py @@ -17,8 +17,8 @@ def __init__(self, meta): super().__init__(meta) except KeyError as e: raise ValidationError( - dict((key, f"{key} not found in asset metadata") for key in e.args) - ) + {key: f"{key} not found in asset metadata" for key in e.args} + ) from e def handle_action(self, action): action_type = action["type"] diff --git a/care/utils/cache/__init__.py b/care/utils/cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/csp/__init__.py b/care/utils/csp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/csp/config.py b/care/utils/csp/config.py index edff720dc9..25b25c314a 100644 --- a/care/utils/csp/config.py +++ b/care/utils/csp/config.py @@ -1,5 +1,5 @@ import enum -from typing import TypeAlias, TypedDict +from typing import TypedDict from django.conf import settings @@ -11,7 +11,7 @@ class ClientConfig(TypedDict): endpoint_url: str -BucketName: TypeAlias = str +type BucketName = str class CSProvider(enum.Enum): @@ -57,6 +57,7 @@ def get_patient_bucket_config(external) -> tuple[ClientConfig, BucketName]: def get_client_config(bucket_type: BucketType, external=False): if bucket_type == BucketType.FACILITY: return get_facility_bucket_config(external=external) - elif bucket_type == BucketType.PATIENT: + if bucket_type == BucketType.PATIENT: return get_patient_bucket_config(external=external) - raise ValueError("Invalid Bucket Type") + msg = "Invalid Bucket Type" + raise ValueError(msg) diff --git a/care/utils/event_utils.py b/care/utils/event_utils.py index cb18c22128..02789c28f2 100644 --- a/care/utils/event_utils.py +++ b/care/utils/event_utils.py @@ -14,17 +14,17 @@ def is_null(data): def get_changed_fields(old: Model, new: Model) -> set[str]: changed_fields: set[str] = set() - for field in new._meta.fields: + for field in new._meta.fields: # noqa: SLF001 field_name = field.name if getattr(old, field_name, None) != getattr(new, field_name, None): changed_fields.add(field_name) return changed_fields -def serialize_field(object: Model, field_name: str): +def serialize_field(obj: Model, field_name: str): if "__" in field_name: field_name, sub_field = field_name.split("__", 1) - related_object = getattr(object, field_name, None) + related_object = getattr(obj, field_name, None) return serialize_field(related_object, sub_field) value = None @@ -33,13 +33,13 @@ def serialize_field(object: Model, field_name: str): except AttributeError: if object is not None: logger.warning( - f"Field {field_name} not found in {object.__class__.__name__}" + "Field %s not found in %s", field_name, object.__class__.__name__ ) return None try: # serialize choice fields with display value - field = object._meta.get_field(field_name) + field = object._meta.get_field(field_name) # noqa: SLF001 if issubclass(field.__class__, Field) and field.choices: value = getattr(object, f"get_{field_name}_display", lambda: value)() except FieldDoesNotExist: @@ -51,7 +51,7 @@ def serialize_field(object: Model, field_name: str): def model_diff(old, new): diff = {} - for field in new._meta.fields: + for field in new._meta.fields: # noqa: SLF001 field_name = field.name if getattr(old, field_name, None) != getattr(new, field_name, None): diff[field_name] = getattr(new, field_name, None) @@ -65,5 +65,5 @@ def default(self, o): return list(o) if isinstance(o, datetime): return o.isoformat() - logger.warning(f"Serializing Unknown Type {type(o)}, {o}") + logger.warning("Serializing Unknown Type %s, %s", type(o), o) return str(o) diff --git a/care/utils/exceptions.py b/care/utils/exceptions.py index de8c19ad8f..69609ddfd2 100644 --- a/care/utils/exceptions.py +++ b/care/utils/exceptions.py @@ -1,2 +1,2 @@ -class CeleryTaskException(Exception): +class CeleryTaskError(Exception): pass diff --git a/care/utils/file_uploads/cover_image.py b/care/utils/file_uploads/cover_image.py index 55f1183782..a774c451bc 100644 --- a/care/utils/file_uploads/cover_image.py +++ b/care/utils/file_uploads/cover_image.py @@ -18,14 +18,14 @@ def delete_cover_image(image_key: str, folder: Literal["cover_images", "avatars" try: s3.delete_object(Bucket=bucket_name, Key=image_key) except Exception: - logger.warning(f"Failed to delete cover image {image_key}") + logger.warning("Failed to delete cover image %s", image_key) def upload_cover_image( image: UploadedFile, object_external_id: str, folder: Literal["cover_images", "avatars"], - old_key: str = None, + old_key: str | None = None, ) -> str: config, bucket_name = get_client_config(BucketType.FACILITY) s3 = boto3.client("s3", **config) @@ -34,7 +34,7 @@ def upload_cover_image( try: s3.delete_object(Bucket=bucket_name, Key=old_key) except Exception: - logger.warning(f"Failed to delete old cover image {old_key}") + logger.warning("Failed to delete old cover image %s", old_key) image_extension = image.name.rsplit(".", 1)[-1] image_key = ( diff --git a/care/utils/filters/multiselect.py b/care/utils/filters/multiselect.py index fa4befe885..cf773127c4 100644 --- a/care/utils/filters/multiselect.py +++ b/care/utils/filters/multiselect.py @@ -9,5 +9,4 @@ def filter(self, qs, value): return None values_list = value.split(",") filters = {self.field_name + "__in": values_list} - qs = qs.filter(**filters) - return qs + return qs.filter(**filters) diff --git a/care/utils/jwks/generate_jwk.py b/care/utils/jwks/generate_jwk.py index b2e1565a43..5969ca2a20 100644 --- a/care/utils/jwks/generate_jwk.py +++ b/care/utils/jwks/generate_jwk.py @@ -17,10 +17,10 @@ def generate_encoded_jwks(): def get_jwks_from_file(base_path: Path): file_path = base_path / "jwks.b64.txt" try: - with open(file_path, "r") as file: + with file_path.open() as file: return file.read() except FileNotFoundError: jwks = generate_encoded_jwks() - with open(file_path, "w") as file: + with file_path.open("w") as file: file.write(jwks) return jwks diff --git a/care/utils/jwks/token_generator.py b/care/utils/jwks/token_generator.py index d6a169334b..0daee0f4f6 100644 --- a/care/utils/jwks/token_generator.py +++ b/care/utils/jwks/token_generator.py @@ -1,7 +1,6 @@ -from datetime import datetime - from authlib.jose import jwt from django.conf import settings +from django.utils.timezone import now def generate_jwt(claims=None, exp=60, jwks=None): @@ -10,7 +9,7 @@ def generate_jwt(claims=None, exp=60, jwks=None): if jwks is None: jwks = settings.JWKS header = {"alg": "RS256"} - time = int(datetime.now().timestamp()) + time = int(now().timestamp()) payload = { "iat": time, "exp": time + exp, diff --git a/care/utils/lock.py b/care/utils/lock.py index 333f8e5dc8..01576eedfd 100644 --- a/care/utils/lock.py +++ b/care/utils/lock.py @@ -15,7 +15,7 @@ def __init__(self, key, timeout=settings.LOCK_TIMEOUT): self.timeout = timeout def acquire(self): - if not cache.set(self.key, True, self.timeout, nx=True): + if not cache.set(self.key, value=True, timeout=self.timeout, nx=True): raise ObjectLocked def release(self): diff --git a/care/utils/models/base.py b/care/utils/models/base.py index f38fb5a501..62da9f4f50 100644 --- a/care/utils/models/base.py +++ b/care/utils/models/base.py @@ -11,12 +11,6 @@ def get_queryset(self): qs = super().get_queryset() return qs.filter(deleted=False) - # def filter(self, *args, **kwargs): - # _id = kwargs.pop("id", "----") - # if _id != "----" and not isinstance(_id, int): - # kwargs["external_id"] = _id - # return super().filter(*args, **kwargs) - class BaseModel(models.Model): external_id = models.UUIDField(default=uuid4, unique=True, db_index=True) @@ -52,6 +46,16 @@ class BaseFlag(BaseModel): class Meta: abstract = True + def save(self, *args, **kwargs): + self.validate_flag(self.flag) + cache.delete( + self.cache_key_template.format( + entity_id=self.entity_id, flag_name=self.flag + ) + ) + cache.delete(self.all_flags_cache_key_template.format(entity_id=self.entity_id)) + return super().save(*args, **kwargs) + @property def entity(self): return getattr(self, self.entity_field_name) @@ -64,16 +68,6 @@ def entity_id(self): def validate_flag(cls, flag_name: FlagName): FlagRegistry.validate_flag_name(cls.flag_type, flag_name) - def save(self, *args, **kwargs): - self.validate_flag(self.flag) - cache.delete( - self.cache_key_template.format( - entity_id=self.entity_id, flag_name=self.flag - ) - ) - cache.delete(self.all_flags_cache_key_template.format(entity_id=self.entity_id)) - return super().save(*args, **kwargs) - @classmethod def check_entity_has_flag(cls, entity_id: int, flag_name: FlagName) -> bool: cls.validate_flag(flag_name) diff --git a/care/utils/models/validators.py b/care/utils/models/validators.py index 1e50d905ea..03ff80fc51 100644 --- a/care/utils/models/validators.py +++ b/care/utils/models/validators.py @@ -48,6 +48,7 @@ def _extract_errors( message = str(error).replace("\n\n", ": ").replace("\n", "") container.append(ValidationError(message)) + return None @deconstructible @@ -98,13 +99,14 @@ class PhoneNumberValidator(RegexValidator): def __init__(self, types: Iterable[str], *args, **kwargs): if not isinstance(types, Iterable) or isinstance(types, str) or len(types) == 0: - raise ValueError("The `types` argument must be a non-empty iterable.") + msg = "The `types` argument must be a non-empty iterable." + raise ValueError(msg) self.types = types self.message = f"Invalid phone number. Must be one of the following types: {', '.join(self.types)}. Received: %(value)s" self.code = "invalid_phone_number" - self.regex = r"|".join([self.regex_map[type] for type in self.types]) + self.regex = r"|".join([self.regex_map[t] for t in self.types]) super().__init__(*args, **kwargs) def __eq__(self, other): @@ -139,39 +141,38 @@ def __init__( if not allow_floats and ( isinstance(min_amount, float) or isinstance(max_amount, float) ): - raise ValueError( + msg = ( "If floats are not allowed, min_amount and max_amount must be integers" ) + raise ValueError(msg) def __call__(self, value: str): try: amount, unit = value.split(" ", maxsplit=1) if unit not in self.allowed_units: - raise ValidationError( - f"Unit must be one of {', '.join(self.allowed_units)}" - ) + msg = f"Unit must be one of {', '.join(self.allowed_units)}" + raise ValidationError(msg) amount_number: int | float = float(amount) if amount_number.is_integer(): amount_number = int(amount_number) elif not self.allow_floats: - raise ValidationError("Input amount must be an integer") + msg = "Input amount must be an integer" + raise ValidationError(msg) elif len(str(amount_number).split(".")[1]) > self.precision: - raise ValidationError("Input amount must have at most 4 decimal places") + msg = "Input amount must have at most 4 decimal places" + raise ValidationError(msg) if len(amount) != len(str(amount_number)): - raise ValidationError( - f"Input amount must be a valid number without leading{' or trailing ' if self.allow_floats else ' '}zeroes" - ) + msg = f"Input amount must be a valid number without leading{' or trailing ' if self.allow_floats else ' '}zeroes" + raise ValidationError(msg) if self.min_amount > amount_number or amount_number > self.max_amount: - raise ValidationError( - f"Input amount must be between {self.min_amount} and {self.max_amount}" - ) - except ValueError: - raise ValidationError( - "Invalid Input, must be in the format: " - ) + msg = f"Input amount must be between {self.min_amount} and {self.max_amount}" + raise ValidationError(msg) + except ValueError as e: + msg = "Invalid Input, must be in the format: " + raise ValidationError(msg) from e def clean(self, value: str): if value is None: @@ -199,6 +200,16 @@ def __eq__(self, __value: object) -> bool: # pragma: no cover ) +class MiddlewareDomainAddressValidator(RegexValidator): + regex = r"^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$" + code = "invalid_domain_name" + message = _( + "The domain name is invalid. " + "It should not start with scheme and " + "should not end with a trailing slash." + ) + + @deconstructible class ImageSizeValidator: message: dict[str, str] = { @@ -242,9 +253,9 @@ def __init__( self.min_size = min_size self.max_size = max_size if aspect_ratio: - self.aspect_ratio = set( + self.aspect_ratio = { Fraction(ratio).limit_denominator(10) for ratio in aspect_ratio - ) + } self.aspect_ratio_str = ", ".join( f"{ratio.numerator}:{ratio.denominator}" for ratio in self.aspect_ratio ) @@ -317,10 +328,11 @@ def __eq__(self, other: object) -> bool: ) def _humanize_bytes(self, size: int) -> str: + byte_size = 1024.0 for unit in ["B", "KB"]: - if size < 1024.0: + if size < byte_size: return f"{f"{size:.2f}".rstrip(".0")} {unit}" - size /= 1024.0 + size /= byte_size return f"{f"{size:.2f}".rstrip(".0")} MB" diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index 97d0f38970..0ec1718ee0 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -1,4 +1,5 @@ import json +import logging from celery import shared_task from django.apps import apps @@ -16,10 +17,12 @@ ) from care.facility.models.shifting import ShiftingRequest from care.users.models import User -from care.utils.sms.sendSMS import sendSMS +from care.utils.sms.send_sms import send_sms +logger = logging.getLogger(__name__) -class NotificationCreationException(Exception): + +class NotificationCreationError(Exception): pass @@ -41,6 +44,7 @@ def get_model_class(model_name): return apps.get_model(f"facility.{model_name}") +# ruff: noqa: SIM102, PLR0912 rebuilding the notification generator would be easier class NotificationGenerator: generate_for_facility = False generate_for_user = False @@ -64,23 +68,23 @@ def __init__( ): if not worker_initated: if not isinstance(event_type, Notification.EventType): - raise NotificationCreationException("Event Type Invalid") + msg = "Event Type Invalid" + raise NotificationCreationError(msg) if not isinstance(event, Notification.Event): - raise NotificationCreationException("Event Invalid") + msg = "Event Invalid" + raise NotificationCreationError(msg) if not isinstance(caused_by, User): - raise NotificationCreationException( - "edited_by must be an instance of a user" - ) - if facility: - if not isinstance(facility, Facility): - raise NotificationCreationException( - "facility must be an instance of Facility" - ) + msg = "edited_by must be an instance of a user" + raise NotificationCreationError(msg) + if facility and not isinstance(facility, Facility): + msg = "facility must be an instance of Facility" + raise NotificationCreationError(msg) mediums = [] if notification_mediums: for medium in notification_mediums: if not isinstance(medium, Notification.Medium): - raise NotificationCreationException("Medium Type Invalid") + msg = "Medium Type Invalid" + raise NotificationCreationError(msg) mediums.append(medium.value) data = { "event_type": event_type.value, @@ -102,8 +106,7 @@ def __init__( self.worker_initiated = False return self.worker_initiated = True - Model = get_model_class(caused_object) - caused_object = Model.objects.get(id=caused_object_pk) + caused_object = get_model_class(caused_object).objects.get(id=caused_object_pk) caused_by = User.objects.get(id=caused_by) facility = Facility.objects.get(id=facility) self.notification_mediums = notification_mediums @@ -231,6 +234,7 @@ def generate_sms_phone_numbers(self): self.caused_object.patient.phone_number, self.caused_object.patient.emergency_phone_number, ] + return None def _get_default_medium(self): return [Notification.Medium.SYSTEM.value] @@ -347,17 +351,17 @@ def send_webpush_user(self, user, message): }, ) except WebPushException as ex: - print("Web Push Failed with Exception: {}", repr(ex)) + logger.info("Web Push Failed with Exception: %s", repr(ex)) if ex.response and ex.response.json(): extra = ex.response.json() - print( - "Remote service replied with a {}:{}, {}", + logger.info( + "Remote service replied with a %s:%s, %s", extra.code, extra.errno, extra.message, ) except Exception as e: - print("Error When Doing WebPush", e) + logger.info("Error When Doing WebPush: %s", e) def generate(self): if not self.worker_initiated: @@ -367,7 +371,7 @@ def generate(self): medium == Notification.Medium.SMS.value and settings.SEND_SMS_NOTIFICATION ): - sendSMS( + send_sms( self.generate_sms_phone_numbers(), self.generate_sms_message(), many=True, diff --git a/care/utils/queryset/__init__.py b/care/utils/queryset/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/registries/feature_flag.py b/care/utils/registries/feature_flag.py index dba42888e5..a1f1406f98 100644 --- a/care/utils/registries/feature_flag.py +++ b/care/utils/registries/feature_flag.py @@ -1,13 +1,12 @@ import enum import logging -from typing import TypeAlias from django.core.exceptions import ValidationError logger = logging.getLogger(__name__) -class FlagNotFoundException(ValidationError): +class FlagNotFoundError(ValidationError): pass @@ -16,9 +15,8 @@ class FlagType(enum.Enum): FACILITY = "FACILITY" -# TODO: convert to type in python 3.12 -FlagName = str -FlagTypeRegistry: TypeAlias = dict[FlagType, dict[FlagName, bool]] +type FlagName = str +type FlagTypeRegistry = dict[FlagType, dict[FlagName, bool]] class FlagRegistry: @@ -35,7 +33,7 @@ def unregister(cls, flag_type, flag_name) -> None: try: del cls._flags[flag_type][flag_name] except KeyError as e: - logger.warning(f"Flag {flag_name} not found in {flag_type}: {e}") + logger.warning("Flag %s not found in %s: %s", flag_name, flag_type, e) @classmethod def register_wrapper(cls, flag_type, flag_name) -> None: @@ -48,13 +46,15 @@ def inner_wrapper(wrapped_class): @classmethod def validate_flag_type(cls, flag_type: FlagType) -> None: if flag_type not in cls._flags: - raise FlagNotFoundException("Invalid Flag Type") + msg = "Invalid Flag Type" + raise FlagNotFoundError(msg) @classmethod def validate_flag_name(cls, flag_type: FlagType, flag_name): cls.validate_flag_type(flag_type) if flag_name not in cls._flags[flag_type]: - raise FlagNotFoundException("Flag not registered") + msg = "Flag not registered" + raise FlagNotFoundError(msg) @classmethod def get_all_flags(cls, flag_type: FlagType) -> list[FlagName]: @@ -65,4 +65,4 @@ def get_all_flags(cls, flag_type: FlagType) -> list[FlagName]: def get_all_flags_as_choices( cls, flag_type: FlagType ) -> list[tuple[FlagName, FlagName]]: - return ((x, x) for x in cls._flags.get(flag_type, {}).keys()) + return ((x, x) for x in cls._flags.get(flag_type, {})) diff --git a/care/utils/serializers/__init__.py b/care/utils/serializers/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/serializer/external_id_field.py b/care/utils/serializers/fields.py similarity index 52% rename from care/utils/serializer/external_id_field.py rename to care/utils/serializers/fields.py index 43619d4334..f6ae57bf0d 100644 --- a/care/utils/serializer/external_id_field.py +++ b/care/utils/serializers/fields.py @@ -9,8 +9,9 @@ class UUIDValidator: def __call__(self, value): try: return uuid.UUID(value) - except ValueError: - raise serializers.ValidationError("invalid uuid") + except ValueError as e: + msg = "invalid uuid" + raise serializers.ValidationError(msg) from e class ExternalIdSerializerField(serializers.UUIDField): @@ -34,6 +35,23 @@ def run_validation(self, data=empty): if value: try: value = self.queryset.get(external_id=value) - except ObjectDoesNotExist: - raise serializers.ValidationError("object with this id not found") + except ObjectDoesNotExist as e: + msg = "object with this id not found" + raise serializers.ValidationError(msg) from e return value + + +class ChoiceField(serializers.ChoiceField): + def to_representation(self, obj): + try: + return self._choices[obj] + except KeyError: + key_type = type(next(iter(self.choices.keys()))) + key = key_type(obj) + return self._choices[key] + + def to_internal_value(self, data): + if isinstance(data, str) and data not in self.choice_strings_to_values: + choice_name_map = {v: k for k, v in self._choices.items()} + data = choice_name_map.get(data) + return super().to_internal_value(data) diff --git a/care/utils/serializer/history_serializer.py b/care/utils/serializers/history_serializer.py similarity index 100% rename from care/utils/serializer/history_serializer.py rename to care/utils/serializers/history_serializer.py diff --git a/care/utils/sms/__init__.py b/care/utils/sms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/utils/sms/sendSMS.py b/care/utils/sms/send_sms.py similarity index 76% rename from care/utils/sms/sendSMS.py rename to care/utils/sms/send_sms.py index dbf0fc9edd..38b4816895 100644 --- a/care/utils/sms/sendSMS.py +++ b/care/utils/sms/send_sms.py @@ -1,10 +1,14 @@ +import logging + import boto3 from django.conf import settings from care.utils.models.validators import mobile_validator +logger = logging.getLogger(__name__) + -def sendSMS(phone_numbers, message, many=False): +def send_sms(phone_numbers, message, many=False): if not many: phone_numbers = [phone_numbers] phone_numbers = list(set(phone_numbers)) @@ -12,6 +16,8 @@ def sendSMS(phone_numbers, message, many=False): try: mobile_validator(phone) except Exception: + if settings.DEBUG: + logger.error("Invalid Phone Number %s", phone) continue client = boto3.client( "sns", diff --git a/care/utils/tests/test_feature_flags.py b/care/utils/tests/test_feature_flags.py index 13324f20f6..6afda21dbb 100644 --- a/care/utils/tests/test_feature_flags.py +++ b/care/utils/tests/test_feature_flags.py @@ -1,12 +1,13 @@ from django.test import TestCase from care.utils.registries.feature_flag import ( - FlagNotFoundException, + FlagNotFoundError, FlagRegistry, FlagType, ) +# ruff: noqa: SLF001 class FeatureFlagTestCase(TestCase): def setUp(self): FlagRegistry._flags = {} @@ -33,7 +34,7 @@ def test_validate_flag_type(self): self.assertIsNone(FlagRegistry.validate_flag_type(FlagType.USER)) def test_validate_flag_type_invalid(self): - with self.assertRaises(FlagNotFoundException): + with self.assertRaises(FlagNotFoundError): FlagRegistry.validate_flag_type( FlagType.USER ) # FlagType.USER is not registered @@ -43,12 +44,12 @@ def test_validate_flag_name(self): self.assertIsNone(FlagRegistry.validate_flag_name(FlagType.USER, "TEST_FLAG")) def test_validate_flag_name_invalid(self): - with self.assertRaises(FlagNotFoundException) as ectx: + with self.assertRaises(FlagNotFoundError) as ectx: FlagRegistry.validate_flag_name(FlagType.USER, "TEST_OTHER_FLAG") self.assertEqual(ectx.exception.message, "Invalid Flag Type") FlagRegistry.register(FlagType.USER, "TEST_FLAG") - with self.assertRaises(FlagNotFoundException) as ectx: + with self.assertRaises(FlagNotFoundError) as ectx: FlagRegistry.validate_flag_name(FlagType.USER, "TEST_OTHER_FLAG") self.assertEqual(ectx.exception.message, "Flag not registered") diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 707f8b8308..1f858c7258 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -56,7 +56,7 @@ fake = Faker() -class override_cache(override_settings): +class OverrideCache(override_settings): """ Overrides the cache settings for the test to use a local memory cache instead of the redis cache @@ -85,16 +85,19 @@ def __eq__(self, other): mock_equal = EverythingEquals() -def assert_equal_dicts(d1, d2, ignore_keys=[]): +def assert_equal_dicts(d1, d2, ignore_keys=None): + if ignore_keys is None: + ignore_keys = [] + def check_equal(): ignored = set(ignore_keys) for k1, v1 in d1.items(): if k1 not in ignored and (k1 not in d2 or d2[k1] != v1): - print(k1, v1, d2[k1]) + print(k1, v1, d2[k1]) # noqa: T201 return False for k2, v2 in d2.items(): if k2 not in ignored and k2 not in d1: - print(k2, v2) + print(k2, v2) # noqa: T201 return False return True @@ -142,7 +145,7 @@ def create_local_body(cls, district: District, **kwargs) -> LocalBody: return LocalBody.objects.create(**data) @classmethod - def get_user_data(cls, district: District, user_type: str = None): + def get_user_data(cls, district: District, user_type: str | None = None): """ Returns the data to be used for API testing @@ -174,7 +177,7 @@ def link_user_with_facility(cls, user: User, facility: Facility, created_by: Use @classmethod def create_user( cls, - username: str = None, + username: str | None = None, district: District = None, local_body: LocalBody = None, **kwargs, @@ -265,8 +268,7 @@ def create_facility( "created_by": user, } data.update(kwargs) - facility = Facility.objects.create(**data) - return facility + return Facility.objects.create(**data) @classmethod def get_patient_data(cls, district, state) -> dict: @@ -348,7 +350,7 @@ def get_consultation_data(cls) -> dict: "discharge_date": None, "consultation_notes": "", "course_in_facility": "", - "patient_no": int(datetime.now().timestamp() * 1000), + "patient_no": int(now().timestamp() * 1000), "route_to_facility": 10, } @@ -492,7 +494,7 @@ def create_patient_consent( @classmethod def clone_object(cls, obj, save=True): - new_obj = obj._meta.model.objects.get(pk=obj.id) + new_obj = obj._meta.model.objects.get(pk=obj.id) # noqa: SLF001 new_obj.pk = None new_obj.id = None try: @@ -625,8 +627,7 @@ def get_patient_investigation_group_data(cls) -> dict: def create_patient_investigation_group(cls, **kwargs) -> PatientInvestigationGroup: data = cls.get_patient_investigation_group_data() data.update(**kwargs) - investigation_group = PatientInvestigationGroup.objects.create(**data) - return investigation_group + return PatientInvestigationGroup.objects.create(**data) @classmethod def get_patient_investigation_session_data(cls, user) -> dict: @@ -640,8 +641,7 @@ def create_patient_investigation_session( ) -> InvestigationSession: data = cls.get_patient_investigation_session_data(user) data.update(**kwargs) - investigation_session = InvestigationSession.objects.create(**data) - return investigation_session + return InvestigationSession.objects.create(**data) @classmethod def get_investigation_value_data( @@ -694,7 +694,7 @@ def get_prescription_data(cls, consultation, user) -> dict: return { "consultation": consultation, "prescription_type": "REGULAR", - "medicine": None, # TODO : Create medibase medicine + "medicine": None, "medicine_old": "Sample old Medicine", "route": "Oral", "base_dosage": "500mg", @@ -761,17 +761,16 @@ def get_local_body_district_state_representation(self, obj): def get_local_body_representation(self, local_body: LocalBody): if local_body is None: return {"local_body": None, "local_body_object": None} - else: - return { - "local_body": local_body.id, - "local_body_object": { - "id": local_body.id, - "name": local_body.name, - "district": local_body.district.id, - "localbody_code": local_body.localbody_code, - "body_type": local_body.body_type, - }, - } + return { + "local_body": local_body.id, + "local_body_object": { + "id": local_body.id, + "name": local_body.name, + "district": local_body.district.id, + "localbody_code": local_body.localbody_code, + "body_type": local_body.body_type, + }, + } def get_district_representation(self, district: District): if district is None: @@ -795,16 +794,16 @@ def dict_to_matching_type(d: dict): return {k: to_matching_type(k, v) for k, v in d.items()} def to_matching_type(name: str, value): - if isinstance(value, (OrderedDict, dict)): + if isinstance(value, OrderedDict | dict): return dict_to_matching_type(dict(value)) - elif isinstance(value, list): + if isinstance(value, list): return [to_matching_type("", v) for v in value] - elif "date" in name and not isinstance( - value, (type(None), EverythingEquals) - ): + if "date" in name and not isinstance(value, type(None) | EverythingEquals): return_value = value if isinstance(value, str): - return_value = datetime.strptime(value, "%Y-%m-%dT%H:%M:%S.%fZ") + return_value = datetime.strptime( + value, "%Y-%m-%dT%H:%M:%S.%fZ" + ).astimezone() return ( return_value.astimezone(tz=UTC) if isinstance(return_value, datetime) @@ -824,16 +823,15 @@ def execute_list(self, user=None): def get_facility_representation(self, facility): if facility is None: return facility - else: - return { - "id": str(facility.external_id), - "name": facility.name, - "facility_type": { - "id": facility.facility_type, - "name": facility.get_facility_type_display(), - }, - **self.get_local_body_district_state_representation(facility), - } + return { + "id": str(facility.external_id), + "name": facility.name, + "facility_type": { + "id": facility.facility_type, + "name": facility.get_facility_type_display(), + }, + **self.get_local_body_district_state_representation(facility), + } def create_patient_note( self, patient=None, note="Patient is doing find", created_by=None, **kwargs @@ -843,14 +841,14 @@ def create_patient_note( "note": note, } data.update(kwargs) - patientId = patient.external_id + patient_id = patient.external_id refresh_token = RefreshToken.for_user(created_by) self.client.credentials( HTTP_AUTHORIZATION=f"Bearer {refresh_token.access_token}" ) - self.client.post(f"/api/v1/patient/{patientId}/notes/", data=data) + self.client.post(f"/api/v1/patient/{patient_id}/notes/", data=data) @classmethod def create_patient_shift( diff --git a/care/utils/ulid/models.py b/care/utils/ulid/models.py index 9305735e30..bc45cf1c26 100644 --- a/care/utils/ulid/models.py +++ b/care/utils/ulid/models.py @@ -35,9 +35,9 @@ def to_python(self, value) -> ULID | None: return None try: return ULID.parse(value) - except (AttributeError, ValueError): + except (AttributeError, ValueError) as e: raise exceptions.ValidationError( self.error_messages["invalid"], code="invalid", params={"value": value}, - ) + ) from e diff --git a/care/utils/ulid/ulid.py b/care/utils/ulid/ulid.py index f697c60041..c718b88031 100644 --- a/care/utils/ulid/ulid.py +++ b/care/utils/ulid/ulid.py @@ -1,35 +1,63 @@ from typing import Self from uuid import UUID -from ulid import ULID as BaseULID +from ulid import ULID as BaseULID # noqa: N811 + +UUID_LEN_WITHOUT_HYPHENS = 32 +UUID_LEN_WITH_HYPHENS = 36 +ULID_STR_LEN = 26 +ULID_BYTES_LEN = 16 +TIMESTAMP_STR_LEN = 10 class ULID(BaseULID): @classmethod def parse(cls, value) -> Self: if isinstance(value, BaseULID): - return value + return cls.parse_baseulid(value) if isinstance(value, UUID): - return cls.from_uuid(value) + return cls.parse_uuid(value) if isinstance(value, str): - len_value = len(value) - if len_value == 32 or len_value == 36: - return cls.from_uuid(UUID(value)) - if len_value == 26: - return cls.from_str(value) - if len_value == 16: - return cls.from_bytes(value) - if len_value == 10: - return cls.from_timestamp(int(value)) - raise ValueError( - f"Cannot create ULID from string of length {len_value}" - ) - if isinstance(value, (int, float)): - return cls.from_int(int(value)) - if isinstance(value, (bytes, bytearray)): - return cls.from_bytes(value) + return cls.parse_str(value) + if isinstance(value, int | float): + return cls.parse_int_float(value) + if isinstance(value, bytes | bytearray): + return cls.parse_bytes(value) if isinstance(value, memoryview): - return cls.from_bytes(value.tobytes()) - raise ValueError( - f"Cannot create ULID from type {value.__class__.__name__}" - ) + return cls.parse_memoryview(value) + msg = f"Cannot create ULID from type {value.__class__.__name__}" + raise ValueError(msg) + + @classmethod + def parse_ulid(cls, value: BaseULID) -> Self: + return value + + @classmethod + def parse_uuid(cls, value: UUID) -> Self: + return cls.from_uuid(value) + + @classmethod + def parse_str(cls, value: str) -> Self: + len_value = len(value) + if len_value in (UUID_LEN_WITHOUT_HYPHENS, UUID_LEN_WITH_HYPHENS): + return cls.from_uuid(UUID(value)) + if len_value == ULID_STR_LEN: + return cls.from_str(value) + if len_value == ULID_BYTES_LEN: + return cls.from_bytes(value.encode()) + if len_value == TIMESTAMP_STR_LEN: + return cls.from_timestamp(int(value)) + msg = f"Cannot create ULID from string of length {len_value}" + raise ValueError(msg) + + @classmethod + def parse_int_float(cls, value: int | float) -> Self: + return cls.from_int(int(value)) + + @classmethod + def parse_bytes(cls, value: bytes | bytearray) -> Self: + return cls.from_bytes(value) + + @classmethod + def parse_memoryview(cls, value: memoryview) -> Self: + return cls.from_bytes(value.tobytes()) diff --git a/care/utils/validation/integer_validation.py b/care/utils/validation/integer_validation.py deleted file mode 100644 index cfc75184e7..0000000000 --- a/care/utils/validation/integer_validation.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework.exceptions import ValidationError - - -def check_integer(vals): - if not isinstance(vals, list): - vals = [vals] - for i in range(len(vals)): - try: - vals[i] = int(vals[i]) - except Exception: - raise ValidationError({"value": "Integer Required"}) - return vals diff --git a/config/adminlogin.py b/config/adminlogin.py index 56e9bb0957..5d1d60c45b 100644 --- a/config/adminlogin.py +++ b/config/adminlogin.py @@ -12,7 +12,6 @@ def admin_login(request, **kwargs): request, "Too many login attempts, please try again in 20 minutes" ) return redirect(reverse("admin:index")) - else: - return login_func(request, **kwargs) + return login_func(request, **kwargs) return admin_login diff --git a/config/api_router.py b/config/api_router.py index bab899767c..917b187395 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -104,10 +104,7 @@ from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet -if settings.DEBUG: - router = DefaultRouter() -else: - router = SimpleRouter() +router = DefaultRouter() if settings.DEBUG else SimpleRouter() router.register("users", UserViewSet, basename="users") user_nested_router = NestedSimpleRouter(router, r"users", lookup="users") diff --git a/config/asgi.py b/config/asgi.py deleted file mode 100644 index 156e6f0e37..0000000000 --- a/config/asgi.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -ASGI config for care project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/dev/howto/deployment/asgi/ - -""" - -import os -import sys -from pathlib import Path - -from django.core.asgi import get_asgi_application - -# This allows easy placement of apps within the interior -# care directory. -BASE_DIR = Path(__file__).resolve(strict=True).parent.parent -sys.path.append(str(BASE_DIR / "care")) - -# If DJANGO_SETTINGS_MODULE is unset, default to the local settings -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") - -# This application object is used by any ASGI server configured to use this file. -django_application = get_asgi_application() -# Apply ASGI middleware here. -# from helloworld.asgi import HelloWorldApplication -# application = HelloWorldApplication(application) - -# Import websocket application here, so apps from django_application are loaded first -from config.websocket import websocket_application # noqa isort:skip - - -async def application(scope, receive, send): - if scope["type"] == "http": - await django_application(scope, receive, send) - elif scope["type"] == "websocket": - await websocket_application(scope, receive, send) - else: - raise NotImplementedError(f"Unknown scope type {scope['type']}") diff --git a/config/auth_views.py b/config/auth_views.py index 36412d618d..4391d37195 100644 --- a/config/auth_views.py +++ b/config/auth_views.py @@ -73,9 +73,8 @@ def validate(self, attrs): @classmethod def get_token(cls, user): - raise NotImplementedError( - "Must implement `get_token` method for `TokenObtainSerializer` subclasses" - ) + msg = "Must implement `get_token` method for `TokenObtainSerializer` subclasses" + raise NotImplementedError(msg) class TokenRefreshSerializer(serializers.Serializer): diff --git a/config/authentication.py b/config/authentication.py index 87d7501b2d..086348b1bc 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -1,6 +1,5 @@ import json import logging -from datetime import datetime import jwt import requests @@ -8,6 +7,7 @@ from django.contrib.auth.models import AnonymousUser from django.core.cache import cache from django.core.exceptions import ValidationError +from django.utils import timezone from django.utils.translation import gettext_lazy as _ from drf_spectacular.extensions import OpenApiAuthenticationExtension from drf_spectacular.plumbing import build_bearer_security_scheme_object @@ -24,6 +24,9 @@ logger = logging.getLogger(__name__) +OPENID_REQUEST_TIMEOUT = 5 + + def jwk_response_cache_key(url: str) -> str: return f"jwk_response:{url}" @@ -77,7 +80,7 @@ class MiddlewareAuthentication(JWTAuthentication): def get_public_key(self, url): public_key_json = cache.get(jwk_response_cache_key(url)) if not public_key_json: - res = requests.get(url) + res = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) res.raise_for_status() public_key_json = res.json() cache.set(jwk_response_cache_key(url), public_key_json, timeout=60 * 5) @@ -136,7 +139,7 @@ def get_raw_token(self, header): # Assume the header does not contain a JSON web token return None - if len(parts) != 2: + if len(parts) != 2: # noqa: PLR2004 raise AuthenticationFailed( _("Authorization header must contain two space-delimited values"), code="bad_authorization_header", @@ -190,7 +193,7 @@ def get_user(self, validated_token, facility): user_type=User.TYPE_VALUE_MAP["Nurse"], verified=True, asset=asset_obj, - date_of_birth=datetime.now().date(), + date_of_birth=timezone.now().date(), ) asset_user.save() return asset_user @@ -198,7 +201,7 @@ def get_user(self, validated_token, facility): class ABDMAuthentication(JWTAuthentication): def open_id_authenticate(self, url, token): - public_key = requests.get(url) + public_key = requests.get(url, timeout=OPENID_REQUEST_TIMEOUT) jwk = public_key.json()["keys"][0] public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(jwk)) return jwt.decode( @@ -227,7 +230,7 @@ def get_validated_token(self, url, token): return self.open_id_authenticate(url, token) except Exception as e: logger.info(e, "Token: ", token) - raise InvalidToken({"detail": f"Invalid Authorization token: {e}"}) + raise InvalidToken({"detail": "Invalid Authorization token"}) from e def get_user(self, validated_token): user = User.objects.filter(username=settings.ABDM_USERNAME).first() @@ -241,7 +244,7 @@ def get_user(self, validated_token): phone_number="917777777777", user_type=User.TYPE_VALUE_MAP["Volunteer"], verified=True, - date_of_birth=datetime.now().date(), + date_of_birth=timezone.now().date(), ) user.save() return user @@ -253,7 +256,9 @@ class CustomJWTAuthenticationScheme(OpenApiAuthenticationExtension): def get_security_definition(self, auto_schema): return build_bearer_security_scheme_object( - header_name="Authorization", token_prefix="Bearer", bearer_format="JWT" + header_name="Authorization", + token_prefix="Bearer", + bearer_format="JWT", ) diff --git a/config/health_views.py b/config/health_views.py index 99628d9c3c..7e0801bb74 100644 --- a/config/health_views.py +++ b/config/health_views.py @@ -9,14 +9,14 @@ class MiddlewareAuthenticationVerifyView(APIView): - authentication_classes = [MiddlewareAuthentication] + authentication_classes = (MiddlewareAuthentication,) def get(self, request): return Response(UserBaseMinimumSerializer(request.user).data) class MiddlewareAssetAuthenticationVerifyView(APIView): - authentication_classes = [MiddlewareAssetAuthentication] + authentication_classes = (MiddlewareAssetAuthentication,) def get(self, request): return Response(UserBaseMinimumSerializer(request.user).data) diff --git a/config/middleware.py b/config/middlewares.py similarity index 82% rename from config/middleware.py rename to config/middlewares.py index 3243201a56..aa8ffe091f 100644 --- a/config/middleware.py +++ b/config/middlewares.py @@ -11,5 +11,5 @@ def __call__(self, request): request.start_time = time.time() response = self.get_response(request) duration = time.time() - request.start_time - self.logger.info(f"Request to {request.path} took {duration:.4f} seconds") + self.logger.info("Request to %s took %.4f seconds", request.path, duration) return response diff --git a/config/ratelimit.py b/config/ratelimit.py index d1faadbf51..8fdbbb6fbd 100644 --- a/config/ratelimit.py +++ b/config/ratelimit.py @@ -2,8 +2,10 @@ from django.conf import settings from django_ratelimit.core import is_ratelimited +VALIDATE_CAPTCHA_REQUEST_TIMEOUT = 5 -def GETKEY(group, request): + +def get_ratelimit_key(group, request): return "ratelimit" @@ -16,44 +18,43 @@ def validatecaptcha(request): "response": recaptcha_response, } captcha_response = requests.post( - "https://www.google.com/recaptcha/api/siteverify", data=values + "https://www.google.com/recaptcha/api/siteverify", + data=values, + timeout=VALIDATE_CAPTCHA_REQUEST_TIMEOUT, ) result = captcha_response.json() - if result["success"] is True: - return True - return False + return bool(result["success"]) # refer https://django-ratelimit.readthedocs.io/en/stable/rates.html for rate def ratelimit( - request, group="", keys=[None], rate=settings.DJANGO_RATE_LIMIT, increment=True + request, group="", keys=None, rate=settings.DJANGO_RATE_LIMIT, increment=True ): + if keys is None: + keys = [None] if settings.DISABLE_RATELIMIT: return False checkcaptcha = False for key in keys: if key == "ip": - group = group - key = "ip" + _group = group + _key = "ip" else: - group = group + f"-{key}" - key = GETKEY + _group = group + f"-{key}" + _key = get_ratelimit_key if is_ratelimited( request, - group=group, - key=key, + group=_group, + key=_key, rate=rate, increment=increment, ): checkcaptcha = True if checkcaptcha: - if not validatecaptcha(request): - return True - else: - return False + return not validatecaptcha(request) return False diff --git a/config/serializers.py b/config/serializers.py deleted file mode 100644 index 5570ff8ad7..0000000000 --- a/config/serializers.py +++ /dev/null @@ -1,25 +0,0 @@ -from rest_framework import serializers - - -class ChoiceField(serializers.ChoiceField): - def to_representation(self, obj): - try: - return self._choices[obj] - except KeyError: - key_type = type(list(self.choices.keys())[0]) - key = key_type(obj) - return self._choices[key] - - def to_internal_value(self, data): - if isinstance(data, str) and data not in self.choice_strings_to_values: - choice_name_map = {v: k for k, v in self._choices.items()} - data = choice_name_map.get(data) - return super(ChoiceField, self).to_internal_value(data) - - -class MultipleChoiceField(serializers.MultipleChoiceField): - def to_representation(self, value): - return super(MultipleChoiceField, self).to_representation(value) - - def to_internal_value(self, data): - return super(MultipleChoiceField, self).to_internal_value(data) diff --git a/config/settings/base.py b/config/settings/base.py index 4c439790b6..5bf8ddd3b6 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -2,6 +2,7 @@ Base settings to build other settings files upon. """ +import logging from datetime import datetime, timedelta from pathlib import Path @@ -16,6 +17,8 @@ from care.utils.csp import config as csp_config from plug_config import manager +logger = logging.getLogger(__name__) + BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent APPS_DIR = BASE_DIR / "care" env = environ.Env() @@ -197,7 +200,7 @@ # add RequestTimeLoggingMiddleware based on the environment variable if env.bool("ENABLE_REQUEST_TIME_LOGGING", default=False): - MIDDLEWARE.insert(0, "config.middleware.RequestTimeLoggingMiddleware") + MIDDLEWARE.insert(0, "config.middlewares.RequestTimeLoggingMiddleware") # STATIC # ------------------------------------------------------------------------------ @@ -275,7 +278,7 @@ CSRF_TRUSTED_ORIGINS = env.json("CSRF_TRUSTED_ORIGINS", default=[]) # https://github.com/adamchainz/django-cors-headers#cors_allowed_origin_regexes-sequencestr--patternstr -# CORS_URLS_REGEX = r"^/api/.*$" +# CORS_URLS_REGEX = r"^/api/.*$" # noqa: ERA001 # EMAIL # ------------------------------------------------------------------------------ @@ -303,9 +306,9 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#server-email # SERVER_EMAIL = env("DJANGO_SERVER_EMAIL", default=DEFAULT_FROM_EMAIL) # noqa F405 # https://docs.djangoproject.com/en/dev/ref/settings/#admins -# ADMINS = [("""👪""", "admin@ohc.network")] +# ADMINS = [("""👪""", "admin@ohc.network")] # noqa: ERA001 # https://docs.djangoproject.com/en/dev/ref/settings/#managers -# MANAGERS = ADMINS +# MANAGERS = ADMINS # noqa: ERA001 # Django Admin URL. ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin") @@ -379,7 +382,6 @@ "TITLE": "Care API", "DESCRIPTION": "Documentation of API endpoints of Care ", "VERSION": "1.0.0", - # "SERVE_PERMISSIONS": ["rest_framework.permissions.IsAdminUser"], } # Simple JWT (JWT Authentication) @@ -542,7 +544,7 @@ BUCKET_HAS_FINE_ACL = env.bool("BUCKET_HAS_FINE_ACL", default=False) if BUCKET_PROVIDER not in csp_config.CSProvider.__members__: - print(f"Warning Invalid CSP Found! {BUCKET_PROVIDER}") + logger.error("invalid CSP found: %s", BUCKET_PROVIDER) FILE_UPLOAD_BUCKET = env("FILE_UPLOAD_BUCKET", default="") FILE_UPLOAD_REGION = env("FILE_UPLOAD_REGION", default=BUCKET_REGION) @@ -615,10 +617,11 @@ # for setting the shifting mode PEACETIME_MODE = env.bool("PEACETIME_MODE", default=True) +# we are making this tz aware in the app so no need to make it aware here MIN_ENCOUNTER_DATE = env( "MIN_ENCOUNTER_DATE", - cast=lambda d: datetime.strptime(d, "%Y-%m-%d"), - default=datetime(2020, 1, 1), + cast=lambda d: datetime.strptime(d, "%Y-%m-%d"), # noqa: DTZ007 + default=datetime(2020, 1, 1), # noqa: DTZ001 ) # for exporting csv diff --git a/config/settings/local.py b/config/settings/local.py index 0edae64230..ccaf43208c 100644 --- a/config/settings/local.py +++ b/config/settings/local.py @@ -14,7 +14,7 @@ # WhiteNoise # ------------------------------------------------------------------------------ # http://whitenoise.evans.io/en/latest/django.html#using-whitenoise-in-development -INSTALLED_APPS = ["whitenoise.runserver_nostatic"] + INSTALLED_APPS +INSTALLED_APPS = ["whitenoise.runserver_nostatic", *INSTALLED_APPS] # django-silk # ------------------------------------------------------------------------------ diff --git a/config/urls.py b/config/urls.py index d06994121e..c90bd2adc2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -72,9 +72,8 @@ path("health/", include("healthy_django.urls", namespace="healthy_django")), # OpenID Connect path(".well-known/jwks.json", PublicJWKsView.as_view(), name="jwks-json"), - # TODO: Remove the config url as its not a standard implementation - path(".well-known/openid-configuration", PublicJWKsView.as_view()), -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + *static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT), +] if settings.ENABLE_ABDM: urlpatterns += abdm_urlpatterns diff --git a/config/utils.py b/config/utils.py deleted file mode 100644 index be3feadf2d..0000000000 --- a/config/utils.py +++ /dev/null @@ -1,2 +0,0 @@ -def get_psql_search_tokens(text, operator="&"): - return f" {operator} ".join([f"{word}:*" for word in text.strip().split(" ")]) diff --git a/config/validators.py b/config/validators.py deleted file mode 100644 index 754cb99496..0000000000 --- a/config/validators.py +++ /dev/null @@ -1,69 +0,0 @@ -import re - -from django.core.exceptions import ValidationError -from django.core.validators import RegexValidator -from django.utils.translation import gettext_lazy as _ - - -class NumberValidator: - def validate(self, password, user=None): - if not re.findall(r"\d", password): - raise ValidationError( - _("The password must contain at least 1 digit, 0-9."), - code="password_no_number", - ) - - def get_help_text(self): - return _("Your password must contain at least 1 digit, 0-9.") - - -class UppercaseValidator: - def validate(self, password, user=None): - if not re.findall("[A-Z]", password): - raise ValidationError( - _("The password must contain at least 1 uppercase letter, A-Z."), - code="password_no_upper", - ) - - def get_help_text(self): - return _("Your password must contain at least 1 uppercase letter, A-Z.") - - -class LowercaseValidator: - def validate(self, password, user=None): - if not re.findall("[a-z]", password): - raise ValidationError( - _("The password must contain at least 1 lowercase letter, a-z."), - code="password_no_lower", - ) - - def get_help_text(self): - return _("Your password must contain at least 1 lowercase letter, a-z.") - - -class SymbolValidator: - def validate(self, password, user=None): - if not re.findall(r"[()[\]{}|\\`~!@#$%^&*_\-+=;:'\",<>./?]", password): - raise ValidationError( - _( - "The password must contain at least 1 symbol: " - + r"()[]{}|\`~!@#$%^&*_-+=;:'\",<>./?" - ), - code="password_no_symbol", - ) - - def get_help_text(self): - return _( - "Your password must contain at least 1 symbol: " - + r"()[]{}|\`~!@#$%^&*_-+=;:'\",<>./?" - ) - - -class MiddlewareDomainAddressValidator(RegexValidator): - regex = r"^(?!https?:\/\/)[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)*\.[a-zA-Z]{2,}$" - code = "invalid_domain_name" - message = _( - "The domain name is invalid. " - "It should not start with scheme and " - "should not end with a trailing slash." - ) diff --git a/config/views.py b/config/views.py index 1cd9d3e310..30bde91d67 100644 --- a/config/views.py +++ b/config/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render -def app_version(_): +def app_version(request): return JsonResponse({"version": settings.APP_VERSION}) @@ -11,5 +11,5 @@ def home_view(request): return render(request, "pages/home.html") -def ping(_): +def ping(request): return JsonResponse({"status": "OK"}) diff --git a/config/websocket.py b/config/websocket.py deleted file mode 100644 index 81adfbc664..0000000000 --- a/config/websocket.py +++ /dev/null @@ -1,13 +0,0 @@ -async def websocket_application(scope, receive, send): - while True: - event = await receive() - - if event["type"] == "websocket.connect": - await send({"type": "websocket.accept"}) - - if event["type"] == "websocket.disconnect": - break - - if event["type"] == "websocket.receive": - if event["text"] == "ping": - await send({"type": "websocket.send", "text": "pong!"}) diff --git a/config/wsgi.py b/config/wsgi.py index c9f43409b4..31a9197142 100644 --- a/config/wsgi.py +++ b/config/wsgi.py @@ -27,7 +27,7 @@ # We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks # if running multiple sites in the same mod_wsgi process. To fix this, use # mod_wsgi daemon mode with each site in its own daemon process, or use -# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" +# os.environ["DJANGO_SETTINGS_MODULE"] = "config.settings.production" # noqa: ERA001 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.production") # This application object is used by any WSGI server configured to use this @@ -35,5 +35,5 @@ # setting points here. application = get_wsgi_application() # Apply WSGI middleware here. -# from helloworld.wsgi import HelloWorldApplication -# application = HelloWorldApplication(application) +# from helloworld.wsgi import HelloWorldApplication # noqa: ERA001 +# application = HelloWorldApplication(application) # noqa: ERA001 diff --git a/docs/conf.py b/docs/conf.py index 5bbd458924..0c47d8089c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,19 +10,11 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys - -# import django -# sys.path.insert(0, os.path.abspath('..')) -# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local") -# django.setup() - # -- Project information ----------------------------------------------------- project = "Care" -copyright = """2023, Open Healthcare Network""" +copyright = """2023, Open Healthcare Network""" # noqa: A001 author = "ohcnetwork" diff --git a/manage.py b/manage.py index 73f24a795d..fdfa506885 100755 --- a/manage.py +++ b/manage.py @@ -1,5 +1,6 @@ #!/usr/bin/env python """Django's command-line utility for administrative tasks.""" + import os import sys @@ -19,11 +20,12 @@ def main(): try: from django.core.management import execute_from_command_line except ImportError as exc: - raise ImportError( + msg = ( "Couldn't import Django. Are you sure it's installed and " "available on your PYTHONPATH environment variable? Did you " "forget to activate a virtual environment?" - ) from exc + ) + raise ImportError(msg) from exc execute_from_command_line(sys.argv) diff --git a/merge_production_dotenvs_in_dotenv.py b/merge_production_dotenvs_in_dotenv.py deleted file mode 100644 index 6fdb55325c..0000000000 --- a/merge_production_dotenvs_in_dotenv.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -from collections.abc import Sequence - -ROOT_DIR_PATH = os.path.dirname(os.path.realpath(__file__)) -PRODUCTION_DOTENVS_DIR_PATH = os.path.join(ROOT_DIR_PATH, ".envs", ".production") -PRODUCTION_DOTENV_FILE_PATHS = [ - os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".django"), - os.path.join(PRODUCTION_DOTENVS_DIR_PATH, ".postgres"), -] -DOTENV_FILE_PATH = os.path.join(ROOT_DIR_PATH, ".env") - - -def merge( - output_file_path: str, merged_file_paths: Sequence[str], append_linesep: bool = True -) -> None: - with open(output_file_path, "w") as output_file: - for merged_file_path in merged_file_paths: - with open(merged_file_path) as merged_file: - merged_file_content = merged_file.read() - output_file.write(merged_file_content) - if append_linesep: - output_file.write(os.linesep) - - -def main(): - merge(DOTENV_FILE_PATH, PRODUCTION_DOTENV_FILE_PATHS) - - -if __name__ == "__main__": - main() diff --git a/pyproject.toml b/pyproject.toml index 2ab41f7ca4..93d676ccd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,7 @@ ignore_errors = true [tool.ruff] target-version = "py312" -extend-exclude = ["*/migrations_old/*"] +extend-exclude = ["*/migrations*/*", "care/abdm/*"] include = ["*.py", "pyproject.toml"] [tool.ruff.lint] @@ -34,34 +34,34 @@ select = [ "N", # pep8-naming "UP", # pyupgrade # "ANN", # flake8-annotations - "S", # flake8-bandit - "FBT", # flake8-boolean-trap - "B", # flake8-bugbear - "A", # flake8-builtins - "COM", # flake8-commas - "C4", # flake8-comprehensions - "DTZ", # flake8-datetimez - "T10", # flake8-debugger - "DJ", # flake8-django - "EM", # flake8-errmsg - "ISC", # flake8-import-conventions - "ICN", # flake8-import-order - "LOG", # flake8-logging - "G", # flake8-logging-format - "INP", # flake8-no-pep420 - "PIE", # flake8-pie - "T20", # flake8-print - "Q", # flake8-quotes - "RSE", # flake8-raise - "RET", # flake8-return - "SLF", # flake8-self - "SIM", # flake8-simplify - "TID", # flake8-tidy-imports - "TCH", # flake8-todo - "INT", # flake8-gettext - "ARG", # flake8-unused-arguments + "S", # flake8-bandit + "FBT", # flake8-boolean-trap + "B", # flake8-bugbear + "A", # flake8-builtins + "COM", # flake8-commas + "C4", # flake8-comprehensions + "DTZ", # flake8-datetimez + "T10", # flake8-debugger + "DJ", # flake8-django + "EM", # flake8-errmsg + "ISC", # flake8-import-conventions + "ICN", # flake8-import-order + "LOG", # flake8-logging + "G", # flake8-logging-format + "INP", # flake8-no-pep420 + "PIE", # flake8-pie + "T20", # flake8-print + "Q", # flake8-quotes + "RSE", # flake8-raise + "RET", # flake8-return + "SLF", # flake8-self + "SIM", # flake8-simplify + "TID", # flake8-tidy-imports + "TCH", # flake8-todo + "INT", # flake8-gettext + # "ARG", # flake8-unused-arguments "PTH", # flake8-use-pathlib - "TD", # flake8-todo + # "TD", # flake8-todo # disabling this for now "ERA", # eradicate "PL", # pylint "FURB", # refurb @@ -79,6 +79,10 @@ ignore = [ "DJ001", # django-nullable-model-string-field "ISC001", # conflicts with format "COM812", # conflicts with format + "RUF012", # Too hard + "FBT001", # why not! + "S106", + "S105", ] @@ -86,6 +90,10 @@ ignore = [ line-ending = "lf" +[tool.ruff.lint.per-file-ignores] +"**/__init__.py" = ["E402", "F401"] # for imports +"**/tests/**" = ["DTZ001"] + [tool.ruff.lint.flake8-builtins] builtins-ignorelist = ["id", "list", "filter"] @@ -94,6 +102,5 @@ builtins-ignorelist = ["id", "list", "filter"] docstring-quotes = "double" -[tool.ruff.lint.per-file-ignores] -"**/__init__.py" = ["E402", "F401"] # for imports -"**/tests/**" = ["DTZ001"] +[tool.ruff.lint.flake8-unused-arguments] +ignore-variadic-names = true From 11b62ee48998edad6db55fee337ff8e063bd1a0e Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Wed, 25 Sep 2024 20:41:09 +0530 Subject: [PATCH 44/46] fix ulid class (#2498) --- care/utils/ulid/ulid.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/utils/ulid/ulid.py b/care/utils/ulid/ulid.py index c718b88031..bc4302d2ce 100644 --- a/care/utils/ulid/ulid.py +++ b/care/utils/ulid/ulid.py @@ -14,7 +14,7 @@ class ULID(BaseULID): @classmethod def parse(cls, value) -> Self: if isinstance(value, BaseULID): - return cls.parse_baseulid(value) + return cls.parse_ulid(value) if isinstance(value, UUID): return cls.parse_uuid(value) if isinstance(value, str): From ed4a7e14c42dd1a43b29ac2e1a58633784a0fd09 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 26 Sep 2024 21:29:00 +0530 Subject: [PATCH 45/46] fix events create (#2501) --- care/utils/event_utils.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/care/utils/event_utils.py b/care/utils/event_utils.py index 02789c28f2..55f1a183a5 100644 --- a/care/utils/event_utils.py +++ b/care/utils/event_utils.py @@ -24,24 +24,24 @@ def get_changed_fields(old: Model, new: Model) -> set[str]: def serialize_field(obj: Model, field_name: str): if "__" in field_name: field_name, sub_field = field_name.split("__", 1) - related_object = getattr(obj, field_name, None) - return serialize_field(related_object, sub_field) + related_obj = getattr(obj, field_name, None) + return serialize_field(related_obj, sub_field) value = None try: - value = getattr(object, field_name) + value = getattr(obj, field_name) except AttributeError: - if object is not None: + if obj is not None: logger.warning( - "Field %s not found in %s", field_name, object.__class__.__name__ + "Field %s not found in %s", field_name, obj.__class__.__name__ ) return None try: # serialize choice fields with display value - field = object._meta.get_field(field_name) # noqa: SLF001 + field = obj._meta.get_field(field_name) # noqa: SLF001 if issubclass(field.__class__, Field) and field.choices: - value = getattr(object, f"get_{field_name}_display", lambda: value)() + value = getattr(obj, f"get_{field_name}_display", lambda: value)() except FieldDoesNotExist: # the required field is a property and not a model field pass From b9a169bebcb7942689f4e5b985e4277b74510a79 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 26 Sep 2024 21:29:53 +0530 Subject: [PATCH 46/46] Add leeway for validating taken at in future (#2502) add leeway to taken at in future Co-authored-by: Vignesh Hari <14056798+vigneshhari@users.noreply.github.com> --- care/facility/api/serializers/daily_round.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/facility/api/serializers/daily_round.py b/care/facility/api/serializers/daily_round.py index 92d06c3c5c..768ddbabfd 100644 --- a/care/facility/api/serializers/daily_round.py +++ b/care/facility/api/serializers/daily_round.py @@ -326,7 +326,7 @@ def validate(self, attrs): return validated def validate_taken_at(self, value): - if value and value > timezone.now(): + if value and value > timezone.now() + timedelta(minutes=2): msg = "Cannot create an update in the future" raise serializers.ValidationError(msg) return value