Files
serienbrief_django/app/mailmerge/services/preview.py
T
2026-05-22 08:13:05 +02:00

96 lines
2.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Single-Letter-Preview: rendert ausschließlich die erste CSV-Zeile gegen das
Template und gibt das resultierende PDF als Bytes zurück. Synchroner Pfad,
keine Celery-Beteiligung die View ruft das direkt im Request-Kontext auf.
"""
from __future__ import annotations
import csv
import io
import logging
import tempfile
from dataclasses import dataclass
from pathlib import Path
from .docx_renderer import docx_to_pdf, extract_placeholders, render_docx
logger = logging.getLogger(__name__)
class PreviewError(Exception):
"""Erwartete Fehler beim Preview werden dem User angezeigt."""
@dataclass
class PreviewResult:
pdf_bytes: bytes
used_row: dict[str, str]
placeholders: list[str]
csv_columns: list[str]
missing_columns: list[str]
extra_columns: list[str]
def _read_first_row(csv_file) -> tuple[list[str], dict[str, str]]:
"""Liest CSV-Header + erste Datenzeile.
Akzeptiert UploadedFile oder Path-ähnliches. UTF-8, Komma-getrennt.
"""
if hasattr(csv_file, "read"):
raw = csv_file.read()
if isinstance(raw, bytes):
text = raw.decode("utf-8-sig")
else:
text = raw
else:
text = Path(csv_file).read_text(encoding="utf-8-sig")
reader = csv.DictReader(io.StringIO(text))
if not reader.fieldnames:
raise PreviewError("CSV-Datei enthält keine Header-Zeile.")
try:
row = next(reader)
except StopIteration as exc:
raise PreviewError(
"CSV-Datei enthält außer dem Header keine Datenzeile."
) from exc
return list(reader.fieldnames), row
def build_preview(template_path: Path, csv_file) -> PreviewResult:
"""Rendert die erste CSV-Zeile gegen das Template und liefert ein PDF.
Validiert vorab Header gegen die Template-Platzhalter und meldet
fehlende / überflüssige Spalten.
"""
placeholders = extract_placeholders(template_path)
columns, first_row = _read_first_row(csv_file)
placeholder_set = set(placeholders)
column_set = set(columns)
missing = sorted(placeholder_set - column_set)
extra = sorted(column_set - placeholder_set)
if missing:
raise PreviewError(
"Folgende Platzhalter sind im Template enthalten, "
"fehlen aber als Spalte in der CSV: "
+ ", ".join(missing)
)
with tempfile.TemporaryDirectory(prefix="serienbrief-preview-") as tmp:
tmpdir = Path(tmp)
docx_out = tmpdir / "preview.docx"
render_docx(template_path, first_row, docx_out)
pdf_path = docx_to_pdf(docx_out, tmpdir)
pdf_bytes = pdf_path.read_bytes()
return PreviewResult(
pdf_bytes=pdf_bytes,
used_row=first_row,
placeholders=placeholders,
csv_columns=columns,
missing_columns=missing,
extra_columns=extra,
)