Ali2206 commited on
Commit
5a50355
·
1 Parent(s): f0d524c

device token

Browse files
Files changed (3) hide show
  1. api/routes/patients.py +2 -2
  2. api/routes/pdf.py +50 -213
  3. routes/pdf.py +9 -1
api/routes/patients.py CHANGED
@@ -357,7 +357,7 @@ async def import_patients(
357
  logger.info(f"Starting import request {request_id} by user {current_user.get('email')}")
358
  start_time = time.time()
359
 
360
- if current_user.get('role') not in ['admin', 'doctor']:
361
  logger.warning(f"Unauthorized import attempt by {current_user.get('email')}")
362
  raise HTTPException(
363
  status_code=status.HTTP_403_FORBIDDEN,
@@ -965,7 +965,7 @@ async def add_note(
965
  current_user: dict = Depends(get_current_user)
966
  ):
967
  logger.info(f"Adding note for patient {patient_id} by user {current_user.get('email')}")
968
- if current_user.get('role') not in ['doctor', 'admin']:
969
  logger.warning(f"Unauthorized note addition attempt by {current_user.get('email')}")
970
  raise HTTPException(
971
  status_code=status.HTTP_403_FORBIDDEN,
 
357
  logger.info(f"Starting import request {request_id} by user {current_user.get('email')}")
358
  start_time = time.time()
359
 
360
+ if not any(role in current_user.get('roles', []) for role in ['admin', 'doctor']):
361
  logger.warning(f"Unauthorized import attempt by {current_user.get('email')}")
362
  raise HTTPException(
363
  status_code=status.HTTP_403_FORBIDDEN,
 
965
  current_user: dict = Depends(get_current_user)
966
  ):
967
  logger.info(f"Adding note for patient {patient_id} by user {current_user.get('email')}")
968
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
969
  logger.warning(f"Unauthorized note addition attempt by {current_user.get('email')}")
970
  raise HTTPException(
971
  status_code=status.HTTP_403_FORBIDDEN,
api/routes/pdf.py CHANGED
@@ -1,14 +1,11 @@
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
@@ -26,7 +23,7 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
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
@@ -40,218 +37,58 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
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
 
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, format_timestamp
5
  from datetime import datetime
6
  from bson import ObjectId
7
  from bson.errors import InvalidId
8
+
 
 
 
9
  import logging
10
 
11
  # Configure logging
 
23
  logger.setLevel(logging.CRITICAL)
24
 
25
  try:
26
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
27
  raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
28
 
29
  # Determine if patient_id is ObjectId or fhir_id
 
37
  if not patient:
38
  raise HTTPException(status_code=404, detail="Patient not found")
39
 
40
+ # Get patient data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  notes = patient.get("notes", [])
 
 
 
 
 
 
 
 
 
 
 
42
  conditions = patient.get("conditions", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
43
  medications = patient.get("medications", [])
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  encounters = patient.get("encounters", [])
45
+
46
+ # Set the generated_on date
47
+ generated_on = datetime.now().strftime("%A, %B %d, %Y at %I:%M %p %Z")
48
+
49
+ # For now, always generate a text report to avoid LaTeX dependencies
50
+ print("📄 Generating text report (LaTeX not available)")
51
+
52
+ # Create a simple text report
53
+ text_report = f"""
54
+ PATIENT MEDICAL REPORT
55
+ Generated on {generated_on}
56
+
57
+ DEMOGRAPHICS:
58
+ - FHIR ID: {patient.get('fhir_id', 'N/A')}
59
+ - Full Name: {patient.get('full_name', 'N/A')}
60
+ - Gender: {patient.get('gender', 'N/A')}
61
+ - Date of Birth: {patient.get('date_of_birth', 'N/A')}
62
+ - Age: {calculate_age(patient.get('date_of_birth', '')) or 'N/A'}
63
+ - Address: {', '.join(filter(None, [
64
+ patient.get('address', ''),
65
+ patient.get('city', ''),
66
+ patient.get('state', ''),
67
+ patient.get('postal_code', ''),
68
+ patient.get('country', '')
69
+ ]))}
70
+ - Marital Status: {patient.get('marital_status', 'N/A')}
71
+ - Language: {patient.get('language', 'N/A')}
72
+
73
+ CLINICAL NOTES:
74
+ {chr(10).join([f"- {n.get('date', 'N/A')} | {n.get('type', 'N/A')}: {n.get('text', 'N/A')}" for n in notes]) if notes else "No notes available"}
75
+
76
+ CONDITIONS:
77
+ {chr(10).join([f"- {c.get('code', 'N/A')} | Status: {c.get('status', 'N/A')} | Onset: {c.get('onset_date', 'N/A')}" for c in conditions]) if conditions else "No conditions available"}
78
+
79
+ MEDICATIONS:
80
+ {chr(10).join([f"- {m.get('name', 'N/A')} | Status: {m.get('status', 'N/A')} | Dosage: {m.get('dosage', 'N/A')}" for m in medications]) if medications else "No medications available"}
81
+
82
+ ENCOUNTERS:
83
+ {chr(10).join([f"- {e.get('type', 'N/A')} | Status: {e.get('status', 'N/A')} | Provider: {e.get('service_provider', 'N/A')}" for e in encounters]) if encounters else "No encounters available"}
84
+ """
85
+
86
+ response = Response(
87
+ content=text_report,
88
+ media_type="text/plain",
89
+ headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_report.txt"}
90
  )
91
+ return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
 
93
  except HTTPException as http_error:
94
  raise http_error
routes/pdf.py CHANGED
@@ -26,8 +26,16 @@ async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get
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:
 
26
  logger.setLevel(logging.CRITICAL)
27
 
28
  try:
29
+ # Debug logging
30
+ print(f"🔍 PDF access check - User: {current_user.get('email')}")
31
+ print(f"🔍 User roles: {current_user.get('roles', [])}")
32
+ print(f"🔍 Required roles: ['doctor', 'admin']")
33
+
34
+ if not any(role in current_user.get('roles', []) for role in ['doctor', 'admin']):
35
+ print(f"❌ Access denied - User roles don't match required roles")
36
  raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
37
+
38
+ print(f"✅ Access granted - User has required role")
39
 
40
  # Determine if patient_id is ObjectId or fhir_id
41
  try: