Files
stiftung-management-system/app/stiftung/views/vorlagen.py
SysAdmin Agent aed540fe4b
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy (push) Has been cancelled
Code Quality / quality (push) Has been cancelled
Add Vorlagen editor, upload portal, onboarding, and participant import command
- Dokument-Vorlagen-Editor: create/edit/reset document templates (admin)
- Upload-Portal: public portal for Nachweis uploads via token
- Onboarding: invite Destinatäre via email with multi-step wizard
- Bestätigungsschreiben: preview and send confirmation letters
- Email settings: SMTP configuration UI
- Management command: import_veranstaltung_teilnehmer for bulk participant import

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 09:25:18 +00:00

212 lines
7.2 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.

"""Views für den web-basierten Dokument-Vorlagen-Editor."""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.views.decorators.http import require_POST
from stiftung.models import DokumentVorlage
from stiftung.utils.vorlagen import get_vorlage_original
@login_required
def vorlagen_liste(request):
"""Übersicht aller Dokument-Vorlagen nach Kategorie."""
vorlagen = DokumentVorlage.objects.select_related("zuletzt_bearbeitet_von").all()
kategorien = {}
for v in vorlagen:
if v.kategorie not in kategorien:
kategorien[v.kategorie] = []
kategorien[v.kategorie].append(v)
# Kategorie-Labels
kategorie_labels = dict(DokumentVorlage.KATEGORIE_CHOICES)
return render(request, "stiftung/vorlagen_liste.html", {
"kategorien": kategorien,
"kategorie_labels": kategorie_labels,
"vorlagen_count": vorlagen.count(),
})
@login_required
def vorlage_editor(request, pk):
"""Editor für eine einzelne Vorlage."""
vorlage = get_object_or_404(DokumentVorlage, pk=pk)
if request.method == "POST":
html_inhalt = request.POST.get("html_inhalt", "")
vorlage.html_inhalt = html_inhalt
vorlage.zuletzt_bearbeitet_von = request.user
vorlage.save()
messages.success(request, f'Vorlage „{vorlage.bezeichnung}" wurde gespeichert.')
return redirect("stiftung:vorlage_editor", pk=pk)
try:
original = get_vorlage_original(vorlage.schluessel)
hat_original = True
except FileNotFoundError:
original = None
hat_original = False
import json
from django.utils.safestring import mark_safe
# JSON-encode and escape </script> to prevent XSS in script tag
html_json = json.dumps(vorlage.html_inhalt)
html_json = html_json.replace("<", "\\u003c").replace(">", "\\u003e")
# Serienbrief templates are full HTML documents with Django template tags
# ({% for %}, {% if %}) — Summernote WYSIWYG mangles these.
# Use a plain code editor textarea instead.
use_code_editor = vorlage.kategorie == "serienbrief"
return render(request, "stiftung/vorlage_editor.html", {
"vorlage": vorlage,
"hat_original": hat_original,
"variablen": vorlage.verfuegbare_variablen,
"html_inhalt_json": mark_safe(html_json),
"use_code_editor": use_code_editor,
})
@login_required
@require_POST
def vorlage_zuruecksetzen(request, pk):
"""Setzt eine Vorlage auf den Datei-Original-Inhalt zurück."""
vorlage = get_object_or_404(DokumentVorlage, pk=pk)
try:
original = get_vorlage_original(vorlage.schluessel)
vorlage.html_inhalt = original
vorlage.zuletzt_bearbeitet_von = request.user
vorlage.save()
messages.success(request, f'Vorlage „{vorlage.bezeichnung}" wurde auf die Original-Datei zurückgesetzt.')
except FileNotFoundError:
messages.error(request, "Original-Datei nicht gefunden. Zurücksetzen nicht möglich.")
return redirect("stiftung:vorlage_editor", pk=pk)
@login_required
@require_POST
def vorlagen_alle_zuruecksetzen(request):
"""Setzt ALLE Vorlagen auf die Original-Datei-Inhalte zurück."""
vorlagen = DokumentVorlage.objects.all()
restored = 0
for vorlage in vorlagen:
try:
original = get_vorlage_original(vorlage.schluessel)
vorlage.html_inhalt = original
vorlage.zuletzt_bearbeitet_von = request.user
vorlage.save()
restored += 1
except FileNotFoundError:
pass
messages.success(request, f"{restored} Vorlage(n) auf Original zurückgesetzt.")
return redirect("stiftung:vorlagen_liste")
@login_required
def vorlage_vorschau(request, pk):
"""Rendert eine Vorschau der Vorlage mit Beispieldaten (JSON-Response)."""
vorlage = get_object_or_404(DokumentVorlage, pk=pk)
# Rohinhalt aus POST (live-preview) oder aus DB
html_inhalt = request.POST.get("html_inhalt") if request.method == "POST" else vorlage.html_inhalt
# Einfache Beispieldaten je Kategorie
beispiel_context = _get_beispiel_context(vorlage.schluessel)
try:
from django.template import Context, Engine
engine = Engine.get_default()
t = engine.from_string(html_inhalt)
rendered = t.render(Context(beispiel_context))
return HttpResponse(rendered, content_type="text/html; charset=utf-8")
except Exception as exc:
return HttpResponse(
f"<pre style='color:red'>Template-Fehler: {exc}</pre>",
content_type="text/html; charset=utf-8",
)
def _get_beispiel_context(schluessel: str) -> dict:
"""Gibt Beispieldaten für Vorschau-Rendering zurück."""
from datetime import date, time
class FakeObj(dict):
def __getattr__(self, k):
return self.get(k, "")
destinataer = FakeObj(
vorname="Maria",
nachname="Mustermann",
anrede="Frau",
strasse="Musterstraße 1",
plz="46499",
ort="Hamminkeln",
email="m.mustermann@example.com",
)
einladung = FakeObj(
vorname="Maria",
nachname="Mustermann",
email="m.mustermann@example.com",
)
base = {
"destinataer": destinataer,
"einladung": einladung,
"datum": date.today(),
"zeitraum": "01.01.2025 31.12.2025",
"betrag_quartal": 500,
"betrag_jaehrlich": 2000,
"gesamtbetrag": 2000,
"zweck": "Studienförderung",
"unterstuetzungen": [],
"halbjahr_label": "1. Halbjahr 2025",
"upload_url": "https://vhtv-stiftung.de/portal/upload/beispiel-token/",
"gueltig_bis": date.today(),
"qr_code_base64": "",
"ist_erinnerung": False,
"onboarding_url": "https://vhtv-stiftung.de/portal/onboarding/beispiel/",
"veranstaltung": FakeObj(titel="Stiftungsessen 2025"),
"teilnehmer_list": [],
}
# Serienbrief-Vorlage: vollständige Veranstaltungs- und Teilnehmer-Beispieldaten
if "serienbrief" in schluessel:
base["veranstaltung"] = FakeObj(
titel="Stiftungsessen 2025",
datum=date.today(),
uhrzeit=time(18, 0),
ort="Gasthaus zur Linde",
adresse="Lindenstraße 12, 46499 Hamminkeln",
betreff="",
briefvorlage="",
unterschrift_1_name="Katrin Kleinpaß",
unterschrift_1_titel="Rentmeisterin",
unterschrift_2_name="Jan Remmer Siebels",
unterschrift_2_titel="Rentmeister",
)
base["teilnehmer"] = [
FakeObj(
anrede="Frau",
vorname="Maria",
nachname="Mustermann",
strasse="Musterstraße 1",
plz="46499",
ort="Hamminkeln",
),
FakeObj(
anrede="Herr",
vorname="Hans",
nachname="Beispiel",
strasse="Beispielweg 7",
plz="46499",
ort="Hamminkeln",
),
]
return base