"""Admin-Panel (Flask): Schema-Designer, Dropzone, Linkliste, Versionen. In Produktion liegt das Panel hinter Nginx Basic Auth; die oeffentlichen Seiten liefert Nginx direkt aus public/ aus. """ from __future__ import annotations import io import os from flask import ( Flask, abort, flash, redirect, render_template, request, send_file, url_for, ) from . import engine, storage app = Flask(__name__) # Secret nur fuer Flash-Messages; bei Neustart verfallende Session ist ok. app.secret_key = os.environ.get("QT_SECRET_KEY") or os.urandom(16) @app.get("/") def root(): return redirect(url_for("admin_index")) @app.get("/admin/") def admin_index(): return render_template( "admin/index.html", tables=storage.list_tables(), qr_svg=engine.qr_svg, ) @app.route("/admin/neu", methods=["GET", "POST"]) def admin_new(): if request.method == "GET": return render_template("admin/new.html", types=engine.TYPE_LABELS) title = request.form.get("title", "").strip() labels = [l.strip() for l in request.form.getlist("label")] types = request.form.getlist("type") columns = [] seen_keys = set() for label, ctype in zip(labels, types): if not label: continue if ctype not in engine.COLUMN_TYPES: ctype = "text" key = engine.column_key(label) while key in seen_keys: key += "_2" seen_keys.add(key) columns.append({"key": key, "label": label, "type": ctype}) error = None if not title: error = "Bitte einen Titel angeben." elif not columns: error = "Mindestens eine Spalte anlegen." else: slug = engine.slugify(title) if storage.load_schema(slug) is not None: error = f'Es gibt schon eine Tabelle mit dem Slug "{slug}".' if error: flash(error, "error") return render_template( "admin/new.html", types=engine.TYPE_LABELS, title=title, columns=columns, ) storage.save_schema({"slug": slug, "title": title, "columns": columns}) flash(f'Tabelle "{title}" angelegt. Jetzt die CSV-Vorlage herunterladen.', "ok") return redirect(url_for("admin_index")) @app.get("/admin/vorlage/") def admin_template(slug): schema = storage.load_schema(slug) if schema is None: abort(404) return send_file( io.BytesIO(engine.template_csv(schema)), mimetype="text/csv", as_attachment=True, download_name=f"{slug}.csv", ) @app.post("/admin/upload") def admin_upload(): file = request.files.get("csv") if file is None or not file.filename: flash("Keine Datei ausgewaehlt.", "error") return redirect(url_for("admin_index")) raw = file.read() header, _ = engine.parse_csv(raw) slug = request.form.get("slug", "").strip() if not slug: # automatisch erkennen slug = storage.find_table(file.filename, header) if not slug: flash( "Tabelle nicht erkannt — bitte im Auswahlfeld die Zieltabelle " "waehlen und erneut hochladen.", "error", ) return redirect(url_for("admin_index")) ok, errors, warnings, version = storage.publish_upload(slug, raw) for w in warnings: flash(w, "warn") if not ok: for e in errors[:10]: flash(e, "error") if len(errors) > 10: flash(f"... und {len(errors) - 10} weitere Fehler.", "error") flash("Nichts veroeffentlicht — bitte CSV korrigieren und erneut hochladen.", "error") else: flash( f'"{slug}" aktualisiert (Version {version}) und veroeffentlicht: ' f"{storage.public_url(slug)}", "ok", ) return redirect(url_for("admin_index")) @app.get("/admin/csv//") def admin_download(slug, filename): raw = storage.read_version(slug, filename) if raw is None: abort(404) return send_file( io.BytesIO(raw), mimetype="text/csv", as_attachment=True, download_name=f"{slug}_{filename}", ) @app.post("/admin/loeschen/") def admin_delete(slug): if storage.load_schema(slug) is None: abort(404) storage.delete_table(slug) flash(f'Tabelle "{slug}" samt Versionen geloescht.', "ok") return redirect(url_for("admin_index")) @app.get("//") def public_page(slug): """Nur fuer die lokale Entwicklung — in Produktion macht das Nginx.""" path = storage.PUBLIC_DIR / slug / "index.html" if not path.exists(): abort(404) return send_file(path.resolve()) if __name__ == "__main__": app.run(debug=True)