You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
end_of_term_surveys/generate_pdf_emoji_support.py

317 lines
8.8 KiB
Python

import argparse
import csv
import re
import tempfile
import jinja2
import os
from playwright.sync_api import sync_playwright
def is_number(s):
"""Returns True if s can be interpreted as a float."""
try:
float(s)
return True
except ValueError:
return False
def ensure_colon(heading: str) -> str:
"""Append ':' only if heading does not already end with one."""
heading = heading.strip()
if not heading.endswith(":"):
heading += ":"
return heading
def main():
parser = argparse.ArgumentParser(description="Generate a PDF report from a TSV input using Playwright.")
parser.add_argument("-i", "--input_tsv", required=True, help="Path to the input TSV file.")
parser.add_argument("-o", "--output_pdf", required=True, help="Path to the output PDF file.")
parser.add_argument("--browser", choices=["chromium", "firefox", "webkit"], default="chromium",
help="Which browser engine to use (default: chromium).")
args = parser.parse_args()
input_tsv = args.input_tsv
output_pdf = args.output_pdf
# 1. Read the TSV data
with open(input_tsv, 'r', encoding='utf-8-sig') as f:
reader = csv.DictReader(f, delimiter='\t')
rows = list(reader)
# 2. Sort rows by sending locality
rows = sorted(rows, key=lambda r: r.get("Sending locality:", "").strip())
# Known columns to skip
skip_cols = {
'Timestamp',
'Email Address',
'Name (last, first):',
'Sending locality:'
}
# 3. Process each row into a data structure for Jinja2
processed_rows = []
for row in rows:
name = row.get('Name (last, first):', '').strip()
locality = row.get('Sending locality:', '').strip()
q_and_a = []
for col in row.keys():
if col in skip_cols:
continue
col_title = col.strip()
value = row[col].strip()
if not value:
continue
# Check if "Comments" column
if re.match(r'^Comments\s\d+$', col_title, re.IGNORECASE):
# e.g. "Comments 1" => italic "Comments:"
q_and_a.append({
"type": "comments",
"label": "Comments",
"value": value
})
else:
# Normal question
if is_number(value):
# Numeric => "Truth: 4" on one line
q_and_a.append({
"type": "inline",
"label": ensure_colon(col_title),
"value": value
})
else:
# Non-numeric => label on one line, answer on next
q_and_a.append({
"type": "block",
"label": col_title,
"value": value
})
processed_rows.append({
"name": name,
"locality": locality,
"entries": q_and_a
})
# 4. Create an HTML template (Jinja2)
# We'll use page-break-after so each row is on a new PDF page.
html_template_str = r"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Data Report</title>
<style>
@page {
size: Letter;
margin: 50px;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 11pt;
line-height: 1.5;
margin: 0;
padding: 0;
color: #2c3e50;
}
.trainee-page {
margin-bottom: 2em;
page-break-after: always;
}
h1 {
font-size: 18pt;
margin: 0 0 0.5em 0;
color: #1a365d;
}
h2 {
font-size: 14pt;
margin: 0;
color: #4a5568;
}
p {
margin: 0.5em 0;
white-space: pre-wrap;
}
.locality {
margin-bottom: 0 !important;
}
.locality-label {
font-size: 1.15rem;
color: #4a5568;
margin: 0;
}
.locality-value {
font-size: 1.15rem;
margin: 0;
font-weight: bold;
color: #4a5568;
}
.name-label {
font-size: 1.5rem;
color: #4a5568;
margin: 0;
}
.name-value {
font-size: 1.5rem;
font-weight: bold;
color: #165f86;
}
.comments-label {
font-weight: 500;
margin-top: 1em;
margin-bottom: 0.2em;
color: #7c3aed;
font-size: 10.5pt;
}
.comments-value {
margin-top: 0;
color: #6c757d;
font-style: italic;
}
.bold {
font-weight: bold;
color: #2d3748;
}
.evaluation-header {
font-weight: bold;
margin: 1em 0 0.5em 0;
color: #2d3748;
font-size: 11pt;
}
.evaluation-item {
margin: 0.2em 0;
}
.evaluation-item .bold {
color: #1a365d;
font-weight: 600;
min-width: 110px;
display: inline-block;
font-size: 10.5pt;
text-transform: uppercase;
letter-spacing: 0.3px;
}
.block-label {
font-weight: bold;
margin-top: 1em;
margin-bottom: 0.3em;
}
.block-value {
margin-bottom: 0.5em;
color: #4a5568;
}
.entry-value {
color: #059669;
font-weight: 600;
font-size: 12pt;
}
.line-separator {
border: none;
border-top: 1px solid rgba(0, 0, 0, 0.1);
margin-bottom: 1em;
}
</style>
</head>
<body>
{% for row in rows %}
<div class="trainee-page">
<h1>{{ row.name }}</h1>
<p class="locality"><span class="locality-label">Sending Locality: </span><span class="locality-value">{{ row.locality }}</span></p>
{% for entry in row.entries %}
{% if entry.type == "comments" %}
<p class="comments-label">Comments:</p>
<p class="comments-value">{{ entry.value }}</p>
{% elif entry.type == "inline" %}
<div class="inline-answer">
{% if "Column 4" in entry.label %}
<p class="evaluation-header">Please evaluate your progress for this past term (1-5, 5 indicating the most progress):</p>
<p class="evaluation-item"><span class="bold">Truth:</span> <span class="entry-value">{{ entry.value }}</span></p>
{% elif "Column 6" in entry.label %}
<p class="evaluation-item"><span class="bold">Life:</span> <span class="entry-value">{{ entry.value }}</span></p>
{% elif "Column 8" in entry.label %}
<p class="evaluation-item"><span class="bold">Gospel:</span> <span class="entry-value">{{ entry.value }}</span></p>
{% elif "Column 10" in entry.label %}
<p class="evaluation-item"><span class="bold">Character:</span> <span class="entry-value">{{ entry.value }}</span></p>
{% elif "Column 12" in entry.label %}
<p class="evaluation-item"><span class="bold">Service:</span> <span class="entry-value">{{ entry.value }}</span></p>
{% else %}
<span class="bold">{{ entry.label }}</span>
<p class="entry-value">{{ entry.value }}</p>
{% endif %}
</div>
{% else %}
{% if "Name" in entry.label %}
<p class="name-label">{{ entry.label }}<span class="name-value"> {{ entry.value }}</span></p>
{% elif "Column" in entry.label %}
<p class="comments-label">Comments:</p>
<p class="comments-value">{{ entry.value }}</p>
<hr class="line-separator">
{% else %}
<p class="block-label">{{ entry.label }}</p>
<p class="comments-value block-value">{{ entry.value }}</p>
{% endif %}
{% endif %}
{% endfor %}
</div>
{% endfor %}
</body>
</html>
"""
# 5. Render the template with Jinja2
template = jinja2.Template(html_template_str)
rendered_html = template.render(rows=processed_rows)
# 6. Convert the HTML to PDF using Playwright
# We'll create a temporary HTML file, open it in a headless browser, and save as PDF.
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp_file:
tmp_html_path = tmp_file.name
tmp_file.write(rendered_html.encode("utf-8"))
tmp_file.flush()
with sync_playwright() as p:
# launch the selected browser (chromium/firefox/webkit)
browser = p.__getattribute__(args.browser).launch()
context = browser.new_context()
page = context.new_page()
# Load the local HTML file
page.goto(f"file://{tmp_html_path}")
# PDF Options: letter format, etc.
# For more options, see: https://playwright.dev/python/docs/api/class-page#pagepdfoptions
page.pdf(
path=output_pdf,
format="letter",
margin={"top": "0.75in", "right": "0.75in", "bottom": "0.75in", "left": "0.75in"}
)
browser.close()
if __name__ == "__main__":
main()