Skip to content

Commit

Permalink
Merge pull request #2700 from ohcnetwork/sainak/discharge-summary
Browse files Browse the repository at this point in the history
Add discharge summary for encounters
  • Loading branch information
vigneshhari authored Jan 3, 2025
2 parents a46919e + b66dd84 commit ffbbf3f
Show file tree
Hide file tree
Showing 25 changed files with 774 additions and 854 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ load-db:
docker compose exec db sh -c "pg_restore -U postgres --clean --if-exists -d care /tmp/care_db.dump"

reset-db:
docker compose exec backend bash -c "python manage.py reset_db --noinput"
docker compose exec db sh -c "dropdb -U postgres care -f"
docker compose exec db sh -c "createdb -U postgres care"

ruff-all:
ruff check .
Expand Down
156 changes: 155 additions & 1 deletion care/emr/api/viewsets/encounter.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import tempfile

from django.core.validators import validate_email as django_validate_email
from django.db import transaction
from django.http import HttpResponse
from django.utils import timezone
from django_filters import rest_framework as filters
from pydantic import UUID4, BaseModel
from drf_spectacular.utils import extend_schema
from pydantic import UUID4, BaseModel, field_validator
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied, ValidationError
from rest_framework.generics import get_object_or_404
Expand All @@ -17,8 +24,10 @@
Encounter,
EncounterOrganization,
FacilityOrganization,
FileUpload,
Patient,
)
from care.emr.reports import discharge_summary
from care.emr.resources.encounter.constants import COMPLETED_CHOICES
from care.emr.resources.encounter.spec import (
EncounterCreateSpec,
Expand All @@ -27,6 +36,18 @@
EncounterUpdateSpec,
)
from care.emr.resources.facility_organization.spec import FacilityOrganizationReadSpec
from care.emr.resources.file_upload.spec import (
FileCategoryChoices,
FileTypeChoices,
FileUploadRetrieveSpec,
)
from care.emr.tasks.discharge_summary import (
email_discharge_summary_task,
generate_discharge_summary_task,
)
from care.facility.api.serializers.patient_consultation import (
EmailDischargeSummarySerializer,
)
from care.facility.models import Facility
from care.security.authorization import AuthorizationController

Expand Down Expand Up @@ -196,3 +217,136 @@ def organizations_remove(self, request, *args, **kwargs):
encounter=instance, organization=organization
).delete()
return Response({}, status=204)

def _check_discharge_summary_access(self, encounter):
if not AuthorizationController.call(
"can_view_clinical_data", self.request.user, encounter.patient
):
raise PermissionDenied("Permission denied to user")

def _generate_discharge_summary(self, encounter_ext_id: str):
if current_progress := discharge_summary.get_progress(encounter_ext_id):
return Response(
{
"detail": (
"Discharge Summary is already being generated, "
f"current progress {current_progress}%"
)
},
status=status.HTTP_406_NOT_ACCEPTABLE,
)
discharge_summary.set_lock(encounter_ext_id, 1)
generate_discharge_summary_task.delay(encounter_ext_id)
return Response(
{"detail": "Discharge Summary will be generated shortly"},
status=status.HTTP_202_ACCEPTED,
)

@extend_schema(
description="Generate a discharge summary",
responses={
200: "Success",
},
tags=["encounter"],
)
@action(detail=True, methods=["POST"])
def generate_discharge_summary(self, request, *args, **kwargs):
encounter = self.get_object()
self._check_discharge_summary_access(encounter)
return self._generate_discharge_summary(encounter.external_id)

@extend_schema(
description="Get the discharge summary",
responses={200: "Success"},
tags=["encounter"],
)
@action(detail=True, methods=["GET"])
def preview_discharge_summary(self, request, *args, **kwargs):
encounter = self.get_object()
self._check_discharge_summary_access(encounter)
summary_file = (
FileUpload.objects.filter(
file_type=FileTypeChoices.encounter.value,
file_category=FileCategoryChoices.discharge_summary.value,
associating_id=encounter.external_id,
upload_completed=True,
)
.order_by("id")
.last()
)
if summary_file:
return Response(FileUploadRetrieveSpec.serialize(summary_file).to_json())
return self._generate_discharge_summary(encounter.external_id)

class EmailDischargeSummarySpec(BaseModel):
email: str

@field_validator("email")
@classmethod
def validate_email(cls, value):
django_validate_email(value)
return value

