From d815de4ceb80c2f7967c75534585979e08afb29c Mon Sep 17 00:00:00 2001 From: Prafful Date: Mon, 17 Feb 2025 23:24:42 +0530 Subject: [PATCH 1/4] initial commit --- care/emr/models/consent.py | 16 +++++++++ care/emr/resources/consent/__init__.py | 0 care/emr/resources/consent/spec.py | 50 ++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 care/emr/models/consent.py create mode 100644 care/emr/resources/consent/__init__.py create mode 100644 care/emr/resources/consent/spec.py diff --git a/care/emr/models/consent.py b/care/emr/models/consent.py new file mode 100644 index 0000000000..69dfb001c4 --- /dev/null +++ b/care/emr/models/consent.py @@ -0,0 +1,16 @@ +from django.db import models + +from care.emr.models import EMRBaseModel + + +class Consent(EMRBaseModel): + status = models.CharField(max_length=50) + category = models.JSONField(default=list) + date = models.DateTimeField() + period = models.JSONField(null=True, blank=True) + encounter = models.ForeignKey( + "emr.Encounter", on_delete=models.CASCADE, related_name="consents" + ) + decision = models.CharField(max_length=10) + # source_attachment = # need to think more about it + verification_details = models.JSONField(null=True, blank=True) diff --git a/care/emr/resources/consent/__init__.py b/care/emr/resources/consent/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/care/emr/resources/consent/spec.py b/care/emr/resources/consent/spec.py new file mode 100644 index 0000000000..28431dfbf3 --- /dev/null +++ b/care/emr/resources/consent/spec.py @@ -0,0 +1,50 @@ +from datetime import datetime +from enum import Enum + +from pydantic import UUID4, Field + +from care.emr.fhir.schema.base import Coding, Period +from care.emr.models.consent import Consent +from care.emr.resources.base import EMRResource +from care.emr.resources.file_upload.spec import FileUploadBaseSpec + + +class ConsentStatusChoices(str, Enum): + draft = "draft" + active = "active" + inactive = "inactive" + not_done = "not_done" + entered_in_error = "entered_in_error" + unknown = "unknown" + + +class VerificationType(str, Enum): + family = "family" + validation = "validation" + + +class ConsentVerificationSpec(EMRResource): + verified: bool + verified_by: UUID4 + verification_date: datetime + verification_type: VerificationType + + +class DecisionType(str, Enum): + deny = "deny" + permit = "permit" + + +class ConsentSpec(EMRResource): + __model__ = Consent + id: UUID4 | None = Field( + default=None, description="Unique identifier for the consent record" + ) + status: ConsentStatusChoices + category: list[Coding] + date: datetime + period: Period | None = None + encounter: UUID4 + decision: DecisionType + source_attachment: list[FileUploadBaseSpec] | None = None + verification_details: list[ConsentVerificationSpec] | None = None From b17a490f65052aa2416e4991c8807c3b15d25189 Mon Sep 17 00:00:00 2001 From: Prafful Sharma <115104695+DraKen0009@users.noreply.github.com> Date: Tue, 18 Feb 2025 19:13:14 +0530 Subject: [PATCH 2/4] Fix for empty questionnaire title (#2834) --- Makefile | 4 ++++ care/emr/resources/questionnaire/spec.py | 7 +++++++ care/emr/tests/test_questionnaire_api.py | 10 +++++++--- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9476042de1..dc0afd3c1a 100644 --- a/Makefile +++ b/Makefile @@ -37,6 +37,10 @@ teardown: load-dummy-data: docker compose exec backend bash -c "python manage.py load_dummy_data" +load-seed-data: + docker compose exec backend bash -c "python manage.py load_govt_organization --state kerala --load-districts --load-local-bodies --load-wards" + docker compose exec backend bash -c "python manage.py sync_permissions_roles" + list: docker compose -f docker-compose.yaml -f $(docker_config_file) ps diff --git a/care/emr/resources/questionnaire/spec.py b/care/emr/resources/questionnaire/spec.py index c0f79775c5..f35560528a 100644 --- a/care/emr/resources/questionnaire/spec.py +++ b/care/emr/resources/questionnaire/spec.py @@ -212,6 +212,13 @@ def validate_slug(cls, slug: str, info): raise ValueError(err) return slug + @field_validator("title") + @classmethod + def validate_title(cls, title: str, info): + if not title.strip(): + raise ValueError("Title cannot be empty") + return title.strip() + def get_all_ids(self): ids = [] for question in self.questions: diff --git a/care/emr/tests/test_questionnaire_api.py b/care/emr/tests/test_questionnaire_api.py index 4525c5c489..38c98e9cc1 100644 --- a/care/emr/tests/test_questionnaire_api.py +++ b/care/emr/tests/test_questionnaire_api.py @@ -501,9 +501,13 @@ def test_questionnaire_creation_access_granted(self): role = self.create_role_with_permissions(permissions) self.attach_role_organization_user(self.organization, self.user, role) - response = self.client.post( - self.base_url, self._create_questionnaire(), format="json" - ) + questionnaire_data = self._create_questionnaire() + questionnaire_data["title"] = "" + response = self.client.post(self.base_url, questionnaire_data, format="json") + self.assertEqual(response.status_code, 400) + + questionnaire_data["title"] = self.fake.text(max_nb_chars=255) + response = self.client.post(self.base_url, questionnaire_data, format="json") self.assertEqual(response.status_code, 200) def test_questionnaire_retrieval_access_denied(self): From 42b2f2e0ab65c3310fc2c080f0296d4d9cb5dbde Mon Sep 17 00:00:00 2001 From: Prafful Date: Tue, 18 Feb 2025 23:33:08 +0530 Subject: [PATCH 3/4] adding viewset for consent --- care/emr/api/viewsets/consent.py | 82 ++++++++++++++++++++++++++ care/emr/models/consent.py | 3 +- care/emr/resources/base.py | 13 +++- care/emr/resources/consent/spec.py | 77 ++++++++++++++++++++---- care/emr/resources/encounter/spec.py | 15 +---- care/emr/resources/file_upload/spec.py | 2 + 6 files changed, 164 insertions(+), 28 deletions(-) create mode 100644 care/emr/api/viewsets/consent.py diff --git a/care/emr/api/viewsets/consent.py b/care/emr/api/viewsets/consent.py new file mode 100644 index 0000000000..5e7319aabc --- /dev/null +++ b/care/emr/api/viewsets/consent.py @@ -0,0 +1,82 @@ +from django.db import transaction +from pydantic import BaseModel, ValidationError +from pydantic.v1 import UUID4 +from rest_framework.decorators import action +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.api.viewsets.file_upload import file_authorizer +from care.emr.models import FileUpload +from care.emr.models.consent import Consent +from care.emr.resources.consent.spec import ( + ConsentCreateSpec, + ConsentListSpec, + ConsentRetrieveSpec, + ConsentUpdateSpec, +) +from care.emr.resources.file_upload.spec import FileUploadCreateSpec + + +class ConsentViewSet(EMRModelViewSet): + database_model = Consent + pydantic_model = ConsentCreateSpec + pydantic_read_model = ConsentListSpec + pydantic_update_model = ConsentUpdateSpec + pydantic_retrieve_model = ConsentRetrieveSpec + + def perform_create(self, instance): + with transaction.atomic(): + attachment_ids = [] + attachments = instance.pop("source_attachment", []) + for attachment in attachments: + file_authorizer( + self.request.user, + attachment.file_type, + attachment.associating_id, + "write", + ) + file = FileUpload.objects.create(attachment) + attachment_ids.append(file.external_id) + instance["source_attachment"] = attachment_ids + super().perform_create(instance) + + class AttachmentAdditionSchema(BaseModel): + source_attachment: FileUploadCreateSpec + + @action(detail=True, methods=["POST"]) + def add_attachment(self, request, *args, **kwargs): + instance = self.get_object() + request_data = self.AttachmentAdditionSchema(**request.data) + file_authorizer( + request.user, + request_data.source_attachment.file_type, + request_data.source_attachment.associating_id, + "write", + ) + file = FileUpload.objects.create(request_data.source_attachment) + instance.source_attachment.append(file.external_id) + instance.save(update_fields=["source_attachment"]) + return Response(ConsentRetrieveSpec.serialize(instance).to_json()) + + class AttachmentRemovalSchema(BaseModel): + attachment_id: UUID4 + + @action(detail=True, methods=["POST"]) + def remove_attachment(self, request, *args, **kwargs): + instance = self.get_object() + request_data = self.AttachmentRemovalSchema(**request.data) + if request_data.attachment_id not in instance.source_attachment: + raise ValidationError("Attachment not associated with the consent") + attachment = get_object_or_404( + FileUpload, external_id=request_data.attachment_id + ) + file_authorizer( + request.user, + attachment.file_type, + attachment.associating_id, + "write", + ) + instance.source_attachment.remove(request_data.attachment_id) + instance.save(update_fields=["source_attachment"]) + return Response(ConsentRetrieveSpec.serialize(instance).to_json()) diff --git a/care/emr/models/consent.py b/care/emr/models/consent.py index 69dfb001c4..8aaf7a76a0 100644 --- a/care/emr/models/consent.py +++ b/care/emr/models/consent.py @@ -1,3 +1,4 @@ +from django.contrib.postgres.fields import ArrayField from django.db import models from care.emr.models import EMRBaseModel @@ -12,5 +13,5 @@ class Consent(EMRBaseModel): "emr.Encounter", on_delete=models.CASCADE, related_name="consents" ) decision = models.CharField(max_length=10) - # source_attachment = # need to think more about it verification_details = models.JSONField(null=True, blank=True) + source_attachment = ArrayField(models.UUIDField(), default=[]) diff --git a/care/emr/resources/base.py b/care/emr/resources/base.py index 6d016185b1..427bc1d1e6 100644 --- a/care/emr/resources/base.py +++ b/care/emr/resources/base.py @@ -5,7 +5,7 @@ from typing import Annotated, Union, get_origin import phonenumbers -from pydantic import BaseModel +from pydantic import BaseModel, model_validator from pydantic_extra_types.phone_numbers import PhoneNumberValidator from care.emr.fhir.schema.base import Coding @@ -163,3 +163,14 @@ def serialize_audit_users(cls, mapping, obj): number_format="E164", ), ] + + +class PeriodSpec(BaseModel): + start: datetime.datetime | None = None + end: datetime.datetime | None = None + + @model_validator(mode="after") + def validate_period(self): + if (self.start and self.end) and (self.start > self.end): + raise ValueError("Start Date cannot be greater than End Date") + return self diff --git a/care/emr/resources/consent/spec.py b/care/emr/resources/consent/spec.py index 28431dfbf3..d07b8d8d7c 100644 --- a/care/emr/resources/consent/spec.py +++ b/care/emr/resources/consent/spec.py @@ -3,10 +3,16 @@ from pydantic import UUID4, Field -from care.emr.fhir.schema.base import Coding, Period +from care.emr.models import FileUpload from care.emr.models.consent import Consent -from care.emr.resources.base import EMRResource -from care.emr.resources.file_upload.spec import FileUploadBaseSpec +from care.emr.resources.base import EMRResource, PeriodSpec +from care.emr.resources.file_upload.spec import ( + FileCategoryChoices, + FileTypeChoices, + FileUploadCreateSpec, + FileUploadListSpec, + FileUploadRetrieveSpec, +) class ConsentStatusChoices(str, Enum): @@ -23,6 +29,17 @@ class VerificationType(str, Enum): validation = "validation" +class DecisionType(str, Enum): + deny = "deny" + permit = "permit" + + +class CategoryChoice(str, Enum): + research = "research" + privacy_consent = "privacy_consent" + treatment = "treatment" + + class ConsentVerificationSpec(EMRResource): verified: bool verified_by: UUID4 @@ -30,21 +47,55 @@ class ConsentVerificationSpec(EMRResource): verification_type: VerificationType -class DecisionType(str, Enum): - deny = "deny" - permit = "permit" - - -class ConsentSpec(EMRResource): +class ConsentBaseSpec(EMRResource): __model__ = Consent + id: UUID4 | None = Field( default=None, description="Unique identifier for the consent record" ) status: ConsentStatusChoices - category: list[Coding] + category: CategoryChoice date: datetime - period: Period | None = None + period: PeriodSpec = dict encounter: UUID4 decision: DecisionType - source_attachment: list[FileUploadBaseSpec] | None = None - verification_details: list[ConsentVerificationSpec] | None = None + verification_details: list[ConsentVerificationSpec] | None = [] + + +class ConsentCreateSpec(ConsentBaseSpec): + source_attachment: list[FileUploadCreateSpec] | None = [] + + def perform_extra_deserialization(self, is_update, obj): + if not is_update: + for attachment in self.source_attachment: + attachment.file_type = FileTypeChoices.consent + attachment.file_category = FileCategoryChoices.consent_attachment + # obj.source_attachment = [attachment.id for attachment in self.source_attachment] + + +class ConsentUpdateSpec(ConsentBaseSpec): + pass + + +class ConsentListSpec(ConsentBaseSpec): + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id + mapping["source_attachment"] = [ + FileUploadListSpec.serialize( + FileUpload.objects.get(external_id=attachment) + ).to_json() + for attachment in obj.source_attachment or [] + ] + + +class ConsentRetrieveSpec(ConsentBaseSpec): + @classmethod + def perform_extra_serialization(cls, mapping, obj): + mapping["id"] = obj.external_id + mapping["source_attachment"] = [ + FileUploadRetrieveSpec.serialize( + FileUpload.objects.get(external_id=attachment) + ).to_json() + for attachment in obj.source_attachment or [] + ] diff --git a/care/emr/resources/encounter/spec.py b/care/emr/resources/encounter/spec.py index 467a50a1ab..efda863925 100644 --- a/care/emr/resources/encounter/spec.py +++ b/care/emr/resources/encounter/spec.py @@ -2,7 +2,7 @@ import datetime from django.utils import timezone -from pydantic import UUID4, BaseModel, model_validator +from pydantic import UUID4, BaseModel from care.emr.models import ( Encounter, @@ -11,7 +11,7 @@ TokenBooking, ) from care.emr.models.patient import Patient -from care.emr.resources.base import EMRResource +from care.emr.resources.base import EMRResource, PeriodSpec from care.emr.resources.encounter.constants import ( AdmitSourcesChoices, ClassChoices, @@ -31,17 +31,6 @@ from care.facility.models import Facility -class PeriodSpec(BaseModel): - start: datetime.datetime | None = None - end: datetime.datetime | None = None - - @model_validator(mode="after") - def validate_period(self): - if (self.start and self.end) and (self.start > self.end): - raise ValueError("Start Date cannot be greater than End Date") - return self - - class HospitalizationSpec(BaseModel): re_admission: bool | None = None admit_source: AdmitSourcesChoices | None = None diff --git a/care/emr/resources/file_upload/spec.py b/care/emr/resources/file_upload/spec.py index f8c5341dda..006f4cc1b9 100644 --- a/care/emr/resources/file_upload/spec.py +++ b/care/emr/resources/file_upload/spec.py @@ -11,6 +11,7 @@ class FileTypeChoices(str, Enum): patient = "patient" encounter = "encounter" + consent = "consent" class FileCategoryChoices(str, Enum): @@ -19,6 +20,7 @@ class FileCategoryChoices(str, Enum): identity_proof = "identity_proof" unspecified = "unspecified" discharge_summary = "discharge_summary" + consent_attachment = "consent_attachment" class FileUploadBaseSpec(EMRResource): From a90a31bf9313536c47ac0f7f381a486fdd6c127e Mon Sep 17 00:00:00 2001 From: Prafful Date: Fri, 21 Feb 2025 02:24:17 +0530 Subject: [PATCH 4/4] adding permissions to consent viewset --- care/emr/api/viewsets/consent.py | 152 ++++++++++++++---- care/emr/api/viewsets/file_upload.py | 13 ++ ...eviceservicehistory_serviced_on_consent.py | 48 ++++++ care/emr/resources/base.py | 1 + care/emr/resources/consent/spec.py | 39 +++-- care/emr/resources/file_upload/spec.py | 12 ++ care/security/authorization/__init__.py | 1 + config/api_router.py | 3 + 8 files changed, 224 insertions(+), 45 deletions(-) create mode 100644 care/emr/migrations/0020_alter_deviceservicehistory_serviced_on_consent.py diff --git a/care/emr/api/viewsets/consent.py b/care/emr/api/viewsets/consent.py index 5e7319aabc..fcf316451c 100644 --- a/care/emr/api/viewsets/consent.py +++ b/care/emr/api/viewsets/consent.py @@ -1,67 +1,113 @@ -from django.db import transaction -from pydantic import BaseModel, ValidationError -from pydantic.v1 import UUID4 +import logging + +from django.utils.timezone import now +from drf_spectacular.utils import extend_schema +from pydantic import UUID4, BaseModel from rest_framework.decorators import action +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.api.viewsets.encounter_authz_base import EncounterBasedAuthorizationBase from care.emr.api.viewsets.file_upload import file_authorizer -from care.emr.models import FileUpload +from care.emr.models import Encounter, FileUpload from care.emr.models.consent import Consent from care.emr.resources.consent.spec import ( ConsentCreateSpec, ConsentListSpec, ConsentRetrieveSpec, ConsentUpdateSpec, + ConsentVerificationSpec, +) +from care.emr.resources.file_upload.spec import ( + ConsentFileUploadCreateSpec, + FileUploadRetrieveSpec, ) -from care.emr.resources.file_upload.spec import FileUploadCreateSpec +from care.security.authorization import AuthorizationController + +logger = logging.getLogger(__name__) -class ConsentViewSet(EMRModelViewSet): +class ConsentViewSet(EMRModelViewSet, EncounterBasedAuthorizationBase): database_model = Consent pydantic_model = ConsentCreateSpec pydantic_read_model = ConsentListSpec pydantic_update_model = ConsentUpdateSpec pydantic_retrieve_model = ConsentRetrieveSpec - def perform_create(self, instance): - with transaction.atomic(): - attachment_ids = [] - attachments = instance.pop("source_attachment", []) - for attachment in attachments: - file_authorizer( - self.request.user, - attachment.file_type, - attachment.associating_id, - "write", + def get_patient_obj(self): + return self.get_object().encounter.patient + + def authorize_read_encounter(self): + if not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + if encounter := self.request.GET.get("encounter"): + encounter_obj = get_object_or_404(Encounter, external_id=encounter) + if not AuthorizationController.call( + "can_view_encounter_obj", self.request.user, encounter_obj + ): + raise PermissionDenied("Permission denied to user") + else: + raise PermissionDenied("Permission denied to user") + + def get_queryset(self): + # Todo: Implement File authorization so that only attachments that the user has access to are returned + if self.action == "list": + # Todo: Implement permission checks for encounters to return only consent's whose encounters the user has access to + pass + elif not AuthorizationController.call( + "can_view_clinical_data", self.request.user, self.get_patient_obj() + ): + if encounter := self.get_object().encounter: + encounter_obj = get_object_or_404( + Encounter, external_id=encounter.external_id ) - file = FileUpload.objects.create(attachment) - attachment_ids.append(file.external_id) - instance["source_attachment"] = attachment_ids - super().perform_create(instance) + if not AuthorizationController.call( + "can_view_encounter_obj", self.request.user, encounter_obj + ): + raise PermissionDenied("Permission denied to user") + else: + raise PermissionDenied("Permission denied to user") - class AttachmentAdditionSchema(BaseModel): - source_attachment: FileUploadCreateSpec + return super().get_queryset() + @action(detail=True, methods=["GET"]) + def get_attachments(self, request, *args, **kwargs): + instance = self.get_object() + attachments = [ + FileUploadRetrieveSpec.serialize( + FileUpload.objects.get(external_id=attachment) + ).to_json() + for attachment in instance.source_attachment or [] + ] + return Response(attachments) + + @extend_schema(request=ConsentFileUploadCreateSpec) @action(detail=True, methods=["POST"]) def add_attachment(self, request, *args, **kwargs): instance = self.get_object() - request_data = self.AttachmentAdditionSchema(**request.data) + request.data["associating_id"] = instance.external_id + file_obj = ConsentFileUploadCreateSpec(**request.data).de_serialize() file_authorizer( request.user, - request_data.source_attachment.file_type, - request_data.source_attachment.associating_id, + file_obj.file_type, + file_obj.associating_id, "write", ) - file = FileUpload.objects.create(request_data.source_attachment) - instance.source_attachment.append(file.external_id) + file_obj.created_by = self.request.user + file_obj.updated_by = self.request.user + file_obj.save() + instance.source_attachment.append(file_obj.external_id) instance.save(update_fields=["source_attachment"]) - return Response(ConsentRetrieveSpec.serialize(instance).to_json()) + serialized = ConsentRetrieveSpec.serialize(instance).to_json() + return Response(serialized) class AttachmentRemovalSchema(BaseModel): attachment_id: UUID4 + @extend_schema(request=AttachmentRemovalSchema) @action(detail=True, methods=["POST"]) def remove_attachment(self, request, *args, **kwargs): instance = self.get_object() @@ -79,4 +125,52 @@ def remove_attachment(self, request, *args, **kwargs): ) instance.source_attachment.remove(request_data.attachment_id) instance.save(update_fields=["source_attachment"]) - return Response(ConsentRetrieveSpec.serialize(instance).to_json()) + serialized = ConsentRetrieveSpec.serialize(instance).to_json() + return Response(serialized) + + @extend_schema(request=ConsentVerificationSpec) + @action(detail=True, methods=["POST"]) + def add_verification(self, request, *args, **kwargs): + instance = self.get_object() + request_data = ConsentVerificationSpec(**request.data) + request_data.verification.verified_by = self.request.user.external_id + + if request_data.verified_by in [ + verification.verified_by for verification in instance.verification_details + ]: + raise ValidationError("Consent is already verified by the user") + + request_data.verification.verification_date = now() + instance.verification_details.append(request_data.verification) + instance.save(update_fields=["verification_details"]) + serialized = ConsentRetrieveSpec.serialize(instance).to_json() + return Response(serialized) + + class VerificationRemovalSchema(BaseModel): + verified_by: UUID4 | None = None + + @extend_schema(request=VerificationRemovalSchema) + @action(detail=True, methods=["POST"]) + def remove_verification(self, request, *args, **kwargs): + instance = self.get_object() + request_data = self.VerificationRemovalSchema(**request.data) + + match = None + for verification in instance.verification_details: + if str(verification.get("verified_by")) == str(request_data.verified_by): + match = verification + break + + if not match: + raise ValidationError("Consent is not verified by the user") + + instance.verification_details.remove(match) + instance.save(update_fields=["verification_details"]) + + serialized = ConsentRetrieveSpec.serialize(instance).to_json() + return Response(serialized) + + @action(detail=True, methods=["GET"]) + def get_verification_details(self, request, *args, **kwargs): + instance = self.get_object() + return Response(instance.verification_details) diff --git a/care/emr/api/viewsets/file_upload.py b/care/emr/api/viewsets/file_upload.py index 73c2061c08..a5a95f9466 100644 --- a/care/emr/api/viewsets/file_upload.py +++ b/care/emr/api/viewsets/file_upload.py @@ -15,6 +15,7 @@ EMRUpdateMixin, ) from care.emr.models import Encounter, FileUpload, Patient +from care.emr.models.consent import Consent from care.emr.resources.file_upload.spec import ( FileTypeChoices, FileUploadCreateSpec, @@ -49,6 +50,18 @@ def file_authorizer(user, file_type, associating_id, permission): allowed = AuthorizationController.call( "can_update_encounter_obj", user, encounter_obj ) + elif file_type == FileTypeChoices.consent.value: + consent_obj = get_object_or_404(Consent, external_id=associating_id) + if permission == "read": + allowed = AuthorizationController.call( + "can_view_clinical_data", user, consent_obj.encounter.patient + ) or AuthorizationController.call( + "can_view_encounter_obj", user, consent_obj.encounter.patient + ) + elif permission == "write": + allowed = AuthorizationController.call( + "can_update_encounter_obj", user, consent_obj.encounter.patient + ) if not allowed: raise PermissionDenied("Cannot View File") diff --git a/care/emr/migrations/0020_alter_deviceservicehistory_serviced_on_consent.py b/care/emr/migrations/0020_alter_deviceservicehistory_serviced_on_consent.py new file mode 100644 index 0000000000..a47046f140 --- /dev/null +++ b/care/emr/migrations/0020_alter_deviceservicehistory_serviced_on_consent.py @@ -0,0 +1,48 @@ +# Generated by Django 5.1.4 on 2025-02-19 11:13 + +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', '0019_device_metadata'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='deviceservicehistory', + name='serviced_on', + field=models.DateTimeField(default=None, null=True), + ), + migrations.CreateModel( + name='Consent', + 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)), + ('status', models.CharField(max_length=50)), + ('category', models.JSONField(default=list)), + ('date', models.DateTimeField()), + ('period', models.JSONField(blank=True, null=True)), + ('decision', models.CharField(max_length=10)), + ('verification_details', models.JSONField(blank=True, null=True)), + ('source_attachment', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(), default=[], size=None)), + ('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)), + ('encounter', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='consents', to='emr.encounter')), + ('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/resources/base.py b/care/emr/resources/base.py index 427bc1d1e6..30868c6055 100644 --- a/care/emr/resources/base.py +++ b/care/emr/resources/base.py @@ -88,6 +88,7 @@ def de_serialize(self, obj=None): elif field not in self.__exclude__ and self.__store_metadata__: meta[field] = dump[field] obj.meta = meta + self.perform_extra_deserialization(is_update, obj) return obj diff --git a/care/emr/resources/consent/spec.py b/care/emr/resources/consent/spec.py index d07b8d8d7c..9e013fbbb8 100644 --- a/care/emr/resources/consent/spec.py +++ b/care/emr/resources/consent/spec.py @@ -1,15 +1,12 @@ from datetime import datetime from enum import Enum -from pydantic import UUID4, Field +from pydantic import UUID4, BaseModel, Field -from care.emr.models import FileUpload +from care.emr.models import Encounter, FileUpload from care.emr.models.consent import Consent from care.emr.resources.base import EMRResource, PeriodSpec from care.emr.resources.file_upload.spec import ( - FileCategoryChoices, - FileTypeChoices, - FileUploadCreateSpec, FileUploadListSpec, FileUploadRetrieveSpec, ) @@ -40,15 +37,16 @@ class CategoryChoice(str, Enum): treatment = "treatment" -class ConsentVerificationSpec(EMRResource): +class ConsentVerificationSpec(BaseModel): verified: bool - verified_by: UUID4 - verification_date: datetime + verified_by: UUID4 | None + verification_date: datetime | None verification_type: VerificationType class ConsentBaseSpec(EMRResource): __model__ = Consent + __exclude__ = ["encounter"] id: UUID4 | None = Field( default=None, description="Unique identifier for the consent record" @@ -63,21 +61,19 @@ class ConsentBaseSpec(EMRResource): class ConsentCreateSpec(ConsentBaseSpec): - source_attachment: list[FileUploadCreateSpec] | None = [] - def perform_extra_deserialization(self, is_update, obj): - if not is_update: - for attachment in self.source_attachment: - attachment.file_type = FileTypeChoices.consent - attachment.file_category = FileCategoryChoices.consent_attachment - # obj.source_attachment = [attachment.id for attachment in self.source_attachment] + obj.encounter = Encounter.objects.get(external_id=self.encounter) class ConsentUpdateSpec(ConsentBaseSpec): - pass + def perform_extra_deserialization(self, is_update, obj): + self.verification_details = obj.verification_details # Not updating this field + self.encounter = obj.encounter # Not updating this field class ConsentListSpec(ConsentBaseSpec): + source_attachment: list[dict] = [] + @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id @@ -87,15 +83,26 @@ def perform_extra_serialization(cls, mapping, obj): ).to_json() for attachment in obj.source_attachment or [] ] + mapping["encounter"] = obj.encounter.external_id + mapping["source_attachment"] = [ + FileUploadRetrieveSpec.serialize( + FileUpload.objects.get(external_id=attachment) + ).to_json() + for attachment in obj.source_attachment or [] + ] class ConsentRetrieveSpec(ConsentBaseSpec): + source_attachment: list[dict] = [] + @classmethod def perform_extra_serialization(cls, mapping, obj): mapping["id"] = obj.external_id + mapping["source_attachment"] = [ FileUploadRetrieveSpec.serialize( FileUpload.objects.get(external_id=attachment) ).to_json() for attachment in obj.source_attachment or [] ] + mapping["encounter"] = obj.encounter.external_id diff --git a/care/emr/resources/file_upload/spec.py b/care/emr/resources/file_upload/spec.py index 006f4cc1b9..f810039d6a 100644 --- a/care/emr/resources/file_upload/spec.py +++ b/care/emr/resources/file_upload/spec.py @@ -83,3 +83,15 @@ def perform_extra_serialization(cls, mapping, obj): if obj.updated_by: mapping["updated_by"] = UserSpec.serialize(obj.updated_by) + + +class ConsentFileUploadCreateSpec(FileUploadBaseSpec): + original_name: str + associating_id: UUID4 + + def perform_extra_deserialization(self, is_update, obj): + # Authz Performed in the request + obj._just_created = True # noqa SLF001 + obj.internal_name = self.original_name + obj.file_type = FileTypeChoices.consent + obj.file_category = FileCategoryChoices.consent_attachment diff --git a/care/security/authorization/__init__.py b/care/security/authorization/__init__.py index 08721a8635..931501756a 100644 --- a/care/security/authorization/__init__.py +++ b/care/security/authorization/__init__.py @@ -9,3 +9,4 @@ from .user_schedule import * # noqa from .facility_location import * # noqa from .device import * # noqa +from .consent import * # noqa diff --git a/config/api_router.py b/config/api_router.py index 0d0a7578e0..b801c1ae1b 100644 --- a/config/api_router.py +++ b/config/api_router.py @@ -13,6 +13,7 @@ DiagnosisViewSet, SymptomViewSet, ) +from care.emr.api.viewsets.consent import ConsentViewSet from care.emr.api.viewsets.device import ( DeviceEncounterHistoryViewSet, DeviceLocationHistoryViewSet, @@ -104,6 +105,8 @@ router.register("role", RoleViewSet, basename="role") +router.register("consent", ConsentViewSet, basename="consent") + router.register("encounter", EncounterViewSet, basename="encounter") organization_nested_router = NestedSimpleRouter(