-
Notifications
You must be signed in to change notification settings - Fork 347
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
base: vigneshhari/devices
Are you sure you want to change the base?
Adding consent Viewset #2856
Changes from all commits
d815de4
b17a490
42b2f2e
99aba1e
a90a31b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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): | ||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this different from just using the API for file uploads? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
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, | ||
}, | ||
), | ||
] |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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=[]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
There was a problem hiding this comment.
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?