import logging from pathlib import Path from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import FileResponse, Http404, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.views.decorators.http import require_http_methods, require_POST from .forms import LetterTemplateForm, MailMergeJobForm from .models import LetterTemplate, MailMergeJob from .services.docx_renderer import extract_placeholders from .services.preview import PreviewError, build_preview from .tasks import run_mailmerge logger = logging.getLogger(__name__) @login_required def dashboard(request): templates = LetterTemplate.objects.all()[:20] jobs = MailMergeJob.objects.select_related("template")[:20] return render(request, "mailmerge/dashboard.html", {"templates": templates, "jobs": jobs}) @login_required @require_http_methods(["GET", "POST"]) def template_upload(request): form = LetterTemplateForm(request.POST or None, request.FILES or None) if request.method == "POST" and form.is_valid(): tpl = form.save(commit=False) tpl.created_by = request.user tpl.save() # Platzhalter extrahieren – Datei liegt jetzt auf Disk tpl.placeholders = extract_placeholders(Path(tpl.file.path)) tpl.save(update_fields=["placeholders"]) return redirect(reverse("template-detail", args=[tpl.id])) return render(request, "mailmerge/template_form.html", {"form": form}) @login_required def template_detail(request, pk): tpl = get_object_or_404(LetterTemplate, pk=pk) return render(request, "mailmerge/template_detail.html", {"template": tpl}) @login_required @require_http_methods(["GET", "POST"]) def job_create(request): form = MailMergeJobForm(request.POST or None, request.FILES or None) if request.method == "POST" and form.is_valid(): job = form.save(commit=False) job.created_by = request.user job.save() run_mailmerge.delay(str(job.id)) return redirect(reverse("job-detail", args=[job.id])) return render(request, "mailmerge/job_form.html", {"form": form}) @login_required @require_POST def job_preview(request): """Rendert nur die erste CSV-Zeile gegen das gewählte Template und liefert das PDF inline aus. Persistiert nichts.""" form = MailMergeJobForm(request.POST, request.FILES) if not form.is_valid(): # Form-Errors zurück ins Hauptformular leiten return render(request, "mailmerge/job_form.html", {"form": form, "preview_error": None}) template = form.cleaned_data["template"] csv_upload = form.cleaned_data["recipients_csv"] try: result = build_preview( template_path=Path(template.file.path), csv_file=csv_upload, ) except PreviewError as exc: messages.error(request, str(exc)) return render(request, "mailmerge/job_form.html", {"form": form, "preview_error": str(exc)}) except Exception as exc: # noqa: BLE001 – defensiv, breite Fehlerklasse von LibreOffice logger.exception("Preview-Render fehlgeschlagen") messages.error( request, "Vorschau konnte nicht erstellt werden: " + str(exc), ) return render(request, "mailmerge/job_form.html", {"form": form, "preview_error": str(exc)}) response = HttpResponse(result.pdf_bytes, content_type="application/pdf") response["Content-Disposition"] = 'inline; filename="vorschau.pdf"' response["X-Preview-Placeholders"] = ",".join(result.placeholders) response["X-Preview-Extra-Columns"] = ",".join(result.extra_columns) return response @login_required def job_detail(request, pk): job = get_object_or_404( MailMergeJob.objects.select_related("template"), pk=pk ) logs = job.logs.all() # HTMX partials: nur das Status-Fragment ausliefern if request.headers.get("HX-Request"): return render(request, "mailmerge/_job_status.html", {"job": job, "logs": logs}) return render(request, "mailmerge/job_detail.html", {"job": job, "logs": logs}) @login_required def job_download(request, pk): """PDF-Download – via X-Accel-Redirect in Production, direkter Stream im Dev.""" job = get_object_or_404(MailMergeJob, pk=pk) if not job.result_pdf: raise Http404("Kein Ergebnis-PDF vorhanden.") # In Dev (settings.DEBUG) direkt streamen from django.conf import settings if settings.DEBUG: return FileResponse(job.result_pdf.open("rb"), as_attachment=True, filename=Path(job.result_pdf.name).name) # In Production: Nginx serviert die Datei via internem Mount. response = HttpResponse() response["Content-Type"] = "application/pdf" response["Content-Disposition"] = ( f'attachment; filename="{Path(job.result_pdf.name).name}"' ) # Pfad relativ zu MEDIA_ROOT, gemappt auf /protected-media/ relative = job.result_pdf.name response["X-Accel-Redirect"] = f"/protected-media/{relative}" return response