From 911293741ac0d9b33776c4921282909a726d482f Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 23 Jan 2025 15:22:39 +0530 Subject: [PATCH 1/4] Add `requester` to `MedicationRequest` and tests --- care/emr/api/viewsets/medication_request.py | 29 ++ .../0011_medicationrequest_requester.py | 21 ++ care/emr/models/medication_request.py | 3 + care/emr/resources/medication/request/spec.py | 14 +- care/emr/tests/test_medication_request.py | 248 ++++++++++++++++++ 5 files changed, 309 insertions(+), 6 deletions(-) create mode 100644 care/emr/migrations/0011_medicationrequest_requester.py create mode 100644 care/emr/tests/test_medication_request.py diff --git a/care/emr/api/viewsets/medication_request.py b/care/emr/api/viewsets/medication_request.py index 6786e8eeb3..903a865f44 100644 --- a/care/emr/api/viewsets/medication_request.py +++ b/care/emr/api/viewsets/medication_request.py @@ -1,4 +1,6 @@ from django_filters import rest_framework as filters +from rest_framework.exceptions import PermissionDenied +from rest_framework.generics import get_object_or_404 from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin from care.emr.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase @@ -12,6 +14,9 @@ MedicationRequestUpdateSpec, ) from care.emr.resources.questionnaire.spec import SubjectType +from care.security.authorization import AuthorizationController +from care.users.models import User +from care.emr.models.encounter import Encounter class MedicationRequestFilter(filters.FilterSet): @@ -41,5 +46,29 @@ def get_queryset(self): .select_related("patient", "encounter", "created_by", "updated_by") ) + def authorize_create(self, instance): + super().authorize_create(instance) + if instance.requester: + encounter = get_object_or_404(Encounter, external_id=instance.encounter) + requester = get_object_or_404(User, external_id=instance.requester) + if not AuthorizationController.call( + "can_update_encounter_obj", requester, encounter + ): + raise PermissionDenied( + "Requester does not have permission to update encounter" + ) + + def authorize_update(self, request_obj, model_instance): + super().authorize_update(request_obj, model_instance) + if model_instance.requester: + if not AuthorizationController.call( + "can_update_encounter_obj", + model_instance.requester, + model_instance.encounter, + ): + raise PermissionDenied( + "Requester does not have permission to update encounter" + ) + InternalQuestionnaireRegistry.register(MedicationRequestViewSet) diff --git a/care/emr/migrations/0011_medicationrequest_requester.py b/care/emr/migrations/0011_medicationrequest_requester.py new file mode 100644 index 0000000000..d18a83330f --- /dev/null +++ b/care/emr/migrations/0011_medicationrequest_requester.py @@ -0,0 +1,21 @@ +# Generated by Django 5.1.4 on 2025-01-23 05:04 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('emr', '0010_condition_abatement'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='medicationrequest', + name='requester', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/care/emr/models/medication_request.py b/care/emr/models/medication_request.py index 04ed5fef77..56f67e1ca2 100644 --- a/care/emr/models/medication_request.py +++ b/care/emr/models/medication_request.py @@ -18,3 +18,6 @@ class MedicationRequest(EMRBaseModel): dosage_instruction = models.JSONField(default=list, null=True, blank=True) note = models.TextField(null=True, blank=True) authored_on = models.DateTimeField(null=True, blank=True, default=timezone.now) + requester = models.ForeignKey( + "users.User", on_delete=models.SET_NULL, null=True, blank=True + ) diff --git a/care/emr/resources/medication/request/spec.py b/care/emr/resources/medication/request/spec.py index bc08d30aa6..e657f896b3 100644 --- a/care/emr/resources/medication/request/spec.py +++ b/care/emr/resources/medication/request/spec.py @@ -1,6 +1,7 @@ from datetime import datetime from enum import Enum +from django.shortcuts import get_object_or_404 from pydantic import UUID4, BaseModel, Field, field_validator from care.emr.models.encounter import Encounter @@ -21,6 +22,7 @@ from care.emr.resources.medication.valueset.medication import CARE_MEDICATION_VALUESET from care.emr.resources.medication.valueset.route import CARE_ROUTE_VALUESET from care.emr.resources.user.spec import UserSpec +from care.users.models import User class MedicationRequestStatus(str, Enum): @@ -197,7 +199,7 @@ def validate_method(cls, code): class MedicationRequestResource(EMRResource): __model__ = MedicationRequest - __exclude__ = ["patient", "encounter"] + __exclude__ = ["patient", "encounter", "requester"] class BaseMedicationRequestSpec(MedicationRequestResource): @@ -222,6 +224,7 @@ class BaseMedicationRequestSpec(MedicationRequestResource): dosage_instruction: list[DosageInstruction] = Field() authored_on: datetime + requester: UUID4 | None = None note: str | None = Field(None) @@ -245,11 +248,10 @@ def validate_medication(cls, code): ) def perform_extra_deserialization(self, is_update, obj): - if not is_update: - obj.encounter = Encounter.objects.get( - external_id=self.encounter - ) # Needs more validation - obj.patient = obj.encounter.patient + obj.encounter = Encounter.objects.get(external_id=self.encounter) + obj.patient = obj.encounter.patient + if self.requester: + obj.requester = get_object_or_404(User, external_id=self.requester) class MedicationRequestUpdateSpec(MedicationRequestResource): diff --git a/care/emr/tests/test_medication_request.py b/care/emr/tests/test_medication_request.py new file mode 100644 index 0000000000..02dff34585 --- /dev/null +++ b/care/emr/tests/test_medication_request.py @@ -0,0 +1,248 @@ +from care.security.permissions.encounter import EncounterPermissions +from care.security.permissions.patient import PatientPermissions +from care.utils.tests.base import CareAPITestBase +from django.urls import reverse +from unittest.mock import patch +from model_bakery import baker +from datetime import datetime, UTC + + +class TestMedicationRequestApi(CareAPITestBase): + def setUp(self): + super().setUp() + self.user = self.create_user() + self.facility = self.create_facility(user=self.user) + self.organization = self.create_facility_organization(facility=self.facility) + self.patient = self.create_patient() + self.encounter = self.create_encounter( + patient=self.patient, + facility=self.facility, + organization=self.organization, + ) + self.client.force_authenticate(user=self.user) + + self.base_url = reverse( + "medication-request-list", + kwargs={"patient_external_id": self.patient.external_id}, + ) + self.valid_code = { + "display": "Test Value", + "system": "http://test_system.care/test", + "code": "123", + } + # Mocking validate_valueset + self.patcher = patch( + "care.emr.resources.medication.request.spec.validate_valueset", + return_value=self.valid_code, + ) + self.mock_validate_valueset = self.patcher.start() + + def tearDown(self): + self.patcher.stop() + + def _get_medication_request_url(self, medication_request_id): + """Helper to get the detail URL for a specific medication request.""" + return reverse( + "medication-request-detail", + kwargs={ + "patient_external_id": self.patient.external_id, + "external_id": medication_request_id, + }, + ) + + def create_medication_request(self, **kwargs): + data = { + "patient": self.patient, + "encounter": self.encounter, + "status": "active", + "intent": "order", + "category": "inpatient", + "priority": "routine", + "do_not_perform": False, + "medication": self.valid_code, + "dosage_instruction": [], + "authored_on": datetime.now(UTC), + } + data.update(kwargs) + return baker.make("emr.MedicationRequest", **data) + + def get_medication_request_data(self, **kwargs): + data = { + "status": "active", + "intent": "order", + "category": "inpatient", + "priority": "routine", + "do_not_perform": False, + "medication": self.valid_code, + "dosage_instruction": [], + "authored_on": datetime.now(UTC), + "encounter": self.encounter.external_id, + } + data.update(kwargs) + return data + + def test_list_medication_request_with_permissions(self): + """ + Users with `can_view_clinical_data` on a non-completed encounter + can list medication requests (HTTP 200). + """ + # Attach the needed role/permission + permissions = [PatientPermissions.can_view_clinical_data.name] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + + response = self.client.get(self.base_url) + self.assertEqual(response.status_code, 200) + + def test_list_medication_request_without_permissions(self): + """ + Users without `can_view_clinical_data` => (HTTP 403). + """ + response = self.client.get(self.base_url) + self.assertEqual(response.status_code, 403) + + def test_create_medication_request_with_permission(self): + """ + Users with `can_write_encounter_obj` permission can create medication requests (HTTP 200). + """ + permissions = [ + PatientPermissions.can_view_clinical_data.name, + EncounterPermissions.can_write_encounter.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + + data = self.get_medication_request_data() + response = self.client.post(self.base_url, data, format="json") + self.assertEqual(response.status_code, 200) + + def test_create_medication_request_with_permission_for_requester(self): + """ + Users with `can_write_encounter_obj` permission can create medication requests as long as requester has the same permissions (HTTP 200). + """ + requester = self.create_user() + + permissions = [ + PatientPermissions.can_view_clinical_data.name, + EncounterPermissions.can_write_encounter.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + self.attach_role_facility_organization_user(self.organization, requester, role) + + data = self.get_medication_request_data(requester=requester.external_id) + response = self.client.post(self.base_url, data, format="json") + self.assertEqual(response.status_code, 200) + + def test_create_medication_request_without_permission_for_requester(self): + """ + Requester without `can_write_encounter_obj` permission cannot create medication requests (HTTP 200). + """ + permissions = [ + PatientPermissions.can_view_clinical_data.name, + EncounterPermissions.can_write_encounter.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + + requester = self.create_user() + requester_role = self.create_role_with_permissions([]) + self.attach_role_facility_organization_user( + self.organization, requester, requester_role + ) + + data = self.get_medication_request_data(requester=requester.external_id) + response = self.client.post(self.base_url, data, format="json") + self.assertContains( + response, + "Requester does not have permission to update encounter", + status_code=403, + ) + + def test_create_medication_request_without_permission(self): + """ + Users without `can_write_encounter_obj` permission => (HTTP 403). + """ + data = self.get_medication_request_data() + response = self.client.post(self.base_url, data, format="json") + self.assertEqual(response.status_code, 403) + + def test_update_medication_request_with_permission(self): + """ + Users with `can_write_encounter_obj` and `can_view_clinical_data` permission can update medication requests (HTTP 200). + """ + permissions = [ + PatientPermissions.can_view_clinical_data.name, + EncounterPermissions.can_write_encounter.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + + obj = self.create_medication_request() + url = self._get_medication_request_url(obj.external_id) + data = self.get_medication_request_data() + response = self.client.put(url, data, format="json") + self.assertEqual(response.status_code, 200) + + def test_update_medication_request_without_permission(self): + """ + Users without `can_write_encounter_obj` => HTTP 403 + """ + permissions = [ + PatientPermissions.can_view_clinical_data.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + + obj = self.create_medication_request() + url = self._get_medication_request_url(obj.external_id) + data = self.get_medication_request_data() + response = self.client.put(url, data, format="json") + self.assertEqual(response.status_code, 403) + + def test_update_medication_request_with_permission_for_requester(self): + """ + Users with `can_write_encounter_obj` permission can update medication requests as long as requester has the same permissions (HTTP 200). + """ + requester = self.create_user() + + permissions = [ + PatientPermissions.can_view_clinical_data.name, + EncounterPermissions.can_write_encounter.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + self.attach_role_facility_organization_user(self.organization, requester, role) + + obj = self.create_medication_request(requester=requester) + url = self._get_medication_request_url(obj.external_id) + data = self.get_medication_request_data() + response = self.client.put(url, data, format="json") + self.assertEqual(response.status_code, 200) + + def test_update_medication_request_without_permission_for_requester(self): + """ + Requester without `can_write_encounter_obj` permission cannot update medication requests (HTTP 403). + """ + permissions = [ + PatientPermissions.can_view_clinical_data.name, + EncounterPermissions.can_write_encounter.name, + ] + role = self.create_role_with_permissions(permissions) + self.attach_role_facility_organization_user(self.organization, self.user, role) + + requester = self.create_user() + requester_role = self.create_role_with_permissions([]) + self.attach_role_facility_organization_user( + self.organization, requester, requester_role + ) + + obj = self.create_medication_request(requester=requester) + url = self._get_medication_request_url(obj.external_id) + data = self.get_medication_request_data(requester=requester.external_id) + response = self.client.put(url, data, format="json") + self.assertContains( + response, + "Requester does not have permission to update encounter", + status_code=403, + ) From 47d6b861c5dfccb1f66cc492de4c76ad9c3c9aa1 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 23 Jan 2025 15:28:55 +0530 Subject: [PATCH 2/4] sort imports --- care/emr/tests/test_medication_request.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/care/emr/tests/test_medication_request.py b/care/emr/tests/test_medication_request.py index 02dff34585..0b8c0007f9 100644 --- a/care/emr/tests/test_medication_request.py +++ b/care/emr/tests/test_medication_request.py @@ -1,10 +1,12 @@ +from datetime import UTC, datetime +from unittest.mock import patch + +from django.urls import reverse +from model_bakery import baker + from care.security.permissions.encounter import EncounterPermissions from care.security.permissions.patient import PatientPermissions from care.utils.tests.base import CareAPITestBase -from django.urls import reverse -from unittest.mock import patch -from model_bakery import baker -from datetime import datetime, UTC class TestMedicationRequestApi(CareAPITestBase): From 809c9cd57b8a2dcbd149802ef9446580e9106617 Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 23 Jan 2025 15:35:43 +0530 Subject: [PATCH 3/4] sort imports --- care/emr/api/viewsets/medication_request.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/care/emr/api/viewsets/medication_request.py b/care/emr/api/viewsets/medication_request.py index 903a865f44..9020866515 100644 --- a/care/emr/api/viewsets/medication_request.py +++ b/care/emr/api/viewsets/medication_request.py @@ -4,6 +4,7 @@ from care.emr.api.viewsets.base import EMRModelViewSet, EMRQuestionnaireResponseMixin from care.emr.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase +from care.emr.models.encounter import Encounter from care.emr.models.medication_request import MedicationRequest from care.emr.registries.system_questionnaire.system_questionnaire import ( InternalQuestionnaireRegistry, @@ -16,7 +17,6 @@ from care.emr.resources.questionnaire.spec import SubjectType from care.security.authorization import AuthorizationController from care.users.models import User -from care.emr.models.encounter import Encounter class MedicationRequestFilter(filters.FilterSet): @@ -60,15 +60,14 @@ def authorize_create(self, instance): def authorize_update(self, request_obj, model_instance): super().authorize_update(request_obj, model_instance) - if model_instance.requester: - if not AuthorizationController.call( - "can_update_encounter_obj", - model_instance.requester, - model_instance.encounter, - ): - raise PermissionDenied( - "Requester does not have permission to update encounter" - ) + if model_instance.requester and not AuthorizationController.call( + "can_update_encounter_obj", + model_instance.requester, + model_instance.encounter, + ): + raise PermissionDenied( + "Requester does not have permission to update encounter" + ) InternalQuestionnaireRegistry.register(MedicationRequestViewSet) From 17ca4767f5eb2b19426b0adaa8948382bd10da9f Mon Sep 17 00:00:00 2001 From: rithviknishad Date: Thu, 23 Jan 2025 16:18:03 +0530 Subject: [PATCH 4/4] disallow update requester --- care/emr/api/viewsets/medication_request.py | 11 ----- care/emr/resources/medication/request/spec.py | 3 +- care/emr/tests/test_medication_request.py | 46 ++++++------------- 3 files changed, 15 insertions(+), 45 deletions(-) diff --git a/care/emr/api/viewsets/medication_request.py b/care/emr/api/viewsets/medication_request.py index 9020866515..f96c88894b 100644 --- a/care/emr/api/viewsets/medication_request.py +++ b/care/emr/api/viewsets/medication_request.py @@ -58,16 +58,5 @@ def authorize_create(self, instance): "Requester does not have permission to update encounter" ) - def authorize_update(self, request_obj, model_instance): - super().authorize_update(request_obj, model_instance) - if model_instance.requester and not AuthorizationController.call( - "can_update_encounter_obj", - model_instance.requester, - model_instance.encounter, - ): - raise PermissionDenied( - "Requester does not have permission to update encounter" - ) - InternalQuestionnaireRegistry.register(MedicationRequestViewSet) diff --git a/care/emr/resources/medication/request/spec.py b/care/emr/resources/medication/request/spec.py index e657f896b3..431ad73f1c 100644 --- a/care/emr/resources/medication/request/spec.py +++ b/care/emr/resources/medication/request/spec.py @@ -224,12 +224,13 @@ class BaseMedicationRequestSpec(MedicationRequestResource): dosage_instruction: list[DosageInstruction] = Field() authored_on: datetime - requester: UUID4 | None = None note: str | None = Field(None) class MedicationRequestSpec(BaseMedicationRequestSpec): + requester: UUID4 | None = None + @field_validator("encounter") @classmethod def validate_encounter_exists(cls, encounter): diff --git a/care/emr/tests/test_medication_request.py b/care/emr/tests/test_medication_request.py index 0b8c0007f9..0c77a16dc7 100644 --- a/care/emr/tests/test_medication_request.py +++ b/care/emr/tests/test_medication_request.py @@ -202,11 +202,11 @@ def test_update_medication_request_without_permission(self): response = self.client.put(url, data, format="json") self.assertEqual(response.status_code, 403) - def test_update_medication_request_with_permission_for_requester(self): + def test_update_medication_request_requester(self): """ - Users with `can_write_encounter_obj` permission can update medication requests as long as requester has the same permissions (HTTP 200). + Requester cannot be updated. """ - requester = self.create_user() + requester_initial, requester_updated = self.create_user(), self.create_user() permissions = [ PatientPermissions.can_view_clinical_data.name, @@ -214,37 +214,17 @@ def test_update_medication_request_with_permission_for_requester(self): ] role = self.create_role_with_permissions(permissions) self.attach_role_facility_organization_user(self.organization, self.user, role) - self.attach_role_facility_organization_user(self.organization, requester, role) - - obj = self.create_medication_request(requester=requester) - url = self._get_medication_request_url(obj.external_id) - data = self.get_medication_request_data() - response = self.client.put(url, data, format="json") - self.assertEqual(response.status_code, 200) - - def test_update_medication_request_without_permission_for_requester(self): - """ - Requester without `can_write_encounter_obj` permission cannot update medication requests (HTTP 403). - """ - permissions = [ - PatientPermissions.can_view_clinical_data.name, - EncounterPermissions.can_write_encounter.name, - ] - role = self.create_role_with_permissions(permissions) - self.attach_role_facility_organization_user(self.organization, self.user, role) - - requester = self.create_user() - requester_role = self.create_role_with_permissions([]) self.attach_role_facility_organization_user( - self.organization, requester, requester_role + self.organization, requester_initial, role + ) + self.attach_role_facility_organization_user( + self.organization, requester_updated, role ) - obj = self.create_medication_request(requester=requester) + obj = self.create_medication_request(requester=requester_initial) url = self._get_medication_request_url(obj.external_id) - data = self.get_medication_request_data(requester=requester.external_id) - response = self.client.put(url, data, format="json") - self.assertContains( - response, - "Requester does not have permission to update encounter", - status_code=403, - ) + data = self.get_medication_request_data(requester=requester_updated.external_id) + self.client.put(url, data, format="json") + + obj.refresh_from_db() + self.assertEqual(obj.requester, requester_initial)