From a92b60a8382d5011589e549773584039dbc54b74 Mon Sep 17 00:00:00 2001 From: Aakash Singh Date: Thu, 11 Jan 2024 21:19:58 +0530 Subject: [PATCH] Add nurse user type (#1819) * add nurse user type * migrate staff users to nurse role * add update permissions checks * rebase migrations * fix issues * move permission checks out of get_queryset * fix file upload post * restrict nurse users from accessing external results --- care/facility/api/viewsets/bed.py | 28 ++++---- care/facility/api/viewsets/file_upload.py | 28 +++++++- care/facility/api/viewsets/hospital_doctor.py | 12 ++-- care/facility/api/viewsets/notification.py | 5 +- .../api/viewsets/patient_external_test.py | 17 ++++- care/facility/api/viewsets/patient_sample.py | 8 ++- care/facility/api/viewsets/shifting.py | 2 +- .../migrations/0001_initial_squashed.py | 2 +- care/facility/models/asset.py | 6 +- care/facility/models/base.py | 7 -- care/facility/models/daily_round.py | 61 +++++----------- .../models/mixins/permissions/asset.py | 24 ------- .../models/mixins/permissions/base.py | 18 +---- .../models/mixins/permissions/facility.py | 52 +++++++------- .../models/mixins/permissions/patient.py | 71 +++++++------------ care/facility/models/patient_consultation.py | 39 ++-------- care/facility/models/patient_sample.py | 47 +++++------- care/facility/models/resources.py | 19 ++--- care/facility/models/shifting.py | 48 +++++++++---- care/users/api/serializers/user.py | 14 ++-- care/users/api/viewsets/users.py | 7 +- care/users/api/viewsets/userskill.py | 3 +- .../migrations/0012_alter_user_user_type.py | 37 ++++++++++ care/users/migrations/0013_staff_to_nurse.py | 33 +++++++++ care/users/models.py | 9 +++ care/utils/notification_handler.py | 7 ++ care/utils/tests/test_utils.py | 4 +- config/authentication.py | 2 +- data/dummy/users.json | 2 +- 29 files changed, 316 insertions(+), 296 deletions(-) create mode 100644 care/users/migrations/0012_alter_user_user_type.py create mode 100644 care/users/migrations/0013_staff_to_nurse.py diff --git a/care/facility/api/viewsets/bed.py b/care/facility/api/viewsets/bed.py index 263f6e7627..ac5bda1b17 100644 --- a/care/facility/api/viewsets/bed.py +++ b/care/facility/api/viewsets/bed.py @@ -59,6 +59,20 @@ class BedViewSet( search_fields = ["name"] filterset_class = BedFilter + def get_queryset(self): + user = self.request.user + queryset = self.queryset + if user.is_superuser: + pass + elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: + queryset = queryset.filter(facility__state=user.state) + elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return queryset + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -83,20 +97,6 @@ def create(self, request, *args, **kwargs): serializer.data, status=status.HTTP_201_CREATED, headers=headers ) - def get_queryset(self): - user = self.request.user - queryset = self.queryset - if user.is_superuser: - pass - elif user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - queryset = queryset.filter(facility__state=user.state) - elif user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - queryset = queryset.filter(facility__district=user.district) - else: - allowed_facilities = get_accessible_facilities(user) - queryset = queryset.filter(facility__id__in=allowed_facilities) - return queryset - def destroy(self, request, *args, **kwargs): if request.user.user_type < User.TYPE_VALUE_MAP["DistrictLabAdmin"]: raise PermissionDenied() diff --git a/care/facility/api/viewsets/file_upload.py b/care/facility/api/viewsets/file_upload.py index 5a238e476c..666eac03c4 100644 --- a/care/facility/api/viewsets/file_upload.py +++ b/care/facility/api/viewsets/file_upload.py @@ -7,7 +7,7 @@ RetrieveModelMixin, UpdateModelMixin, ) -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.file_upload import ( @@ -18,6 +18,7 @@ check_permissions, ) from care.facility.models.file_upload import FileUpload +from care.users.models import User class FileUploadFilter(filters.FilterSet): @@ -25,6 +26,28 @@ class FileUploadFilter(filters.FilterSet): is_archived = filters.BooleanFilter(field_name="is_archived") +class FileUploadPermission(BasePermission): + def has_permission(self, request, view) -> bool: + if request.user.user_type in ( + User.TYPE_VALUE_MAP["StaffReadOnly"], + User.TYPE_VALUE_MAP["Staff"], + ): + if request.method == "GET": + return request.query_params.get("file_type") not in ( + "PATIENT", + "CONSULTATION", + ) + else: + return request.data.get("file_type") not in ( + "PATIENT", + "CONSULTATION", + ) + return True + + def has_object_permission(self, request, view, obj) -> bool: + return self.has_permission(request, view) + + class FileUploadViewSet( CreateModelMixin, RetrieveModelMixin, @@ -36,7 +59,7 @@ class FileUploadViewSet( queryset = ( FileUpload.objects.all().select_related("uploaded_by").order_by("-created_date") ) - permission_classes = [IsAuthenticated] + permission_classes = [IsAuthenticated, FileUploadPermission] lookup_field = "external_id" filter_backends = (filters.DjangoFilterBackend,) filterset_class = FileUploadFilter @@ -54,6 +77,7 @@ def get_serializer_class(self): def get_queryset(self): if "file_type" not in self.request.GET: raise ValidationError({"file_type": "file_type missing in request params"}) + if "associating_id" not in self.request.GET: raise ValidationError( {"associating_id": "associating_id missing in request params"} diff --git a/care/facility/api/viewsets/hospital_doctor.py b/care/facility/api/viewsets/hospital_doctor.py index c3d14c258d..e2c1297d3e 100644 --- a/care/facility/api/viewsets/hospital_doctor.py +++ b/care/facility/api/viewsets/hospital_doctor.py @@ -7,6 +7,7 @@ from care.facility.api.viewsets import FacilityBaseViewset from care.facility.models import Facility, HospitalDoctors from care.users.models import User +from care.utils.cache.cache_allowed_facilities import get_accessible_facilities class HospitalDoctorViewSet(FacilityBaseViewset, ListModelMixin): @@ -24,12 +25,15 @@ def get_queryset(self): facility__external_id=self.kwargs.get("facility_external_id") ) if user.is_superuser: - return queryset + pass elif self.request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"]: - return queryset.filter(facility__state=user.state) + queryset = queryset.filter(facility__state=user.state) elif self.request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"]: - return queryset.filter(facility__district=user.district) - return queryset.filter(facility__users__id__exact=user.id) + queryset = queryset.filter(facility__district=user.district) + else: + allowed_facilities = get_accessible_facilities(user) + queryset = queryset.filter(facility__id__in=allowed_facilities) + return queryset def get_object(self): return get_object_or_404(self.get_queryset(), area=self.kwargs.get("pk")) diff --git a/care/facility/api/viewsets/notification.py b/care/facility/api/viewsets/notification.py index 05f42ff6e0..183e873a3f 100644 --- a/care/facility/api/viewsets/notification.py +++ b/care/facility/api/viewsets/notification.py @@ -3,7 +3,7 @@ from drf_spectacular.utils import extend_schema, inline_serializer from rest_framework import status from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.generics import get_object_or_404 from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin from rest_framework.permissions import IsAuthenticated, IsAuthenticatedOrReadOnly @@ -13,6 +13,7 @@ from care.facility.api.serializers.notification import NotificationSerializer from care.facility.models.notification import Notification +from care.users.models import User from care.utils.filters.choicefilter import CareChoiceFilter, inverse_choices from care.utils.notification_handler import NotificationGenerator from care.utils.queryset.facility import get_facility_queryset @@ -67,6 +68,8 @@ def public_key(self, request, *args, **kwargs): @action(detail=False, methods=["POST"]) def notify(self, request, *args, **kwargs): user = request.user + if user.user_type < User.TYPE_VALUE_MAP["Doctor"]: + raise PermissionDenied() if "facility" not in request.data or request.data["facility"] == "": raise ValidationError({"facility": "is required"}) if "message" not in request.data or request.data["message"] == "": diff --git a/care/facility/api/viewsets/patient_external_test.py b/care/facility/api/viewsets/patient_external_test.py index 4587dc2992..f5d983a9ed 100644 --- a/care/facility/api/viewsets/patient_external_test.py +++ b/care/facility/api/viewsets/patient_external_test.py @@ -16,7 +16,7 @@ UpdateModelMixin, ) from rest_framework.parsers import FormParser, JSONParser, MultiPartParser -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import BasePermission, IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import GenericViewSet @@ -64,6 +64,19 @@ class PatientExternalTestFilter(filters.FilterSet): created_date = DateFromToRangeFilter(field_name="created_date") +class PatientExternalTestPermission(BasePermission): + def has_permission(self, request, view): + return request.user.user_type not in ( + User.TYPE_VALUE_MAP["StaffReadOnly"], + User.TYPE_VALUE_MAP["Staff"], + User.TYPE_VALUE_MAP["NurseReadOnly"], + User.TYPE_VALUE_MAP["Nurse"], + ) + + def has_object_permission(self, request, view, obj): + return self.has_permission(request, view) + + class PatientExternalTestViewSet( RetrieveModelMixin, ListModelMixin, @@ -77,7 +90,7 @@ class PatientExternalTestViewSet( .all() .order_by("-id") ) - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, PatientExternalTestPermission) filter_backends = (filters.DjangoFilterBackend,) filterset_class = PatientExternalTestFilter parser_classes = (MultiPartParser, FormParser, JSONParser) diff --git a/care/facility/api/viewsets/patient_sample.py b/care/facility/api/viewsets/patient_sample.py index 419e7a0126..1dfb0ffab1 100644 --- a/care/facility/api/viewsets/patient_sample.py +++ b/care/facility/api/viewsets/patient_sample.py @@ -6,7 +6,7 @@ from dry_rest_permissions.generics import DRYPermissionFiltersBase, DRYPermissions from rest_framework import mixins, viewsets from rest_framework.decorators import action -from rest_framework.exceptions import ValidationError +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response @@ -117,6 +117,12 @@ def list(self, request, *args, **kwargs): - district - District ID - district_name - District name - case insensitive match """ + if ( + not self.kwargs.get("patient_external_id") + and request.user.user_type < User.TYPE_VALUE_MAP["Doctor"] + ): + raise PermissionDenied() + if settings.CSV_REQUEST_PARAMETER in request.GET: queryset = self.filter_queryset(self.get_queryset()).values( *PatientSample.CSV_MAPPING.keys() diff --git a/care/facility/api/viewsets/shifting.py b/care/facility/api/viewsets/shifting.py index f2f18f57ee..5e62a1766d 100644 --- a/care/facility/api/viewsets/shifting.py +++ b/care/facility/api/viewsets/shifting.py @@ -197,7 +197,7 @@ class ShifitngRequestCommentViewSet( lookup_field = "external_id" queryset = ShiftingRequestComment.objects.all().order_by("-created_date") - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, DRYPermissions) def get_queryset(self): queryset = self.queryset.filter( diff --git a/care/facility/migrations/0001_initial_squashed.py b/care/facility/migrations/0001_initial_squashed.py index 7ec400916a..13c562ae68 100644 --- a/care/facility/migrations/0001_initial_squashed.py +++ b/care/facility/migrations/0001_initial_squashed.py @@ -825,7 +825,7 @@ class Migration(migrations.Migration): }, bases=( models.Model, - care.facility.models.mixins.permissions.asset.AssetsPermissionMixin, + # care.facility.models.mixins.permissions.asset.AssetsPermissionMixin, ), ), migrations.CreateModel( diff --git a/care/facility/models/asset.py b/care/facility/models/asset.py index 81e43cb82f..3d4dc38646 100644 --- a/care/facility/models/asset.py +++ b/care/facility/models/asset.py @@ -7,7 +7,9 @@ from care.facility.models import reverse_choices from care.facility.models.facility import Facility from care.facility.models.json_schema.asset import ASSET_META -from care.facility.models.mixins.permissions.asset import AssetsPermissionMixin +from care.facility.models.mixins.permissions.facility import ( + FacilityRelatedPermissionMixin, +) from care.users.models import User from care.utils.assetintegration.asset_classes import AssetClasses from care.utils.models.base import BaseModel @@ -25,7 +27,7 @@ class AvailabilityStatus(models.TextChoices): UNDER_MAINTENANCE = "Under Maintenance" -class AssetLocation(BaseModel, AssetsPermissionMixin): +class AssetLocation(BaseModel, FacilityRelatedPermissionMixin): """ This model is also used to store rooms that the assets are in, Since these rooms are mapped to actual rooms in the hospital, Beds are also connected to this model to remove duplication of efforts diff --git a/care/facility/models/base.py b/care/facility/models/base.py index a8993980c9..87c877665f 100644 --- a/care/facility/models/base.py +++ b/care/facility/models/base.py @@ -1,12 +1,5 @@ -from care.users.models import User from care.utils.models.base import BaseModel -READ_ONLY_USER_TYPES = [ - User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"], - User.TYPE_VALUE_MAP["StateReadOnlyAdmin"], - User.TYPE_VALUE_MAP["StaffReadOnly"], -] - def pretty_boolean(val, a="YES", b="NO", c="Not Specified"): if val is None: diff --git a/care/facility/models/daily_round.py b/care/facility/models/daily_round.py index 0b95a0720d..d8ced82c78 100644 --- a/care/facility/models/daily_round.py +++ b/care/facility/models/daily_round.py @@ -519,19 +519,11 @@ def save(self, *args, **kwargs): super(DailyRound, self).save(*args, **kwargs) - @staticmethod - def has_write_permission(request): - if "/analyse" not in request.get_full_path(): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False - return DailyRound.has_read_permission(request) - @staticmethod def has_read_permission(request): + if request.user.user_type < User.TYPE_VALUE_MAP["NurseReadOnly"]: + return False + consultation = get_object_or_404( PatientConsultation, external_id=request.parser_context["kwargs"]["consultation_external_id"], @@ -552,7 +544,21 @@ def has_read_permission(request): ) ) + @staticmethod + def has_write_permission(request): + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and DailyRound.has_read_permission(request) + ) + + @staticmethod + def has_analyse_permission(request): + return DailyRound.has_read_permission(request) + def has_object_read_permission(self, request): + if request.user.user_type < User.TYPE_VALUE_MAP["NurseReadOnly"]: + return False + return ( request.user.is_superuser or ( @@ -582,38 +588,9 @@ def has_object_read_permission(self, request): ) def has_object_write_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False return ( - request.user.is_superuser - or ( - self.consultation.patient.facility - and self.consultation.patient.facility == request.user.home_facility - ) - or ( - self.consultation.assigned_to == request.user - or request.user == self.consultation.patient.assigned_to - ) - or ( - request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - and ( - self.consultation.patient.facility - and request.user.district - == self.consultation.patient.facility.district - ) - ) - or ( - request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and ( - self.consultation.patient.facility - and request.user.state - == self.consultation.patient.facility.district - ) - ) + request.user.user_type not in User.READ_ONLY_TYPES + and self.has_object_read_permission(request) ) def has_object_asset_read_permission(self, request): diff --git a/care/facility/models/mixins/permissions/asset.py b/care/facility/models/mixins/permissions/asset.py index 8affe28606..0373441eb9 100644 --- a/care/facility/models/mixins/permissions/asset.py +++ b/care/facility/models/mixins/permissions/asset.py @@ -1,8 +1,5 @@ from dry_rest_permissions.generics import DRYPermissions -from care.facility.models.mixins.permissions.base import BasePermissionMixin -from care.users.models import User - class IsAssetUser: def has_permission(self, request, view): @@ -23,24 +20,3 @@ class DRYAssetPermissions(DRYPermissions): def _get_action(self, action): return f"asset_{super()._get_action(action)}" - - -class AssetsPermissionMixin(BasePermissionMixin): - def has_object_read_permission(self, request): - return True - - def has_object_write_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False - - return True - - def has_object_update_permission(self, request): - return self.has_object_write_permission(request) - - def has_object_destroy_permission(self, request): - return self.has_object_write_permission(request) diff --git a/care/facility/models/mixins/permissions/base.py b/care/facility/models/mixins/permissions/base.py index 1b9056238a..baeaa83210 100644 --- a/care/facility/models/mixins/permissions/base.py +++ b/care/facility/models/mixins/permissions/base.py @@ -8,11 +8,7 @@ def has_read_permission(request): @staticmethod def has_write_permission(request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): + if request.user.user_type in User.READ_ONLY_TYPES: return False return ( request.user.is_superuser @@ -36,11 +32,7 @@ def has_object_read_permission(self, request): ) def has_object_update_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): + if request.user.user_type in User.READ_ONLY_TYPES: return False return (request.user.is_superuser) or ( (hasattr(self, "created_by") and request.user == self.created_by) @@ -57,11 +49,7 @@ def has_object_update_permission(self, request): ) def has_object_destroy_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): + if request.user.user_type in User.READ_ONLY_TYPES: return False return request.user.is_superuser or ( hasattr(self, "created_by") and request.user == self.created_by diff --git a/care/facility/models/mixins/permissions/facility.py b/care/facility/models/mixins/permissions/facility.py index 468463822f..2dc74eb0ae 100644 --- a/care/facility/models/mixins/permissions/facility.py +++ b/care/facility/models/mixins/permissions/facility.py @@ -38,15 +38,17 @@ def has_write_permission(request): except Exception: return False + @staticmethod + def has_update_permission(request): + return request.user.is_authenticated + @staticmethod def has_cover_image_permission(request): - # Returning true here as the permission is validated at object level for this action - return True + return request.user.is_authenticated @staticmethod def has_cover_image_delete_permission(request): - # Returning true here as the permission is validated at object level for this action - return True + return request.user.is_authenticated def has_object_read_permission(self, request): return ( @@ -65,23 +67,22 @@ def has_object_read_permission(self, request): ) def has_object_write_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False - if request.user.user_type < User.TYPE_VALUE_MAP["Staff"]: # todo Temporary - return False - return self.has_object_read_permission(request) + print( + request.user.user_type not in User.READ_ONLY_TYPES, + request.user.user_type >= User.TYPE_VALUE_MAP["Staff"], + self.has_object_read_permission(request), + ) + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["Staff"] + and self.has_object_read_permission(request) + ) def has_object_update_permission(self, request): - return super().has_object_update_permission( - request - ) or self.has_object_write_permission(request) + return self.has_object_write_permission(request) def has_object_destroy_permission(self, request): - return self.has_object_read_permission(request) + return self.has_object_update_permission(request) def has_object_cover_image_permission(self, request): return self.has_object_update_permission(request) @@ -92,14 +93,10 @@ class FacilityRelatedPermissionMixin(BasePermissionMixin): def has_write_permission(request): from care.facility.models.facility import Facility - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): + if request.user.user_type in User.READ_ONLY_TYPES: return False - facility = False + facility: Facility = None try: facility = Facility.objects.get( external_id=request.parser_context["kwargs"]["facility_external_id"] @@ -129,14 +126,13 @@ def has_object_read_permission(self, request): ) def has_object_write_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): + if request.user.user_type in User.READ_ONLY_TYPES: return False return ( super().has_write_permission(request) or request.user.is_superuser or request.user in self.facility.users.all() ) + + def has_object_update_permission(self, request): + return self.has_object_write_permission(request) diff --git a/care/facility/models/mixins/permissions/patient.py b/care/facility/models/mixins/permissions/patient.py index c81685392b..9cbd335df2 100644 --- a/care/facility/models/mixins/permissions/patient.py +++ b/care/facility/models/mixins/permissions/patient.py @@ -5,21 +5,18 @@ class PatientPermissionMixin(BasePermissionMixin): @staticmethod def has_write_permission(request): - if request.user.asset: - return False - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): + if request.user.asset or request.user.user_type in User.READ_ONLY_TYPES: return False return ( request.user.is_superuser or request.user.verified - and request.user.user_type >= User.TYPE_VALUE_MAP["Staff"] + and request.user.user_type >= User.TYPE_VALUE_MAP["Nurse"] ) def has_object_read_permission(self, request): + if request.user.user_type < User.TYPE_VALUE_MAP["NurseReadOnly"]: + return False + doctor_allowed = False if self.last_consultation: doctor_allowed = ( @@ -53,20 +50,20 @@ def has_object_read_permission(self, request): ) def has_object_write_permission(self, request): - if request.user.asset: + if ( + request.user.asset + or request.user.user_type in User.READ_ONLY_TYPES + or request.user.user_type < User.TYPE_VALUE_MAP["NurseReadOnly"] + ): return False + doctor_allowed = False if self.last_consultation: doctor_allowed = ( self.last_consultation.assigned_to == request.user or request.user == self.assigned_to ) - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False + return request.user.is_superuser or ( (hasattr(self, "created_by") and request.user == self.created_by) or (doctor_allowed) @@ -92,20 +89,20 @@ def has_object_write_permission(self, request): ) def has_object_update_permission(self, request): - if request.user.asset: + if ( + request.user.asset + or request.user.user_type in User.READ_ONLY_TYPES + or request.user.user_type < User.TYPE_VALUE_MAP["NurseReadOnly"] + ): return False + doctor_allowed = False if self.last_consultation: doctor_allowed = ( self.last_consultation.assigned_to == request.user or request.user == self.assigned_to ) - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False + return ( request.user.is_superuser or (hasattr(self, "created_by") and request.user == self.created_by) @@ -134,14 +131,6 @@ def has_object_icmr_sample_permission(self, request): return self.has_object_read_permission(request) def has_object_transfer_permission(self, request): - if request.user.asset: - return False - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False new_facility = Facility.objects.filter( id=request.data.get("facility", None) ).first() @@ -156,23 +145,17 @@ def get_related_consultation(self): @staticmethod def has_write_permission(request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False - return True + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["Nurse"] + ) def has_object_read_permission(self, request): # This is because, `get_queryset` for related models already filters by consultation. return True def has_object_update_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False - return True + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["Nurse"] + ) diff --git a/care/facility/models/patient_consultation.py b/care/facility/models/patient_consultation.py index 06cd0e222f..a16c08e2ae 100644 --- a/care/facility/models/patient_consultation.py +++ b/care/facility/models/patient_consultation.py @@ -291,12 +291,9 @@ class Meta: @staticmethod def has_write_permission(request): - if not ConsultationRelatedPermissionMixin.has_write_permission(request): - return False - return ( - request.user.is_superuser - or request.user.verified - and request.user.user_type >= User.TYPE_VALUE_MAP["Staff"] + return request.user.is_superuser or ( + request.user.verified + and ConsultationRelatedPermissionMixin.has_write_permission(request) ) def has_object_read_permission(self, request): @@ -329,33 +326,9 @@ def has_object_read_permission(self, request): ) def has_object_update_permission(self, request): - if not super().has_object_update_permission(request): - return False - return ( - request.user.is_superuser - or ( - self.patient.facility - and request.user in self.patient.facility.users.all() - ) - or ( - self.assigned_to == request.user - or request.user == self.patient.assigned_to - ) - or ( - request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - and ( - self.patient.facility - and request.user.district == self.patient.facility.district - ) - ) - or ( - request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - and ( - self.patient.facility - and request.user.state == self.patient.facility.state - ) - ) - ) + return super().has_object_update_permission( + request + ) and self.has_object_read_permission(request) def has_object_discharge_patient_permission(self, request): return self.has_object_update_permission(request) diff --git a/care/facility/models/patient_sample.py b/care/facility/models/patient_sample.py index a3a7405521..999d435fba 100644 --- a/care/facility/models/patient_sample.py +++ b/care/facility/models/patient_sample.py @@ -152,25 +152,31 @@ def flow(self): @staticmethod def has_write_permission(request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False + if request.user.is_superuser: + return True return ( - request.user.is_superuser - or request.user.user_type >= User.TYPE_VALUE_MAP["Staff"] + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["Nurse"] ) @staticmethod def has_read_permission(request): return ( request.user.is_superuser - or request.user.user_type >= User.TYPE_VALUE_MAP["StaffReadOnly"] + or request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + ) + + @staticmethod + def has_update_permission(request): + return ( + request.user.is_superuser + or request.user.user_type >= User.TYPE_VALUE_MAP["Doctor"] ) def has_object_read_permission(self, request): + if request.user.user_type < User.TYPE_VALUE_MAP["NurseReadOnly"]: + return False + if self.testing_facility: test_facility = request.user in self.testing_facility.users.all() @@ -190,25 +196,10 @@ def has_object_read_permission(self, request): ) def has_object_update_permission(self, request): - if ( - request.user.user_type == User.TYPE_VALUE_MAP["DistrictReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StateReadOnlyAdmin"] - or request.user.user_type == User.TYPE_VALUE_MAP["StaffReadOnly"] - ): - return False - if not self.has_object_read_permission(request): - return False - # if request.user.is_superuser: - # return True - # map_ = self.SAMPLE_TEST_FLOW_CHOICES - # if map_[self.status - 1][1] in ("REQUEST_SUBMITTED", "SENT_TO_COLLECTON_CENTRE"): - # return request.user.user_type >= User.TYPE_VALUE_MAP["DistrictLabAdmin"] - # elif map_[self.status - 1][1] in ("APPROVED", "DENIED"): - # return request.user.user_type >= User.TYPE_VALUE_MAP["Staff"] - # elif map_[self.status - 1][1] in ("RECEIVED_AND_FORWARED", "RECEIVED_AT_LAB"): - # return request.user.user_type >= User.TYPE_VALUE_MAP["StateLabAdmin"] - # The view shall raise a 400 - return True + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and self.has_object_read_permission(request) + ) def has_object_destroy_permission(self, request): return request.user.is_superuser diff --git a/care/facility/models/resources.py b/care/facility/models/resources.py index 492e893aff..e1cb853c28 100644 --- a/care/facility/models/resources.py +++ b/care/facility/models/resources.py @@ -1,11 +1,6 @@ from django.db import models -from care.facility.models import ( - READ_ONLY_USER_TYPES, - FacilityBaseModel, - pretty_boolean, - reverse_choices, -) +from care.facility.models import FacilityBaseModel, pretty_boolean, reverse_choices from care.users.models import User from care.utils.models.validators import mobile_or_landline_number_validator @@ -130,9 +125,7 @@ class Meta: @staticmethod def has_write_permission(request): - if request.user.user_type in READ_ONLY_USER_TYPES: - return False - return True + return request.user.user_type not in User.READ_ONLY_TYPES @staticmethod def has_read_permission(request): @@ -142,17 +135,13 @@ def has_object_read_permission(self, request): return True def has_object_write_permission(self, request): - if request.user.user_type in READ_ONLY_USER_TYPES: - return False - return True + return request.user.user_type not in User.READ_ONLY_TYPES def has_object_transfer_permission(self, request): return True def has_object_update_permission(self, request): - if request.user.user_type in READ_ONLY_USER_TYPES: - return False - return True + return self.has_object_write_permission(request) class ResourceRequestComment(FacilityBaseModel): diff --git a/care/facility/models/shifting.py b/care/facility/models/shifting.py index 4b88925b60..815ce51479 100644 --- a/care/facility/models/shifting.py +++ b/care/facility/models/shifting.py @@ -2,7 +2,6 @@ from care.facility.models import ( FACILITY_TYPES, - READ_ONLY_USER_TYPES, FacilityBaseModel, pretty_boolean, reverse_choices, @@ -152,29 +151,29 @@ class Meta: @staticmethod def has_write_permission(request): - if request.user.user_type in READ_ONLY_USER_TYPES: - return False - return True + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + ) @staticmethod def has_read_permission(request): - return True + return request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] def has_object_read_permission(self, request): - return True + return request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] def has_object_write_permission(self, request): - if request.user.user_type in READ_ONLY_USER_TYPES: - return False - return True + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + ) def has_object_transfer_permission(self, request): - return True + return self.has_object_write_permission(request) def has_object_update_permission(self, request): - if request.user.user_type in READ_ONLY_USER_TYPES: - return False - return True + return self.has_object_write_permission(request) class ShiftingRequestComment(FacilityBaseModel): @@ -187,3 +186,26 @@ class ShiftingRequestComment(FacilityBaseModel): null=True, ) comment = models.TextField(default="", blank=True) + + @staticmethod + def has_write_permission(request): + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + ) + + @staticmethod + def has_read_permission(request): + return request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + + def has_object_read_permission(self, request): + return request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + + def has_object_write_permission(self, request): + return ( + request.user.user_type not in User.READ_ONLY_TYPES + and request.user.user_type >= User.TYPE_VALUE_MAP["NurseReadOnly"] + ) + + def has_object_update_permission(self, request): + return self.has_object_write_permission(request) diff --git a/care/users/api/serializers/user.py b/care/users/api/serializers/user.py index 780a2ee76e..f4df1a426e 100644 --- a/care/users/api/serializers/user.py +++ b/care/users/api/serializers/user.py @@ -1,25 +1,22 @@ from datetime import date -from django.contrib.auth import get_user_model from django.contrib.auth.hashers import make_password from django.db import transaction from rest_framework import exceptions, serializers from care.facility.api.serializers.facility import FacilityBareMinimumSerializer -from care.facility.models import READ_ONLY_USER_TYPES, Facility, FacilityUser +from care.facility.models import Facility, FacilityUser from care.users.api.serializers.lsg import ( DistrictSerializer, LocalBodySerializer, StateSerializer, ) from care.users.api.serializers.skill import UserSkillSerializer -from care.users.models import GENDER_CHOICES +from care.users.models import GENDER_CHOICES, User from care.utils.queryset.facility import get_home_facility_queryset from care.utils.serializer.external_id_field import ExternalIdSerializerField from config.serializers import ChoiceField -User = get_user_model() - class SignUpSerializer(serializers.ModelSerializer): user_type = ChoiceField(choices=User.TYPE_CHOICES) @@ -186,8 +183,8 @@ def validate(self, attrs): }, ) - if self.context["created_by"].user_type in READ_ONLY_USER_TYPES: - if validated["user_type"] not in READ_ONLY_USER_TYPES: + if self.context["created_by"].user_type in User.READ_ONLY_TYPES: + if validated["user_type"] not in User.READ_ONLY_TYPES: raise exceptions.ValidationError( { "user_type": [ @@ -197,7 +194,8 @@ def validate(self, attrs): ) if ( - self.context["created_by"].user_type == User.TYPE_VALUE_MAP["Staff"] + self.context["created_by"].user_type + in (User.TYPE_VALUE_MAP["Staff"], User.TYPE_VALUE_MAP["Nurse"]) and validated["user_type"] == User.TYPE_VALUE_MAP["Doctor"] ): pass diff --git a/care/users/api/viewsets/users.py b/care/users/api/viewsets/users.py index 8570d433eb..aeb51bcf13 100644 --- a/care/users/api/viewsets/users.py +++ b/care/users/api/viewsets/users.py @@ -1,4 +1,3 @@ -from django.contrib.auth import get_user_model from django.core.cache import cache from django.db.models import F from django_filters import rest_framework as filters @@ -15,15 +14,13 @@ from rest_framework.viewsets import GenericViewSet from care.facility.api.serializers.facility import FacilityBasicInfoSerializer -from care.facility.models.base import READ_ONLY_USER_TYPES from care.facility.models.facility import Facility, FacilityUser from care.users.api.serializers.user import ( UserCreateSerializer, UserListSerializer, UserSerializer, ) - -User = get_user_model() +from care.users.models import User def remove_facility_user_cache(user_id): @@ -252,7 +249,7 @@ def clear_home_facility(self, request, *args, **kwargs): raise ValidationError({"home_facility": "No Home Facility Present"}) if ( requesting_user.user_type < User.TYPE_VALUE_MAP["DistrictAdmin"] - or requesting_user.user_type in READ_ONLY_USER_TYPES + or requesting_user.user_type in User.READ_ONLY_TYPES ): raise ValidationError({"home_facility": "Insufficient Permissions"}) diff --git a/care/users/api/viewsets/userskill.py b/care/users/api/viewsets/userskill.py index 085c46a885..59c689396b 100644 --- a/care/users/api/viewsets/userskill.py +++ b/care/users/api/viewsets/userskill.py @@ -4,7 +4,6 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission from rest_framework.viewsets import GenericViewSet, ModelViewSet -from care.facility.models.base import READ_ONLY_USER_TYPES from care.users.api.serializers.userskill import UserSkillSerializer from care.users.models import User, UserSkill from care.utils.queryset.user import get_users @@ -22,7 +21,7 @@ def has_permission(self, request, view): if ( requesting_user.user_type < User.TYPE_VALUE_MAP["DistrictAdmin"] - or requesting_user.user_type in READ_ONLY_USER_TYPES + or requesting_user.user_type in User.READ_ONLY_TYPES ): return False diff --git a/care/users/migrations/0012_alter_user_user_type.py b/care/users/migrations/0012_alter_user_user_type.py new file mode 100644 index 0000000000..fcf02a3745 --- /dev/null +++ b/care/users/migrations/0012_alter_user_user_type.py @@ -0,0 +1,37 @@ +# Generated by Django 4.2.5 on 2024-01-07 15:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0011_user_video_connect_link"), + ] + + operations = [ + migrations.AlterField( + model_name="user", + name="user_type", + field=models.IntegerField( + 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"), + ] + ), + ), + ] diff --git a/care/users/migrations/0013_staff_to_nurse.py b/care/users/migrations/0013_staff_to_nurse.py new file mode 100644 index 0000000000..759c70c71d --- /dev/null +++ b/care/users/migrations/0013_staff_to_nurse.py @@ -0,0 +1,33 @@ +# Generated by Django 4.2.5 on 2024-01-07 15:15 + +from django.db import migrations + +from care.users.models import User + + +def migrate_staff_to_nurse(apps, schema_editor): + User.objects.filter(user_type=User.TYPE_VALUE_MAP["Staff"]).update( + user_type=User.TYPE_VALUE_MAP["Nurse"] + ) + User.objects.filter(user_type=User.TYPE_VALUE_MAP["StaffReadOnly"]).update( + user_type=User.TYPE_VALUE_MAP["NurseReadOnly"] + ) + + +def migrate_nurse_to_staff(apps, schema_editor): + User.objects.filter(user_type=User.TYPE_VALUE_MAP["Nurse"]).update( + user_type=User.TYPE_VALUE_MAP["Staff"] + ) + User.objects.filter(user_type=User.TYPE_VALUE_MAP["NurseReadOnly"]).update( + user_type=User.TYPE_VALUE_MAP["StaffReadOnly"] + ) + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0012_alter_user_user_type"), + ] + + operations = [ + migrations.RunPython(migrate_staff_to_nurse, migrate_nurse_to_staff), + ] diff --git a/care/users/models.py b/care/users/models.py index fa41e241bc..955ee9c1bc 100644 --- a/care/users/models.py +++ b/care/users/models.py @@ -186,6 +186,8 @@ class User(AbstractUser): "Volunteer": 5, "StaffReadOnly": 9, "Staff": 10, + "NurseReadOnly": 13, + "Nurse": 14, "Doctor": 15, "Reserved": 20, "WardAdmin": 21, @@ -198,6 +200,13 @@ class User(AbstractUser): "StateAdmin": 40, } + READ_ONLY_TYPES = ( + TYPE_VALUE_MAP["StaffReadOnly"], + TYPE_VALUE_MAP["NurseReadOnly"], + TYPE_VALUE_MAP["DistrictReadOnlyAdmin"], + TYPE_VALUE_MAP["StateReadOnlyAdmin"], + ) + TYPE_CHOICES = [(value, name) for name, value in TYPE_VALUE_MAP.items()] REVERSE_TYPE_MAP = reverse_choices(TYPE_CHOICES) diff --git a/care/utils/notification_handler.py b/care/utils/notification_handler.py index bbe942c1ce..bbb910fb7f 100644 --- a/care/utils/notification_handler.py +++ b/care/utils/notification_handler.py @@ -329,6 +329,13 @@ def generate_system_users(self): extra_users = self.extra_users caused_user = self.caused_by facility_users = FacilityUser.objects.filter(facility_id=self.facility.id) + if self.event != Notification.Event.MESSAGE: + facility_users.exclude( + user__user_type__in=( + User.TYPE_VALUE_MAP["Staff"], + User.TYPE_VALUE_MAP["StaffReadOnly"], + ) + ) for facility_user in facility_users: if facility_user.user.id != caused_user.id: users.append(facility_user.user) diff --git a/care/utils/tests/test_utils.py b/care/utils/tests/test_utils.py index 1a960ef452..4110f70631 100644 --- a/care/utils/tests/test_utils.py +++ b/care/utils/tests/test_utils.py @@ -123,7 +123,7 @@ def get_user_data(cls, district: District, user_type: str = None): """ return { - "user_type": user_type or User.TYPE_VALUE_MAP["Staff"], + "user_type": user_type or User.TYPE_VALUE_MAP["Nurse"], "district": district, "state": district.state, "phone_number": "8887776665", @@ -160,7 +160,7 @@ def create_user( "state": district.state, "district": district, "local_body": local_body, - "user_type": User.TYPE_VALUE_MAP["Staff"], + "user_type": User.TYPE_VALUE_MAP["Nurse"], } data.update(kwargs) user = User.objects.create_user(**data) diff --git a/config/authentication.py b/config/authentication.py index 8d5a3e8281..ee86acab94 100644 --- a/config/authentication.py +++ b/config/authentication.py @@ -148,7 +148,7 @@ def get_user(self, validated_token, facility): password=f"{password}123", # The 123 makes it inaccessible without hashing gender=3, phone_number="919999999999", - user_type=User.TYPE_VALUE_MAP["Staff"], + user_type=User.TYPE_VALUE_MAP["Nurse"], verified=True, asset=asset_obj, age=10, diff --git a/data/dummy/users.json b/data/dummy/users.json index c48e8f5aa1..70fdf67fe0 100644 --- a/data/dummy/users.json +++ b/data/dummy/users.json @@ -748,7 +748,7 @@ "is_active": true, "date_joined": "2022-10-28T06:48:51.373Z", "username": "devstaff2", - "user_type": 10, + "user_type": 14, "created_by": 1, "ward": null, "local_body": null,