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

PDF Generation Alternative - GSoC #2132

Merged
merged 48 commits into from
Aug 23, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
1943e30
updated docker file
DraKen0009 May 6, 2024
b35f1db
added test for testing Typst installations
DraKen0009 May 6, 2024
a1d0f6a
specified typst version
DraKen0009 May 7, 2024
46348ac
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 May 13, 2024
0bc5076
updated helper functions and tests for it
DraKen0009 May 21, 2024
20a9c79
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Jun 4, 2024
630b930
Merge branch 'refs/heads/master' into pdf-generation-gsoc
DraKen0009 Jun 9, 2024
6bc8d1c
updated helper functions and added typst template
DraKen0009 Jun 10, 2024
99adc8b
integrated typst with django template
DraKen0009 Jul 1, 2024
0782258
deleted example.typ
DraKen0009 Jul 1, 2024
5d5ca46
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Jul 1, 2024
56b68fb
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Jul 9, 2024
1b733cf
updated prod docker and removed old dependencies
DraKen0009 Jul 10, 2024
2bd0889
fixed prod.Dockerfile
DraKen0009 Jul 12, 2024
f4f1746
Merge branch 'refs/heads/master' into pdf-generation-gsoc
DraKen0009 Jul 20, 2024
81b3513
added test utils
DraKen0009 Jul 21, 2024
aff72b8
added test for generated pdf
DraKen0009 Jul 21, 2024
f74cb0a
removed print statements
DraKen0009 Jul 22, 2024
eeba958
updated template and prescription formatting
DraKen0009 Jul 26, 2024
9eda9f3
added test data
DraKen0009 Jul 29, 2024
4d0713c
improved prescription tag
DraKen0009 Jul 29, 2024
cf4e670
updated test images for the prev update
DraKen0009 Jul 29, 2024
16e4d66
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Jul 29, 2024
3967672
fixed created_on date in template
DraKen0009 Jul 30, 2024
9e0e105
completed the todos mentioned
DraKen0009 Aug 5, 2024
01a65d1
updated discharge notes field name
DraKen0009 Aug 5, 2024
c780bff
Merge branch 'develop' into pdf-generation-gsoc
sainak Aug 6, 2024
f0705cb
updated sample png files
DraKen0009 Aug 6, 2024
3a7dbb1
updated test for diagnosis
DraKen0009 Aug 6, 2024
cfd181d
fixed the test failing issue
DraKen0009 Aug 8, 2024
417d716
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Aug 8, 2024
e92985c
fixed the fetch_icd11_data by ids function
DraKen0009 Aug 8, 2024
459f983
updated sample pngs
DraKen0009 Aug 12, 2024
4eb27a7
added data formatting tags
DraKen0009 Aug 15, 2024
1b8927e
removed print statement
DraKen0009 Aug 15, 2024
d3d14c5
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Aug 16, 2024
fd31406
updated sample png files
DraKen0009 Aug 16, 2024
5104868
Merge remote-tracking branch 'origin/pdf-generation-gsoc' into pdf-ge…
DraKen0009 Aug 16, 2024
f88a376
improved formatting and updated tests
DraKen0009 Aug 16, 2024
2347619
show age with days if patient less than 1 year old
sainak Aug 19, 2024
edb9bfc
fixed date formatting
DraKen0009 Aug 19, 2024
32729b3
Merge branch 'develop' into pdf-generation-gsoc
sainak Aug 19, 2024
c92d365
updated the docker files
DraKen0009 Aug 23, 2024
15df057
Merge branch 'refs/heads/master' into pdf-generation-gsoc
DraKen0009 Aug 23, 2024
e78db68
relock
sainak Aug 23, 2024
53ac47a
Merge branch 'develop' into pdf-generation-gsoc
DraKen0009 Aug 23, 2024
d36a29d
improved docker files
DraKen0009 Aug 23, 2024
fe02b4f
Merge branch 'develop' into pdf-generation-gsoc
sainak Aug 23, 2024
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
1 change: 0 additions & 1 deletion Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ django = "==4.2.10"
django-environ = "==0.11.2"
django-cors-headers = "==4.3.1"
django-filter = "==24.2"
django-hardcopy = "==0.1.4"
django-maintenance-mode = "==0.21.1"
django-model-utils = "==4.5.1"
django-multiselectfield = "==0.1.12"
Expand Down
469 changes: 230 additions & 239 deletions Pipfile.lock

Large diffs are not rendered by default.

19 changes: 17 additions & 2 deletions care/facility/api/viewsets/patient_consultation.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import tempfile

from django.db.models import Prefetch
from django.db.models.query_utils import Q
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.utils import timezone
from django_filters import rest_framework as filters
from drf_spectacular.utils import extend_schema
from dry_rest_permissions.generics import DRYPermissions
Expand Down Expand Up @@ -291,7 +295,18 @@ def dev_preview_discharge_summary(request, consultation_id):
if not consultation:
raise NotFound({"detail": "Consultation not found"})
data = discharge_summary.get_discharge_summary_data(consultation)
return render(request, "reports/patient_discharge_summary_pdf.html", data)
data["date"] = timezone.now()

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

