Panel report and RQC PDFs carry the Institute letterhead, the document kind, the submission id, the review date and page numbers
Browse files- intake/notify_author.py +62 -7
intake/notify_author.py
CHANGED
|
@@ -43,7 +43,17 @@ def _delivery_tier(sub_id: str, tier: int) -> int:
|
|
| 43 |
# per-dimension score tables render cleanly. No vendor/model styling
|
| 44 |
# concerns since the PDFs render the SCRUBBED markdown.
|
| 45 |
_PDF_CSS = """
|
| 46 |
-
@page {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
body { font-family: Georgia, "Times New Roman", serif; font-size: 10.5pt; line-height: 1.5; color: #1f1f1f; }
|
| 48 |
h1, h2, h3, h4 { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; color: #111; font-weight: 600; page-break-after: avoid; }
|
| 49 |
h1 { font-size: 1.5em; border-bottom: 2px solid #888; padding-bottom: 0.3em; margin-top: 0; }
|
|
@@ -65,22 +75,65 @@ details > summary { cursor: pointer; font-weight: 600; margin: 0.6em 0; }
|
|
| 65 |
"""
|
| 66 |
|
| 67 |
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
"""Render markdown to a print-quality PDF via WeasyPrint.
|
| 70 |
|
| 71 |
Used for the panel-report and RQC attachments on decision emails. Input
|
| 72 |
is the SCRUBBED markdown (vendor/model identifiers and the RQC
|
| 73 |
injection_indicators dim already stripped upstream); we don't
|
| 74 |
-
re-validate here β caller owns the redaction gate.
|
|
|
|
|
|
|
| 75 |
"""
|
| 76 |
import markdown as _md_lib
|
| 77 |
import weasyprint
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
title_html = f"<title>{doc_title}</title>" if doc_title else ""
|
| 80 |
full = (
|
| 81 |
f"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
| 82 |
f"{title_html}<style>{_PDF_CSS}</style></head>"
|
| 83 |
-
f"<body>{inner}</body></html>"
|
| 84 |
)
|
| 85 |
return weasyprint.HTML(string=full).write_pdf()
|
| 86 |
|
|
@@ -370,12 +423,14 @@ def send_decision(*, to: str, sub_id: str, title: str, author_name: str,
|
|
| 370 |
if panel_report_md.strip():
|
| 371 |
attachments.append((
|
| 372 |
f"icsac-review-{sub_id}.pdf",
|
| 373 |
-
_md_to_pdf_bytes(panel_report_md, doc_title=f"ICSAC Review β {sub_id}"
|
|
|
|
| 374 |
))
|
| 375 |
if rqc_md.strip():
|
| 376 |
attachments.append((
|
| 377 |
f"icsac-rqc-{sub_id}.pdf",
|
| 378 |
-
_md_to_pdf_bytes(rqc_md, doc_title=f"ICSAC Review Quality Control β {sub_id}"
|
|
|
|
| 379 |
))
|
| 380 |
|
| 381 |
# Decision emails (accept/revise/scope_reject) are HIGH-STAKES author-facing
|
|
|
|
| 43 |
# per-dimension score tables render cleanly. No vendor/model styling
|
| 44 |
# concerns since the PDFs render the SCRUBBED markdown.
|
| 45 |
_PDF_CSS = """
|
| 46 |
+
@page {
|
| 47 |
+
size: Letter; margin: 0.85in 0.9in 0.95in 0.9in;
|
| 48 |
+
@bottom-left { content: "Institute for Complexity Science and Advanced Computing \\00b7 icsacinstitute.org"; font-family: "Helvetica Neue", Arial, sans-serif; font-size: 8pt; color: #666; }
|
| 49 |
+
@bottom-right { content: "Page " counter(page) " of " counter(pages); font-family: "Helvetica Neue", Arial, sans-serif; font-size: 8pt; color: #666; }
|
| 50 |
+
}
|
| 51 |
+
table.lh { width: 100%; border-collapse: collapse; margin: 0 0 0.3in 0; border-bottom: 1.5px solid #1f1f1f; }
|
| 52 |
+
table.lh td { border: none; padding: 0 0 0.2in 0; vertical-align: middle; }
|
| 53 |
+
td.lh-logo-cell { width: 1.1in; }
|
| 54 |
+
img.lh-logo { height: 0.62in; width: auto; display: block; }
|
| 55 |
+
.lh-name { font-family: "Helvetica Neue", Arial, sans-serif; font-size: 11.5pt; font-weight: 600; letter-spacing: 0.01em; color: #111; }
|
| 56 |
+
.lh-meta { font-family: "Helvetica Neue", Arial, sans-serif; font-size: 9pt; color: #555; margin-top: 0.1in; }
|
| 57 |
body { font-family: Georgia, "Times New Roman", serif; font-size: 10.5pt; line-height: 1.5; color: #1f1f1f; }
|
| 58 |
h1, h2, h3, h4 { font-family: -apple-system, "Helvetica Neue", Arial, sans-serif; color: #111; font-weight: 600; page-break-after: avoid; }
|
| 59 |
h1 { font-size: 1.5em; border-bottom: 2px solid #888; padding-bottom: 0.3em; margin-top: 0; }
|
|
|
|
| 75 |
"""
|
| 76 |
|
| 77 |
|
| 78 |
+
_LOGO_PATH = _REPO_ROOT / "assets" / "icsac-logo.png"
|
| 79 |
+
_MONTHS = ("January", "February", "March", "April", "May", "June", "July", "August",
|
| 80 |
+
"September", "October", "November", "December")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def _logo_data_uri() -> str:
|
| 84 |
+
import base64
|
| 85 |
+
try:
|
| 86 |
+
return "data:image/png;base64," + base64.b64encode(_LOGO_PATH.read_bytes()).decode()
|
| 87 |
+
except OSError:
|
| 88 |
+
return ""
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def _frontmatter_date(md_text: str) -> str:
|
| 92 |
+
"""'review_date: 2026-09-27T16:40:58Z' in the frontmatter -> '27 September 2026'."""
|
| 93 |
+
m = re.search(r"^(?:review_date|audit_date):\s*(\d{4})-(\d{2})-(\d{2})", md_text, re.MULTILINE)
|
| 94 |
+
if not m:
|
| 95 |
+
return ""
|
| 96 |
+
y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3))
|
| 97 |
+
return f"{d} {_MONTHS[mo - 1]} {y}"
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
def _letterhead_html(kind: str | None, sub_id: str | None, dated: str) -> str:
|
| 101 |
+
import html as _html
|
| 102 |
+
meta = " \u00b7 ".join(x for x in (kind or "", f"Submission {sub_id}" if sub_id else "", dated) if x)
|
| 103 |
+
logo = _logo_data_uri()
|
| 104 |
+
logo_cell = f'<td class="lh-logo-cell"><img class="lh-logo" src="{logo}" alt="ICSAC"></td>' if logo else ""
|
| 105 |
+
return (
|
| 106 |
+
'<table class="lh"><tr>' + logo_cell +
|
| 107 |
+
'<td><div class="lh-name">Institute for Complexity Science and Advanced Computing</div>'
|
| 108 |
+
f'<div class="lh-meta">{_html.escape(meta)}</div></td></tr></table>'
|
| 109 |
+
)
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def _md_to_pdf_bytes(md_text: str, *, doc_title: str | None = None,
|
| 113 |
+
kind: str | None = None, sub_id: str | None = None) -> bytes:
|
| 114 |
"""Render markdown to a print-quality PDF via WeasyPrint.
|
| 115 |
|
| 116 |
Used for the panel-report and RQC attachments on decision emails. Input
|
| 117 |
is the SCRUBBED markdown (vendor/model identifiers and the RQC
|
| 118 |
injection_indicators dim already stripped upstream); we don't
|
| 119 |
+
re-validate here β caller owns the redaction gate. The page carries the
|
| 120 |
+
Institute's letterhead (logo, name, document kind, submission id, the
|
| 121 |
+
review date) and a running footer with page numbers (2026-09-28).
|
| 122 |
"""
|
| 123 |
import markdown as _md_lib
|
| 124 |
import weasyprint
|
| 125 |
+
dated = _frontmatter_date(md_text)
|
| 126 |
+
body_md = md_text
|
| 127 |
+
if body_md.startswith("---\n"):
|
| 128 |
+
end = body_md.find("\n---", 4)
|
| 129 |
+
if end != -1:
|
| 130 |
+
body_md = body_md[end + 4:].lstrip("\n")
|
| 131 |
+
inner = _md_lib.markdown(body_md, extensions=["extra", "sane_lists", "tables"])
|
| 132 |
title_html = f"<title>{doc_title}</title>" if doc_title else ""
|
| 133 |
full = (
|
| 134 |
f"<!DOCTYPE html><html><head><meta charset='utf-8'>"
|
| 135 |
f"{title_html}<style>{_PDF_CSS}</style></head>"
|
| 136 |
+
f"<body>{_letterhead_html(kind, sub_id, dated)}{inner}</body></html>"
|
| 137 |
)
|
| 138 |
return weasyprint.HTML(string=full).write_pdf()
|
| 139 |
|
|
|
|
| 423 |
if panel_report_md.strip():
|
| 424 |
attachments.append((
|
| 425 |
f"icsac-review-{sub_id}.pdf",
|
| 426 |
+
_md_to_pdf_bytes(panel_report_md, doc_title=f"ICSAC Review β {sub_id}",
|
| 427 |
+
kind="Panel report", sub_id=sub_id),
|
| 428 |
))
|
| 429 |
if rqc_md.strip():
|
| 430 |
attachments.append((
|
| 431 |
f"icsac-rqc-{sub_id}.pdf",
|
| 432 |
+
_md_to_pdf_bytes(rqc_md, doc_title=f"ICSAC Review Quality Control β {sub_id}",
|
| 433 |
+
kind="Review Quality Control audit", sub_id=sub_id),
|
| 434 |
))
|
| 435 |
|
| 436 |
# Decision emails (accept/revise/scope_reject) are HIGH-STAKES author-facing
|