-
Notifications
You must be signed in to change notification settings - Fork 330
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PDF Generation Alternative - GSoC (#2132)
* updated docker file * added test for testing Typst installations * specified typst version * updated helper functions and tests for it * updated helper functions and added typst template * integrated typst with django template * deleted example.typ * updated prod docker and removed old dependencies * fixed prod.Dockerfile * added test utils * added test for generated pdf * removed print statements * updated template and prescription formatting * added test data * improved prescription tag * updated test images for the prev update * fixed created_on date in template * completed the todos mentioned * updated discharge notes field name * updated sample png files * updated test for diagnosis * fixed the test failing issue * fixed the fetch_icd11_data by ids function * updated sample pngs * added data formatting tags * removed print statement * updated sample png files * improved formatting and updated tests * show age with days if patient less than 1 year old * fixed date formatting * updated the docker files * relock * improved docker files --------- Co-authored-by: Aakash Singh <[email protected]>
- Loading branch information
1 parent
8cd1032
commit 45b51a8
Showing
15 changed files
with
1,139 additions
and
1,133 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) |
Oops, something went wrong.