with open(tmp_file.name, "rb") as pdf_file:
pdf_content = pdf_file.read()

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

return response


class PatientConsentViewSet(
Expand Down
27 changes: 24 additions & 3 deletions care/facility/models/patient.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import enum
from datetime import date

from dateutil.relativedelta import relativedelta
from django.contrib.postgres.aggregates import ArrayAgg
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Case, F, Func, JSONField, Value, When
from django.db.models.functions import Coalesce, Now
from django.template.defaultfilters import pluralize
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from simple_history.models import HistoricalRecords
Expand Down Expand Up @@ -481,10 +483,29 @@ def save(self, *args, **kwargs) -> None:
self._alias_recovery_to_recovered()
super().save(*args, **kwargs)

def get_age(self) -> int:
start = self.date_of_birth or timezone.datetime(self.year_of_birth, 1, 1).date()
def get_age(self) -> str:
start = self.date_of_birth or date(self.year_of_birth, 1, 1)
end = (self.death_datetime or timezone.now()).date()
return relativedelta(end, start).years

delta = relativedelta(end, start)

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

elif 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}"

elif delta.days > 0:
day_str = f"{delta.days} day{pluralize(delta.days)}"
return day_str

else:
return "0 days"

def annotate_diagnosis_ids(*args, **kwargs):
return ArrayAgg(
Expand Down
35 changes: 35 additions & 0 deletions care/facility/templatetags/data_formatting_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django import template

register = template.Library()


@register.filter(name="format_empty_data")
def format_empty_data(data):
if data is None or data == "" or data == 0.0 or data == []:
return "N/A"

return data


@register.filter(name="format_to_sentence_case")
def format_to_sentence_case(data):
if data is None:
return

def convert_to_sentence_case(s):
if s == "ICU":
return "ICU"
s = s.lower()
s = s.replace("_", " ")
return s.capitalize()

if isinstance(data, str):
items = data.split(", ")
converted_items = [convert_to_sentence_case(item) for item in items]
return ", ".join(converted_items)

elif isinstance(data, (list, tuple)):
converted_items = [convert_to_sentence_case(item) for item in data]
return ", ".join(converted_items)

return data
13 changes: 13 additions & 0 deletions care/facility/templatetags/prescription_tags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from django import template

register = template.Library()


@register.filter(name="format_prescription")
def format_prescription(prescription):
if prescription.dosage_type == "TITRATED":
return f"{prescription.medicine_name}, titration from {prescription.base_dosage} to {prescription.target_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days."
if prescription.dosage_type == "PRN":
return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}"
else:
return f"{prescription.medicine_name}, {prescription.base_dosage}, {prescription.route}, {prescription.frequency} for {prescription.days} days."
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
230 changes: 230 additions & 0 deletions care/facility/tests/test_pdf_generation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import os
import subprocess
import tempfile
from datetime import date
from pathlib import Path

from django.conf import settings
from django.template.loader import render_to_string
from django.test import TestCase
from PIL import Image
from rest_framework.test import APIClient

from care.facility.models import (
ConditionVerificationStatus,
ICD11Diagnosis,
PrescriptionDosageType,
PrescriptionType,
)
from care.facility.utils.reports import discharge_summary
from care.facility.utils.reports.discharge_summary import compile_typ
from care.utils.tests.test_utils import TestUtils


def compare_pngs(png_path1, png_path2):
with Image.open(png_path1) as img1, Image.open(png_path2) as img2:
if img1.mode != img2.mode:
return False

if img1.size != img2.size:
return False

img1_data = list(img1.getdata())
img2_data = list(img2.getdata())

if img1_data == img2_data:
return True
else:
return False


def test_compile_typ(data):
sample_file_path = os.path.join(
os.getcwd(), "care", "facility", "tests", "sample_reports", "sample{n}.png"
)
test_output_file_path = os.path.join(
os.getcwd(), "care", "facility", "tests", "sample_reports", "test_output{n}.png"
)
try:
logo_path = (
Path(settings.BASE_DIR)
/ "staticfiles"
/ "images"
/ "logos"
/ "black-logo.svg"
)
data["logo_path"] = str(logo_path)
content = render_to_string(
"reports/patient_discharge_summary_pdf_template.typ", context=data
)
subprocess.run(
["typst", "compile", "-", test_output_file_path, "--format", "png"],
input=content.encode("utf-8"),
capture_output=True,
check=True,
cwd="/",
)

number_of_pngs_generated = 2
# To be updated only if the number of sample png increase in future

for i in range(1, number_of_pngs_generated + 1):
current_sample_file_path = sample_file_path
current_sample_file_path = str(current_sample_file_path).replace(
"{n}", str(i)
)

