174 lines
4.7 KiB
Python
174 lines
4.7 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
|
|
|
|
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/<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)
|