Spaces:
Sleeping
Sleeping
Update api/routes.py
Browse files- api/routes.py +73 -204
api/routes.py
CHANGED
|
@@ -22,6 +22,7 @@ import uuid
|
|
| 22 |
import re
|
| 23 |
import subprocess
|
| 24 |
from tempfile import NamedTemporaryFile
|
|
|
|
| 25 |
|
| 26 |
# Configure logging
|
| 27 |
logging.basicConfig(
|
|
@@ -566,228 +567,96 @@ async def add_note(
|
|
| 566 |
detail=f"Failed to add note: {str(e)}"
|
| 567 |
)
|
| 568 |
|
|
|
|
| 569 |
@router.get("/ehr/patients/{patient_id}/pdf", response_class=Response)
|
| 570 |
async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
|
| 571 |
-
|
| 572 |
-
logger.setLevel(logging.CRITICAL)
|
| 573 |
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
if current_user.get('role') not in ['doctor', 'admin']:
|
| 577 |
-
raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
|
| 578 |
|
| 579 |
-
|
| 580 |
try:
|
| 581 |
obj_id = ObjectId(patient_id)
|
| 582 |
-
query = {
|
| 583 |
-
except
|
| 584 |
-
query = {
|
| 585 |
|
| 586 |
patient = await patients_collection.find_one(query)
|
| 587 |
|
| 588 |
if not patient:
|
| 589 |
raise HTTPException(status_code=404, detail="Patient not found")
|
| 590 |
|
| 591 |
-
#
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
|
| 601 |
-
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
\
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
\begin{longtable}{p{2cm}p{4cm}p{2cm}p{2cm}p{2cm}}
|
| 645 |
-
\toprule
|
| 646 |
-
\textbf{ID} & \textbf{Name} & \textbf{Status} & \textbf{Date} & \textbf{Dosage} \\
|
| 647 |
-
\midrule
|
| 648 |
-
%(medications)s
|
| 649 |
-
\bottomrule
|
| 650 |
-
\end{longtable}
|
| 651 |
-
|
| 652 |
-
\section*{Encounters}
|
| 653 |
-
\begin{longtable}{p{2cm}p{4cm}p{2cm}p{3cm}p{3cm}}
|
| 654 |
-
\toprule
|
| 655 |
-
\textbf{ID} & \textbf{Type} & \textbf{Status} & \textbf{Start} & \textbf{Provider} \\
|
| 656 |
-
\midrule
|
| 657 |
-
%(encounters)s
|
| 658 |
-
\bottomrule
|
| 659 |
-
\end{longtable}
|
| 660 |
-
|
| 661 |
-
\end{document}
|
| 662 |
-
"""
|
| 663 |
-
|
| 664 |
-
# Build the LaTeX-safe rows with escaped special characters
|
| 665 |
-
notes = "\n".join([
|
| 666 |
-
"{} & {} & {} \\\\".format(
|
| 667 |
-
escape_latex_special_chars(n.get("date", "")),
|
| 668 |
-
escape_latex_special_chars(n.get("type", "")),
|
| 669 |
-
escape_latex_special_chars(n.get("text", ""))
|
| 670 |
-
)
|
| 671 |
-
for n in patient.get("notes", [])
|
| 672 |
-
])
|
| 673 |
-
conditions = "\n".join([
|
| 674 |
-
"{} & {} & {} & {} & {} \\\\".format(
|
| 675 |
-
escape_latex_special_chars(c.get("id", "")),
|
| 676 |
-
escape_latex_special_chars(c.get("code", "")),
|
| 677 |
-
escape_latex_special_chars(c.get("status", "")),
|
| 678 |
-
escape_latex_special_chars(c.get("onset_date", "")),
|
| 679 |
-
escape_latex_special_chars(c.get("verification_status", ""))
|
| 680 |
-
)
|
| 681 |
-
for c in patient.get("conditions", [])
|
| 682 |
-
])
|
| 683 |
-
medications = "\n".join([
|
| 684 |
-
"{} & {} & {} & {} & {} \\\\".format(
|
| 685 |
-
escape_latex_special_chars(m.get("id", "")),
|
| 686 |
-
escape_latex_special_chars(m.get("name", "")),
|
| 687 |
-
escape_latex_special_chars(m.get("status", "")),
|
| 688 |
-
escape_latex_special_chars(m.get("prescribed_date", "")),
|
| 689 |
-
escape_latex_special_chars(m.get("dosage", ""))
|
| 690 |
-
)
|
| 691 |
-
for m in patient.get("medications", [])
|
| 692 |
-
])
|
| 693 |
-
encounters = "\n".join([
|
| 694 |
-
"{} & {} & {} & {} & {} \\\\".format(
|
| 695 |
-
escape_latex_special_chars(e.get("id", "")),
|
| 696 |
-
escape_latex_special_chars(e.get("type", "")),
|
| 697 |
-
escape_latex_special_chars(e.get("status", "")),
|
| 698 |
-
escape_latex_special_chars(e.get("period", {}).get("start", "")),
|
| 699 |
-
escape_latex_special_chars(e.get("service_provider", ""))
|
| 700 |
-
)
|
| 701 |
-
for e in patient.get("encounters", [])
|
| 702 |
-
])
|
| 703 |
-
|
| 704 |
-
# Update the generated_on date to reflect the current time: 03:14 PM CET, May 16, 2025
|
| 705 |
-
latex_filled = latex_template % {
|
| 706 |
-
"generated_on": datetime.strptime("2025-05-16 15:14:00+01:00", "%Y-%m-%d %H:%M:%S%z").strftime("%A, %B %d, %Y at %I:%M %p"),
|
| 707 |
-
"fhir_id": escape_latex_special_chars(patient.get("fhir_id", "")),
|
| 708 |
-
"full_name": escape_latex_special_chars(patient.get("full_name", "")),
|
| 709 |
-
"gender": escape_latex_special_chars(patient.get("gender", "")),
|
| 710 |
-
"dob": escape_latex_special_chars(patient.get("date_of_birth", "")),
|
| 711 |
-
"age": escape_latex_special_chars(str(calculate_age(patient.get("date_of_birth", "")) or "N/A")),
|
| 712 |
-
"address": escape_latex_special_chars("{}, {}, {}, {}, {}".format(
|
| 713 |
-
patient.get("address", ""),
|
| 714 |
-
patient.get("city", ""),
|
| 715 |
-
patient.get("state", ""),
|
| 716 |
-
patient.get("postal_code", ""),
|
| 717 |
-
patient.get("country", "")
|
| 718 |
-
)),
|
| 719 |
-
"marital_status": escape_latex_special_chars(patient.get("marital_status", "")),
|
| 720 |
-
"language": escape_latex_special_chars(patient.get("language", "")),
|
| 721 |
-
"notes": notes or "No notes available \\\\",
|
| 722 |
-
"conditions": conditions or "No conditions available \\\\",
|
| 723 |
-
"medications": medications or "No medications available \\\\",
|
| 724 |
-
"encounters": encounters or "No encounters available \\\\"
|
| 725 |
-
}
|
| 726 |
-
|
| 727 |
-
# Write LaTeX to file with error handling
|
| 728 |
-
try:
|
| 729 |
-
with NamedTemporaryFile(suffix=".tex", mode="w", delete=False, encoding="utf-8") as tex_file:
|
| 730 |
-
tex_file.write(latex_filled)
|
| 731 |
-
tex_path = tex_file.name
|
| 732 |
-
|
| 733 |
-
# Compile LaTeX to PDF
|
| 734 |
-
try:
|
| 735 |
-
result = subprocess.run(
|
| 736 |
-
["latexmk", "-pdf", tex_path],
|
| 737 |
-
check=True,
|
| 738 |
-
capture_output=True,
|
| 739 |
-
text=True
|
| 740 |
-
)
|
| 741 |
-
except subprocess.CalledProcessError as e:
|
| 742 |
-
raise HTTPException(
|
| 743 |
-
status_code=500,
|
| 744 |
-
detail=f"LaTeX compilation failed: {e.stderr}"
|
| 745 |
-
)
|
| 746 |
-
|
| 747 |
-
# Read the generated PDF
|
| 748 |
-
pdf_path = tex_path.replace(".tex", ".pdf")
|
| 749 |
-
if not os.path.exists(pdf_path):
|
| 750 |
-
raise HTTPException(
|
| 751 |
-
status_code=500,
|
| 752 |
-
detail="PDF file was not generated"
|
| 753 |
-
)
|
| 754 |
-
|
| 755 |
-
with open(pdf_path, "rb") as f:
|
| 756 |
-
pdf_bytes = f.read()
|
| 757 |
|
| 758 |
-
# Clean up temporary files
|
| 759 |
try:
|
| 760 |
-
subprocess.run(["latexmk", "-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
except Exception as cleanup_error:
|
| 764 |
-
raise HTTPException(
|
| 765 |
-
status_code=500,
|
| 766 |
-
detail=f"Failed to clean up temporary files: {str(cleanup_error)}"
|
| 767 |
-
)
|
| 768 |
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
headers={"Content-Disposition": f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"}
|
| 773 |
-
)
|
| 774 |
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
status_code=500,
|
| 778 |
-
detail=f"Failed to write LaTeX file: {str(io_error)}"
|
| 779 |
-
)
|
| 780 |
|
| 781 |
-
except HTTPException as http_error:
|
| 782 |
-
raise http_error
|
| 783 |
except Exception as e:
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
detail=f"Failed to generate PDF: {str(e)}"
|
| 787 |
-
)
|
| 788 |
-
finally:
|
| 789 |
-
# Restore the logger level for other routes
|
| 790 |
-
logger.setLevel(logging.INFO)
|
| 791 |
|
| 792 |
@router.post("/signup", status_code=status.HTTP_201_CREATED)
|
| 793 |
async def signup(data: SignupForm):
|
|
|
|
| 22 |
import re
|
| 23 |
import subprocess
|
| 24 |
from tempfile import NamedTemporaryFile
|
| 25 |
+
import tempfile
|
| 26 |
|
| 27 |
# Configure logging
|
| 28 |
logging.basicConfig(
|
|
|
|
| 567 |
detail=f"Failed to add note: {str(e)}"
|
| 568 |
)
|
| 569 |
|
| 570 |
+
|
| 571 |
@router.get("/ehr/patients/{patient_id}/pdf", response_class=Response)
|
| 572 |
async def generate_patient_pdf(patient_id: str, current_user: dict = Depends(get_current_user)):
|
| 573 |
+
logger.info(f"Generating PDF for patient: {patient_id} by user {current_user.get('email')}")
|
|
|
|
| 574 |
|
| 575 |
+
if current_user.get('role') not in ['doctor', 'admin']:
|
| 576 |
+
raise HTTPException(status_code=403, detail="Only clinicians can generate patient PDFs")
|
|
|
|
|
|
|
| 577 |
|
| 578 |
+
try:
|
| 579 |
try:
|
| 580 |
obj_id = ObjectId(patient_id)
|
| 581 |
+
query = {"$or": [{"_id": obj_id}, {"fhir_id": patient_id}]}
|
| 582 |
+
except Exception:
|
| 583 |
+
query = {"fhir_id": patient_id}
|
| 584 |
|
| 585 |
patient = await patients_collection.find_one(query)
|
| 586 |
|
| 587 |
if not patient:
|
| 588 |
raise HTTPException(status_code=404, detail="Patient not found")
|
| 589 |
|
| 590 |
+
# Create temporary directory
|
| 591 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
| 592 |
+
tex_path = os.path.join(tmpdir, "patient.tex")
|
| 593 |
+
pdf_path = os.path.join(tmpdir, "patient.pdf")
|
| 594 |
+
|
| 595 |
+
latex = f"""
|
| 596 |
+
\\documentclass[a4paper,12pt]{{article}}
|
| 597 |
+
\\usepackage[utf8]{{inputenc}}
|
| 598 |
+
\\usepackage[T1]{{fontenc}}
|
| 599 |
+
\\usepackage{{geometry}}
|
| 600 |
+
\\geometry{{margin=1in}}
|
| 601 |
+
\\usepackage{{booktabs,longtable,fancyhdr}}
|
| 602 |
+
\\pagestyle{{fancy}}
|
| 603 |
+
\\fancyhf{{}}
|
| 604 |
+
\\fancyhead[L]{{Patient Report}}
|
| 605 |
+
\\fancyhead[R]{{Generated: \\today}}
|
| 606 |
+
\\fancyfoot[C]{{\\thepage}}
|
| 607 |
+
|
| 608 |
+
\\begin{{document}}
|
| 609 |
+
|
| 610 |
+
\\begin{{center}}
|
| 611 |
+
\\Large\\textbf{{Patient Medical Report}} \\\\
|
| 612 |
+
\\vspace{{0.2cm}}
|
| 613 |
+
\\textit{{Generated on {datetime.now().strftime('%A, %B %d, %Y at %I:%M %p')}}}
|
| 614 |
+
\\end{{center}}
|
| 615 |
+
|
| 616 |
+
\\section*{{Demographics}}
|
| 617 |
+
\\begin{{itemize}}
|
| 618 |
+
\\item \\textbf{{FHIR ID:}} {patient.get("fhir_id", "")}
|
| 619 |
+
\\item \\textbf{{Full Name:}} {patient.get("full_name", "")}
|
| 620 |
+
\\item \\textbf{{Gender:}} {patient.get("gender", "")}
|
| 621 |
+
\\item \\textbf{{Date of Birth:}} {patient.get("date_of_birth", "")}
|
| 622 |
+
\\item \\textbf{{Age:}} {calculate_age(patient.get("date_of_birth", ""))}
|
| 623 |
+
\\item \\textbf{{Address:}} {patient.get("address", "")}, {patient.get("city", "")}, {patient.get("state", "")}, {patient.get("postal_code", "")}, {patient.get("country", "")}
|
| 624 |
+
\\item \\textbf{{Marital Status:}} {patient.get("marital_status", "")}
|
| 625 |
+
\\item \\textbf{{Language:}} {patient.get("language", "")}
|
| 626 |
+
\\end{{itemize}}
|
| 627 |
+
|
| 628 |
+
\\section*{{Clinical Notes}}
|
| 629 |
+
\\begin{{longtable}}{{p{{3cm}}p{{3cm}}p{{7cm}}}}
|
| 630 |
+
\\toprule
|
| 631 |
+
\\textbf{{Date}} & \\textbf{{Type}} & \\textbf{{Text}} \\\\
|
| 632 |
+
\\midrule
|
| 633 |
+
\\endhead
|
| 634 |
+
{" \\\\ \n".join([f"{n.get('date', '')} & {n.get('type', '')} & {n.get('text', '')}" for n in patient.get("notes", [])])}
|
| 635 |
+
\\bottomrule
|
| 636 |
+
\\end{{longtable}}
|
| 637 |
+
|
| 638 |
+
\\end{{document}}
|
| 639 |
+
"""
|
| 640 |
+
|
| 641 |
+
with open(tex_path, "w") as f:
|
| 642 |
+
f.write(latex)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 643 |
|
|
|
|
| 644 |
try:
|
| 645 |
+
subprocess.run(["latexmk", "-pdf", tex_path], check=True, cwd=tmpdir)
|
| 646 |
+
with open(pdf_path, "rb") as f:
|
| 647 |
+
pdf_bytes = f.read()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
+
response = Response(content=pdf_bytes, media_type="application/pdf")
|
| 650 |
+
response.headers["Content-Disposition"] = f"attachment; filename=patient_{patient.get('fhir_id', 'unknown')}_record.pdf"
|
| 651 |
+
return response
|
|
|
|
|
|
|
| 652 |
|
| 653 |
+
except subprocess.CalledProcessError as e:
|
| 654 |
+
logger.error(f"LaTeX compilation failed: {e.stderr}")
|
| 655 |
+
raise HTTPException(status_code=500, detail="LaTeX compilation failed")
|
|
|
|
|
|
|
| 656 |
|
|
|
|
|
|
|
| 657 |
except Exception as e:
|
| 658 |
+
logger.error(f"PDF generation error: {e}")
|
| 659 |
+
raise HTTPException(status_code=500, detail="Failed to generate PDF")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 660 |
|
| 661 |
@router.post("/signup", status_code=status.HTTP_201_CREATED)
|
| 662 |
async def signup(data: SignupForm):
|