From 627088cbc90fbd27cdf9945225d4796fc604f121 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 16:35:14 +0530 Subject: [PATCH 01/27] Fixing organization permissions --- care/emr/api/viewsets/base.py | 2 +- care/emr/api/viewsets/file_upload.py | 21 ++-- care/emr/api/viewsets/organization.py | 117 ++++++++++++++------ care/emr/fhir/resources/base.py | 3 +- care/emr/resources/base.py | 8 +- care/emr/resources/file_upload/spec.py | 1 + care/emr/resources/organization/spec.py | 11 ++ care/security/authorization/organization.py | 77 ++++++++++++- care/security/permissions/organization.py | 17 ++- config/settings/base.py | 4 + 10 files changed, 213 insertions(+), 48 deletions(-) diff --git a/care/emr/api/viewsets/base.py b/care/emr/api/viewsets/base.py index 1bc551414a..34b00846f1 100644 --- a/care/emr/api/viewsets/base.py +++ b/care/emr/api/viewsets/base.py @@ -62,7 +62,7 @@ def json_schema_spec(self, *args, **kwargs): class EMRRetrieveMixin: def retrieve(self, request, *args, **kwargs): instance = self.get_object() - data = self.get_retrieve_pydantic_model().serialize(instance) + data = self.get_retrieve_pydantic_model().serialize(instance, request.user) return Response(data.to_json()) diff --git a/care/emr/api/viewsets/file_upload.py b/care/emr/api/viewsets/file_upload.py index a430fd4e18..eba3398252 100644 --- a/care/emr/api/viewsets/file_upload.py +++ b/care/emr/api/viewsets/file_upload.py @@ -1,3 +1,4 @@ +from django.utils import timezone from pydantic import BaseModel from rest_framework.decorators import action from rest_framework.generics import get_object_or_404 @@ -7,11 +8,11 @@ from care.emr.models import FileUpload from care.emr.resources.file_upload.spec import ( FileUploadCreateSpec, + FileUploadListSpec, FileUploadRetrieveSpec, - FileUploadUpdateSpec, FileUploadListSpec, + FileUploadUpdateSpec, ) -from django.utils import timezone def file_authorizer(file_type, associating_id, permission): return @@ -51,9 +52,8 @@ def get_queryset(self): file_authorizer(obj.file_type, obj.associating_id, "read") return super().get_queryset() - @action(detail=True, methods=["POST"]) - def mark_upload_completed(self, request,*args, **kwargs): + def mark_upload_completed(self, request, *args, **kwargs): obj = self.get_object() file_authorizer(obj.file_type, obj.associating_id, "write") obj.upload_completed = True @@ -61,10 +61,10 @@ def mark_upload_completed(self, request,*args, **kwargs): return Response(FileUploadListSpec.serialize(obj).to_json()) class ArchiveRequestSpec(BaseModel): - archive_reason : str + archive_reason: str @action(detail=True, methods=["POST"]) - def archive(self, request,*args, **kwargs): + def archive(self, request, *args, **kwargs): obj = self.get_object() request_data = self.ArchiveRequestSpec(**request.data) file_authorizer(obj.file_type, obj.associating_id, "write") @@ -72,5 +72,12 @@ def archive(self, request,*args, **kwargs): obj.archive_reason = request_data.archive_reason obj.archived_datetime = timezone.now() obj.archived_by = request.user - obj.save(update_fields=["is_archived" , "archive_reason" , "archived_datetime" , "archived_by"]) + obj.save( + update_fields=[ + "is_archived", + "archive_reason", + "archived_datetime", + "archived_by", + ] + ) return Response(FileUploadListSpec.serialize(obj).to_json()) diff --git a/care/emr/api/viewsets/organization.py b/care/emr/api/viewsets/organization.py index cb258c8463..969ce63449 100644 --- a/care/emr/api/viewsets/organization.py +++ b/care/emr/api/viewsets/organization.py @@ -15,6 +15,7 @@ ) from care.emr.resources.organization.spec import ( OrganizationReadSpec, + OrganizationRetrieveSpec, OrganizationTypeChoices, OrganizationUpdateSpec, OrganizationWriteSpec, @@ -35,6 +36,7 @@ class OrganizationViewSet(EMRModelViewSet): pydantic_model = OrganizationWriteSpec pydantic_read_model = OrganizationReadSpec pydantic_update_model = OrganizationUpdateSpec + pydantic_retrieve_model = OrganizationRetrieveSpec filterset_class = OrganizationFilter filter_backends = [filters.DjangoFilterBackend] authentication_classes = [ @@ -43,15 +45,39 @@ class OrganizationViewSet(EMRModelViewSet): ] def permissions_controller(self, request): - if self.action in ["list", "retrieve"]: + if self.action in ["list"]: # All users including otp users can view the list of organizations return True - if getattr(request.user, "is_alternative_login", False): - # Deny all other permissions in OTP mode - return False - return request.user.is_authenticated + # Deny all other permissions in OTP mode + return not getattr(request.user, "is_alternative_login", False) + + def authorize_delete(self, instance): + if self.request.user.is_superuser: + return + + if instance.org_type in [ + OrganizationTypeChoices.govt.value, + OrganizationTypeChoices.role.value, + ]: + raise PermissionDenied("Organization Type cannot be deleted") + + if not AuthorizationController.call( + "can_manage_organization_obj", self.request.user, instance + ): + raise PermissionDenied( + "User does not have the required permissions to update organizations" + ) def authorize_update(self, request_obj, model_instance): + if self.request.user.is_superuser: + return + + if model_instance.org_type in [ + OrganizationTypeChoices.govt.value, + OrganizationTypeChoices.role.value, + ]: + raise PermissionDenied("Organization Type cannot be updated") + if not AuthorizationController.call( "can_manage_organization_obj", self.request.user, model_instance ): @@ -92,11 +118,21 @@ def get_queryset(self): super().get_queryset().select_related("parent", "created_by", "updated_by") ) if "parent" in self.request.GET and not self.request.GET.get("parent"): + # Filter for root organizations, For some reason its not working as intended in Django Filters queryset = queryset.filter(parent__isnull=True) + if getattr(self.request.user, "is_alternative_login", False): + # OTP Mode can only access organizations of the type govt and role + # OTP Users do not have any more permissions + return queryset.filter( + org_type__in=[ + OrganizationTypeChoices.govt.value, + ] + ) if "permission" in self.request.GET and ( not self.request.user.is_superuser or not getattr(self.request.user, "is_alternative_login", False) ): + # Filter by a permission, this is used to list organizations that the user has a permission over permission = get_object_or_404( PermissionModel, slug=self.request.GET.get("permission") ) @@ -108,10 +144,17 @@ def get_queryset(self): user=self.request.user, role_id__in=roles ).values_list("organization_id", flat=True) ) - return queryset + + # Filter organizations based on the user's permissions + return AuthorizationController.call( + "get_accessible_organizations", queryset, self.request.user + ) @action(detail=False, methods=["GET"]) def mine(self, request, *args, **kwargs): + """ + Get organizations that are directly attached to the given user + """ orgusers = OrganizationUser.objects.filter(user=request.user).select_related( "organization" ) @@ -152,11 +195,32 @@ def validate_data(self, instance, model_obj=None): raise ValidationError("User association already exists") def authorize_update(self, request_obj, model_instance): - # TODO : This logic is flawed, the users current permissions needs to be checks to understand - # if the user is capable of the edit, lower permission person should not move high permission user lower - self.authorize_create(request_obj) + organization = self.get_organization_obj() + requested_role = get_object_or_404(RoleModel, external_id=request_obj.role) + if not AuthorizationController.call( + "can_manage_organization_users_obj", + self.request.user, + organization, + model_instance.role, + ): + raise PermissionDenied("User does not have permission for this action") + if not AuthorizationController.call( + "can_manage_organization_users_obj", + self.request.user, + organization, + requested_role, + ): + raise PermissionDenied("User does not have permission for this action") - # TODO Deletes needs to be authorized, we cannot delete a user higher in prvilage than the user + def authorize_delete(self, instance): + organization = self.get_organization_obj() + if not AuthorizationController.call( + "can_manage_organization_users_obj", + self.request.user, + organization, + instance.role, + ): + raise PermissionDenied("User does not have permission for this action") def authorize_create(self, instance): """ @@ -167,31 +231,14 @@ def authorize_create(self, instance): if self.request.user.is_superuser: return organization = self.get_organization_obj() - organization_parents = [*organization.parent_cache, organization.id] - AuthorizationController.call( - "can_manage_organization_users_obj", self.request.user, organization - ) - user_roles = RoleModel.objects.filter( - id__in=OrganizationUser.objects.filter( - organization_id__in=organization_parents, user=self.request.user - ).values("role_id") - ) - merged_permissions = set() - for role in user_roles: - merged_permissions = merged_permissions.union( - set(role.get_permission_sk_for_role()) - ) - requested_role = RoleModel.objects.filter(external_id=instance.role).first() - if not requested_role: - raise Exception("Role does not exist") - requested_role = set(requested_role.get_permission_sk_for_role()) - # Confirm if requested role's permission are the subset of the users roles - if not requested_role.issubset(merged_permissions): - raise PermissionDenied( - "User does not have the required permissions to assign the role" - ) - - ## Check for duplicates + requested_role = get_object_or_404(RoleModel, external_id=instance.role) + if not AuthorizationController.call( + "can_manage_organization_users_obj", + self.request.user, + organization, + requested_role, + ): + raise PermissionDenied("User does not have permission for this action") def get_queryset(self): """ diff --git a/care/emr/fhir/resources/base.py b/care/emr/fhir/resources/base.py index ef94dbbee4..8a18da3f69 100644 --- a/care/emr/fhir/resources/base.py +++ b/care/emr/fhir/resources/base.py @@ -3,12 +3,13 @@ import json_fingerprint import simplejson as json +from django.conf import settings from django.core.cache import cache from json_fingerprint import hash_functions from care.emr.fhir.client import FHIRClient -default_fhir_client = FHIRClient(server_url="http://165.22.211.144/fhir") +default_fhir_client = FHIRClient(server_url=settings.SNOWSTORM_DEPLOYMENT_URL) class ResourceManger: diff --git a/care/emr/resources/base.py b/care/emr/resources/base.py index 188ecf6890..9f5f4fa46a 100644 --- a/care/emr/resources/base.py +++ b/care/emr/resources/base.py @@ -35,11 +35,15 @@ def get_serializer_context(cls, info): def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id + @classmethod + def perform_extra_user_serialization(cls, mapping, obj, user): + pass + def is_update(self): return getattr("_is_update", False) @classmethod - def serialize(cls, obj: __model__): + def serialize(cls, obj: __model__, user=None): """ Creates a pydantic object from a database object """ @@ -52,6 +56,8 @@ def serialize(cls, obj: __model__): if field in cls.model_fields: constructed[field] = obj.meta[field] cls.perform_extra_serialization(constructed, obj) + if user: + cls.perform_extra_user_serialization(constructed, obj, user=user) return cls.model_construct(**constructed) def perform_extra_deserialization(self, is_update, obj): diff --git a/care/emr/resources/file_upload/spec.py b/care/emr/resources/file_upload/spec.py index 75744bbe30..69a866e3f7 100644 --- a/care/emr/resources/file_upload/spec.py +++ b/care/emr/resources/file_upload/spec.py @@ -30,6 +30,7 @@ class FileUploadBaseSpec(EMRResource): class FileUploadUpdateSpec(FileUploadBaseSpec): pass + class FileUploadCreateSpec(FileUploadBaseSpec): original_name: str file_type: FileTypeChoices diff --git a/care/emr/resources/organization/spec.py b/care/emr/resources/organization/spec.py index ee0aca9041..77df508740 100644 --- a/care/emr/resources/organization/spec.py +++ b/care/emr/resources/organization/spec.py @@ -5,6 +5,7 @@ from care.emr.models.organziation import Organization from care.emr.resources.base import EMRResource from care.emr.resources.user.spec import UserSpec +from care.security.authorization import AuthorizationController class OrganizationTypeChoices(str, Enum): @@ -75,3 +76,13 @@ def perform_extra_serialization(cls, mapping, obj): mapping["created_by"] = UserSpec.serialize(obj.created_by) if obj.updated_by: mapping["updated_by"] = UserSpec.serialize(obj.updated_by) + + +class OrganizationRetrieveSpec(OrganizationReadSpec): + permissions: list[str] = [] + + @classmethod + def perform_extra_user_serialization(cls, mapping, obj, user): + mapping["permissions"] = AuthorizationController.call( + "get_permission_on_organization", obj, user + ) diff --git a/care/security/authorization/organization.py b/care/security/authorization/organization.py index b79b934885..bd0853bac7 100644 --- a/care/security/authorization/organization.py +++ b/care/security/authorization/organization.py @@ -1,7 +1,11 @@ +from django.db.models import Q + +from care.emr.models.organziation import OrganizationUser from care.security.authorization.base import ( AuthorizationController, AuthorizationHandler, ) +from care.security.models import RoleModel from care.security.permissions.organization import OrganizationPermissions @@ -26,14 +30,40 @@ def can_manage_organization_obj(self, user, organization): [*organization.parent_cache, organization.id], ) - def can_manage_organization_users_obj(self, user, organization): + def check_role_subset(self, user, organization_parents, requested_role): + """ + Check if the requested role is a subset of user's roles in an organization + """ + # Get users roles on organization, ideally only one role should be present at some level + user_roles = RoleModel.objects.filter( + id__in=OrganizationUser.objects.filter( + organization_id__in=organization_parents, user=user + ).values("role_id") + ) + merged_permissions = set() + # Convert role into a list of permissions for the user + for role in user_roles: + merged_permissions = merged_permissions.union( + set(role.get_permission_sk_for_role()) + ) + # Get the requested role's permissions + requested_role = set(requested_role.get_permission_sk_for_role()) + # Confirm if requested role's permission are the subset of the users roles + return requested_role.issubset(merged_permissions) + + def can_manage_organization_users_obj(self, user, organization, requested_role): """ Check if the user has permission to create organizations under the given organization """ + organization_parents = [*organization.parent_cache, organization.id] + + if not self.check_role_subset(user, organization_parents, requested_role): + return False + return self.check_permission_in_organization( [OrganizationPermissions.can_manage_organization_users.name], user, - [*organization.parent_cache, organization.id], + organization_parents, ) def can_list_organization_users_obj(self, user, organization): @@ -46,5 +76,48 @@ def can_list_organization_users_obj(self, user, organization): [*organization.parent_cache, organization.id], ) + def can_delete_organization(self, user, organization): + """ + Check if the user has permission to delete the given organization + """ + return self.check_permission_in_organization( + [OrganizationPermissions.can_delete_organization.name], + user, + [*organization.parent_cache, organization.id], + ) + + def get_accessible_organizations(self, qs, user): + from care.emr.resources.organization.spec import OrganizationTypeChoices + + if user.is_superuser: + return qs + roles = self.get_role_from_permissions( + [OrganizationPermissions.can_view_organization.name] + ) + organization_ids = list( + OrganizationUser.objects.filter(user=user, role_id__in=roles).values_list( + "organization_id", flat=True + ) + ) + return qs.filter( + Q(parent_cache__overlap=organization_ids) + | Q(org_type=OrganizationTypeChoices.govt.value) + | Q(id__in=organization_ids) + ) + + def get_permission_on_organization(self, organization, user): + organization_parents = [*organization.parent_cache, organization.id] + user_roles = RoleModel.objects.filter( + id__in=OrganizationUser.objects.filter( + organization_id__in=organization_parents, user=user + ).values("role_id") + ) + merged_permissions = set() + for role in user_roles: + merged_permissions = merged_permissions.union( + set(role.get_permission_sk_for_role()) + ) + return merged_permissions + AuthorizationController.register_internal_controller(OrganizationAccess) diff --git a/care/security/permissions/organization.py b/care/security/permissions/organization.py index 1d2d526db4..44a27bc00e 100644 --- a/care/security/permissions/organization.py +++ b/care/security/permissions/organization.py @@ -3,17 +3,32 @@ from care.security.permissions.constants import Permission, PermissionContext from care.security.roles.role import ( ADMIN_ROLE, + DOCTOR_ROLE, GEO_ADMIN, + NURSE_ROLE, + STAFF_ROLE, ) class OrganizationPermissions(enum.Enum): + can_view_organization = Permission( + "Can View Organizations", + "", + PermissionContext.ORGANIZATION, + [ADMIN_ROLE, STAFF_ROLE, DOCTOR_ROLE, GEO_ADMIN, NURSE_ROLE], + ) can_create_organization = Permission( "Can Create Organizations", "", PermissionContext.ORGANIZATION, [ADMIN_ROLE], ) + can_delete_organization = Permission( + "Can Delete Organizations", + "", + PermissionContext.ORGANIZATION, + [ADMIN_ROLE], + ) can_manage_organization = Permission( "Can Manage Organizations", "This includes changing names, descriptions, metadata, etc..", @@ -30,5 +45,5 @@ class OrganizationPermissions(enum.Enum): "Can List Users in an Organizations", "", PermissionContext.ORGANIZATION, - [ADMIN_ROLE, GEO_ADMIN], + [ADMIN_ROLE, STAFF_ROLE, DOCTOR_ROLE, GEO_ADMIN, NURSE_ROLE], ) diff --git a/config/settings/base.py b/config/settings/base.py index e5968e12a0..7472f91a0d 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -644,3 +644,7 @@ # Timeout for middleware request (in seconds) MIDDLEWARE_REQUEST_TIMEOUT = env.int("MIDDLEWARE_REQUEST_TIMEOUT", 20) + +SNOWSTORM_DEPLOYMENT_URL = env( + "SNOWSTORM_DEPLOYMENT_URL", default="http://165.22.211.144/fhir" +) From 8b4a8191ba3ca3b0f5b4ae155ef5c1249da33866 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 16:56:49 +0530 Subject: [PATCH 02/27] Fix for superadmins --- care/users/api/serializers/user.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 3e8092621e..b4215817fc 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -294,11 +294,14 @@ class UserSerializer(SignUpSerializer): facilities = serializers.SerializerMethodField() def get_organizations(self, user): - organizations = Organization.objects.filter( - id__in=OrganizationUser.objects.filter(user=user).values_list( - "organization_id", flat=True + if user.is_superuser: + organizations = Organization.objects.filter(parent_is_null=True) + else: + organizations = Organization.objects.filter( + id__in=OrganizationUser.objects.filter(user=user).values_list( + "organization_id", flat=True + ) ) - ) return [OrganizationReadSpec.serialize(obj).to_json() for obj in organizations] def get_permissions(self, user): From 6b219c5a58a8d7c42b41ac81d572caa61d61a2f4 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 16:59:14 +0530 Subject: [PATCH 03/27] Temporary hack for patient --- care/emr/api/viewsets/patient.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/care/emr/api/viewsets/patient.py b/care/emr/api/viewsets/patient.py index 59628716bb..d6855a3408 100644 --- a/care/emr/api/viewsets/patient.py +++ b/care/emr/api/viewsets/patient.py @@ -39,9 +39,10 @@ def get_queryset(self): .get_queryset() .select_related("created_by", "updated_by", "geo_organization") ) - return AuthorizationController.call( - "get_filtered_patients", qs, self.request.user - ) + return qs + # return AuthorizationController.call( + # "get_filtered_patients", qs, self.request.user + # ) class SearchRequestSpec(BaseModel): name: str = "" From f886cb47358f9ec540b55933185001484eafab66 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 17:00:42 +0530 Subject: [PATCH 04/27] Temporary hack for patient --- care/users/api/serializers/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index b4215817fc..fd18875b01 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -295,7 +295,7 @@ class UserSerializer(SignUpSerializer): def get_organizations(self, user): if user.is_superuser: - organizations = Organization.objects.filter(parent_is_null=True) + organizations = Organization.objects.filter(parent__is_null=True) else: organizations = Organization.objects.filter( id__in=OrganizationUser.objects.filter(user=user).values_list( From fec19c7625dc25f3223b11d5dab1104106accfbd Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 17:06:32 +0530 Subject: [PATCH 05/27] Temporary hack for patient --- care/users/api/serializers/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index fd18875b01..25aa0f9e03 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -295,7 +295,7 @@ class UserSerializer(SignUpSerializer): def get_organizations(self, user): if user.is_superuser: - organizations = Organization.objects.filter(parent__is_null=True) + organizations = Organization.objects.filter(parent__isnull=True) else: organizations = Organization.objects.filter( id__in=OrganizationUser.objects.filter(user=user).values_list( From c4364b814c165cc25a1fd063e6c6af405f8a196c Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 17:25:44 +0530 Subject: [PATCH 06/27] Fix issues with validaiton in organization --- care/emr/api/viewsets/organization.py | 11 ++++++----- care/emr/api/viewsets/patient.py | 4 +--- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/care/emr/api/viewsets/organization.py b/care/emr/api/viewsets/organization.py index 969ce63449..6085790ae6 100644 --- a/care/emr/api/viewsets/organization.py +++ b/care/emr/api/viewsets/organization.py @@ -184,14 +184,15 @@ def validate_data(self, instance, model_obj=None): if model_obj: return organization = self.get_organization_obj() - if ( - OrganizationUser.objects.filter( + queryset = OrganizationUser.objects.filter(user__external_id=instance.user) + if organization.root_org is None: + queryset = queryset.filter(organization=organization) + else: + queryset = queryset.filter( Q(organization=organization) | Q(organization__root_org=organization.root_org) ) - .filter(user__external_id=instance.user) - .exists() - ): + if queryset.exists(): raise ValidationError("User association already exists") def authorize_update(self, request_obj, model_instance): diff --git a/care/emr/api/viewsets/patient.py b/care/emr/api/viewsets/patient.py index d6855a3408..cb6efdbf9f 100644 --- a/care/emr/api/viewsets/patient.py +++ b/care/emr/api/viewsets/patient.py @@ -15,7 +15,6 @@ PatientPartialSpec, PatientRetrieveSpec, ) -from care.security.authorization import AuthorizationController class PatientFilters(FilterSet): @@ -34,12 +33,11 @@ class PatientViewSet(EMRModelViewSet): # TODO : Retrieve will work if an active encounter exists on the patient def get_queryset(self): - qs = ( + return ( super() .get_queryset() .select_related("created_by", "updated_by", "geo_organization") ) - return qs # return AuthorizationController.call( # "get_filtered_patients", qs, self.request.user # ) From 8f229be0e6069e408e75bb06a4efe48d8279e544 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 19:07:32 +0530 Subject: [PATCH 07/27] add created time in patient list spec --- care/emr/resources/patient/spec.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/care/emr/resources/patient/spec.py b/care/emr/resources/patient/spec.py index fd6cbbcc50..611879cfa6 100644 --- a/care/emr/resources/patient/spec.py +++ b/care/emr/resources/patient/spec.py @@ -79,6 +79,9 @@ def perform_extra_deserialization(self, is_update, obj): class PatientListSpec(PatientBaseSpec): + created_date : datetime.datetime + modified_date : datetime.datetime + @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id From 2400606af6c52d377ebc7ae4b7bbfccc7b01d502 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 19:50:55 +0530 Subject: [PATCH 08/27] Add Facility orgnaization permissions --- .../emr/api/viewsets/facility_organization.py | 148 ++++++++++++++++-- care/emr/api/viewsets/organization.py | 2 +- .../resources/facility_organization/spec.py | 13 +- care/emr/resources/patient/spec.py | 4 +- .../authorization/facilityorganization.py | 101 ++++++++++++ .../permissions/facility_organization.py | 37 +++++ 6 files changed, 283 insertions(+), 22 deletions(-) diff --git a/care/emr/api/viewsets/facility_organization.py b/care/emr/api/viewsets/facility_organization.py index 781af0576a..0d077eb376 100644 --- a/care/emr/api/viewsets/facility_organization.py +++ b/care/emr/api/viewsets/facility_organization.py @@ -16,6 +16,7 @@ ) from care.facility.models import Facility from care.security.authorization import AuthorizationController +from care.security.models import RoleModel class FacilityOrganizationFilter(filters.FilterSet): @@ -31,17 +32,90 @@ class FacilityOrganizationViewSet(EMRModelViewSet): filterset_class = FacilityOrganizationFilter filter_backends = [filters.DjangoFilterBackend] + def get_organization_obj(self): + return get_object_or_404( + FacilityOrganization, external_id=self.kwargs["external_id"] + ) + + def get_facility_obj(self): + return get_object_or_404( + Facility, external_id=self.kwargs["facility_external_id"] + ) + + def validate_data(self, instance, model_obj=None): + if instance.org_type == "root": + raise PermissionDenied("Cannot create root organization") + if instance.parent: + parent = get_object_or_404( + FacilityOrganization, external_id=instance.parent + ) + if parent.org_type == "root": + raise PermissionDenied("Cannot create multiple root organizations") + + def authorize_delete(self, instance): + if instance.type == "root": + raise PermissionDenied("Cannot delete root organization") + + if self.request.user.is_superuser: + return + + if not AuthorizationController.call( + "can_delete_facility_organization", self.request.user, instance + ): + raise PermissionDenied( + "User does not have the required permissions to update organization" + ) + # TODO delete should not be allowed if there are any children left + + def authorize_update(self, request_obj, model_instance): + if self.request.user.is_superuser: + return + + if not AuthorizationController.call( + "can_manage_facility_organization_obj", self.request.user, model_instance + ): + raise PermissionDenied( + "User does not have the required permissions to update organization" + ) + + def authorize_create(self, instance): + if self.request.user.is_superuser: + return True + # Organization creates require the Organization Create Permission + + if instance.parent: + parent = get_object_or_404( + FacilityOrganization, external_id=instance.parent + ) + else: + parent = None + facility = self.get_facility_obj() + if not AuthorizationController.call( + "can_create_facility_organization_obj", self.request.user, parent, facility + ): + raise PermissionDenied( + "User does not have the required permissions to create organizations" + ) + return True + def clean_create_data(self, request_data): request_data["facility"] = self.kwargs["facility_external_id"] return request_data def get_queryset(self): - return ( + facility = self.get_facility_obj() + queryset = ( super() .get_queryset() - .filter(facility__external_id=self.kwargs["facility_external_id"]) + .filter(facility=facility) .select_related("facility", "parent", "created_by", "updated_by") ) + return AuthorizationController.call( + "get_accessible_facility_organizations", + queryset, + self.request.user, + facility, + ) class FacilityOrganizationUsersViewSet(EMRModelViewSet): @@ -51,9 +125,6 @@ class FacilityOrganizationUsersViewSet(EMRModelViewSet): pydantic_update_model = FacilityOrganizationUserUpdateSpec def get_organization_obj(self): - import logging - - logging.info(self.kwargs) return get_object_or_404( FacilityOrganization, external_id=self.kwargs["facility_organizations_external_id"], @@ -69,25 +140,68 @@ def perform_create(self, instance): instance.facility = self.get_facility_obj() super().perform_create(instance) - def authorize_delete(self, instance): - if instance.org_type == "root": - raise PermissionDenied("Cannot delete root organization") - def validate_data(self, instance, model_obj=None): if model_obj: return organization = self.get_organization_obj() - if ( - FacilityOrganizationUser.objects.filter( + queryset = FacilityOrganizationUser.objects.filter( + user__external_id=instance.user + ) + if organization.root_org is None: + queryset = queryset.filter(organization=organization) + else: + queryset = queryset.filter( Q(organization=organization) | Q(organization__root_org=organization.root_org) ) - .filter(user__external_id=instance.user) - .exists() - ): + if queryset.exists(): raise ValidationError("User association already exists") - # TODO Add AuthZ, abstract based on organization users, cleanup required. + def authorize_delete(self, instance): + organization = self.get_organization_obj() + if not AuthorizationController.call( + "can_manage_facility_organization_users_obj", + self.request.user, + organization, + instance.role, + ): + raise PermissionDenied("User does not have permission for this action") + + def authorize_update(self, request_obj, model_instance): + organization = self.get_organization_obj() + requested_role = get_object_or_404(RoleModel, external_id=request_obj.role) + if not AuthorizationController.call( + "can_manage_facility_organization_users_obj", + self.request.user, + organization, + model_instance.role, + ): + raise PermissionDenied("User does not have permission for this action") + if not AuthorizationController.call( + "can_manage_facility_organization_users_obj", + self.request.user, + organization, + requested_role, + ): + raise PermissionDenied("User does not have permission for this action") + + def authorize_create(self, instance): + """ + - Creates are only allowed if the user is part of the organization + - The role applied to the new user must be equal or lower in privilege to the user created + - Maintain a permission to add users to an organization + """ + if self.request.user.is_superuser: + return + organization = self.get_organization_obj() + requested_role = get_object_or_404(RoleModel, external_id=instance.role) + if not AuthorizationController.call( + "can_manage_facility_organization_users_obj", + self.request.user, + organization, + requested_role, + ): + raise PermissionDenied("User does not have permission for this action") def get_queryset(self): """ @@ -100,4 +214,6 @@ def get_queryset(self): raise PermissionDenied( "User does not have the required permissions to list users" ) - return FacilityOrganizationUser.objects.filter(organization=organization) + return FacilityOrganizationUser.objects.filter( + organization=organization + ).select_related("organization", "user", "role") diff --git a/care/emr/api/viewsets/organization.py b/care/emr/api/viewsets/organization.py index 6085790ae6..3ba75e73d5 100644 --- a/care/emr/api/viewsets/organization.py +++ b/care/emr/api/viewsets/organization.py @@ -67,6 +67,7 @@ def authorize_delete(self, instance): raise PermissionDenied( "User does not have the required permissions to update organizations" ) + # TODO delete should not be allowed if there are any children left def authorize_update(self, request_obj, model_instance): if self.request.user.is_superuser: @@ -110,7 +111,6 @@ def authorize_create(self, instance): raise PermissionDenied( "User does not have the required permissions to create organizations" ) - # TODO Deletes are not allowed if there are child organizations return True def get_queryset(self): diff --git a/care/emr/resources/facility_organization/spec.py b/care/emr/resources/facility_organization/spec.py index 6dfdfb0c19..0c98dfbca6 100644 --- a/care/emr/resources/facility_organization/spec.py +++ b/care/emr/resources/facility_organization/spec.py @@ -18,17 +18,21 @@ class FacilityOrganizationTypeChoices(str, Enum): class FacilityOrganizationBaseSpec(EMRResource): __model__ = FacilityOrganization __exclude__ = ["facility", "parent"] - id: str = None + id: UUID4 = None active: bool = True - org_type: FacilityOrganizationTypeChoices name: str description: str = "" - parent: UUID4 | None = None metadata: dict = {} +class FacilityOrganizationUpdateSpec(FacilityOrganizationBaseSpec): + pass + + class FacilityOrganizationWriteSpec(FacilityOrganizationBaseSpec): facility: UUID4 + org_type: FacilityOrganizationTypeChoices + parent: UUID4 | None = None # TODO Validations to confirm facility and org exists @@ -74,6 +78,9 @@ def perform_extra_deserialization(self, is_update, obj): class FacilityOrganizationReadSpec(FacilityOrganizationBaseSpec): + org_type: FacilityOrganizationTypeChoices + parent: UUID4 | None = None + created_by: UserSpec = dict updated_by: UserSpec = dict system_generated: bool diff --git a/care/emr/resources/patient/spec.py b/care/emr/resources/patient/spec.py index 611879cfa6..159a5f890e 100644 --- a/care/emr/resources/patient/spec.py +++ b/care/emr/resources/patient/spec.py @@ -79,8 +79,8 @@ def perform_extra_deserialization(self, is_update, obj): class PatientListSpec(PatientBaseSpec): - created_date : datetime.datetime - modified_date : datetime.datetime + created_date: datetime.datetime + modified_date: datetime.datetime @classmethod def perform_extra_serialization(cls, mapping, obj): diff --git a/care/security/authorization/facilityorganization.py b/care/security/authorization/facilityorganization.py index 8af51b3cc2..49d33c67b6 100644 --- a/care/security/authorization/facilityorganization.py +++ b/care/security/authorization/facilityorganization.py @@ -1,13 +1,95 @@ +from care.emr.models import FacilityOrganization +from care.emr.models.organziation import FacilityOrganizationUser from care.security.authorization.base import ( AuthorizationController, AuthorizationHandler, ) +from care.security.models import RoleModel from care.security.permissions.facility_organization import ( FacilityOrganizationPermissions, ) class FacilityOrganizationAccess(AuthorizationHandler): + def check_role_subset(self, user, organization_parents, requested_role): + """ + Check if the requested role is a subset of user's roles in an organization + """ + # Get users roles on organization, ideally only one role should be present at some level + user_roles = RoleModel.objects.filter( + id__in=FacilityOrganizationUser.objects.filter( + organization_id__in=organization_parents, user=user + ).values("role_id") + ) + merged_permissions = set() + # Convert role into a list of permissions for the user + for role in user_roles: + merged_permissions = merged_permissions.union( + set(role.get_permission_sk_for_role()) + ) + # Get the requested role's permissions + requested_role = set(requested_role.get_permission_sk_for_role()) + # Confirm if requested role's permission are the subset of the users roles + return requested_role.issubset(merged_permissions) + + def can_create_facility_organization_obj(self, user, organization, facility): + """ + Check if the user has permission to create organizations under the given organization + """ + if organization: + return self.check_permission_in_facility_organization( + [FacilityOrganizationPermissions.can_create_facility_organization.name], + user, + [*organization.parent_cache, organization.id], + ) + return self.check_permission_in_facility_organization( + [FacilityOrganizationPermissions.can_create_facility_organization.name], + user, + facility=facility, + ) + + def can_manage_facility_organization_obj(self, user, organization): + """ + Check if the user has permission to manage given organization. + """ + return self.check_permission_in_facility_organization( + [FacilityOrganizationPermissions.can_manage_facility_organization.name], + user, + [*organization.parent_cache, organization.id], + ) + + def can_delete_facility_organization(self, user, organization): + """ + Check if the user has permission to delete the given organization + """ + return self.check_permission_in_facility_organization( + [FacilityOrganizationPermissions.can_delete_facility_organization.name], + user, + [*organization.parent_cache, organization.id], + ) + + def get_accessible_facility_organizations(self, qs, user, facility): + if user.is_superuser: + return qs + permission = self.check_permission_in_facility_organization( + [FacilityOrganizationPermissions.can_view_facility_organization.name], + user, + facility=facility, + ) + root_facility_organization = FacilityOrganization.objects.get( + facility=facility, org_type="root" + ) + root_permission = self.check_permission_in_facility_organization( + [FacilityOrganizationPermissions.can_view_facility_organization.name], + user, + [*root_facility_organization.parent_cache, root_facility_organization.id], + ) + if root_permission: + return qs + if permission: + return qs.exclude(org_type="root") + return qs.none() + def can_list_facility_organization_users_obj(self, user, organization): """ Check if the user has permission to create organizations under the given organization @@ -18,5 +100,24 @@ def can_list_facility_organization_users_obj(self, user, organization): orgs=[*organization.parent_cache, organization.id], ) + def can_manage_facility_organization_users_obj( + self, user, organization, requested_role + ): + """ + Check if the user has permission to create organizations under the given organization + """ + organization_parents = [*organization.parent_cache, organization.id] + + if not self.check_role_subset(user, organization_parents, requested_role): + return False + + return self.check_permission_in_facility_organization( + [ + FacilityOrganizationPermissions.can_manage_facility_organization_users.name + ], + user, + organization_parents, + ) + AuthorizationController.register_internal_controller(FacilityOrganizationAccess) diff --git a/care/security/permissions/facility_organization.py b/care/security/permissions/facility_organization.py index 705c3fc27a..0a705bb1f1 100644 --- a/care/security/permissions/facility_organization.py +++ b/care/security/permissions/facility_organization.py @@ -18,6 +18,37 @@ class FacilityOrganizationPermissions(enum.Enum): PermissionContext.FACILITY_ORGANIZATION, [FACILITY_ADMIN_ROLE], ) + can_create_facility_organization_root = Permission( + "Can Create Facility Organizations Root", + "", + PermissionContext.FACILITY_ORGANIZATION, + [FACILITY_ADMIN_ROLE], + ) + can_view_facility_organization = Permission( + "Can View Facility Organizations", + "", + PermissionContext.FACILITY_ORGANIZATION, + [ + FACILITY_ADMIN_ROLE, + ADMIN_ROLE, + STAFF_ROLE, + DOCTOR_ROLE, + GEO_ADMIN, + NURSE_ROLE, + ], + ) + can_delete_facility_organization = Permission( + "Can Delete Facility Organizations", + "", + PermissionContext.FACILITY_ORGANIZATION, + [FACILITY_ADMIN_ROLE], + ) + can_manage_facility_organization = Permission( + "Can Manage Facility Organizations", + "This includes changing names, descriptions, metadata, etc..", + PermissionContext.FACILITY_ORGANIZATION, + [FACILITY_ADMIN_ROLE], + ) can_list_facility_organization_users = Permission( "Can List Users in a Facility Organizations", "", @@ -31,3 +62,9 @@ class FacilityOrganizationPermissions(enum.Enum): GEO_ADMIN, ], ) + can_manage_facility_organization_users = Permission( + "Can Manage Users in an Organizations", + "", + PermissionContext.FACILITY_ORGANIZATION, + [FACILITY_ADMIN_ROLE], + ) From 561c92ee8b8b72caa472ec259698ade555adf0ef Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 20:03:01 +0530 Subject: [PATCH 09/27] Fix permissions list --- care/emr/api/viewsets/facility_organization.py | 2 ++ care/emr/resources/facility_organization/spec.py | 11 +++++++++++ .../security/authorization/facilityorganization.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/care/emr/api/viewsets/facility_organization.py b/care/emr/api/viewsets/facility_organization.py index 0d077eb376..96aaebd2bd 100644 --- a/care/emr/api/viewsets/facility_organization.py +++ b/care/emr/api/viewsets/facility_organization.py @@ -12,6 +12,7 @@ ) from care.emr.resources.facility_organization.spec import ( FacilityOrganizationReadSpec, + FacilityOrganizationRetrieveSpec, FacilityOrganizationWriteSpec, ) from care.facility.models import Facility @@ -29,6 +30,7 @@ class FacilityOrganizationViewSet(EMRModelViewSet): database_model = FacilityOrganization pydantic_model = FacilityOrganizationWriteSpec pydantic_read_model = FacilityOrganizationReadSpec + pydantic_retrieve_model = FacilityOrganizationRetrieveSpec filterset_class = FacilityOrganizationFilter filter_backends = [filters.DjangoFilterBackend] diff --git a/care/emr/resources/facility_organization/spec.py b/care/emr/resources/facility_organization/spec.py index 0c98dfbca6..d26ef2621f 100644 --- a/care/emr/resources/facility_organization/spec.py +++ b/care/emr/resources/facility_organization/spec.py @@ -6,6 +6,7 @@ from care.emr.resources.base import EMRResource from care.emr.resources.user.spec import UserSpec from care.facility.models import Facility +from care.security.authorization import AuthorizationController class FacilityOrganizationTypeChoices(str, Enum): @@ -96,3 +97,13 @@ def perform_extra_serialization(cls, mapping, obj): mapping["created_by"] = UserSpec.serialize(obj.created_by) if obj.updated_by: mapping["updated_by"] = UserSpec.serialize(obj.updated_by) + + +class FacilityOrganizationRetrieveSpec(FacilityOrganizationReadSpec): + permissions: list[str] = [] + + @classmethod + def perform_extra_user_serialization(cls, mapping, obj, user): + mapping["permissions"] = AuthorizationController.call( + "get_permission_on_facility_organization", obj, user + ) diff --git a/care/security/authorization/facilityorganization.py b/care/security/authorization/facilityorganization.py index 49d33c67b6..93e678f285 100644 --- a/care/security/authorization/facilityorganization.py +++ b/care/security/authorization/facilityorganization.py @@ -119,5 +119,19 @@ def can_manage_facility_organization_users_obj( organization_parents, ) + def get_permission_on_facility_organization(self, organization, user): + organization_parents = [*organization.parent_cache, organization.id] + user_roles = RoleModel.objects.filter( + id__in=FacilityOrganization.objects.filter( + organization_id__in=organization_parents, user=user + ).values("role_id") + ) + merged_permissions = set() + for role in user_roles: + merged_permissions = merged_permissions.union( + set(role.get_permission_sk_for_role()) + ) + return merged_permissions + AuthorizationController.register_internal_controller(FacilityOrganizationAccess) From cf33fc9bb3bb49bc811e17b8071a571f4f1f20b1 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 20:08:49 +0530 Subject: [PATCH 10/27] Fix permissions list --- care/emr/api/viewsets/valueset.py | 7 +++++++ care/security/authorization/facilityorganization.py | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/care/emr/api/viewsets/valueset.py b/care/emr/api/viewsets/valueset.py index 7606306aad..dacc25d3ff 100644 --- a/care/emr/api/viewsets/valueset.py +++ b/care/emr/api/viewsets/valueset.py @@ -21,6 +21,13 @@ class ValueSetViewSet(EMRModelViewSet): pydantic_read_model = ValueSetReadSpec lookup_field = "slug" + def permissions_controller(self, request): + if self.action in ["list","retrieve"]: + return True + # Only superusers have write permission over valuesets + return request.user.is_superuser + + def get_queryset(self): return ValueSet.objects.all().select_related("created_by", "updated_by") diff --git a/care/security/authorization/facilityorganization.py b/care/security/authorization/facilityorganization.py index 93e678f285..c214bc76ec 100644 --- a/care/security/authorization/facilityorganization.py +++ b/care/security/authorization/facilityorganization.py @@ -122,7 +122,7 @@ def can_manage_facility_organization_users_obj( def get_permission_on_facility_organization(self, organization, user): organization_parents = [*organization.parent_cache, organization.id] user_roles = RoleModel.objects.filter( - id__in=FacilityOrganization.objects.filter( + id__in=FacilityOrganizationUser.objects.filter( organization_id__in=organization_parents, user=user ).values("role_id") ) From 12cf11eb56e4a5f07f0fce7fdf0020db29210ca5 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 20:16:16 +0530 Subject: [PATCH 11/27] Fix permissions for superuser --- care/security/authorization/facilityorganization.py | 3 +++ care/security/authorization/organization.py | 2 ++ care/security/permissions/facility_organization.py | 4 ++-- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/care/security/authorization/facilityorganization.py b/care/security/authorization/facilityorganization.py index c214bc76ec..5102df25cd 100644 --- a/care/security/authorization/facilityorganization.py +++ b/care/security/authorization/facilityorganization.py @@ -106,6 +106,9 @@ def can_manage_facility_organization_users_obj( """ Check if the user has permission to create organizations under the given organization """ + if user.is_superuser: + return True + organization_parents = [*organization.parent_cache, organization.id] if not self.check_role_subset(user, organization_parents, requested_role): diff --git a/care/security/authorization/organization.py b/care/security/authorization/organization.py index bd0853bac7..ba27fe3312 100644 --- a/care/security/authorization/organization.py +++ b/care/security/authorization/organization.py @@ -55,6 +55,8 @@ def can_manage_organization_users_obj(self, user, organization, requested_role): """ Check if the user has permission to create organizations under the given organization """ + if user.is_superuser: + return True organization_parents = [*organization.parent_cache, organization.id] if not self.check_role_subset(user, organization_parents, requested_role): diff --git a/care/security/permissions/facility_organization.py b/care/security/permissions/facility_organization.py index 0a705bb1f1..760efce80b 100644 --- a/care/security/permissions/facility_organization.py +++ b/care/security/permissions/facility_organization.py @@ -55,11 +55,11 @@ class FacilityOrganizationPermissions(enum.Enum): PermissionContext.FACILITY_ORGANIZATION, [ FACILITY_ADMIN_ROLE, + ADMIN_ROLE, STAFF_ROLE, DOCTOR_ROLE, - NURSE_ROLE, - ADMIN_ROLE, GEO_ADMIN, + NURSE_ROLE, ], ) can_manage_facility_organization_users = Permission( From 53e72542fbd255b8dc964480267d34f898dcc63a Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 20:20:00 +0530 Subject: [PATCH 12/27] Fix permissions --- care/emr/api/viewsets/valueset.py | 3 +-- care/security/authorization/facilityorganization.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/care/emr/api/viewsets/valueset.py b/care/emr/api/viewsets/valueset.py index dacc25d3ff..be2eb2dec7 100644 --- a/care/emr/api/viewsets/valueset.py +++ b/care/emr/api/viewsets/valueset.py @@ -22,12 +22,11 @@ class ValueSetViewSet(EMRModelViewSet): lookup_field = "slug" def permissions_controller(self, request): - if self.action in ["list","retrieve"]: + if self.action in ["list", "retrieve"]: return True # Only superusers have write permission over valuesets return request.user.is_superuser - def get_queryset(self): return ValueSet.objects.all().select_related("created_by", "updated_by") diff --git a/care/security/authorization/facilityorganization.py b/care/security/authorization/facilityorganization.py index 5102df25cd..14cf0b7918 100644 --- a/care/security/authorization/facilityorganization.py +++ b/care/security/authorization/facilityorganization.py @@ -97,7 +97,7 @@ def can_list_facility_organization_users_obj(self, user, organization): return self.check_permission_in_facility_organization( [FacilityOrganizationPermissions.can_list_facility_organization_users.name], user, - orgs=[*organization.parent_cache, organization.id], + facility=organization.facility ) def can_manage_facility_organization_users_obj( From 24ba9d24a44d7a5e5a349f56a657bea5a9873d91 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 23:29:01 +0530 Subject: [PATCH 13/27] Fix valueset permissions --- care/emr/api/viewsets/allergy_intolerance.py | 22 ++++--- care/emr/api/viewsets/authz_base.py | 32 ++++++++++ care/emr/api/viewsets/base.py | 1 + care/emr/api/viewsets/encounter.py | 58 +++++++++++++++---- care/emr/api/viewsets/valueset.py | 8 ++- .../0057_patient_users_cache_patientuser.py | 44 ++++++++++++++ care/emr/models/patient.py | 23 ++++++++ care/security/authorization/encounter.py | 14 ++++- .../authorization/facilityorganization.py | 2 +- care/security/authorization/patient.py | 46 ++++++++++++++- care/security/permissions/encounter.py | 26 ++++++++- care/security/permissions/facility.py | 12 +++- care/security/permissions/patient.py | 42 +++++++++++++- 13 files changed, 299 insertions(+), 31 deletions(-) create mode 100644 care/emr/api/viewsets/authz_base.py create mode 100644 care/emr/migrations/0057_patient_users_cache_patientuser.py diff --git a/care/emr/api/viewsets/allergy_intolerance.py b/care/emr/api/viewsets/allergy_intolerance.py index a61077ffb4..16460a9c36 100644 --- a/care/emr/api/viewsets/allergy_intolerance.py +++ b/care/emr/api/viewsets/allergy_intolerance.py @@ -3,6 +3,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_view from rest_framework.exceptions import PermissionDenied +from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin from care.emr.models import Encounter from care.emr.models.allergy_intolerance import AllergyIntolerance @@ -15,6 +16,7 @@ AllergyIntrolanceSpecRead, ) from care.emr.resources.questionnaire.spec import SubjectType +from care.security.authorization import AuthorizationController class AllergyIntoleranceFilters(FilterSet): @@ -24,7 +26,9 @@ class AllergyIntoleranceFilters(FilterSet): @extend_schema_view( create=extend_schema(request=AllergyIntoleranceSpec), ) -class AllergyIntoleranceViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): +class AllergyIntoleranceViewSet( + EncounterBasedAuthorizationBase, EMRQuestionnaireResponseMixin, EMRModelViewSet +): database_model = AllergyIntolerance pydantic_model = AllergyIntoleranceSpec pydantic_read_model = AllergyIntrolanceSpecRead @@ -36,14 +40,18 @@ class AllergyIntoleranceViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): filterset_class = AllergyIntoleranceFilters filter_backends = [DjangoFilterBackend] - def authorize_create(self, instance: AllergyIntoleranceSpec): - encounter = Encounter.objects.get(external_id=instance.encounter) - if str(encounter.patient.external_id) != self.kwargs["patient_external_id"]: - err = "Malformed request" - raise PermissionDenied(err) - # Check if the user has access to the patient and write access to the encounter + def validate_data(self, instance: AllergyIntoleranceSpec, model_instance=None): + if not model_instance: + encounter = Encounter.objects.get(external_id=instance.encounter) + if str(encounter.patient.external_id) != self.kwargs["patient_external_id"]: + err = "Malformed request" + raise PermissionDenied(err) def get_queryset(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") return ( super() .get_queryset() diff --git a/care/emr/api/viewsets/authz_base.py b/care/emr/api/viewsets/authz_base.py new file mode 100644 index 0000000000..f9a6604a10 --- /dev/null +++ b/care/emr/api/viewsets/authz_base.py @@ -0,0 +1,32 @@ +from rest_framework.exceptions import PermissionDenied +from rest_framework.generics import get_object_or_404 + +from care.emr.models import Encounter, Patient +from care.security.authorization import AuthorizationController + + +class EncounterBasedAuthorizationBase: + def get_patient_obj(self): + return get_object_or_404( + Patient, external_id=self.kwargs["patient_external_id"] + ) + + def authorize_update(self, request_obj, model_instance): + encounter = get_object_or_404(Encounter, external_id=model_instance.encounter) + if not AuthorizationController.call( + "can_update_encounter_obj", self.request.user, encounter + ): + raise PermissionDenied("You do not have permission to update encounter") + + def authorize_create(self, instance): + encounter = get_object_or_404(Encounter, external_id=instance.encounter) + if not AuthorizationController.call( + "can_update_encounter_obj", self.request.user, encounter + ): + raise PermissionDenied("You do not have permission to update encounter") + + def authorize_delete(self, instance): + if not AuthorizationController.call( + "can_update_encounter_obj", self.request.user, instance.encounter + ): + raise PermissionDenied("You do not have permission to update encounter") diff --git a/care/emr/api/viewsets/base.py b/care/emr/api/viewsets/base.py index 34b00846f1..c8322282b2 100644 --- a/care/emr/api/viewsets/base.py +++ b/care/emr/api/viewsets/base.py @@ -168,6 +168,7 @@ def handle_update(self, instance, request_data): serializer_obj = pydantic_model.model_validate( clean_data, context={"is_update": True, "object": instance} ) + self.validate_data(serializer_obj, instance) self.authorize_update(serializer_obj, instance) model_instance = serializer_obj.de_serialize(obj=instance) self.perform_update(model_instance) diff --git a/care/emr/api/viewsets/encounter.py b/care/emr/api/viewsets/encounter.py index c9c0786801..b5fee0c84b 100644 --- a/care/emr/api/viewsets/encounter.py +++ b/care/emr/api/viewsets/encounter.py @@ -13,7 +13,12 @@ EMRRetrieveMixin, EMRUpdateMixin, ) -from care.emr.models import Encounter, EncounterOrganization, FacilityOrganization +from care.emr.models import ( + Encounter, + EncounterOrganization, + FacilityOrganization, + Patient, +) from care.emr.resources.encounter.constants import COMPLETED_CHOICES from care.emr.resources.encounter.spec import ( EncounterCreateSpec, @@ -39,7 +44,6 @@ def filter(self, qs, value): class EncounterFilters(filters.FilterSet): - patient = filters.UUIDFilter(field_name="patient__external_id") facility = filters.UUIDFilter(field_name="facility__external_id") status = filters.CharFilter(field_name="status", lookup_expr="iexact") encounter_class = filters.CharFilter( @@ -67,11 +71,6 @@ class EncounterViewSet( filterset_class = EncounterFilters filter_backends = [filters.DjangoFilterBackend] - def get_facility_obj(self): - return get_object_or_404( - Facility, external_id=self.kwargs["facility_external_id"] - ) - def perform_create(self, instance): with transaction.atomic(): organizations = getattr(instance, "_organizations", []) @@ -79,8 +78,10 @@ def perform_create(self, instance): for organization in organizations: EncounterOrganization.objects.create( encounter=instance, - organization=FacilityOrganization.objects.get( - external_id=organization, facility=instance.facility + organization=get_object_or_404( + FacilityOrganization, + external_id=organization, + facility=instance.facility, ), ) if not organizations: @@ -88,9 +89,9 @@ def perform_create(self, instance): def authorize_update(self, request_obj, model_instance): if not AuthorizationController.call( - "can_create_encounter_obj", self.request.user, model_instance.facility + "can_update_encounter_obj", self.request.user, model_instance ): - raise PermissionDenied("You do not have permission to create encounter") + raise PermissionDenied("You do not have permission to update encounter") def authorize_create(self, instance): # Check if encounter create permission exists on Facility Organization @@ -101,16 +102,47 @@ def authorize_create(self, instance): raise PermissionDenied("You do not have permission to create encounter") def get_queryset(self): - return ( + qs = ( super() .get_queryset() .select_related("patient", "facility", "appointment") .order_by("-created_date") ) + if ( + self.action in ["list", "retrieve"] + and "patient" in self.request.GET + and self.request.GET["patient"] + ): + # If the user has view access to the patient, then encounter view is also granted for that patient + patient = get_object_or_404( + Patient, external_id=self.request.GET["patient"] + ) + if AuthorizationController.call( + "can_view_patient_obj", self.request.user, patient + ): + return qs.filter(patient=patient) + raise PermissionDenied("User Cannot access patient") + + if ( + self.action in ["list", "retrieve"] + and "facility" in self.request.GET + and self.request.GET["facility"] + ): + # If the user has view access to the patient, then encounter view is also granted for that patient + facility = get_object_or_404( + Facility, external_id=self.request.GET["facility"] + ) + return AuthorizationController.call( + "get_filtered_encounters", qs, self.request.user, facility + ) + if self.action in ["list", "retrieve"]: + raise PermissionDenied("Cannot access encounters") + return qs # Authz Exists separately for update and deletes @action(detail=True, methods=["GET"]) def organizations(self, request, *args, **kwargs): instance = self.get_object() + self.authorize_update({}, instance) encounter_organizations = EncounterOrganization.objects.filter( encounter=instance ).select_related("organization") @@ -128,6 +160,7 @@ class EncounterOrganizationManageSpec(BaseModel): @action(detail=True, methods=["POST"]) def organizations_add(self, request, *args, **kwargs): instance = self.get_object() + self.authorize_update({}, instance) request_data = self.EncounterOrganizationManageSpec(**request.data) organization = get_object_or_404( FacilityOrganization, external_id=request_data.organization @@ -147,6 +180,7 @@ def organizations_add(self, request, *args, **kwargs): @action(detail=True, methods=["DELETE"]) def organizations_remove(self, request, *args, **kwargs): instance = self.get_object() + self.authorize_update({}, instance) request_data = self.EncounterOrganizationManageSpec(**request.data) organization = get_object_or_404( FacilityOrganization, external_id=request_data.organization diff --git a/care/emr/api/viewsets/valueset.py b/care/emr/api/viewsets/valueset.py index be2eb2dec7..5765d5d1f6 100644 --- a/care/emr/api/viewsets/valueset.py +++ b/care/emr/api/viewsets/valueset.py @@ -22,7 +22,13 @@ class ValueSetViewSet(EMRModelViewSet): lookup_field = "slug" def permissions_controller(self, request): - if self.action in ["list", "retrieve"]: + if self.action in [ + "list", + "retrieve", + "lookup_code", + "expand", + "validate_code", + ]: return True # Only superusers have write permission over valuesets return request.user.is_superuser diff --git a/care/emr/migrations/0057_patient_users_cache_patientuser.py b/care/emr/migrations/0057_patient_users_cache_patientuser.py new file mode 100644 index 0000000000..c1ec118c89 --- /dev/null +++ b/care/emr/migrations/0057_patient_users_cache_patientuser.py @@ -0,0 +1,44 @@ +# Generated by Django 5.1.3 on 2024-12-29 17:32 + +import django.contrib.postgres.fields +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0056_fileupload'), + ('security', '0002_remove_rolemodel_unique_order_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='patient', + name='users_cache', + field=django.contrib.postgres.fields.ArrayField(base_field=models.IntegerField(), default=list, size=None), + ), + migrations.CreateModel( + name='PatientUser', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='emr.patient')), + ('role', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='security.rolemodel')), + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/emr/models/patient.py b/care/emr/models/patient.py index 157742623f..ef8716571f 100644 --- a/care/emr/models/patient.py +++ b/care/emr/models/patient.py @@ -3,6 +3,7 @@ from django.db import models from care.emr.models import EMRBaseModel +from care.users.models import User from care.utils.models.validators import mobile_or_landline_number_validator @@ -36,6 +37,8 @@ class Patient(EMRBaseModel): organization_cache = ArrayField(models.IntegerField(), default=list) + users_cache = ArrayField(models.IntegerField(), default=list) + def rebuild_organization_cache(self): organization_parents = [] if self.geo_organization: @@ -49,6 +52,12 @@ def rebuild_organization_cache(self): ) self.organization_cache = list(set(organization_parents)) + def rebuild_users_cache(self): + users = list( + PatientUser.objects.filter(patient=self).values_list("user_id", flat=True) + ) + self.users_cache = users + def save(self, *args, **kwargs) -> None: self.rebuild_organization_cache() if self.date_of_birth and not self.year_of_birth: @@ -65,3 +74,17 @@ def save(self, *args, **kwargs) -> None: super().save(*args, **kwargs) self.patient.rebuild_organization_cache() self.patient.save(update_fields=["organization_cache"]) + + +class PatientUser(EMRBaseModel): + """ + Add a user that can access the patient + """ + + user = models.ForeignKey(User, on_delete=models.CASCADE) + patient = models.ForeignKey(Patient, on_delete=models.CASCADE) + role = models.ForeignKey("security.RoleModel", on_delete=models.PROTECT) + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.patient.save() diff --git a/care/security/authorization/encounter.py b/care/security/authorization/encounter.py index d81727d3ed..e685325f70 100644 --- a/care/security/authorization/encounter.py +++ b/care/security/authorization/encounter.py @@ -12,14 +12,24 @@ def can_create_encounter_obj(self, user, facility): Check if the user has permission to create encounter under this facility """ return self.check_permission_in_facility_organization( - [EncounterPermissions.can_write_encounter.name], user, facility=facility + [EncounterPermissions.can_create_encounter.name], user, facility=facility + ) + + def can_update_encounter_obj(self, user, encounter): + """ + Check if the user has permission to create encounter under this facility + """ + return self.check_permission_in_facility_organization( + [EncounterPermissions.can_write_encounter.name], + user, + orgs=encounter.facility_organization_cache, ) def get_filtered_encounters(self, qs, user, facility): if user.is_superuser: return qs roles = self.get_role_from_permissions( - [EncounterPermissions.can_list_encoutners.name] + [EncounterPermissions.can_list_encounter.name] ) organization_ids = list( FacilityOrganizationUser.objects.filter( diff --git a/care/security/authorization/facilityorganization.py b/care/security/authorization/facilityorganization.py index 14cf0b7918..14716bd072 100644 --- a/care/security/authorization/facilityorganization.py +++ b/care/security/authorization/facilityorganization.py @@ -97,7 +97,7 @@ def can_list_facility_organization_users_obj(self, user, organization): return self.check_permission_in_facility_organization( [FacilityOrganizationPermissions.can_list_facility_organization_users.name], user, - facility=organization.facility + facility=organization.facility, ) def can_manage_facility_organization_users_obj( diff --git a/care/security/authorization/patient.py b/care/security/authorization/patient.py index 6d9c9f31fa..7bc173e4f6 100644 --- a/care/security/authorization/patient.py +++ b/care/security/authorization/patient.py @@ -1,12 +1,56 @@ -from care.emr.models.organziation import OrganizationUser +from care.emr.models import Encounter, PatientUser +from care.emr.models.organziation import FacilityOrganizationUser, OrganizationUser from care.security.authorization.base import ( AuthorizationController, AuthorizationHandler, ) +from care.security.models import RolePermission from care.security.permissions.patient import PatientPermissions class PatientAccess(AuthorizationHandler): + def find_roles_on_patient(self, user, patient): + role_ids = set() + # Through Encounter + encounters = Encounter.objects.filter(patient=patient).values_list( + "facility_organization_cache", flat=True + ) + encounter_set = set() + for encounter in encounters: + encounter_set = encounter_set.union(set(encounter)) + roles = FacilityOrganizationUser.objects.filter( + organization_id__in=encounter_set, user=user + ).values_list("role_id", flat=True) + role_ids = role_ids.union(set(roles)) + # Through Organization + roles = OrganizationUser.objects.filter( + organization_id__in=patient.organization_cache, user=user + ).values_list("role_id", flat=True) + role_ids = role_ids.union(set(roles)) + # Through Direct association + roles = PatientUser.objects.filter(patient=patient, user=user).values_list( + "role_id", flat=True + ) + return role_ids.union(set(roles)) + + def can_view_patient_obj(self, user, patient): + if user.is_superuser: + return True + user_roles = self.find_roles_on_patient(user, patient) + return RolePermission.objects.filter( + permission__slug__in=[PatientPermissions.can_list_patients.name], + role__in=user_roles, + ).exists() + + def can_view_clinical_data(self, user, patient): + if user.is_superuser: + return True + user_roles = self.find_roles_on_patient(user, patient) + return RolePermission.objects.filter( + permission__slug__in=[PatientPermissions.can_view_clinical_data.name], + role__in=user_roles, + ).exists() + def get_filtered_patients(self, qs, user): if user.is_superuser: return qs diff --git a/care/security/permissions/encounter.py b/care/security/permissions/encounter.py index e0739c2213..b7fc15c5ad 100644 --- a/care/security/permissions/encounter.py +++ b/care/security/permissions/encounter.py @@ -19,17 +19,37 @@ FACILITY_ADMIN_ROLE, ] +CLINICAL_DATA_ACCESS_ROLES = [ + ADMIN_ROLE, + DOCTOR_ROLE, + NURSE_ROLE, + STAFF_ROLE, + FACILITY_ADMIN_ROLE, +] + class EncounterPermissions(enum.Enum): - can_write_encounter = Permission( + can_create_encounter = Permission( "Can write encounter", "", PermissionContext.ENCOUNTER, ALL_ROLES, ) - can_list_encoutners = Permission( + can_list_encounter = Permission( "Can list encounters", - "", + "Clinical data is not associated with this permission", PermissionContext.ENCOUNTER, ALL_ROLES, ) + can_write_encounter = Permission( + "Update Encounter and Create all associated datapoints", + "", + PermissionContext.ENCOUNTER, + CLINICAL_DATA_ACCESS_ROLES, + ) + can_read_encounter = Permission( + "Can Read encounter and related data", + "", + PermissionContext.ENCOUNTER, + CLINICAL_DATA_ACCESS_ROLES, + ) diff --git a/care/security/permissions/facility.py b/care/security/permissions/facility.py index 94bdd5205c..debd027e19 100644 --- a/care/security/permissions/facility.py +++ b/care/security/permissions/facility.py @@ -1,19 +1,25 @@ import enum from care.security.permissions.constants import Permission, PermissionContext -from care.security.roles.role import DOCTOR_ROLE, STAFF_ROLE +from care.security.roles.role import ADMIN_ROLE, DOCTOR_ROLE, GEO_ADMIN, STAFF_ROLE class FacilityPermissions(enum.Enum): + can_create_facility = Permission( + "Can Read on Facility", + "Something Here", + PermissionContext.FACILITY, + [GEO_ADMIN, ADMIN_ROLE], + ) can_read_facility = Permission( "Can Read on Facility", "Something Here", PermissionContext.FACILITY, - [STAFF_ROLE, DOCTOR_ROLE], + [GEO_ADMIN, ADMIN_ROLE, STAFF_ROLE, DOCTOR_ROLE], ) can_update_facility = Permission( "Can Update on Facility", "Something Here", PermissionContext.FACILITY, - [STAFF_ROLE], + [GEO_ADMIN, ADMIN_ROLE, STAFF_ROLE], ) diff --git a/care/security/permissions/patient.py b/care/security/permissions/patient.py index ef79e835f7..d86843c410 100644 --- a/care/security/permissions/patient.py +++ b/care/security/permissions/patient.py @@ -4,6 +4,7 @@ from care.security.roles.role import ( ADMIN_ROLE, DOCTOR_ROLE, + FACILITY_ADMIN_ROLE, GEO_ADMIN, NURSE_ROLE, STAFF_ROLE, @@ -11,9 +12,48 @@ class PatientPermissions(enum.Enum): + can_create_patient = Permission( + "Can Create Patient", + "", + PermissionContext.PATIENT, + [ + STAFF_ROLE, + DOCTOR_ROLE, + NURSE_ROLE, + GEO_ADMIN, + ADMIN_ROLE, + FACILITY_ADMIN_ROLE, + ], + ) + can_write_patient = Permission( + "Can Update a Patient's data", + "", + PermissionContext.PATIENT, + [ + STAFF_ROLE, + DOCTOR_ROLE, + NURSE_ROLE, + GEO_ADMIN, + ADMIN_ROLE, + FACILITY_ADMIN_ROLE, + ], + ) can_list_patients = Permission( "Can list patients", "", PermissionContext.PATIENT, - [STAFF_ROLE, DOCTOR_ROLE, NURSE_ROLE, GEO_ADMIN, ADMIN_ROLE], + [ + STAFF_ROLE, + DOCTOR_ROLE, + NURSE_ROLE, + GEO_ADMIN, + ADMIN_ROLE, + FACILITY_ADMIN_ROLE, + ], ) + can_view_clinical_data = Permission( + "Can view clinical data about patients", + "", + PermissionContext.PATIENT, + [STAFF_ROLE, DOCTOR_ROLE, NURSE_ROLE, ADMIN_ROLE, FACILITY_ADMIN_ROLE], + ) # To be split into finer grain permissions From ba31740cb6ffe9a47d5939c9b4cae353982c5097 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Sun, 29 Dec 2024 23:31:10 +0530 Subject: [PATCH 14/27] Add parent null filter for patient --- care/emr/api/viewsets/facility_organization.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/care/emr/api/viewsets/facility_organization.py b/care/emr/api/viewsets/facility_organization.py index 96aaebd2bd..30fec47968 100644 --- a/care/emr/api/viewsets/facility_organization.py +++ b/care/emr/api/viewsets/facility_organization.py @@ -112,6 +112,9 @@ def get_queryset(self): .filter(facility=facility) .select_related("facility", "parent", "created_by", "updated_by") ) + if "parent" in self.request.GET and not self.request.GET.get("parent"): + # Filter for root organizations, For some reason its not working as intended in Django Filters + queryset = queryset.filter(parent__isnull=True) return AuthorizationController.call( "get_accessible_facility_organizations", queryset, From c38be3cb0c729dc778a3afcf2bfee3b31b27e5ef Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 00:03:00 +0530 Subject: [PATCH 15/27] Add patient users api --- care/emr/api/viewsets/patient.py | 47 ++++++++++++++++++++++++++-- care/emr/resources/encounter/spec.py | 9 +++++- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/care/emr/api/viewsets/patient.py b/care/emr/api/viewsets/patient.py index cb6efdbf9f..c5d2b5f5f5 100644 --- a/care/emr/api/viewsets/patient.py +++ b/care/emr/api/viewsets/patient.py @@ -2,12 +2,14 @@ from django_filters import CharFilter, FilterSet from django_filters.rest_framework import DjangoFilterBackend -from pydantic import BaseModel +from pydantic import UUID4, BaseModel from rest_framework.decorators import action -from rest_framework.exceptions import PermissionDenied +from rest_framework.exceptions import PermissionDenied, ValidationError +from rest_framework.generics import get_object_or_404 from rest_framework.response import Response from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.models import PatientUser from care.emr.models.patient import Patient from care.emr.resources.patient.spec import ( PatientCreateSpec, @@ -15,6 +17,9 @@ PatientPartialSpec, PatientRetrieveSpec, ) +from care.emr.resources.user.spec import UserSpec +from care.security.models import RoleModel +from care.users.models import User class PatientFilters(FilterSet): @@ -78,3 +83,41 @@ def search_retrieve(self, request, *args, **kwargs): if str(patient.external_id)[:5] == request_data.partial_id: return Response(PatientRetrieveSpec.serialize(patient).to_json()) raise PermissionDenied("No valid patients found") + + @action(detail=True, methods=["GET"]) + def get_users(self, request, *args, **kwargs): + patient = self.get_object() + patient_users = PatientUser.objects.filter(patient=patient) + data = [ + UserSpec.serialize(patient_user.user).to_json() + for patient_user in patient_users + ] + return Response({"results": data}) + + class PatientUserCreateSpec(BaseModel): + user: UUID4 + role: UUID4 + + @action(detail=True, methods=["POST"]) + def add_user(self, request, *args, **kwargs): + request_data = self.PatientUserCreateSpec(**self.request.data) + user = get_object_or_404(User, external_id=request_data.user) + role = get_object_or_404(RoleModel, external_id=request_data.role) + patient = self.get_object() + if PatientUser.objects.filter(user=user, patient=patient).exists(): + raise ValidationError("User already exists") + PatientUser.objects.create(user=user, patient=patient, role=role) + return Response(UserSpec.serialize(user).to_json()) + + class PatientUserDeleteSpec(BaseModel): + user: UUID4 + + @action(detail=True, methods=["DELETE"]) + def delete_user(self, request, *args, **kwargs): + request_data = self.PatientUserDeleteSpec(**self.request.data) + user = get_object_or_404(User, external_id=request_data.user) + patient = self.get_object() + if not PatientUser.objects.filter(user=user, patient=patient).exists(): + raise ValidationError("User does not exist") + PatientUser.objects.filter(user=user, patient=patient).delete() + return Response({}, status=204) diff --git a/care/emr/resources/encounter/spec.py b/care/emr/resources/encounter/spec.py index 8731b1954a..67f2c7668d 100644 --- a/care/emr/resources/encounter/spec.py +++ b/care/emr/resources/encounter/spec.py @@ -4,7 +4,7 @@ from django.utils import timezone from pydantic import UUID4, BaseModel, model_validator -from care.emr.models import Encounter, TokenBooking +from care.emr.models import Encounter, EncounterOrganization, TokenBooking from care.emr.models.patient import Patient from care.emr.resources.base import EMRResource from care.emr.resources.encounter.constants import ( @@ -16,6 +16,7 @@ StatusChoices, ) from care.emr.resources.facility.spec import FacilityBareMinimumSpec +from care.emr.resources.facility_organization.spec import FacilityOrganizationReadSpec from care.emr.resources.patient.spec import PatientListSpec from care.emr.resources.scheduling.slot.spec import TokenBookingReadSpec from care.emr.resources.user.spec import UserSpec @@ -108,6 +109,7 @@ class EncounterRetrieveSpec(EncounterListSpec): appointment: dict = {} created_by: dict = {} updated_by: dict = {} + organizations: list[dict] = [] @classmethod def perform_extra_serialization(cls, mapping, obj): @@ -116,6 +118,11 @@ def perform_extra_serialization(cls, mapping, obj): mapping["appointment"] = TokenBookingReadSpec.serialize( obj.appointment ).to_json() + organizations = EncounterOrganization.objects.filter(encounter=obj) + mapping["organizations"] = [ + FacilityOrganizationReadSpec.serialize(encounter_org.organization).to_json() + for encounter_org in organizations + ] if obj.created_by: mapping["created_by"] = UserSpec.serialize(obj.created_by) if obj.updated_by: From 04bdb81345f04d608c99c210216051af8d72826c Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 00:18:14 +0530 Subject: [PATCH 16/27] Add Authz to everything --- care/emr/api/viewsets/authz_base.py | 3 +- care/emr/api/viewsets/condition.py | 18 ++++++++-- care/emr/api/viewsets/medication_request.py | 11 +++++- care/emr/api/viewsets/medication_statement.py | 11 +++++- care/emr/api/viewsets/notes.py | 34 ++++++++++++++++++- care/emr/api/viewsets/observation.py | 10 +++++- 6 files changed, 79 insertions(+), 8 deletions(-) diff --git a/care/emr/api/viewsets/authz_base.py b/care/emr/api/viewsets/authz_base.py index f9a6604a10..c9734c6ee9 100644 --- a/care/emr/api/viewsets/authz_base.py +++ b/care/emr/api/viewsets/authz_base.py @@ -12,9 +12,8 @@ def get_patient_obj(self): ) def authorize_update(self, request_obj, model_instance): - encounter = get_object_or_404(Encounter, external_id=model_instance.encounter) if not AuthorizationController.call( - "can_update_encounter_obj", self.request.user, encounter + "can_update_encounter_obj", self.request.user, model_instance.encounter ): raise PermissionDenied("You do not have permission to update encounter") diff --git a/care/emr/api/viewsets/condition.py b/care/emr/api/viewsets/condition.py index 1d032d0a61..cdf9ec9819 100644 --- a/care/emr/api/viewsets/condition.py +++ b/care/emr/api/viewsets/condition.py @@ -2,6 +2,7 @@ from django_filters.rest_framework import DjangoFilterBackend from rest_framework.exceptions import PermissionDenied +from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin from care.emr.models.condition import Condition from care.emr.models.encounter import Encounter @@ -14,6 +15,7 @@ ConditionSpecRead, ) from care.emr.resources.questionnaire.spec import SubjectType +from care.security.authorization import AuthorizationController class ConditionFilters(FilterSet): @@ -25,7 +27,9 @@ class ConditionFilters(FilterSet): severity = CharFilter(field_name="severity", lookup_expr="iexact") -class SymptomViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): +class SymptomViewSet( + EncounterBasedAuthorizationBase, EMRQuestionnaireResponseMixin, EMRModelViewSet +): database_model = Condition pydantic_model = ConditionSpec pydantic_read_model = ConditionSpecRead @@ -51,6 +55,10 @@ def authorize_create(self, instance: ConditionSpec): def get_queryset(self): # Check if the user has read access to the patient and their EMR Data + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") return ( super() .get_queryset() @@ -65,7 +73,9 @@ def get_queryset(self): InternalQuestionnaireRegistry.register(SymptomViewSet) -class DiagnosisViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): +class DiagnosisViewSet( + EncounterBasedAuthorizationBase, EMRQuestionnaireResponseMixin, EMRModelViewSet +): database_model = Condition pydantic_model = ConditionSpec pydantic_read_model = ConditionSpecRead @@ -91,6 +101,10 @@ def authorize_create(self, instance: ConditionSpec): def get_queryset(self): # Check if the user has read access to the patient and their EMR Data + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") return ( super() .get_queryset() diff --git a/care/emr/api/viewsets/medication_request.py b/care/emr/api/viewsets/medication_request.py index 3d27d3b018..d9aee0b70d 100644 --- a/care/emr/api/viewsets/medication_request.py +++ b/care/emr/api/viewsets/medication_request.py @@ -1,5 +1,7 @@ from django_filters import rest_framework as filters +from rest_framework.exceptions import PermissionDenied +from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin from care.emr.models.medication_request import MedicationRequest from care.emr.registries.system_questionnaire.system_questionnaire import ( @@ -10,13 +12,16 @@ MedicationRequestSpec, ) from care.emr.resources.questionnaire.spec import SubjectType +from care.security.authorization import AuthorizationController class MedicationRequestFilter(filters.FilterSet): encounter = filters.UUIDFilter(field_name="encounter__external_id") -class MedicationRequestViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): +class MedicationRequestViewSet( + EncounterBasedAuthorizationBase, EMRQuestionnaireResponseMixin, EMRModelViewSet +): database_model = MedicationRequest pydantic_model = MedicationRequestSpec pydantic_read_model = MedicationRequestReadSpec @@ -28,6 +33,10 @@ class MedicationRequestViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): filter_backends = [filters.DjangoFilterBackend] def get_queryset(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") return ( super() .get_queryset() diff --git a/care/emr/api/viewsets/medication_statement.py b/care/emr/api/viewsets/medication_statement.py index c80df6ba22..d244798c95 100644 --- a/care/emr/api/viewsets/medication_statement.py +++ b/care/emr/api/viewsets/medication_statement.py @@ -1,5 +1,7 @@ from django_filters import rest_framework as filters +from rest_framework.exceptions import PermissionDenied +from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin from care.emr.models.medication_statement import MedicationStatement from care.emr.registries.system_questionnaire.system_questionnaire import ( @@ -10,13 +12,16 @@ MedicationStatementSpec, ) from care.emr.resources.questionnaire.spec import SubjectType +from care.security.authorization import AuthorizationController class MedicationStatementFilter(filters.FilterSet): encounter = filters.UUIDFilter(field_name="encounter__external_id") -class MedicationStatementViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet): +class MedicationStatementViewSet( + EncounterBasedAuthorizationBase, EMRQuestionnaireResponseMixin, EMRModelViewSet +): database_model = MedicationStatement pydantic_model = MedicationStatementSpec pydantic_read_model = MedicationStatementReadSpec @@ -28,6 +33,10 @@ class MedicationStatementViewSet(EMRQuestionnaireResponseMixin, EMRModelViewSet) filter_backends = [filters.DjangoFilterBackend] def get_queryset(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") return ( super() .get_queryset() diff --git a/care/emr/api/viewsets/notes.py b/care/emr/api/viewsets/notes.py index 32a28f5567..015e677eed 100644 --- a/care/emr/api/viewsets/notes.py +++ b/care/emr/api/viewsets/notes.py @@ -1,6 +1,7 @@ from rest_framework.exceptions import PermissionDenied from rest_framework.generics import get_object_or_404 +from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.base import ( EMRBaseViewSet, EMRCreateMixin, @@ -20,9 +21,11 @@ NoteThreadReadSpec, NoteThreadUpdateSpec, ) +from care.security.authorization import AuthorizationController class NoteThreadViewSet( + EncounterBasedAuthorizationBase, EMRCreateMixin, EMRRetrieveMixin, EMRUpdateMixin, @@ -50,12 +53,15 @@ def get_object(self): return super().get_object() def get_queryset(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") queryset = ( super() .get_queryset() .filter(patient__external_id=self.kwargs["patient_external_id"]) ) - encounter = self.request.GET.get("encounter", None) if encounter and self.action == "list": # TODO Authorise Encounter @@ -87,7 +93,33 @@ def authorize_update(self, request_obj, model_instance): if self.request.user != model_instance.created_by: raise PermissionDenied("Cannot Update Message Created by Other User") + if not AuthorizationController.call( + "can_update_encounter_obj", + self.request.user, + model_instance.thread.encounter, + ): + raise PermissionDenied("You do not have permission to update encounter") + + def authorize_create(self, instance): + thread = get_object_or_404( + NoteThread, external_id=self.kwargs["thread_external_id"] + ) + if not AuthorizationController.call( + "can_update_encounter_obj", self.request.user, thread.encounter + ): + raise PermissionDenied("You do not have permission to update encounter") + + def authorize_delete(self, instance): + if not AuthorizationController.call( + "can_update_encounter_obj", self.request.user, instance.encounter + ): + raise PermissionDenied("You do not have permission to update encounter") + def get_queryset(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") return ( super() .get_queryset() diff --git a/care/emr/api/viewsets/observation.py b/care/emr/api/viewsets/observation.py index 90b0e81f5f..13237c0f0a 100644 --- a/care/emr/api/viewsets/observation.py +++ b/care/emr/api/viewsets/observation.py @@ -1,13 +1,16 @@ from django_filters import rest_framework as filters from pydantic import BaseModel, Field from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response +from care.emr.api.viewsets.authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.base import EMRModelReadOnlyViewSet from care.emr.models.observation import Observation from care.emr.resources.common.coding import Coding from care.emr.resources.observation.spec import ObservationReadSpec from care.emr.resources.questionnaire.spec import QuestionType +from care.security.authorization import AuthorizationController class MultipleCodeFilter(filters.CharFilter): @@ -36,13 +39,18 @@ class ObservationAnalyseRequest(BaseModel): page_size: int = Field(10, le=30) -class ObservationViewSet(EMRModelReadOnlyViewSet): +class ObservationViewSet(EncounterBasedAuthorizationBase, EMRModelReadOnlyViewSet): database_model = Observation pydantic_model = ObservationReadSpec filterset_class = ObservationFilter filter_backends = [filters.DjangoFilterBackend] def get_queryset(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + raise PermissionDenied("Permission denied to user") + queryset = ( super() .get_queryset() From c7156a7db0f051e07c7a6ba93d566be5c7dbfa44 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 01:04:41 +0530 Subject: [PATCH 17/27] Add Organization public API, facility API, remove shifting --- care/emr/api/viewsets/facility.py | 55 +++++++++++++++++++++++ care/emr/api/viewsets/organization.py | 14 +++++- care/emr/api/viewsets/patient.py | 2 +- care/emr/api/viewsets/resource_request.py | 5 +++ care/emr/resources/facility/spec.py | 48 ++++++++++++++++++-- care/emr/resources/organization/spec.py | 11 ++--- care/facility/api/viewsets/facility.py | 21 +-------- care/facility/models/facility.py | 1 + config/api_router.py | 24 +++++----- 9 files changed, 138 insertions(+), 43 deletions(-) create mode 100644 care/emr/api/viewsets/facility.py create mode 100644 care/emr/api/viewsets/resource_request.py diff --git a/care/emr/api/viewsets/facility.py b/care/emr/api/viewsets/facility.py new file mode 100644 index 0000000000..b992457901 --- /dev/null +++ b/care/emr/api/viewsets/facility.py @@ -0,0 +1,55 @@ +from django.db.models import Q +from django.utils.decorators import method_decorator +from rest_framework.decorators import action, parser_classes +from rest_framework.parsers import MultiPartParser +from rest_framework.response import Response + +from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.models.organziation import FacilityOrganizationUser, OrganizationUser +from care.emr.resources.facility.spec import FacilityCreateSpec, FacilityReadSpec +from care.facility.api.serializers.facility import FacilityImageUploadSerializer +from care.facility.models import Facility +from care.utils.file_uploads.cover_image import delete_cover_image + + +class FacilityViewSet(EMRModelViewSet): + database_model = Facility + pydantic_model = FacilityCreateSpec + pydantic_read_model = FacilityReadSpec + + def get_queryset(self): + # TODO Add Permission checks + organization_ids = list( + OrganizationUser.objects.filter(user=self.request.user).values_list( + "organization_id", flat=True + ) + ) + return ( + super() + .get_queryset() + .filter( + Q( + id__in=FacilityOrganizationUser.objects.filter( + user=self.request.user + ).values_list("organization__facility_id") + ) + | Q(geo_organization_cache__overlap=organization_ids) + ) + ) + + @method_decorator(parser_classes([MultiPartParser])) + @action(methods=["POST"], detail=True) + def cover_image(self, request, external_id): + facility = self.get_object() + serializer = FacilityImageUploadSerializer(facility, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(serializer.data) + + @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=204) diff --git a/care/emr/api/viewsets/organization.py b/care/emr/api/viewsets/organization.py index 3ba75e73d5..1a33c387ff 100644 --- a/care/emr/api/viewsets/organization.py +++ b/care/emr/api/viewsets/organization.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from rest_framework.settings import api_settings -from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.api.viewsets.base import EMRModelReadOnlyViewSet, EMRModelViewSet from care.emr.models.organziation import Organization, OrganizationUser from care.emr.resources.organization.organization_user_spec import ( OrganizationUserReadSpec, @@ -31,6 +31,18 @@ class OrganizationFilter(filters.FilterSet): org_type = filters.CharFilter(field_name="org_type", lookup_expr="iexact") +class OrganizationPublicViewSet(EMRModelReadOnlyViewSet): + database_model = Organization + pydantic_read_model = OrganizationReadSpec + filterset_class = OrganizationFilter + filter_backends = [filters.DjangoFilterBackend] + authentication_classes = [] + permission_classes = [] + + def get_queryset(self): + return Organization.objects.filter(org_type="govt") + + class OrganizationViewSet(EMRModelViewSet): database_model = Organization pydantic_model = OrganizationWriteSpec diff --git a/care/emr/api/viewsets/patient.py b/care/emr/api/viewsets/patient.py index c5d2b5f5f5..61a975cb39 100644 --- a/care/emr/api/viewsets/patient.py +++ b/care/emr/api/viewsets/patient.py @@ -112,7 +112,7 @@ def add_user(self, request, *args, **kwargs): class PatientUserDeleteSpec(BaseModel): user: UUID4 - @action(detail=True, methods=["DELETE"]) + @action(detail=True, methods=["POST"]) def delete_user(self, request, *args, **kwargs): request_data = self.PatientUserDeleteSpec(**self.request.data) user = get_object_or_404(User, external_id=request_data.user) diff --git a/care/emr/api/viewsets/resource_request.py b/care/emr/api/viewsets/resource_request.py new file mode 100644 index 0000000000..6aee52762e --- /dev/null +++ b/care/emr/api/viewsets/resource_request.py @@ -0,0 +1,5 @@ +from care.emr.resources.base import EMRResource + + +class ResourceRequestViewSet(EMRResource): + pass diff --git a/care/emr/resources/facility/spec.py b/care/emr/resources/facility/spec.py index d75c725f31..f623f7f368 100644 --- a/care/emr/resources/facility/spec.py +++ b/care/emr/resources/facility/spec.py @@ -1,11 +1,53 @@ from pydantic import UUID4 +from care.emr.models import Organization from care.emr.resources.base import EMRResource -from care.facility.models import Facility +from care.emr.resources.user.spec import UserSpec +from care.facility.models import ( + REVERSE_FACILITY_TYPES, + REVERSE_REVERSE_FACILITY_TYPES, + Facility, +) class FacilityBareMinimumSpec(EMRResource): __model__ = Facility - - id: UUID4 + __exclude__ = ["geo_organization"] + id: UUID4 | None = None name: str + + +class FacilityBaseSpec(FacilityBareMinimumSpec): + description: str + longitude: float + latitude: float + pincode: int + address: str + phone_number: str + middleware_address: str | None = None + facility_type: str + + +class FacilityCreateSpec(FacilityBaseSpec): + geo_organization: UUID4 + features: list[int] + + def perform_extra_deserialization(self, is_update, obj): + obj.geo_organization = Organization.objects.filter( + external_id=self.geo_organization, org_type="govt" + ).first() + obj.facility_type = REVERSE_REVERSE_FACILITY_TYPES[self.facility_type] + + +class FacilityReadSpec(FacilityBaseSpec): + features: list[int] + cover_image_url: str + + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id + + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by) + + mapping["facility_type"] = REVERSE_FACILITY_TYPES[obj.facility_type] diff --git a/care/emr/resources/organization/spec.py b/care/emr/resources/organization/spec.py index 77df508740..1c2661f2eb 100644 --- a/care/emr/resources/organization/spec.py +++ b/care/emr/resources/organization/spec.py @@ -60,8 +60,6 @@ def perform_extra_deserialization(self, is_update, obj): class OrganizationReadSpec(OrganizationBaseSpec): - created_by: UserSpec = dict - updated_by: UserSpec = dict level_cache: int = 0 system_generated: bool has_children: bool @@ -72,11 +70,6 @@ def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id mapping["parent"] = obj.get_parent_json() - if obj.created_by: - mapping["created_by"] = UserSpec.serialize(obj.created_by) - if obj.updated_by: - mapping["updated_by"] = UserSpec.serialize(obj.updated_by) - class OrganizationRetrieveSpec(OrganizationReadSpec): permissions: list[str] = [] @@ -86,3 +79,7 @@ def perform_extra_user_serialization(cls, mapping, obj, user): mapping["permissions"] = AuthorizationController.call( "get_permission_on_organization", obj, user ) + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by) + if obj.updated_by: + mapping["updated_by"] = UserSpec.serialize(obj.updated_by) diff --git a/care/facility/api/viewsets/facility.py b/care/facility/api/viewsets/facility.py index c2f8789706..0c7d60ff00 100644 --- a/care/facility/api/viewsets/facility.py +++ b/care/facility/api/viewsets/facility.py @@ -27,7 +27,7 @@ FacilityCapacity, HospitalDoctors, ) -from care.facility.models.facility import FacilityHubSpoke, FacilityUser +from care.facility.models.facility import FacilityHubSpoke 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 @@ -69,7 +69,6 @@ class FacilityViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, - mixins.DestroyModelMixin, viewsets.GenericViewSet, ): """Viewset for facility CRUD operations.""" @@ -118,24 +117,6 @@ def get_serializer_class(self): return FacilityImageUploadSerializer return FacilitySerializer - def destroy(self, request, *args, **kwargs): - instance = self.get_object() - if instance.patientregistration_set.filter(is_active=True).exists(): - return Response( - { - "facility": ( - "You cannot delete a facility with Live patients. " - "Discharge all patients and try again" - ) - }, - status=status.HTTP_400_BAD_REQUEST, - ) - - FacilityUser.objects.filter(facility=instance).delete() - User.objects.filter(home_facility=instance).update(home_facility=None) - self.perform_destroy(instance) - return Response(status=status.HTTP_204_NO_CONTENT) - def list(self, request, *args, **kwargs): if settings.CSV_REQUEST_PARAMETER in request.GET: mapping = Facility.CSV_MAPPING.copy() diff --git a/care/facility/models/facility.py b/care/facility/models/facility.py index de2957c9a6..1b9eca7f3b 100644 --- a/care/facility/models/facility.py +++ b/care/facility/models/facility.py @@ -134,6 +134,7 @@ class FacilityFeature(models.IntegerChoices): ] REVERSE_FACILITY_TYPES = reverse_choices(FACILITY_TYPES) +REVERSE_REVERSE_FACILITY_TYPES = {v: k for k, v in REVERSE_FACILITY_TYPES.items()} DOCTOR_TYPES = [ (1, "General Medicine"), diff --git a/config/api_router.py b/config/api_router.py index 73daa87e68..921f38d209 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -10,6 +10,7 @@ from care.emr.api.viewsets.batch_request import BatchRequestView from care.emr.api.viewsets.condition import DiagnosisViewSet, SymptomViewSet from care.emr.api.viewsets.encounter import EncounterViewSet +from care.emr.api.viewsets.facility import FacilityViewSet from care.emr.api.viewsets.facility_organization import ( FacilityOrganizationUsersViewSet, FacilityOrganizationViewSet, @@ -20,6 +21,7 @@ from care.emr.api.viewsets.notes import NoteMessageViewSet, NoteThreadViewSet from care.emr.api.viewsets.observation import ObservationViewSet from care.emr.api.viewsets.organization import ( + OrganizationPublicViewSet, OrganizationUsersViewSet, OrganizationViewSet, ) @@ -58,7 +60,6 @@ AllFacilityViewSet, FacilityHubsViewSet, FacilitySpokesViewSet, - FacilityViewSet, ) from care.facility.api.viewsets.inventory import ( FacilityInventoryItemViewSet, @@ -76,10 +77,6 @@ ResourceRequestCommentViewSet, ResourceRequestViewSet, ) -from care.facility.api.viewsets.shifting import ( - ShifitngRequestCommentViewSet, - ShiftingViewSet, -) from care.users.api.viewsets.lsg import ( DistrictViewSet, LocalBodyViewSet, @@ -128,6 +125,10 @@ router.register("organization", OrganizationViewSet, basename="organization") +router.register( + "govt/organization", OrganizationPublicViewSet, basename="govt-organization" +) + router.register("role", RoleViewSet, basename="role") router.register("encounter", EncounterViewSet, basename="encounter") @@ -142,11 +143,11 @@ router.register("items", FacilityInventoryItemViewSet, basename="items") -router.register("shift", ShiftingViewSet, basename="patient-shift") -shifting_nested_router = NestedSimpleRouter(router, r"shift", lookup="shift") -shifting_nested_router.register( - r"comment", ShifitngRequestCommentViewSet, basename="patient-shift-comment" -) +# router.register("shift", ShiftingViewSet, basename="patient-shift") +# shifting_nested_router = NestedSimpleRouter(router, r"shift", lookup="shift") +# shifting_nested_router.register( +# r"comment", ShifitngRequestCommentViewSet, basename="patient-shift-comment" +# ) router.register("resource", ResourceRequestViewSet, basename="resource-request") resource_nested_router = NestedSimpleRouter(router, r"resource", lookup="resource") @@ -155,6 +156,7 @@ ) router.register("facility", FacilityViewSet, basename="facility") + router.register("getallfacilities", AllFacilityViewSet, basename="getallfacilities") facility_nested_router = NestedSimpleRouter(router, r"facility", lookup="facility") # facility_nested_router.register( @@ -338,7 +340,7 @@ path("", include(patient_notes_nested_router.urls)), # path("", include(consultation_nested_router.urls)), path("", include(resource_nested_router.urls)), - path("", include(shifting_nested_router.urls)), + # path("", include(shifting_nested_router.urls)), path("", include(organization_nested_router.urls)), path("", include(facility_organization_nested_router.urls)), ] From b43331057ac4fe559dc6f242a66dc73881e8b8ec Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 01:57:25 +0530 Subject: [PATCH 18/27] Add resource request API's --- care/emr/api/viewsets/resource_request.py | 52 ++++++- ..._resourcerequest_resourcerequestcomment.py | 69 ++++++++++ care/emr/models/resource_request.py | 60 ++++++++ .../resources/resource_request/__init__.py | 0 care/emr/resources/resource_request/spec.py | 129 ++++++++++++++++++ config/api_router.py | 8 +- 6 files changed, 311 insertions(+), 7 deletions(-) create mode 100644 care/emr/migrations/0058_resourcerequest_resourcerequestcomment.py create mode 100644 care/emr/models/resource_request.py create mode 100644 care/emr/resources/resource_request/__init__.py create mode 100644 care/emr/resources/resource_request/spec.py diff --git a/care/emr/api/viewsets/resource_request.py b/care/emr/api/viewsets/resource_request.py index 6aee52762e..cbb1f41e8f 100644 --- a/care/emr/api/viewsets/resource_request.py +++ b/care/emr/api/viewsets/resource_request.py @@ -1,5 +1,51 @@ -from care.emr.resources.base import EMRResource +from care.emr.api.viewsets.base import ( + EMRBaseViewSet, + EMRCreateMixin, + EMRDeleteMixin, + EMRListMixin, + EMRModelViewSet, + EMRRetrieveMixin, +) +from care.emr.models.resource_request import ResourceRequest, ResourceRequestComment +from care.emr.resources.resource_request.spec import ( + ResourceRequestCommentCreateSpec, + ResourceRequestCommentListSpec, + ResourceRequestCreateSpec, + ResourceRequestListSpec, + ResourceRequestRetrieveSpec, +) -class ResourceRequestViewSet(EMRResource): - pass +class ResourceRequestViewSet(EMRModelViewSet): + database_model = ResourceRequest + pydantic_model = ResourceRequestCreateSpec + pydantic_read_model = ResourceRequestListSpec + pydantic_retrieve_model = ResourceRequestRetrieveSpec + + def get_queryset(self): + return ResourceRequest.objects.all().select_related( + "origin_facility", + "approving_facility", + "assigned_facility", + "related_patient", + "assigned_to", + ) + + +class ResourceRequestCommentViewSet( + EMRCreateMixin, EMRRetrieveMixin, EMRListMixin, EMRDeleteMixin, EMRBaseViewSet +): + database_model = ResourceRequestComment + pydantic_model = ResourceRequestCommentCreateSpec + pydantic_read_model = ResourceRequestCommentListSpec + + def perform_create(self, instance): + instance.request = ResourceRequest.objects.get( + external_id=self.kwargs["resource_external_id"] + ) + super().perform_create(instance) + + def get_queryset(self): + return ResourceRequestComment.objects.filter( + request__external_id=self.kwargs["resource_external_id"] + ) diff --git a/care/emr/migrations/0058_resourcerequest_resourcerequestcomment.py b/care/emr/migrations/0058_resourcerequest_resourcerequestcomment.py new file mode 100644 index 0000000000..34de7e0029 --- /dev/null +++ b/care/emr/migrations/0058_resourcerequest_resourcerequestcomment.py @@ -0,0 +1,69 @@ +# Generated by Django 5.1.3 on 2024-12-29 20:18 + +import care.utils.models.validators +import django.db.models.deletion +import uuid +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0057_patient_users_cache_patientuser'), + ('facility', '0480_delete_patientorganizations'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ResourceRequest', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('emergency', models.BooleanField(default=False)), + ('title', models.CharField(max_length=255)), + ('reason', models.TextField(default='')), + ('referring_facility_contact_name', models.TextField(blank=True, default='')), + ('referring_facility_contact_number', models.CharField(blank=True, default='', max_length=14, validators=[care.utils.models.validators.PhoneNumberValidator(types=('mobile', 'landline'))])), + ('status', models.CharField(max_length=100)), + ('category', models.CharField(max_length=100)), + ('priority', models.IntegerField(blank=True, default=None, null=True)), + ('is_assigned_to_user', models.BooleanField(default=False)), + ('approving_facility', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resource_approving_facilities', to='facility.facility')), + ('assigned_facility', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resource_assigned_facilities', to='facility.facility')), + ('assigned_to', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='resource_request_assigned', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('origin_facility', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='resource_requesting_facilities', to='facility.facility')), + ('related_patient', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='emr.patient')), + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='ResourceRequestComment', + 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)), + ('history', models.JSONField(default=dict)), + ('meta', models.JSONField(default=dict)), + ('comment', models.TextField(default='')), + ('created_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_created_by', to=settings.AUTH_USER_MODEL)), + ('request', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='emr.resourcerequest')), + ('updated_by', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(app_label)s_%(class)s_updated_by', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/care/emr/models/resource_request.py b/care/emr/models/resource_request.py new file mode 100644 index 0000000000..8a73c16b97 --- /dev/null +++ b/care/emr/models/resource_request.py @@ -0,0 +1,60 @@ +from django.db import models + +from care.emr.models import EMRBaseModel, Patient +from care.users.models import User +from care.utils.models.validators import mobile_or_landline_number_validator + + +class ResourceRequest(EMRBaseModel): + origin_facility = models.ForeignKey( + "facility.Facility", + on_delete=models.PROTECT, + related_name="resource_requesting_facilities", + ) + approving_facility = models.ForeignKey( + "facility.Facility", + on_delete=models.SET_NULL, + null=True, + related_name="resource_approving_facilities", + ) + assigned_facility = models.ForeignKey( + "facility.Facility", + on_delete=models.SET_NULL, + null=True, + related_name="resource_assigned_facilities", + ) + emergency = models.BooleanField(default=False) + title = models.CharField(max_length=255, null=False, blank=False) + reason = models.TextField(default="") + referring_facility_contact_name = models.TextField(default="", blank=True) + referring_facility_contact_number = models.CharField( + max_length=14, + validators=[mobile_or_landline_number_validator], + default="", + blank=True, + ) + status = models.CharField(max_length=100) + category = models.CharField(max_length=100) + priority = models.IntegerField(default=None, null=True, blank=True) + is_assigned_to_user = models.BooleanField(default=False) + assigned_to = models.ForeignKey( + User, + on_delete=models.SET_NULL, + null=True, + related_name="resource_request_assigned", + ) + + related_patient = models.ForeignKey( + Patient, + on_delete=models.CASCADE, + default=None, + null=True, + blank=True, + ) + + +class ResourceRequestComment(EMRBaseModel): + request = models.ForeignKey( + ResourceRequest, on_delete=models.PROTECT, null=False, blank=False + ) + comment = models.TextField(default="") diff --git a/care/emr/resources/resource_request/__init__.py b/care/emr/resources/resource_request/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/emr/resources/resource_request/spec.py b/care/emr/resources/resource_request/spec.py new file mode 100644 index 0000000000..83b81c0325 --- /dev/null +++ b/care/emr/resources/resource_request/spec.py @@ -0,0 +1,129 @@ +from enum import Enum + +from pydantic import UUID4 +from rest_framework.generics import get_object_or_404 + +from care.emr.models import Patient +from care.emr.models.resource_request import ResourceRequest, ResourceRequestComment +from care.emr.resources.base import EMRResource +from care.emr.resources.facility.spec import FacilityReadSpec +from care.emr.resources.patient.spec import PatientListSpec +from care.emr.resources.user.spec import UserSpec +from care.facility.models import Facility +from care.users.models import User + + +class StatusChoices(str, Enum): + pending = "pending" + approved = "approved" + rejected = "rejected" + cancelled = "cancelled" + transportation_to_be_arranged = "transportation_to_be_arranged" + transfer_in_progress = "transfer_in_progress" + completed = "completed" + + +class CategoryChoices(str, Enum): + patient_care = "patient_care" + comfort_devices = "comfort_devices" + medicines = "medicines" + financial = "financial" + supplies = "supplies" + other = "other" + + +class ResourceRequestBaseSpec(EMRResource): + __model__ = ResourceRequest + __exclude__ = [ + "origin_facility", + "approving_facility", + "assigned_facility", + "related_patient", + "assigned_to", + ] + + id: UUID4 | None = None + emergency: bool + title: str + reason: str + referring_facility_contact_name: str + referring_facility_contact_number: str + status: str + category: str + priority: int + + +class ResourceRequestCreateSpec(ResourceRequestBaseSpec): + origin_facility: UUID4 + approving_facility: UUID4 | None = None + assigned_facility: UUID4 | None = None + related_patient: UUID4 | None = None + assigned_to: UUID4 | None = None + + def perform_extra_deserialization(self, is_update, obj): + if not is_update: + obj.origin_facility = get_object_or_404( + Facility, external_id=self.origin_facility + ) + if self.approving_facility: + obj.approving_facility = get_object_or_404( + Facility, external_id=self.approving_facility + ) + if self.assigned_facility: + obj.assigned_facility = get_object_or_404( + Facility, external_id=self.assigned_facility + ) + if self.related_patient and not is_update: + obj.related_patient = get_object_or_404( + Patient, external_id=self.related_patient + ) + if self.assigned_to: + obj.assigned_to = get_object_or_404(User, external_id=self.assigned_to) + + +class ResourceRequestListSpec(ResourceRequestBaseSpec): + pass + + +class ResourceRequestRetrieveSpec(ResourceRequestBaseSpec): + origin_facility: dict + approving_facility: dict | None = None + assigned_facility: dict | None = None + related_patient: dict | None = None + assigned_to: dict | None = None + + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = str(obj.external_id) + mapping["origin_facility"] = FacilityReadSpec.serialize( + obj.origin_facility + ).to_json() + if obj.approving_facility: + mapping["approving_facility"] = FacilityReadSpec.serialize( + obj.approving_facility + ).to_json() + if obj.assigned_facility: + mapping["assigned_facility"] = FacilityReadSpec.serialize( + obj.assigned_facility + ).to_json() + if obj.related_patient: + mapping["related_patient"] = PatientListSpec.serialize( + obj.related_patient + ).to_json() + if obj.assigned_to: + mapping["assigned_to"] = UserSpec.serialize(obj.assigned_to).to_json() + + +class ResourceRequestCommentBaseSpec(EMRResource): + __model__ = ResourceRequestComment + __exclude__ = ["request"] + + comment: str + + +class ResourceRequestCommentCreateSpec(ResourceRequestCommentBaseSpec): + pass + + +class ResourceRequestCommentListSpec(ResourceRequestCommentBaseSpec): + pass diff --git a/config/api_router.py b/config/api_router.py index 921f38d209..0b985edbcf 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -28,6 +28,10 @@ from care.emr.api.viewsets.patient import PatientViewSet from care.emr.api.viewsets.questionnaire import QuestionnaireViewSet from care.emr.api.viewsets.questionnaire_response import QuestionnaireResponseViewSet +from care.emr.api.viewsets.resource_request import ( + ResourceRequestCommentViewSet, + ResourceRequestViewSet, +) from care.emr.api.viewsets.roles import RoleViewSet from care.emr.api.viewsets.scheduling import ScheduleViewSet, SlotViewSet from care.emr.api.viewsets.scheduling.availability_exceptions import ( @@ -73,10 +77,6 @@ PatientNotesEditViewSet, PatientNotesViewSet, ) -from care.facility.api.viewsets.resources import ( - ResourceRequestCommentViewSet, - ResourceRequestViewSet, -) from care.users.api.viewsets.lsg import ( DistrictViewSet, LocalBodyViewSet, From 962ae6084789cc2e6dcae4310e204bfddff5fd54 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 02:11:05 +0530 Subject: [PATCH 19/27] Remove unused API's --- care/emr/api/viewsets/user.py | 5 ++ .../api/viewsets/{ => legacy}/inventory.py | 0 config/api_router.py | 73 +++++++------------ 3 files changed, 33 insertions(+), 45 deletions(-) create mode 100644 care/emr/api/viewsets/user.py rename care/facility/api/viewsets/{ => legacy}/inventory.py (100%) diff --git a/care/emr/api/viewsets/user.py b/care/emr/api/viewsets/user.py new file mode 100644 index 0000000000..4bc0f71aaa --- /dev/null +++ b/care/emr/api/viewsets/user.py @@ -0,0 +1,5 @@ +from care.emr.api.viewsets.base import EMRModelViewSet + + +class UserViewSet(EMRModelViewSet): + pass diff --git a/care/facility/api/viewsets/inventory.py b/care/facility/api/viewsets/legacy/inventory.py similarity index 100% rename from care/facility/api/viewsets/inventory.py rename to care/facility/api/viewsets/legacy/inventory.py diff --git a/config/api_router.py b/config/api_router.py index 0b985edbcf..25b032b78e 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -65,11 +65,8 @@ FacilityHubsViewSet, FacilitySpokesViewSet, ) -from care.facility.api.viewsets.inventory import ( +from care.facility.api.viewsets.legacy.inventory import ( FacilityInventoryItemViewSet, - FacilityInventoryLogViewSet, - FacilityInventoryMinQuantityViewSet, - FacilityInventorySummaryViewSet, ) from care.facility.api.viewsets.notification import NotificationViewSet from care.facility.api.viewsets.patient import ( @@ -93,7 +90,6 @@ router.register("users", UserViewSet, basename="users") router.register("plug_config", PlugConfigViewset, basename="plug_configs") - user_nested_router = NestedSimpleRouter(router, r"users", lookup="users") user_nested_router.register("skill", UserSkillViewSet, basename="users-skill") @@ -141,7 +137,7 @@ "users", OrganizationUsersViewSet, basename="organization-users" ) -router.register("items", FacilityInventoryItemViewSet, basename="items") +# router.register("items", FacilityInventoryItemViewSet, basename="items") # router.register("shift", ShiftingViewSet, basename="patient-shift") # shifting_nested_router = NestedSimpleRouter(router, r"shift", lookup="shift") @@ -162,19 +158,6 @@ # facility_nested_router.register( # r"get_users", FacilityUserViewSet, basename="facility-users" # ) -facility_nested_router.register( - r"inventory", FacilityInventoryLogViewSet, basename="facility-inventory" -) -facility_nested_router.register( - r"inventorysummary", - FacilityInventorySummaryViewSet, - basename="facility-inventory-summary", -) -facility_nested_router.register( - r"min_quantity", - FacilityInventoryMinQuantityViewSet, - basename="facility-inventory-min-quantity", -) facility_nested_router.register( r"asset_location", AssetLocationViewSet, basename="facility-location" ) @@ -227,33 +210,33 @@ basename="schedule-exceptions", ) -router.register("asset", AssetViewSet, basename="asset") -asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") -asset_nested_router.register( - r"camera_presets", CameraPresetViewSet, basename="asset-camera-presets" -) -asset_nested_router.register( - r"availability", AvailabilityViewSet, basename="asset-availability" -) -asset_nested_router.register( - r"service_records", AssetServiceViewSet, basename="asset-service-records" -) - -router.register("asset_config", AssetRetrieveConfigViewSet, basename="asset-config") -router.register("asset_transaction", AssetTransactionViewSet) - -router.register("bed", BedViewSet, basename="bed") -bed_nested_router = NestedSimpleRouter(router, r"bed", lookup="bed") -bed_nested_router.register( - r"camera_presets", CameraPresetViewSet, basename="bed-camera-presets" -) +# router.register("asset", AssetViewSet, basename="asset") +# asset_nested_router = NestedSimpleRouter(router, r"asset", lookup="asset") +# asset_nested_router.register( +# r"camera_presets", CameraPresetViewSet, basename="asset-camera-presets" +# ) +# asset_nested_router.register( +# r"availability", AvailabilityViewSet, basename="asset-availability" +# ) +# asset_nested_router.register( +# r"service_records", AssetServiceViewSet, basename="asset-service-records" +# ) +# +# router.register("asset_config", AssetRetrieveConfigViewSet, basename="asset-config") +# router.register("asset_transaction", AssetTransactionViewSet) -router.register("assetbed", AssetBedViewSet, basename="asset-bed") -router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") -assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") -assetbed_nested_router.register( - r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" -) +# router.register("bed", BedViewSet, basename="bed") +# bed_nested_router = NestedSimpleRouter(router, r"bed", lookup="bed") +# bed_nested_router.register( +# r"camera_presets", CameraPresetViewSet, basename="bed-camera-presets" +# ) +# +# router.register("assetbed", AssetBedViewSet, basename="asset-bed") +# router.register("consultationbed", ConsultationBedViewSet, basename="consultation-bed") +# assetbed_nested_router = NestedSimpleRouter(router, r"assetbed", lookup="assetbed") +# assetbed_nested_router.register( +# r"camera_presets", AssetBedCameraPresetViewSet, basename="assetbed-camera-presets" +# ) # router.register("patient/search", PatientSearchViewSet, basename="patient-search") router.register("patient", PatientViewSet, basename="patient") From 53188e3dc9cf08839ae3006531905433a01a746b Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 02:15:29 +0530 Subject: [PATCH 20/27] Remove unused API's --- config/api_router.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/api_router.py b/config/api_router.py index 25b032b78e..8a0b4f448e 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -315,9 +315,9 @@ path("", include(user_nested_router.urls)), path("", include(facility_nested_router.urls)), path("", include(facility_location_nested_router.urls)), - path("", include(asset_nested_router.urls)), - path("", include(bed_nested_router.urls)), - path("", include(assetbed_nested_router.urls)), + # path("", include(asset_nested_router.urls)), + # path("", include(bed_nested_router.urls)), + # path("", include(assetbed_nested_router.urls)), path("", include(patient_nested_router.urls)), path("", include(thread_nested_router.urls)), path("", include(patient_notes_nested_router.urls)), From e835e743190280a0236b8a9f4a8e11f21cbe7a42 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 02:17:35 +0530 Subject: [PATCH 21/27] Remove unused API's --- config/api_router.py | 78 +++++++++++++++----------------------------- 1 file changed, 26 insertions(+), 52 deletions(-) diff --git a/config/api_router.py b/config/api_router.py index 8a0b4f448e..0dd4dd382d 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -40,37 +40,11 @@ from care.emr.api.viewsets.scheduling.booking import TokenBookingViewSet from care.emr.api.viewsets.units import UnitsView from care.emr.api.viewsets.valueset import ValueSetViewSet -from care.facility.api.viewsets.asset import ( - AssetLocationViewSet, - AssetPublicQRViewSet, - AssetPublicViewSet, - AssetRetrieveConfigViewSet, - AssetServiceViewSet, - AssetTransactionViewSet, - AssetViewSet, - AvailabilityViewSet, -) -from care.facility.api.viewsets.bed import ( - AssetBedViewSet, - BedViewSet, - ConsultationBedViewSet, - PatientAssetBedViewSet, -) -from care.facility.api.viewsets.camera_preset import ( - AssetBedCameraPresetViewSet, - CameraPresetViewSet, -) from care.facility.api.viewsets.facility import ( AllFacilityViewSet, - FacilityHubsViewSet, - FacilitySpokesViewSet, -) -from care.facility.api.viewsets.legacy.inventory import ( - FacilityInventoryItemViewSet, ) from care.facility.api.viewsets.notification import NotificationViewSet from care.facility.api.viewsets.patient import ( - FacilityDischargedPatientViewSet, PatientNotesEditViewSet, PatientNotesViewSet, ) @@ -158,31 +132,31 @@ # facility_nested_router.register( # r"get_users", FacilityUserViewSet, basename="facility-users" # ) -facility_nested_router.register( - r"asset_location", AssetLocationViewSet, basename="facility-location" -) +# facility_nested_router.register( +# r"asset_location", AssetLocationViewSet, basename="facility-location" +# ) -facility_location_nested_router = NestedSimpleRouter( - facility_nested_router, r"asset_location", lookup="asset_location" -) -facility_location_nested_router.register( - r"availability", AvailabilityViewSet, basename="facility-location-availability" -) +# facility_location_nested_router = NestedSimpleRouter( +# facility_nested_router, r"asset_location", lookup="asset_location" +# ) +# facility_location_nested_router.register( +# r"availability", AvailabilityViewSet, basename="facility-location-availability" +# ) -facility_nested_router.register( - r"patient_asset_beds", - PatientAssetBedViewSet, - basename="facility-patient-asset-beds", -) -facility_nested_router.register( - r"discharged_patients", - FacilityDischargedPatientViewSet, - basename="facility-discharged-patients", -) -facility_nested_router.register( - r"spokes", FacilitySpokesViewSet, basename="facility-spokes" -) -facility_nested_router.register(r"hubs", FacilityHubsViewSet, basename="facility-hubs") +# facility_nested_router.register( +# r"patient_asset_beds", +# PatientAssetBedViewSet, +# basename="facility-patient-asset-beds", +# ) +# facility_nested_router.register( +# r"discharged_patients", +# FacilityDischargedPatientViewSet, +# basename="facility-discharged-patients", +# ) +# facility_nested_router.register( +# r"spokes", FacilitySpokesViewSet, basename="facility-spokes" +# ) +# facility_nested_router.register(r"hubs", FacilityHubsViewSet, basename="facility-hubs") facility_nested_router.register( r"organizations", FacilityOrganizationViewSet, basename="facility-organization" @@ -306,15 +280,15 @@ # ) # Public endpoints -router.register("public/asset", AssetPublicViewSet, basename="public-asset") -router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") +# router.register("public/asset", AssetPublicViewSet, basename="public-asset") +# router.register("public/asset_qr", AssetPublicQRViewSet, basename="public-asset-qr") app_name = "api" urlpatterns = [ path("", include(router.urls)), path("", include(user_nested_router.urls)), path("", include(facility_nested_router.urls)), - path("", include(facility_location_nested_router.urls)), + # path("", include(facility_location_nested_router.urls)), # path("", include(asset_nested_router.urls)), # path("", include(bed_nested_router.urls)), # path("", include(assetbed_nested_router.urls)), From ced06edaf4bc1ef42e32ac9307ebe9b9a1e97283 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 03:02:21 +0530 Subject: [PATCH 22/27] Add New User API's --- care/emr/api/viewsets/user.py | 88 ++++++++++++++++++- care/emr/resources/patient/spec.py | 9 +- care/emr/resources/resource_request/spec.py | 10 +++ care/emr/resources/user/spec.py | 73 +++++++++++++-- care/users/api/serializers/user.py | 6 +- ...d_gender_user_geo_organization_and_more.py | 35 ++++++++ care/users/migrations/0022_user_gender.py | 19 ++++ .../migrations/0023_alter_user_old_gender.py | 18 ++++ care/users/models.py | 14 ++- config/api_router.py | 2 +- 10 files changed, 256 insertions(+), 18 deletions(-) create mode 100644 care/users/migrations/0021_rename_gender_user_old_gender_user_geo_organization_and_more.py create mode 100644 care/users/migrations/0022_user_gender.py create mode 100644 care/users/migrations/0023_alter_user_old_gender.py diff --git a/care/emr/api/viewsets/user.py b/care/emr/api/viewsets/user.py index 4bc0f71aaa..3c02390c8d 100644 --- a/care/emr/api/viewsets/user.py +++ b/care/emr/api/viewsets/user.py @@ -1,5 +1,91 @@ +from django.utils.decorators import method_decorator +from rest_framework.decorators import action, parser_classes +from rest_framework.exceptions import PermissionDenied +from rest_framework.parsers import MultiPartParser +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response + from care.emr.api.viewsets.base import EMRModelViewSet +from care.emr.resources.user.spec import ( + UserCreateSpec, + UserRetrieveSpec, + UserSpec, + UserUpdateSpec, +) +from care.users.api.serializers.user import UserImageUploadSerializer, UserSerializer +from care.users.models import User +from care.utils.file_uploads.cover_image import delete_cover_image class UserViewSet(EMRModelViewSet): - pass + database_model = User + pydantic_model = UserCreateSpec + pydantic_update_model = UserUpdateSpec + pydantic_read_model = UserSpec + pydantic_retrieve_model = UserRetrieveSpec + + def authorize_update(self, request_obj, model_instance): + if self.request.user.is_superuser: + return True + return request_obj.user == model_instance + + def authorize_delete(self, instance): + return self.request.user.is_superuser + + @action(detail=False, methods=["GET"]) + def getcurrentuser(self, request): + return Response( + data=UserSerializer(request.user, context={"request": request}).data, + ) + + @action(methods=["GET"], detail=True) + def check_availability(self, request, username): + """ + Checks availability of username by getting as query, returns 200 if available, and 409 otherwise. + """ + if User.check_username_exists(username): + return Response(status=409) + return Response(status=200) + + @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.authorize_update({}, user): + raise PermissionDenied("Permission Denied") + serializer = UserImageUploadSerializer(user, data=request.data) + serializer.is_valid(raise_exception=True) + serializer.save() + return Response(status=200) + + @profile_picture.mapping.delete + def profile_picture_delete(self, request, *args, **kwargs): + user = self.get_object() + if not self.authorize_update({}, user): + raise PermissionDenied("Permission Denied") + delete_cover_image(user.profile_picture_url, "avatars") + user.profile_picture_url = None + user.save() + return Response(status=204) + + @action( + detail=True, + methods=["PATCH", "GET"], + permission_classes=[IsAuthenticated], + ) + def pnconfig(self, request, *args, **kwargs): + user = request.user + if request.method == "GET": + return Response( + { + "pf_endpoint": user.pf_endpoint, + "pf_p256dh": user.pf_p256dh, + "pf_auth": user.pf_auth, + } + ) + acceptable_fields = ["pf_endpoint", "pf_p256dh", "pf_auth"] + for field in acceptable_fields: + if field in request.data: + setattr(user, field, request.data[field]) + user.save() + return Response({}) diff --git a/care/emr/resources/patient/spec.py b/care/emr/resources/patient/spec.py index 159a5f890e..92f0cb3cfc 100644 --- a/care/emr/resources/patient/spec.py +++ b/care/emr/resources/patient/spec.py @@ -8,8 +8,6 @@ from care.emr.models import Organization from care.emr.models.patient import Patient from care.emr.resources.base import EMRResource -from care.emr.resources.organization.spec import OrganizationReadSpec -from care.emr.resources.user.spec import UserSpec class BloodGroupChoices(str, Enum): @@ -105,11 +103,14 @@ def perform_extra_serialization(cls, mapping, obj): class PatientRetrieveSpec(PatientListSpec): geo_organization: dict = {} - created_by: UserSpec | None = None - updated_by: UserSpec | None = None + created_by: dict | None = None + updated_by: dict | None = None @classmethod def perform_extra_serialization(cls, mapping, obj): + from care.emr.resources.organization.spec import OrganizationReadSpec + from care.emr.resources.user.spec import UserSpec + super().perform_extra_serialization(mapping, obj) if obj.geo_organization: mapping["geo_organization"] = OrganizationReadSpec.serialize( diff --git a/care/emr/resources/resource_request/spec.py b/care/emr/resources/resource_request/spec.py index 83b81c0325..f06adf0716 100644 --- a/care/emr/resources/resource_request/spec.py +++ b/care/emr/resources/resource_request/spec.py @@ -1,3 +1,4 @@ +import datetime from enum import Enum from pydantic import UUID4 @@ -91,6 +92,10 @@ class ResourceRequestRetrieveSpec(ResourceRequestBaseSpec): assigned_facility: dict | None = None related_patient: dict | None = None assigned_to: dict | None = None + created_by: dict | None = None + updated_by: dict | None = None + created_date: datetime.datetime + modified_date: datetime.datetime @classmethod def perform_extra_serialization(cls, mapping, obj): @@ -113,6 +118,11 @@ def perform_extra_serialization(cls, mapping, obj): if obj.assigned_to: mapping["assigned_to"] = UserSpec.serialize(obj.assigned_to).to_json() + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by) + if obj.updated_by: + mapping["updated_by"] = UserSpec.serialize(obj.updated_by) + class ResourceRequestCommentBaseSpec(EMRResource): __model__ = ResourceRequestComment diff --git a/care/emr/resources/user/spec.py b/care/emr/resources/user/spec.py index ed0e7eab69..9e75f9c414 100644 --- a/care/emr/resources/user/spec.py +++ b/care/emr/resources/user/spec.py @@ -1,20 +1,83 @@ +from enum import Enum + +from django.contrib.auth.password_validation import validate_password +from pydantic import UUID4, field_validator +from rest_framework.generics import get_object_or_404 + +from care.emr.models import Organization from care.emr.resources.base import EMRResource +from care.emr.resources.patient.spec import GenderChoices from care.users.models import User -class UserSpec(EMRResource): +class UserTypeOptions(str, Enum): + doctor = "doctor" + nurse = "nurse" + staff = "staff" + volunteer = "volunteer" + + +class UserBaseSpec(EMRResource): __model__ = User - id: str + __exclude__ = ["geo_organization"] + + id: UUID4 | None = None + first_name: str + last_name: str + phone_number: str + + +class UserUpdateSpec(UserBaseSpec): + user_type: UserTypeOptions + gender: GenderChoices + + +class UserCreateSpec(UserBaseSpec): + geo_organization: UUID4 + password: str username: str email: str - last_name: str - user_type: str + + @field_validator("password") + @classmethod + def validate_password(cls, password): + try: + validate_password(password) + except Exception as e: + raise ValueError("Password is too weak") from e + return password + + def perform_extra_deserialization(self, is_update, obj): + obj.set_password(self.password) + obj.geo_organization = get_object_or_404( + Organization, external_id=self.geo_organization, org_type="govt" + ) + + +class UserSpec(UserBaseSpec): last_login: str profile_picture_url: str + user_type: str + gender: str @classmethod def perform_extra_serialization(cls, mapping, obj: User): - mapping["user_type"] = User.REVERSE_MAPPING[obj.user_type] mapping["id"] = str(obj.external_id) mapping["profile_picture_url"] = obj.read_profile_picture_url() + + +class UserRetrieveSpec(UserSpec): + geo_organization: dict + created_by: dict + + @classmethod + def perform_extra_serialization(cls, mapping, obj: User): + from care.emr.resources.organization.spec import OrganizationReadSpec + + super().perform_extra_serialization(mapping, obj) + mapping["created_by"] = UserSpec.serialize(obj.created_by).to_json() + if obj.geo_organization: + mapping["geo_organization"] = OrganizationReadSpec.serialize( + obj.geo_organization + ).to_json() diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 25aa0f9e03..73fc1d77c1 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -16,7 +16,7 @@ StateSerializer, ) from care.users.api.serializers.skill import UserSkillSerializer -from care.users.models import GENDER_CHOICES, User +from care.users.models import User from care.utils.file_uploads.cover_image import upload_cover_image from care.utils.models.validators import ( cover_image_validator, @@ -26,8 +26,6 @@ class SignUpSerializer(serializers.ModelSerializer): - user_type = ChoiceField(choices=User.TYPE_CHOICES) - gender = ChoiceField(choices=GENDER_CHOICES) password = serializers.CharField(write_only=True) class Meta: @@ -273,8 +271,6 @@ def create(self, validated_data): class UserSerializer(SignUpSerializer): external_id = serializers.UUIDField(read_only=True) - user_type = ChoiceField(choices=User.TYPE_CHOICES, read_only=True) - created_by = serializers.CharField(source="created_by_user", read_only=True) is_superuser = serializers.BooleanField(read_only=True) local_body_object = LocalBodySerializer(source="local_body", read_only=True) diff --git a/care/users/migrations/0021_rename_gender_user_old_gender_user_geo_organization_and_more.py b/care/users/migrations/0021_rename_gender_user_old_gender_user_geo_organization_and_more.py new file mode 100644 index 0000000000..87f18df1e9 --- /dev/null +++ b/care/users/migrations/0021_rename_gender_user_old_gender_user_geo_organization_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 5.1.3 on 2024-12-29 21:20 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0058_resourcerequest_resourcerequestcomment'), + ('users', '0020_plugconfig'), + ] + + operations = [ + migrations.RenameField( + model_name='user', + old_name='gender', + new_name='old_gender', + ), + migrations.AddField( + model_name='user', + name='geo_organization', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='emr.organization'), + ), + migrations.AddField( + model_name='user', + name='old_user_type', + field=models.IntegerField(blank=True, choices=[(2, 'Transportation'), (3, 'Pharmacist'), (5, 'Volunteer'), (9, 'StaffReadOnly'), (10, 'Staff'), (13, 'NurseReadOnly'), (14, 'Nurse'), (15, 'Doctor'), (20, 'Reserved'), (21, 'WardAdmin'), (23, 'LocalBodyAdmin'), (25, 'DistrictLabAdmin'), (29, 'DistrictReadOnlyAdmin'), (30, 'DistrictAdmin'), (35, 'StateLabAdmin'), (39, 'StateReadOnlyAdmin'), (40, 'StateAdmin')], default=None, null=True), + ), + migrations.AlterField( + model_name='user', + name='user_type', + field=models.CharField(max_length=100), + ), + ] diff --git a/care/users/migrations/0022_user_gender.py b/care/users/migrations/0022_user_gender.py new file mode 100644 index 0000000000..de842a65da --- /dev/null +++ b/care/users/migrations/0022_user_gender.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.3 on 2024-12-29 21:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0021_rename_gender_user_old_gender_user_geo_organization_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='gender', + field=models.CharField(default='others', max_length=100), + preserve_default=False, + ), + ] diff --git a/care/users/migrations/0023_alter_user_old_gender.py b/care/users/migrations/0023_alter_user_old_gender.py new file mode 100644 index 0000000000..467b7b6f3a --- /dev/null +++ b/care/users/migrations/0023_alter_user_old_gender.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.3 on 2024-12-29 21:29 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0022_user_gender'), + ] + + operations = [ + migrations.AlterField( + model_name='user', + name='old_gender', + field=models.IntegerField(blank=True, choices=[(1, 'Male'), (2, 'Female'), (3, 'Non-binary')], default=None, null=True), + ), + ] diff --git a/care/users/models.py b/care/users/models.py index 5b6657394f..9bd4be0701 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -253,7 +253,10 @@ class User(AbstractUser): REVERSE_MAPPING = {value: name for name, value in TYPE_VALUE_MAP.items()} - user_type = models.IntegerField(choices=TYPE_CHOICES, blank=False) + old_user_type = models.IntegerField( + choices=TYPE_CHOICES, blank=True, null=True, default=None + ) + user_type = models.CharField(max_length=100) created_by = models.ForeignKey( "self", on_delete=models.SET_NULL, @@ -271,6 +274,10 @@ class User(AbstractUser): ) state = models.ForeignKey(State, on_delete=models.PROTECT, null=True, blank=True) + geo_organization = models.ForeignKey( + "emr.Organization", on_delete=models.SET_NULL, null=True, blank=True + ) + phone_number = models.CharField( max_length=14, validators=[mobile_or_landline_number_validator] ) @@ -283,7 +290,10 @@ class User(AbstractUser): ) video_connect_link = models.URLField(blank=True, null=True) - gender = models.IntegerField(choices=GENDER_CHOICES, blank=False) + old_gender = models.IntegerField( + choices=GENDER_CHOICES, blank=True, null=True, default=None + ) + gender = models.CharField(max_length=100) date_of_birth = models.DateField(null=True, blank=True) profile_picture_url = models.CharField( blank=True, null=True, default=None, max_length=500 diff --git a/config/api_router.py b/config/api_router.py index 0dd4dd382d..75df8c176b 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -39,6 +39,7 @@ ) from care.emr.api.viewsets.scheduling.booking import TokenBookingViewSet from care.emr.api.viewsets.units import UnitsView +from care.emr.api.viewsets.user import UserViewSet from care.emr.api.viewsets.valueset import ValueSetViewSet from care.facility.api.viewsets.facility import ( AllFacilityViewSet, @@ -56,7 +57,6 @@ ) from care.users.api.viewsets.plug_config import PlugConfigViewset from care.users.api.viewsets.skill import SkillViewSet -from care.users.api.viewsets.users import UserViewSet from care.users.api.viewsets.userskill import UserSkillViewSet router = DefaultRouter() if settings.DEBUG else SimpleRouter() From 33c7d79f17155cd2cd60221d98e1525634822ca5 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 03:13:47 +0530 Subject: [PATCH 23/27] Fix resource request type --- care/emr/resources/resource_request/spec.py | 9 +++++++++ scripts/celery_beat.sh | 11 ++++++++++- scripts/celery_worker.sh | 11 ++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) diff --git a/care/emr/resources/resource_request/spec.py b/care/emr/resources/resource_request/spec.py index f06adf0716..e25ca2d76a 100644 --- a/care/emr/resources/resource_request/spec.py +++ b/care/emr/resources/resource_request/spec.py @@ -137,3 +137,12 @@ class ResourceRequestCommentCreateSpec(ResourceRequestCommentBaseSpec): class ResourceRequestCommentListSpec(ResourceRequestCommentBaseSpec): pass + +class ResourceRequestCommentRetrieveSpec(ResourceRequestCommentListSpec): + created_by: dict | None = None + created_date: datetime.datetime + + @classmethod + def perform_extra_serialization(cls, mapping, obj): + if obj.created_by: + mapping["created_by"] = UserSpec.serialize(obj.created_by) diff --git a/scripts/celery_beat.sh b/scripts/celery_beat.sh index 4dde164a59..f14fd72f3d 100755 --- a/scripts/celery_beat.sh +++ b/scripts/celery_beat.sh @@ -1,7 +1,16 @@ #!/bin/bash printf "celery-beat" > /tmp/container-role -set -euo pipefail +set -eo pipefail + +if [ -z "${DATABASE_URL}" ]; then + export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" +fi + +if [ -z "${REDIS_URL}" ]; then + export REDIS_URL="rediss://:${REDIS_AUTH_TOKEN}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DATABASE}" +fi + ./wait_for_db.sh ./wait_for_redis.sh diff --git a/scripts/celery_worker.sh b/scripts/celery_worker.sh index 6093fec4d9..b5a2c27627 100755 --- a/scripts/celery_worker.sh +++ b/scripts/celery_worker.sh @@ -1,7 +1,16 @@ #!/bin/bash printf "celery-worker" > /tmp/container-role -set -euo pipefail +set -eo pipefail + +if [ -z "${DATABASE_URL}" ]; then + export DATABASE_URL="postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB}" +fi + +if [ -z "${REDIS_URL}" ]; then + export REDIS_URL="rediss://:${REDIS_AUTH_TOKEN}@${REDIS_HOST}:${REDIS_PORT}/${REDIS_DATABASE}" +fi + ./wait_for_db.sh ./wait_for_redis.sh From f43f844bb74897744badfd0400da8b2db7a8c391 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 03:18:20 +0530 Subject: [PATCH 24/27] Fix resource request type --- care/emr/api/viewsets/resource_request.py | 1 + 1 file changed, 1 insertion(+) diff --git a/care/emr/api/viewsets/resource_request.py b/care/emr/api/viewsets/resource_request.py index cbb1f41e8f..da553f8844 100644 --- a/care/emr/api/viewsets/resource_request.py +++ b/care/emr/api/viewsets/resource_request.py @@ -38,6 +38,7 @@ class ResourceRequestCommentViewSet( database_model = ResourceRequestComment pydantic_model = ResourceRequestCommentCreateSpec pydantic_read_model = ResourceRequestCommentListSpec + pydantic_retrieve_model = ResourceRequestRetrieveSpec def perform_create(self, instance): instance.request = ResourceRequest.objects.get( From b07d1efe2408a81e4b81218c05c9a721b71690b8 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 03:24:16 +0530 Subject: [PATCH 25/27] Fix resource request type --- care/emr/api/viewsets/resource_request.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/care/emr/api/viewsets/resource_request.py b/care/emr/api/viewsets/resource_request.py index da553f8844..371045d524 100644 --- a/care/emr/api/viewsets/resource_request.py +++ b/care/emr/api/viewsets/resource_request.py @@ -12,7 +12,7 @@ ResourceRequestCommentListSpec, ResourceRequestCreateSpec, ResourceRequestListSpec, - ResourceRequestRetrieveSpec, + ResourceRequestRetrieveSpec, ResourceRequestCommentRetrieveSpec, ) @@ -38,7 +38,7 @@ class ResourceRequestCommentViewSet( database_model = ResourceRequestComment pydantic_model = ResourceRequestCommentCreateSpec pydantic_read_model = ResourceRequestCommentListSpec - pydantic_retrieve_model = ResourceRequestRetrieveSpec + pydantic_retrieve_model = ResourceRequestCommentRetrieveSpec def perform_create(self, instance): instance.request = ResourceRequest.objects.get( From 4361dc2238bcc8630e846f3aef4490541edf625e Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 03:29:21 +0530 Subject: [PATCH 26/27] Fix resource request type --- care/emr/api/viewsets/resource_request.py | 2 +- care/emr/resources/resource_request/spec.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/care/emr/api/viewsets/resource_request.py b/care/emr/api/viewsets/resource_request.py index 371045d524..cbfcf7f9ba 100644 --- a/care/emr/api/viewsets/resource_request.py +++ b/care/emr/api/viewsets/resource_request.py @@ -49,4 +49,4 @@ def perform_create(self, instance): def get_queryset(self): return ResourceRequestComment.objects.filter( request__external_id=self.kwargs["resource_external_id"] - ) + ).select_related("created_by") diff --git a/care/emr/resources/resource_request/spec.py b/care/emr/resources/resource_request/spec.py index e25ca2d76a..6c65a7f2de 100644 --- a/care/emr/resources/resource_request/spec.py +++ b/care/emr/resources/resource_request/spec.py @@ -136,9 +136,6 @@ class ResourceRequestCommentCreateSpec(ResourceRequestCommentBaseSpec): class ResourceRequestCommentListSpec(ResourceRequestCommentBaseSpec): - pass - -class ResourceRequestCommentRetrieveSpec(ResourceRequestCommentListSpec): created_by: dict | None = None created_date: datetime.datetime @@ -146,3 +143,6 @@ class ResourceRequestCommentRetrieveSpec(ResourceRequestCommentListSpec): def perform_extra_serialization(cls, mapping, obj): if obj.created_by: mapping["created_by"] = UserSpec.serialize(obj.created_by) + +class ResourceRequestCommentRetrieveSpec(ResourceRequestCommentListSpec): + pass From 66b0a74c42e95388cc5fc064e5e1acba2a79cbf9 Mon Sep 17 00:00:00 2001 From: vigneshhari Date: Mon, 30 Dec 2024 04:05:41 +0530 Subject: [PATCH 27/27] Cleanups for deployment --- care/emr/api/otp_viewsets/login.py | 2 +- care/emr/api/viewsets/resource_request.py | 3 ++- care/emr/resources/resource_request/spec.py | 2 ++ care/utils/sms/send_sms.py | 18 ++++++++++++------ config/settings/deployment.py | 1 + 5 files changed, 18 insertions(+), 8 deletions(-) diff --git a/care/emr/api/otp_viewsets/login.py b/care/emr/api/otp_viewsets/login.py index 7fe6e3c07b..67b48a1744 100644 --- a/care/emr/api/otp_viewsets/login.py +++ b/care/emr/api/otp_viewsets/login.py @@ -77,7 +77,7 @@ def login(self, request): if not otp_object: raise ValidationError({"otp": "Invalid OTP"}) - # otp_object.is_used = True # TODO UNCOMMENT THIS !! + otp_object.is_used = True otp_object.save() token = PatientToken() diff --git a/care/emr/api/viewsets/resource_request.py b/care/emr/api/viewsets/resource_request.py index cbfcf7f9ba..16f69ebc95 100644 --- a/care/emr/api/viewsets/resource_request.py +++ b/care/emr/api/viewsets/resource_request.py @@ -10,9 +10,10 @@ from care.emr.resources.resource_request.spec import ( ResourceRequestCommentCreateSpec, ResourceRequestCommentListSpec, + ResourceRequestCommentRetrieveSpec, ResourceRequestCreateSpec, ResourceRequestListSpec, - ResourceRequestRetrieveSpec, ResourceRequestCommentRetrieveSpec, + ResourceRequestRetrieveSpec, ) diff --git a/care/emr/resources/resource_request/spec.py b/care/emr/resources/resource_request/spec.py index 6c65a7f2de..ab18514146 100644 --- a/care/emr/resources/resource_request/spec.py +++ b/care/emr/resources/resource_request/spec.py @@ -144,5 +144,7 @@ def perform_extra_serialization(cls, mapping, obj): if obj.created_by: mapping["created_by"] = UserSpec.serialize(obj.created_by) + class ResourceRequestCommentRetrieveSpec(ResourceRequestCommentListSpec): pass + diff --git a/care/utils/sms/send_sms.py b/care/utils/sms/send_sms.py index 38b4816895..fe5f497b7c 100644 --- a/care/utils/sms/send_sms.py +++ b/care/utils/sms/send_sms.py @@ -19,11 +19,17 @@ def send_sms(phone_numbers, message, many=False): if settings.DEBUG: logger.error("Invalid Phone Number %s", phone) continue - client = boto3.client( - "sns", - aws_access_key_id=settings.SNS_ACCESS_KEY, - aws_secret_access_key=settings.SNS_SECRET_KEY, - region_name=settings.SNS_REGION, - ) + if settings.SNS_ROLE_BASED_MODE: + client = boto3.client( + "sns", + region_name=settings.SNS_REGION, + ) + else: + client = boto3.client( + "sns", + aws_access_key_id=settings.SNS_ACCESS_KEY, + aws_secret_access_key=settings.SNS_SECRET_KEY, + region_name=settings.SNS_REGION, + ) client.publish(PhoneNumber=phone, Message=message) return True diff --git a/config/settings/deployment.py b/config/settings/deployment.py index b7485a45d9..855adc8801 100644 --- a/config/settings/deployment.py +++ b/config/settings/deployment.py @@ -123,6 +123,7 @@ SNS_ACCESS_KEY = env("SNS_ACCESS_KEY", default="") SNS_SECRET_KEY = env("SNS_SECRET_KEY", default="") SNS_REGION = env("SNS_REGION", default="ap-south-1") +SNS_ROLE_BASED_MODE = env.bool("SNS_ROLE_BASED_MODE", default=False) # open id connect JWKS = JsonWebKey.import_key_set(