ICSAC commited on
Commit
435e513
Β·
1 Parent(s): 22497ae

Panel report and RQC PDFs carry the Institute letterhead, the document kind, the submission id, the review date and page numbers

Browse files
Files changed (1) hide show
  1. 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 { size: Letter; margin: 0.85in 0.9in; }
 
 
 
 
 
 
 
 
 
 
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
- def _md_to_pdf_bytes(md_text: str, *, doc_title: str | None = None) -> bytes:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- inner = _md_lib.markdown(md_text, extensions=["extra", "sane_lists", "tables"])
 
 
 
 
 
 
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