Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add requester to MedicationRequest and basic authzn. tests #2773

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions care/emr/api/viewsets/medication_request.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
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
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,
Expand All @@ -12,6 +15,8 @@
MedicationRequestUpdateSpec,
)
from care.emr.resources.questionnaire.spec import SubjectType
from care.security.authorization import AuthorizationController
from care.users.models import User


class MedicationRequestFilter(filters.FilterSet):
Expand Down Expand Up @@ -41,5 +46,17 @@ 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"
)


InternalQuestionnaireRegistry.register(MedicationRequestViewSet)
21 changes: 21 additions & 0 deletions care/emr/migrations/0011_medicationrequest_requester.py
Original file line number Diff line number Diff line change
@@ -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),
]
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved

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),
),
]
3 changes: 3 additions & 0 deletions care/emr/models/medication_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
15 changes: 9 additions & 6 deletions care/emr/resources/medication/request/spec.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -197,7 +199,7 @@ def validate_method(cls, code):

class MedicationRequestResource(EMRResource):
__model__ = MedicationRequest
__exclude__ = ["patient", "encounter"]
__exclude__ = ["patient", "encounter", "requester"]


class BaseMedicationRequestSpec(MedicationRequestResource):
Expand Down Expand Up @@ -227,6 +229,8 @@ class BaseMedicationRequestSpec(MedicationRequestResource):


class MedicationRequestSpec(BaseMedicationRequestSpec):
requester: UUID4 | None = None

@field_validator("encounter")
@classmethod
def validate_encounter_exists(cls, encounter):
Expand All @@ -245,11 +249,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):
Expand Down
230 changes: 230 additions & 0 deletions care/emr/tests/test_medication_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
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


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_requester(self):
"""
Requester cannot be updated.
"""
requester_initial, requester_updated = self.create_user(), 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_initial, role
)
self.attach_role_facility_organization_user(
self.organization, requester_updated, role
)

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_updated.external_id)
self.client.put(url, data, format="json")

obj.refresh_from_db()
self.assertEqual(obj.requester, requester_initial)