Ali2206 commited on
Commit
e55e0b4
·
verified ·
1 Parent(s): 76a9f83

Update api/routes/pdf.py

Browse files
Files changed (1) hide show
  1. api/routes/pdf.py +247 -0
api/routes/pdf.py CHANGED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, HTTPException, Depends, Response
2
+ from ...db.mongo import patients_collection
3
+ from ...core.security import get_current_user
4
+ from ...utils.helpers import calculate_age, escape_latex_special_chars, hyphenate_long_strings, format_timestamp
5
+ from datetime import datetime
6
+ from bson import ObjectId
7
+ from bson.errors import InvalidId
8
+ import os
9
+ import subprocess
10
+ from tempfile import TemporaryDirectory
11
+ from string import Template
12
+ import logging
13
+
14
+ # Configure logging
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format='%(asctime)s - %(levelname)s - %(name)s - %(message)s'
18
+ )
19
+ logger = logging.getLogger(__name__)
20
+
21
+ router = APIRouter()
22
+
23
+ @router.get("/{patient_id}/pdf", response_class=Response)
24
+ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
25
+ # Suppress logging for this route
26
+ logger.setLevel(logging.CRITICAL)
27
+
28
+ try:
29
+ if current_user.get('role') not in ['doctor', 'admin']:
30
+ raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
31
+
32
+ # Determine if patient_id is ObjectId or fhir_id
33
+ try:
34
+ obj_id = ObjectId(patient_id)
35
+ query = {"$or": [{"_id": obj_id}, {"fhir_id": patient_id}]}
36
+ except InvalidId:
37
+ query = {"fhir_id": patient_id}
38
+
39
+ patient = await patients_collection.find_one(query)
40
+ if not patient:
41
+ raise HTTPException(status_code=404, detail="Patient not found")
42
+
43
+ # Prepare table content
44
+ notes = patient.get("notes", [])
45
+ notes_content = ""
46
+ if notes:
47
+ notes_content = "\\toprule\n" + " \\\\\n".join(
48
+ "{} & {} & {}".format(
49
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(n.get("date", "") or ""))),
50
+ escape_latex_special_chars(hyphenate_long_strings(n.get("type", "") or "")),
51
+ escape_latex_special_chars(hyphenate_long_strings(n.get("text", "") or ""))
52
+ )
53
+ for n in notes
54
+ ) + "\n\\bottomrule"
55
+ else:
56
+ notes_content = "\\multicolumn{3}{l}{No notes available}"
57
+
58
+ conditions = patient.get("conditions", [])
59
+ conditions_content = ""
60
+ if conditions:
61
+ conditions_content = "\\toprule\n" + " \\\\\n".join(
62
+ "{} & {} & {} & {} & {}".format(
63
+ escape_latex_special_chars(hyphenate_long_strings(c.get("id", "") or "")),
64
+ escape_latex_special_chars(hyphenate_long_strings(c.get("code", "") or "")),
65
+ escape_latex_special_chars(hyphenate_long_strings(c.get("status", "") or "")),
66
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(c.get("onset_date", "") or ""))),
67
+ escape_latex_special_chars(hyphenate_long_strings(c.get("verification_status", "") or ""))
68
+ )
69
+ for c in conditions
70
+ ) + "\n\\bottomrule"
71
+ else:
72
+ conditions_content = "\\multicolumn{5}{l}{No conditions available}"
73
+
74
+ medications = patient.get("medications", [])
75
+ medications_content = ""
76
+ if medications:
77
+ medications_content = "\\toprule\n" + " \\\\\n".join(
78
+ "{} & {} & {} & {} & {}".format(
79
+ escape_latex_special_chars(hyphenate_long_strings(m.get("id", "") or "")),
80
+ escape_latex_special_chars(hyphenate_long_strings(m.get("name", "") or "")),
81
+ escape_latex_special_chars(hyphenate_long_strings(m.get("status", "") or "")),
82
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(m.get("prescribed_date", "") or ""))),
83
+ escape_latex_special_chars(hyphenate_long_strings(m.get("dosage", "") or ""))
84
+ )
85
+ for m in medications
86
+ ) + "\n\\bottomrule"
87
+ else:
88
+ medications_content = "\\multicolumn{5}{l}{No medications available}"
89
+
90
+ encounters = patient.get("encounters", [])
91
+ encounters_content = ""
92
+ if encounters:
93
+ encounters_content = "\\toprule\n" + " \\\\\n".join(
94
+ "{} & {} & {} & {} & {}".format(
95
+ escape_latex_special_chars(hyphenate_long_strings(e.get("id", "") or "")),
96
+ escape_latex_special_chars(hyphenate_long_strings(e.get("type", "") or "")),
97
+ escape_latex_special_chars(hyphenate_long_strings(e.get("status", "") or "")),
98
+ escape_latex_special_chars(hyphenate_long_strings(format_timestamp(e.get("period", {}).get("start", "") or ""))),
99
+ escape_latex_special_chars(hyphenate_long_strings(e.get("service_provider", "") or ""))
100
+ )
101
+ for e in encounters
102
+ ) + "\n\\bottomrule"
103
+ else:
104
+ encounters_content = "\\multicolumn{5}{l}{No encounters available}"
105
+
106
+ # Use Template for safe insertion
107
+ latex_template = Template(r"""
108
+ \documentclass[a4paper,12pt]{article}
109
+ \usepackage[utf8]{inputenc}
110
+ \usepackage[T1]{fontenc}
111
+ \usepackage{geometry}
112
+ \geometry{margin=1in}
113
+ \usepackage{booktabs,longtable,fancyhdr}
114
+ \usepackage{array}
115
+ \usepackage{microtype}
116
+ \microtypesetup{expansion=false} % Disable font expansion to avoid errors
117
+ \setlength{\headheight}{14.5pt} % Fix fancyhdr warning
118
+ \pagestyle{fancy}
119
+ \fancyhf{}
120
+ \fancyhead[L]{Patient Report}
121
+ \fancyhead[R]{Generated: \today}
122
+ \fancyfoot[C]{\thepage}
123
+
124
+ \begin{document}
125
+
126
+ \begin{center}
127
+ \Large\textbf{Patient Medical Report} \\
128
+ \vspace{0.2cm}
129
+ \textit{Generated on $generated_on}
130
+ \end{center}
131
+
132
+ \section*{Demographics}
133
+ \begin{itemize}
134
+ \item \textbf{FHIR ID:} $fhir_id
135
+ \item \textbf{Full Name:} $full_name
136
+ \item \textbf{Gender:} $gender
137
+ \item \textbf{Date of Birth:} $dob
138
+ \item \textbf{Age:} $age
139
+ \item \textbf{Address:} $address
140
+ \item \textbf{Marital Status:} $marital_status
141
+ \item \textbf{Language:} $language
142
+ \end{itemize}
143
+
144
+ \section*{Clinical Notes}
145
+ \begin{longtable}{>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{6.5cm}}
146
+ \textbf{Date} & \textbf{Type} & \textbf{Text} \\
147
+ \endhead
148
+ $notes
149
+ \end{longtable}
150
+
151
+ \section*{Conditions}
152
+ \begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
153
+ \textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
154
+ \endhead
155
+ $conditions
156
+ \end{longtable}
157
+
158
+ \section*{Medications}
159
+ \begin{longtable}{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
160
+ \textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
161
+ \endhead
162
+ $medications
163
+ \end{longtable}
164
+
165
+ \section*{Encounters}
166
+ \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}}
167
+ \textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
168
+ \endhead
169
+ $encounters
170
+ \end{longtable}
171
+
172
+ \end{document}
173
+ """)
174
+
175
+ # Set the generated_on date to 05:35 PM CET, May 16, 2025
176
+ 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")
177
+
178
+ latex_filled = latex_template.substitute(
179
+ generated_on=generated_on,
180
+ fhir_id=escape_latex_special_chars(hyphenate_long_strings(patient.get("fhir_id", "") or "")),
181
+ full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
182
+ gender=escape_latex_special_chars(patient.get("gender", "") or ""),
183
+ dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
184
+ age=escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
185
+ address=escape_latex_special_chars(", ".join(filter(None, [
186
+ patient.get("address", ""),
187
+ patient.get("city", ""),
188
+ patient.get("state", ""),
189
+ patient.get("postal_code", ""),
190
+ patient.get("country", "")
191
+ ]))),
192
+ marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
193
+ language=escape_latex_special_chars(patient.get("language", "") or ""),
194
+ notes=notes_content,
195
+ conditions=conditions_content,
196
+ medications=medications_content,
197
+ encounters=encounters_content
198
+ )
199
+
200
+ # Compile LaTeX in a temporary directory
201
+ with TemporaryDirectory() as tmpdir:
202
+ tex_path = os.path.join(tmpdir, "report.tex")
203
+ pdf_path = os.path.join(tmpdir, "report.pdf")
204
+
205
+ with open(tex_path, "w", encoding="utf-8") as f:
206
+ f.write(latex_filled)
207
+
208
+ try:
209
+ subprocess.run(
210
+ ["latexmk", "-pdf", "-interaction=nonstopmode", tex_path],
211
+ cwd=tmpdir,
212
+ check=True,
213
+ capture_output=True,
214
+ text=True
215
+ )
216
+ except subprocess.CalledProcessError as e:
217
+ raise HTTPException(
218
+ status_code=500,
219
+ detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
220
+ )
221
+
222
+ if not os.path.exists(pdf_path):
223
+ raise HTTPException(
224
+ status_code=500,
225
+ detail="PDF file was not generated"
226
+ )
227
+
228
+ with open(pdf_path, "rb") as f:
229
+ pdf_bytes = f.read()
230
+
231
+ response = Response(
232
+ content=pdf_bytes,
233
+ media_type="application/pdf",
234
+ headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_report.pdf"}
235
+ )
236
+ return response
237
+
238
+ except HTTPException as http_error:
239
+ raise http_error
240
+ except Exception as e:
241
+ raise HTTPException(
242
+ status_code=500,
243
+ detail=f"Unexpected error generating PDF: {str(e)}"
244
+ )
245
+ finally:
246
+ # Restore the logger level for other routes
247
+ logger.setLevel(logging.INFO)