Plugin export: Listen als CSV (Excel-DE) und PDF (WeasyPrint)

- CSV über stdlib csv: Semikolon-getrennt, UTF-8 mit BOM, korrektes
  Quoting; PDF über WeasyPrint mit deutscher Kopfzeile (je Seite
  wiederholt), Erstell-Datum und Seitenzahlen (Seite X von Y)
- Drei Listen: Neuheiten, Planung, Archiv (Neuheiten gefiltert auf
  Status „archiviert“); Partner-Tabellen über gemeinsame Metadata,
  ohne Plugin-Importe — fehlende Plugins degradieren sauber
- Rollen: Admin/Redakteur alles, Rezensent nur Planungsliste;
  serverseitige Prüfung an den Download-Endpunkten (StreamingResponse),
  UI-Karten für nicht erlaubte Listen ausgegraut
- Jeder Download best effort im Audit-Log (Aktion „exportiert“)
- WeasyPrint lazy importiert: ohne System-Bibliotheken bleiben App und
  CSV nutzbar; Tests skippen PDF-Fälle nach Installationsversuch
- 20 neue Tests (CSV-Inhalt/BOM/Umlaute/Quoting, PDF-Gültigkeit,
  Rollen, Audit, Degradierung); 161 gesamt grün
- README: Plugin-Dokumentation, Fortschritt export auf 
This commit is contained in:
Flo Hartmann
2026-08-21 20:30:19 +00:00
parent 0f12cd952b
commit d024f42f3c
9 changed files with 1248 additions and 25 deletions

View File

@@ -2,9 +2,60 @@
{% block titel %}{{ titel }} — Spiele-Redaktion{% endblock %}
{% block inhalt %}
<h1 class="text-2xl font-bold mb-2">{{ titel }}</h1>
<p class="text-slate-600 max-w-2xl">
Plugin <code class="bg-slate-200 rounded px-1 py-0.5 text-sm">{{ name }}</code>
in Version {{ version }} ist geladen.
Diese Seite ist ein Platzhalter &mdash; die Funktion wird in einer sp&auml;teren Phase implementiert.
<p class="text-slate-600 mb-4 max-w-3xl">
Redaktionslisten als Datei herunterladen:
<strong>CSV</strong> (Semikolon-getrennt, UTF-8 mit BOM &mdash; direkt in Excel
&ouml;ffnbar) oder <strong>PDF</strong> (formatierte Tabelle mit Datum und
Seitenzahlen). Admins und Redakteure k&ouml;nnen alle Listen exportieren;
Rezensentinnen und Rezensenten die Planungsliste.
</p>
{% if fehler %}
<div class="mb-4 px-4 py-3 rounded-lg border bg-red-50 border-red-200 text-red-700 text-sm">{{ fehler }}</div>
{% endif %}
<div class="grid gap-4 md:grid-cols-3 mb-6">
{% for karte in karten %}
<div class="bg-white border border-slate-200 rounded-xl shadow-sm p-4 flex flex-col
{% if not karte.verfuegbar %}opacity-60{% endif %}">
<div class="flex items-start justify-between mb-2">
<h2 class="font-semibold text-slate-800">{{ karte.titel }}</h2>
{% if karte.verfuegbar %}
<span class="text-xs text-slate-500 bg-slate-100 rounded-full px-2 py-0.5 whitespace-nowrap">{{ karte.anzahl }} Eintr&auml;ge</span>
{% else %}
<span class="text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded-full px-2 py-0.5 whitespace-nowrap">Nicht verf&uuml;gbar</span>
{% endif %}
</div>
<p class="text-sm text-slate-600 mb-4 flex-1">{{ karte.beschreibung }}</p>
{% if karte.erlaubt %}
{% if karte.verfuegbar %}
<div class="flex gap-2">
<a href="/export/{{ karte.key }}/csv"
class="flex-1 text-center bg-emerald-600 hover:bg-emerald-700 text-white font-medium px-3 py-2 rounded-lg text-sm">
CSV herunterladen
</a>
<a href="/export/{{ karte.key }}/pdf"
class="flex-1 text-center bg-slate-700 hover:bg-slate-800 text-white font-medium px-3 py-2 rounded-lg text-sm">
PDF herunterladen
</a>
</div>
{% else %}
<p class="text-xs text-slate-500">Das zugeh&ouml;rige Plugin ist nicht geladen &mdash; kein Export m&ouml;glich.</p>
{% endif %}
{% else %}
<p class="text-xs text-slate-400 italic">Nur f&uuml;r Admins und Redakteure &mdash; du hast hier keinen Export-Zugriff.</p>
{% endif %}
</div>
{% endfor %}
</div>
<div class="bg-white border border-slate-200 rounded-xl shadow-sm p-4 text-sm text-slate-600">
<h3 class="font-semibold text-slate-800 mb-1">Hinweise zu den Formaten</h3>
<ul class="list-disc list-inside space-y-1">
<li><strong>CSV:</strong> Semikolon als Trennzeichen (Excel mit deutschem Gebietsschema), UTF-8 mit Byte-Order-Mark &mdash; Umlaute und &szlig; bleiben erhalten.</li>
<li><strong>PDF:</strong> A4, deutsche Kopfzeile auf jeder Seite, Erstell-Datum und Seitenzahlen („Seite X von Y“).</li>
<li>Jeder Download wird im <a href="/audit-log" class="text-emerald-700 underline">Audit-Log</a> protokolliert (nur Admins einsehbar).</li>
</ul>
</div>
{% endblock %}