Ali2206 commited on
Commit
afcd57a
·
verified ·
1 Parent(s): 44fe32c

Update api/routes/pdf.py

Browse files
Files changed (1) hide show
  1. api/routes/pdf.py +237 -101
api/routes/pdf.py CHANGED
@@ -1,4 +1,4 @@
1
- from fastapi import APIRouter, HTTPException, Depends, BackgroundTasks
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
@@ -6,10 +6,10 @@ from datetime import datetime
6
  from bson import ObjectId
7
  from bson.errors import InvalidId
8
  import os
 
 
 
9
  import logging
10
- import asyncio
11
- from typing import List, Dict, Optional
12
- from pymongo.errors import PyMongoError
13
 
14
  # Configure logging
15
  logging.basicConfig(
@@ -20,113 +20,249 @@ logger = logging.getLogger(__name__)
20
 
21
  router = APIRouter()
22
 
23
- # Configuration
24
- SUMMARY_COLLECTION = "patient_summary_json"
25
-
26
- async def generate_summary_json(patient: dict) -> Dict:
27
- """Generate a structured JSON summary from a patient dict."""
28
- summary = {
29
- "patient_id": patient.get("fhir_id"),
30
- "full_name": patient.get("full_name"),
31
- "gender": patient.get("gender"),
32
- "date_of_birth": patient.get("date_of_birth"),
33
- "age": calculate_age(patient.get("date_of_birth")),
34
- "address": ", ".join(filter(None, [
35
- patient.get("address"),
36
- patient.get("city"),
37
- patient.get("state"),
38
- patient.get("postal_code"),
39
- patient.get("country")
40
- ])),
41
- "marital_status": patient.get("marital_status"),
42
- "language": patient.get("language"),
43
- "notes": patient.get("notes", []),
44
- "conditions": patient.get("conditions", []),
45
- "medications": patient.get("medications", []),
46
- "encounters": patient.get("encounters", []),
47
- "generated_at": datetime.utcnow()
48
- }
49
- return summary
50
-
51
- async def generate_and_store_summary(patient: dict) -> Optional[Dict]:
52
- """Generate and store patient summary JSON in MongoDB."""
53
- try:
54
- summary = await generate_summary_json(patient)
55
- db = patients_collection.database
56
- await db[SUMMARY_COLLECTION].update_one(
57
- {"patient_id": summary["patient_id"]},
58
- {"$set": summary},
59
- upsert=True
60
- )
61
- logger.info(f"Inserted JSON summary for patient {summary['patient_id']}")
62
- return summary
63
- except Exception as e:
64
- logger.error(f"Error storing summary for patient {patient.get('fhir_id')}: {str(e)}")
65
- return None
66
-
67
- async def generate_all_summaries() -> List[Dict]:
68
- """Generate and store summaries for all patients."""
69
- results = []
70
- try:
71
- patients = await patients_collection.find({}).to_list(length=None)
72
- for patient in patients:
73
- summary = await generate_and_store_summary(patient)
74
- if summary:
75
- results.append(summary)
76
- return results
77
- except Exception as e:
78
- logger.error(f"Error in generate_all_summaries: {str(e)}")
79
- return results
80
 
81
- async def watch_for_new_patients():
82
- """Watch MongoDB change stream for new patients and generate summaries."""
83
  try:
84
- logger.info("Starting MongoDB change stream for new summaries")
85
- pipeline = [{'$match': {'operationType': 'insert'}}]
86
- while True:
87
- try:
88
- async with patients_collection.watch(pipeline) as stream:
89
- async for change in stream:
90
- patient = change['fullDocument']
91
- logger.info(f"New patient detected: {patient.get('fhir_id')}")
92
- await generate_and_store_summary(patient)
93
- except PyMongoError as e:
94
- logger.error(f"MongoDB change stream error: {str(e)}")
95
- await asyncio.sleep(5)
96
- except Exception as e:
97
- logger.error(f"Fatal error in watch_for_new_patients: {str(e)}")
98
 
99
- @router.on_event("startup")
100
- async def startup_event():
101
- asyncio.create_task(watch_for_new_patients())
102
- asyncio.create_task(generate_all_summaries())
 
 
103
 
104
- @router.get("/generate-summary/{patient_id}")
105
- async def generate_summary_for_one(patient_id: str, current_user: dict = Depends(get_current_user)):
106
- if current_user.get('role') not in ['admin', 'doctor']:
107
- raise HTTPException(status_code=403, detail="Unauthorized")
108
-
109
- try:
110
- query = {"fhir_id": patient_id}
111
  patient = await patients_collection.find_one(query)
112
  if not patient:
113
  raise HTTPException(status_code=404, detail="Patient not found")
114
 
115
- summary = await generate_and_store_summary(patient)
116
- return {"status": "success", "summary": summary}
117
- except Exception as e:
118
- raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
119
 
120
- @router.get("/list-json-summaries", response_model=List[Dict])
121
- async def list_all_summaries(current_user: dict = Depends(get_current_user)):
122
- if current_user.get('role') not in ['admin', 'doctor']:
123
- raise HTTPException(status_code=403, detail="Unauthorized")
 
 
 
 
 
 
 
 
 
124
 
125
- try:
126
- db = patients_collection.database
127
- summaries = await db[SUMMARY_COLLECTION].find({}).to_list(length=None)
128
- return summaries
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  except Exception as e:
130
- raise HTTPException(status_code=500, detail=f"Error listing summaries: {str(e)}")
 
 
 
 
 
 
131
 
 
132
  pdf = router
 
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
 
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(
 
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 with proper LaTeX formatting
44
+ def prepare_table_content(items, columns, default_message):
45
+ if not items:
46
+ return f"\\multicolumn{{{columns}}}{{l}}{{{default_message}}} \\\\"
47
+
48
+ content = []
49
+ for item in items:
50
+ row = []
51
+ for field in item:
52
+ value = item.get(field, "") or ""
53
+ row.append(escape_latex_special_chars(hyphenate_long_strings(value)))
54
+ content.append(" & ".join(row) + " \\\\")
55
+ return "\n".join(content)
56
+
57
+ # Notes table
58
+ notes = patient.get("notes", [])
59
+ notes_content = prepare_table_content(
60
+ [{
61
+ "date": format_timestamp(n.get("date", "")),
62
+ "type": n.get("type", ""),
63
+ "text": n.get("text", "")
64
+ } for n in notes],
65
+ 3,
66
+ "No notes available"
67
+ )
68
 
69
+ # Conditions table
70
+ conditions = patient.get("conditions", [])
71
+ conditions_content = prepare_table_content(
72
+ [{
73
+ "id": c.get("id", ""),
74
+ "code": c.get("code", ""),
75
+ "status": c.get("status", ""),
76
+ "onset": format_timestamp(c.get("onset_date", "")),
77
+ "verification": c.get("verification_status", "")
78
+ } for c in conditions],
79
+ 5,
80
+ "No conditions available"
81
+ )
82
 
83
+ # Medications table
84
+ medications = patient.get("medications", [])
85
+ medications_content = prepare_table_content(
86
+ [{
87
+ "id": m.get("id", ""),
88
+ "name": m.get("name", ""),
89
+ "status": m.get("status", ""),
90
+ "date": format_timestamp(m.get("prescribed_date", "")),
91
+ "dosage": m.get("dosage", "")
92
+ } for m in medications],
93
+ 5,
94
+ "No medications available"
95
+ )
96
+
97
+ # Encounters table
98
+ encounters = patient.get("encounters", [])
99
+ encounters_content = prepare_table_content(
100
+ [{
101
+ "id": e.get("id", ""),
102
+ "type": e.get("type", ""),
103
+ "status": e.get("status", ""),
104
+ "start": format_timestamp(e.get("period", {}).get("start", "")),
105
+ "provider": e.get("service_provider", "")
106
+ } for e in encounters],
107
+ 5,
108
+ "No encounters available"
109
+ )
110
+
111
+ # LaTeX template with improved table formatting
112
+ latex_template = Template(r"""
113
+ \documentclass[a4paper,12pt]{article}
114
+ \usepackage[utf8]{inputenc}
115
+ \usepackage[T1]{fontenc}
116
+ \usepackage{geometry}
117
+ \geometry{margin=1in}
118
+ \usepackage{booktabs,longtable,fancyhdr}
119
+ \usepackage{array}
120
+ \usepackage{microtype}
121
+ \microtypesetup{expansion=false}
122
+ \setlength{\headheight}{14.5pt}
123
+ \pagestyle{fancy}
124
+ \fancyhf{}
125
+ \fancyhead[L]{Patient Report}
126
+ \fancyhead[R]{Generated: \today}
127
+ \fancyfoot[C]{\thepage}
128
+ \begin{document}
129
+ \begin{center}
130
+ \Large\textbf{Patient Medical Report} \\
131
+ \vspace{0.2cm}
132
+ \textit{Generated on $generated_on}
133
+ \end{center}
134
+ \section*{Demographics}
135
+ \begin{itemize}
136
+ \item \textbf{FHIR ID:} $fhir_id
137
+ \item \textbf{Full Name:} $full_name
138
+ \item \textbf{Gender:} $gender
139
+ \item \textbf{Date of Birth:} $dob
140
+ \item \textbf{Age:} $age
141
+ \item \textbf{Address:} $address
142
+ \item \textbf{Marital Status:} $marital_status
143
+ \item \textbf{Language:} $language
144
+ \end{itemize}
145
+ \section*{Clinical Notes}
146
+ \begin{longtable}[l]{>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{6.5cm}}
147
+ \caption{Clinical Notes} \\
148
+ \toprule
149
+ \textbf{Date} & \textbf{Type} & \textbf{Text} \\
150
+ \midrule
151
+ $notes
152
+ \bottomrule
153
+ \end{longtable}
154
+ \section*{Conditions}
155
+ \begin{longtable}[l]{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
156
+ \caption{Conditions} \\
157
+ \toprule
158
+ \textbf{ID} & \textbf{Code} & \textbf{Status} & \textbf{Onset} & \textbf{Verification} \\
159
+ \midrule
160
+ $conditions
161
+ \bottomrule
162
+ \end{longtable}
163
+ \section*{Medications}
164
+ \begin{longtable}[l]{>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{4cm}>{\raggedright\arraybackslash}p{2cm}>{\raggedright\arraybackslash}p{3.5cm}>{\raggedright\arraybackslash}p{3cm}}
165
+ \caption{Medications} \\
166
+ \toprule
167
+ \textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
168
+ \midrule
169
+ $medications
170
+ \bottomrule
171
+ \end{longtable}
172
+ \section*{Encounters}
173
+ \begin{longtable}[l]{>{\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}}
174
+ \caption{Encounters} \\
175
+ \toprule
176
+ \textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
177
+ \midrule
178
+ $encounters
179
+ \bottomrule
180
+ \end{longtable}
181
+ \end{document}
182
+ """)
183
+
184
+ # Set the generated_on date to 02:54 PM CET, May 17, 2025
185
+ generated_on = datetime.strptime("2025-05-17 14:54:00+02:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p %Z")
186
+
187
+ latex_filled = latex_template.substitute(
188
+ generated_on=generated_on,
189
+ fhir_id=escape_latex_special_chars(hyphenate_long_strings(patient.get("fhir_id", "") or "")),
190
+ full_name=escape_latex_special_chars(patient.get("full_name", "") or ""),
191
+ gender=escape_latex_special_chars(patient.get("gender", "") or ""),
192
+ dob=escape_latex_special_chars(patient.get("date_of_birth", "") or ""),
193
+ age=escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
194
+ address=escape_latex_special_chars(", ".join(filter(None, [
195
+ patient.get("address", ""),
196
+ patient.get("city", ""),
197
+ patient.get("state", ""),
198
+ patient.get("postal_code", ""),
199
+ patient.get("country", "")
200
+ ]))),
201
+ marital_status=escape_latex_special_chars(patient.get("marital_status", "") or ""),
202
+ language=escape_latex_special_chars(patient.get("language", "") or ""),
203
+ notes=notes_content,
204
+ conditions=conditions_content,
205
+ medications=medications_content,
206
+ encounters=encounters_content
207
+ )
208
+
209
+ # Compile LaTeX in a temporary directory
210
+ with TemporaryDirectory() as tmpdir:
211
+ tex_path = os.path.join(tmpdir, "report.tex")
212
+ pdf_path = os.path.join(tmpdir, "report.pdf")
213
+
214
+ with open(tex_path, "w", encoding="utf-8") as f:
215
+ f.write(latex_filled)
216
+
217
+ try:
218
+ # Run latexmk twice to ensure proper table rendering
219
+ for _ in range(2):
220
+ result = subprocess.run(
221
+ ["latexmk", "-pdf", "-interaction=nonstopmode", tex_path],
222
+ cwd=tmpdir,
223
+ check=False,
224
+ capture_output=True,
225
+ text=True
226
+ )
227
+
228
+ if result.returncode != 0:
229
+ raise HTTPException(
230
+ status_code=500,
231
+ detail=f"LaTeX compilation failed: stdout={result.stdout}, stderr={result.stderr}"
232
+ )
233
+
234
+ except subprocess.CalledProcessError as e:
235
+ raise HTTPException(
236
+ status_code=500,
237
+ detail=f"LaTeX compilation failed: stdout={e.stdout}, stderr={e.stderr}"
238
+ )
239
+
240
+ if not os.path.exists(pdf_path):
241
+ raise HTTPException(
242
+ status_code=500,
243
+ detail="PDF file was not generated"
244
+ )
245
+
246
+ with open(pdf_path, "rb") as f:
247
+ pdf_bytes = f.read()
248
+
249
+ response = Response(
250
+ content=pdf_bytes,
251
+ media_type="application/pdf",
252
+ headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_report.pdf"}
253
+ )
254
+ return response
255
+
256
+ except HTTPException as http_error:
257
+ raise http_error
258
  except Exception as e:
259
+ raise HTTPException(
260
+ status_code=500,
261
+ detail=f"Unexpected error generating PDF: {str(e)}"
262
+ )
263
+ finally:
264
+ # Restore the logger level for other routes
265
+ logger.setLevel(logging.INFO)
266
 
267
+ # Export the router as 'pdf' for api.__init__.py
268
  pdf = router