current_test_output_file_path = test_output_file_path
current_test_output_file_path = str(current_test_output_file_path).replace(
"{n}", str(i)
)

if not compare_pngs(
Path(current_sample_file_path), Path(current_test_output_file_path)
):
return False
return True
except Exception:
return False
finally:
count = 1
while True:
current_test_output_file_path = test_output_file_path
current_test_output_file_path = current_test_output_file_path.replace(
"{n}", str(count)
)
if Path(current_test_output_file_path).exists():
os.remove(Path(current_test_output_file_path))
else:
break
count += 1


class TestTypstInstallation(TestCase):
def test_typst_installed(self):
try:
subprocess.run(["typst", "--version"], check=True)
typst_installed = True
except subprocess.CalledProcessError:
typst_installed = False

self.assertTrue(typst_installed, "Typst is not installed or not accessible")


class TestGenerateDischargeSummaryPDF(TestCase, TestUtils):
@classmethod
def setUpTestData(cls) -> None:
cls.state = cls.create_state(name="sample_state")
cls.district = cls.create_district(cls.state, name="sample_district")
cls.local_body = cls.create_local_body(cls.district, name="sample_local_body")
cls.super_user = cls.create_super_user("su", cls.district)
cls.facility = cls.create_facility(
cls.super_user, cls.district, cls.local_body, name="_Sample_Facility"
)
cls.user = cls.create_user("staff1", cls.district, home_facility=cls.facility)
cls.treating_physician = cls.create_user(
"test Doctor",
cls.district,
home_facility=cls.facility,
first_name="Doctor",
last_name="Tester",
user_type=15,
)
cls.patient = cls.create_patient(
cls.district, cls.facility, local_body=cls.local_body
)
cls.consultation = cls.create_consultation(
cls.patient,
cls.facility,
patient_no="123456",
doctor=cls.treating_physician,
height=178,
weight=80,
suggestion="A",
)
cls.create_patient_sample(cls.patient, cls.consultation, cls.facility, cls.user)
cls.create_policy(patient=cls.patient, user=cls.user)
cls.create_encounter_symptom(cls.consultation, cls.user)
cls.patient_investigation_group = cls.create_patient_investigation_group()
cls.patient_investigation = cls.create_patient_investigation(
cls.patient_investigation_group
)
cls.patient_investigation_session = cls.create_patient_investigation_session(
cls.user
)
cls.create_investigation_value(
cls.patient_investigation,
cls.consultation,
cls.patient_investigation_session,
cls.patient_investigation_group,
)
cls.create_disease(cls.patient)
cls.create_prescription(cls.consultation, cls.user)
cls.create_prescription(
cls.consultation, cls.user, dosage_type=PrescriptionDosageType.TITRATED
)
cls.create_prescription(
cls.consultation, cls.user, dosage_type=PrescriptionDosageType.PRN
)
cls.create_prescription(
cls.consultation, cls.user, prescription_type=PrescriptionType.DISCHARGE
)
cls.create_prescription(
cls.consultation,
cls.user,
prescription_type=PrescriptionType.DISCHARGE,
dosage_type=PrescriptionDosageType.TITRATED,
)
cls.create_prescription(
cls.consultation,
cls.user,
prescription_type=PrescriptionType.DISCHARGE,
dosage_type=PrescriptionDosageType.PRN,
)
cls.create_consultation_diagnosis(
cls.consultation,
ICD11Diagnosis.objects.filter(
label="SG31 Conception vessel pattern (TM1)"
).first(),
verification_status=ConditionVerificationStatus.CONFIRMED,
)
cls.create_consultation_diagnosis(
cls.consultation,
ICD11Diagnosis.objects.filter(
label="SG2B Liver meridian pattern (TM1)"
).first(),
verification_status=ConditionVerificationStatus.DIFFERENTIAL,
)
cls.create_consultation_diagnosis(
cls.consultation,
ICD11Diagnosis.objects.filter(
label="SG29 Triple energizer meridian pattern (TM1)"
).first(),
verification_status=ConditionVerificationStatus.PROVISIONAL,
)
cls.create_consultation_diagnosis(
cls.consultation,
ICD11Diagnosis.objects.filter(
label="SG60 Early yang stage pattern (TM1)"
).first(),
verification_status=ConditionVerificationStatus.UNCONFIRMED,
)

def setUp(self) -> None:
self.client = APIClient()

def test_pdf_generation_success(self):
test_data = {"consultation": self.consultation}

with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as file:
compile_typ(file.name, test_data)

self.assertTrue(os.path.exists(file.name))
self.assertGreater(os.path.getsize(file.name), 0)

def test_pdf_generation(self):
data = discharge_summary.get_discharge_summary_data(self.consultation)
data["date"] = date(2020, 1, 1)

# This sorting is test's specific and done in order to keep the values in order
self.assertTrue(test_compile_typ(data))
Loading