Files
stiftung-management-system/app/stiftung/views/vorlagen.py
SysAdmin Agent fe2c657586
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
Fix Vorlagen editor: drop Summernote, use code editor for all templates (STI-82)
Summernote WYSIWYG was mangling Django template syntax ({{ }}, {% %})
on save, causing content to revert to corrupted state. Switched all
template types to the plain code editor textarea which preserves
content exactly as-is.

Also removed jQuery/Summernote JS dependencies from the editor page,
and fixed getEditorContent reference in preview code.

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

211 lines
7.1 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")
# All templates contain Django template tags ({{ }}, {% %}) that
# Summernote WYSIWYG mangles on save. Use plain code editor for all.
use_code_editor = True
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