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

Adding consent Viewset #2856

Draft
wants to merge 5 commits into
base: vigneshhari/devices
Choose a base branch
from
Draft
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
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
176 changes: 176 additions & 0 deletions care/emr/api/viewsets/consent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
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 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.security.authorization import AuthorizationController

logger = logging.getLogger(__name__)


class ConsentViewSet(EMRModelViewSet, EncounterBasedAuthorizationBase):
database_model = Consent
pydantic_model = ConsentCreateSpec
pydantic_read_model = ConsentListSpec
pydantic_update_model = ConsentUpdateSpec
pydantic_retrieve_model = ConsentRetrieveSpec

def get_patient_obj(self):
return self.get_object().encounter.patient

def authorize_read_encounter(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isnt this defined in the base?

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implement this. Look at any of the encounter related viewsets

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
)
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")

return super().get_queryset()

@action(detail=True, methods=["GET"])
def get_attachments(self, request, *args, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the retrieve API already gives all the attachments why the separate API ?

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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this different from just using the API for file uploads?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instance.source_attachment.append(file_obj.external_id)

here we are creating the file and also adding it to the source_attachment field of consent object

instance = self.get_object()
request.data["associating_id"] = instance.external_id
file_obj = ConsentFileUploadCreateSpec(**request.data).de_serialize()
file_authorizer(
request.user,
file_obj.file_type,
file_obj.associating_id,
"write",
)
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"])
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()
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"])
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)
3 changes: 3 additions & 0 deletions care/emr/api/viewsets/facility_organization.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from django.db.models import Q
from django_filters import rest_framework as filters
from rest_framework import filters as drf_filters
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.generics import get_object_or_404
Expand Down Expand Up @@ -155,6 +156,8 @@ class FacilityOrganizationUsersViewSet(EMRModelViewSet):
pydantic_model = FacilityOrganizationUserWriteSpec
pydantic_read_model = FacilityOrganizationUserReadSpec
pydantic_update_model = FacilityOrganizationUserUpdateSpec
filter_backends = [drf_filters.SearchFilter]
search_fields = ["user__first_name", "user__last_name", "user__username"]

def get_organization_obj(self):
return get_object_or_404(
Expand Down
13 changes: 13 additions & 0 deletions care/emr/api/viewsets/file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its repeating the same thing as above, can we reuse the logic ?

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")
Expand Down
1 change: 1 addition & 0 deletions care/emr/api/viewsets/organization.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def mine(self, request, *args, **kwargs):

class OrganizationUserFilter(filters.FilterSet):
username = filters.CharFilter(field_name="user__username", lookup_expr="icontains")
phone_number = filters.CharFilter(field_name="user__phone_number", lookup_expr="iexact")


class OrganizationUsersViewSet(EMRModelViewSet):
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
},
),
]
17 changes: 17 additions & 0 deletions care/emr/models/consent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.contrib.postgres.fields import ArrayField
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Category is a code right? can we make it as a single string?

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)
verification_details = models.JSONField(null=True, blank=True)
source_attachment = ArrayField(models.UUIDField(), default=[])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this here? or can we fetch from the other table?

14 changes: 13 additions & 1 deletion care/emr/resources/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -163,3 +164,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
Empty file.
Loading
Loading