@extend_schema(
description="Email the discharge summary to the user",
request=EmailDischargeSummarySerializer,
responses={200: "Success"},
tags=["encounter"],
)
@action(detail=True, methods=["POST"])
def email_discharge_summary(self, request, *args, **kwargs):
encounter = self.get_object()
self._check_discharge_summary_access(encounter)
encounter_ext_id = encounter.external_id
if existing_progress := discharge_summary.get_progress(encounter_ext_id):
return Response(
{
"detail": (
"Discharge Summary is already being generated, "
f"current progress {existing_progress}%"
)
},
status=status.HTTP_406_NOT_ACCEPTABLE,
)

request_data = self.EmailDischargeSummarySpec(**request.data)
email = request_data.email
summary_file = (
FileUpload.objects.filter(
file_type=FileTypeChoices.encounter.value,
file_category=FileCategoryChoices.discharge_summary.value,
associating_id=encounter_ext_id,
upload_completed=True,
)
.order_by("id")
.last()
)
if not summary_file:
(
generate_discharge_summary_task.s(encounter_ext_id)
| email_discharge_summary_task.s(emails=[email])
).delay()
else:
email_discharge_summary_task.delay(summary_file.id, [email])
return Response(
{"detail": "Discharge Summary will be emailed shortly"},
status=status.HTTP_202_ACCEPTED,
)


def dev_preview_discharge_summary(request, encounter_id):
"""
This is a dev only view to preview the discharge summary template
"""
encounter = get_object_or_404(Encounter, external_id=encounter_id)
data = discharge_summary.get_discharge_summary_data(encounter)
data["date"] = timezone.now()

with tempfile.NamedTemporaryFile(delete=False, suffix=".pdf") as tmp_file:
discharge_summary.generate_discharge_summary_pdf(data, tmp_file)
tmp_file.seek(0)

response = HttpResponse(tmp_file, content_type="application/pdf")
response["Content-Disposition"] = 'inline; filename="discharge_summary.pdf"'

return response
10 changes: 5 additions & 5 deletions care/emr/models/file_upload.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ class FileUpload(EMRBaseModel):
files_manager = S3FilesManager(BucketType.PATIENT)

def get_extension(self):
# TODO: improve this logic to handle files with multiple extensions
parts = self.internal_name.split(".")
return f".{parts[-1]}" if len(parts) > 1 else ""

Expand All @@ -40,11 +41,10 @@ def save(self, *args, **kwargs):
Create a random internal name to internally manage the file
This is used as an intermediate step to avoid leakage of PII in-case of data leak
"""
if "force_insert" in kwargs or (not self.internal_name) or not self.id:
skip_internal_name = kwargs.pop("skip_internal_name", False)
if (not self.internal_name or not self.id) and not skip_internal_name:
internal_name = str(uuid4()) + str(int(time.time()))
if self.internal_name:
parts = self.internal_name.split(".")
if len(parts) > 1:
internal_name = f"{internal_name}.{parts[-1]}"
if self.internal_name and (extension := self.get_extension()):
internal_name = f"{internal_name}{extension}"
self.internal_name = internal_name
return super().save(*args, **kwargs)
25 changes: 25 additions & 0 deletions care/emr/models/patient.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
from dateutil.relativedelta import relativedelta
from django.contrib.postgres.fields import ArrayField
from django.core.validators import MinValueValidator
from django.db import models
from django.template.defaultfilters import pluralize
from django.utils import timezone

from care.emr.models import EMRBaseModel
from care.users.models import User
Expand Down Expand Up @@ -39,6 +42,28 @@ class Patient(EMRBaseModel):

users_cache = ArrayField(models.IntegerField(), default=list)

def get_age(self) -> str:
start = self.date_of_birth or timezone.date(self.year_of_birth, 1, 1)
end = (self.deceased_datetime or timezone.now()).date()

delta = relativedelta(end, start)

if delta.years > 0:
year_str = f"{delta.years} year{pluralize(delta.years)}"
return f"{year_str}"

if delta.months > 0:
month_str = f"{delta.months} month{pluralize(delta.months)}"
day_str = (
f" {delta.days} day{pluralize(delta.days)}" if delta.days > 0 else ""
)
return f"{month_str}{day_str}"

if delta.days > 0:
return f"{delta.days} day{pluralize(delta.days)}"

return "0 days"

def rebuild_organization_cache(self):
organization_parents = []
if self.geo_organization:
Expand Down
File renamed without changes.
Loading

0 comments on commit ffbbf3f

Please sign in to comment.