Files
qr-tabellen/app/main.py

175 lines
4.8 KiB
Python

"""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
# Static unter /admin/static, damit Nginx nur eine /admin-Location braucht.
app = Flask(__name__, static_url_path="/admin/static")
# 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/<slug>")
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/<slug>/<filename>")
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/<slug>")
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("/<slug>/")
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)