CPS-API / api /routes /pdf.py
Ali2206's picture
Update api/routes/pdf.py
e55e0b4 verified
raw
history blame
10.5 kB
from fastapi import APIRouter, HTTPException, Depends, Response
from ...db.mongo import patients_collection
from ...core.security import get_current_user
from ...utils.helpers import calculate_age, escape_latex_special_chars, hyphenate_long_strings, format_timestamp
from datetime import datetime
from bson import ObjectId
from bson.errors import InvalidId
import os
import subprocess
from tempfile import TemporaryDirectory
from string import Template
import logging
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
)
logger = logging.getLogger(__name__)
router = APIRouter()
@router.get("/{patient_id}/pdf", response_class=Response)
async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
# Suppress logging for this route
logger.setLevel(logging.CRITICAL)
try:
if current_user.get('role') not in ['doctor', 'admin']:
raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
# Determine if patient_id is ObjectId or fhir_id
try:
obj_id = ObjectId(patient_id)
query = {"$or": [{"_id": obj_id}, {"fhir_id": patient_id}]}
except InvalidId:
query = {"fhir_id": patient_id}
patient = await patients_collection.find_one(query)
if not patient:
raise HTTPException(status_code=404, detail="Patient not found")
# Prepare table content
notes = patient.get("notes", [])
notes_content = ""
if notes:
notes_content = "\\toprule\n" + " \\\\\n".join(
"{} & {} & {}".format(
escape_latex_special_chars(hyphenate_long_strings(format_timestamp(n.get("date", "") or ""))),
escape_latex_special_chars(hyphenate_long_strings(n.get("type", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(n.get("text", "") or ""))
)
for n in notes
) + "\n\\bottomrule"
else:
notes_content = "\\multicolumn{3}{l}{No notes available}"
conditions = patient.get("conditions", [])
conditions_content = ""
if conditions:
conditions_content = "\\toprule\n" + " \\\\\n".join(
"{} & {} & {} & {} & {}".format(
escape_latex_special_chars(hyphenate_long_strings(c.get("id", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(c.get("code", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(c.get("status", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(format_timestamp(c.get("onset_date", "") or ""))),
escape_latex_special_chars(hyphenate_long_strings(c.get("verification_status", "") or ""))
)
for c in conditions
) + "\n\\bottomrule"
else:
conditions_content = "\\multicolumn{5}{l}{No conditions available}"
medications = patient.get("medications", [])
medications_content = ""
if medications:
medications_content = "\\toprule\n" + " \\\\\n".join(
"{} & {} & {} & {} & {}".format(
escape_latex_special_chars(hyphenate_long_strings(m.get("id", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(m.get("name", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(m.get("status", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(format_timestamp(m.get("prescribed_date", "") or ""))),
escape_latex_special_chars(hyphenate_long_strings(m.get("dosage", "") or ""))
)
for m in medications
) + "\n\\bottomrule"
else:
medications_content = "\\multicolumn{5}{l}{No medications available}"
encounters = patient.get("encounters", [])
encounters_content = ""
if encounters:
encounters_content = "\\toprule\n" + " \\\\\n".join(
"{} & {} & {} & {} & {}".format(
escape_latex_special_chars(hyphenate_long_strings(e.get("id", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(e.get("type", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(e.get("status", "") or "")),
escape_latex_special_chars(hyphenate_long_strings(format_timestamp(e.get("period", {}).get("start", "") or ""))),
escape_latex_special_chars(hyphenate_long_strings(e.get("service_provider", "") or ""))
)
for e in encounters
) + "\n\\bottomrule"
else:
encounters_content = "\\multicolumn{5}{l}{No encounters available}"
# Use Template for safe insertion
latex_template = Template(r"""
\documentclass[a4paper,12pt]{article}
\usepackage[utf8]{inputenc}
\usepackage[T1]{fontenc}
\usepackage{geometry}
\geometry{margin=1in}
\usepackage{booktabs,longtable,fancyhdr}
\usepackage{array}
\usepackage{microtype}
\microtypesetup{expansion=false} % Disable font expansion to avoid errors
\setlength{\headheight}{14.5pt} % Fix fancyhdr warning
\pagestyle{fancy}
\fancyhf{}
\fancyhead[L]{Patient Report}
\fancyhead[R]{Generated: \today}
\fancyfoot[C]{\thepage}
\begin{document}
\begin{center}
\Large\textbf{Patient Medical Report} \\
\vspace{0.2cm}
\textit{Generated on $generated_on}
\end{center}
\section*{Demographics}
\begin{itemize}
\item \textbf{FHIR ID:} $fhir_id
\item \textbf{Full Name:} $full_name
\item \textbf{Gender:} $gender
\item \textbf{Date of Birth:} $dob
\item \textbf{Age:} $age
\item \textbf{Address:} $address
\item \textbf{Marital Status:} $marital_status
\item \textbf{Language:} $language
\end{itemize}
\section*{Clinical Notes}
\begin{longtable}{>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{6.5cm}}
\textbf{Date} & \textbf{Type} & \textbf{Text} \\
\endhead
$notes
\end{longtable}
\section*{Conditions}
\begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
\textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
\endhead
$conditions
\end{longtable}
\section*{Medications}
\begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
\textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
\endhead
$medications
\end{longtable}
\section*{Encounters}
\begin{longtable}{>{\raggedright\arraybackslash}p{2.5cm}>{\raggedright\arraybackslash}p{4.5cm}>{\raggedright\arraybackslash}p{2.5cm}>{\raggedright\arraybackslash}p{4.5cm}>{\raggedright\arraybackslash}p{3.5cm}}
\textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
\endhead
$encounters
\end{longtable}
\end{document}
""")
# Set the generated_on date to 05:35 PM CET, May 16, 2025
generated_on = datetime.strptime("2025-05-16 17:35:00+01:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p")
latex_filled = latex_template.substitute(
generated_on=generated_on,
fhir_id=escape_latex_special_chars(hyphenate_long_strings(patient.get("fhir_id", "") or "")),
full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
gender=escape_latex_special_chars(patient.get("gender", "") or ""),
dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
age=escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
address=escape_latex_special_chars(", ".join(filter(None, [
patient.get("address", ""),
patient.get("city", ""),
patient.get("state", ""),
patient.get("postal_code", ""),
patient.get("country", "")
]))),
marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
language=escape_latex_special_chars(patient.get("language", "") or ""),
notes=notes_content,
conditions=conditions_content,
medications=medications_content,
encounters=encounters_content
)
# Compile LaTeX in a temporary directory
with TemporaryDirectory() as tmpdir:
tex_path = os.path.join(tmpdir, "report.tex")
pdf_path = os.path.join(tmpdir, "report.pdf")
with open(tex_path, "w", encoding="utf-8") as f:
f.write(latex_filled)
try:
subprocess.run(
["latexmk", "-pdf", "-interaction=nonstopmode", tex_path],
cwd=tmpdir,
check=True,
capture_output=True,
text=True
)
except subprocess.CalledProcessError as e:
raise HTTPException(
status_code=500,
detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
)
if not os.path.exists(pdf_path):
raise HTTPException(
status_code=500,
detail="PDF file was not generated"
)
with open(pdf_path, "rb") as f:
pdf_bytes = f.read()
response = Response(
content=pdf_bytes,
media_type="application/pdf",
headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_report.pdf"}
)
return response
except HTTPException as http_error:
raise http_error
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Unexpected error generating PDF: {str(e)}"
)
finally:
# Restore the logger level for other routes
logger.setLevel(logging.INFO)