Testversion 1
This commit is contained in:
367
backend/app.py
367
backend/app.py
@@ -1,67 +1,346 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from flask import Flask, request, jsonify
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
|
||||
from flask import (
|
||||
Flask,
|
||||
flash,
|
||||
g,
|
||||
redirect,
|
||||
render_template,
|
||||
request,
|
||||
send_from_directory,
|
||||
session,
|
||||
url_for,
|
||||
)
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
app = Flask(__name__)
|
||||
base_dir = Path(__file__).resolve().parent
|
||||
app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "change-me-in-production")
|
||||
app.config["DB_PATH"] = os.environ.get("DB_PATH", str(base_dir / "app.sqlite3"))
|
||||
app.config["UPLOAD_FOLDER"] = os.environ.get("UPLOAD_FOLDER", str(base_dir / "uploads"))
|
||||
app.config["EVENT_PASSWORD"] = os.environ.get("EVENT_PASSWORD", "wedding2026")
|
||||
app.config["MAX_CONTENT_LENGTH"] = int(os.environ.get("MAX_UPLOAD_BYTES", str(8 * 1024 * 1024)))
|
||||
app.config["LOCATION_NAME"] = os.environ.get("LOCATION_NAME", "Schlossgarten Venue")
|
||||
app.config["LOCATION_ADDRESS"] = os.environ.get(
|
||||
"LOCATION_ADDRESS", "Musterstraße 12, 12345 Musterstadt"
|
||||
)
|
||||
app.config["LOCATION_WEBSITE_URL"] = os.environ.get(
|
||||
"LOCATION_WEBSITE_URL", "https://example.com/location"
|
||||
)
|
||||
app.config["GOOGLE_MAPS_EMBED_URL"] = os.environ.get(
|
||||
"GOOGLE_MAPS_EMBED_URL",
|
||||
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d0",
|
||||
)
|
||||
|
||||
DB_PATH = os.environ.get("DB_PATH", "/app/db/app.sqlite3")
|
||||
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png"}
|
||||
|
||||
def get_db():
|
||||
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
TEXTS = {
|
||||
"de": {
|
||||
"brand": "Svenja & Dominic",
|
||||
"subtitle": "Willkommen zu unserer Hochzeits-App",
|
||||
"login": "Login",
|
||||
"name": "Dein Name",
|
||||
"event_password": "Event-Passwort",
|
||||
"login_submit": "Weiter zum Dashboard",
|
||||
"dashboard": "Dashboard",
|
||||
"logout": "Abmelden",
|
||||
"rsvp": "RSVP",
|
||||
"upload": "Upload",
|
||||
"gallery": "Galerie",
|
||||
"info": "Infos",
|
||||
"save": "Speichern",
|
||||
"attending": "Ich komme",
|
||||
"not_attending": "Ich komme nicht",
|
||||
"plus_one": "Ich bringe eine Begleitperson mit",
|
||||
"file": "Bild auswählen",
|
||||
"upload_submit": "Foto hochladen",
|
||||
"schedule": "Ablauf",
|
||||
"hotels": "Hotels",
|
||||
"taxi": "Taxi",
|
||||
"location": "Location",
|
||||
"visit_location": "Zur Location-Webseite",
|
||||
},
|
||||
"en": {
|
||||
"brand": "Svenja & Dominic",
|
||||
"subtitle": "Welcome to our wedding app",
|
||||
"login": "Login",
|
||||
"name": "Your name",
|
||||
"event_password": "Event password",
|
||||
"login_submit": "Open dashboard",
|
||||
"dashboard": "Dashboard",
|
||||
"logout": "Logout",
|
||||
"rsvp": "RSVP",
|
||||
"upload": "Upload",
|
||||
"gallery": "Gallery",
|
||||
"info": "Info",
|
||||
"save": "Save",
|
||||
"attending": "I will attend",
|
||||
"not_attending": "I cannot attend",
|
||||
"plus_one": "I will bring a plus-one",
|
||||
"file": "Select image",
|
||||
"upload_submit": "Upload photo",
|
||||
"schedule": "Schedule",
|
||||
"hotels": "Hotels",
|
||||
"taxi": "Taxi",
|
||||
"location": "Location",
|
||||
"visit_location": "Visit location website",
|
||||
},
|
||||
}
|
||||
|
||||
def init_db():
|
||||
with get_db() as conn:
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS rsvps (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
attending INTEGER NOT NULL, -- 1 = ja, 0 = nein
|
||||
plus_one INTEGER NOT NULL DEFAULT 0, -- 1 = ja, 0 = nein
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
""")
|
||||
|
||||
def get_lang() -> str:
|
||||
lang = session.get("lang", "de")
|
||||
return lang if lang in TEXTS else "de"
|
||||
|
||||
|
||||
def t(key: str) -> str:
|
||||
return TEXTS[get_lang()].get(key, key)
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_common() -> dict:
|
||||
return {
|
||||
"t": t,
|
||||
"lang": get_lang(),
|
||||
"guest_name": session.get("guest_name"),
|
||||
"location_name": app.config["LOCATION_NAME"],
|
||||
"location_address": app.config["LOCATION_ADDRESS"],
|
||||
"location_website_url": app.config["LOCATION_WEBSITE_URL"],
|
||||
"google_maps_embed_url": app.config["GOOGLE_MAPS_EMBED_URL"],
|
||||
}
|
||||
|
||||
|
||||
def get_db() -> sqlite3.Connection:
|
||||
db_path = app.config["DB_PATH"]
|
||||
os.makedirs(os.path.dirname(db_path), exist_ok=True)
|
||||
|
||||
if "db" not in g:
|
||||
g.db = sqlite3.connect(db_path)
|
||||
g.db.row_factory = sqlite3.Row
|
||||
return g.db
|
||||
|
||||
|
||||
@app.teardown_appcontext
|
||||
def close_db(_error) -> None:
|
||||
db = g.pop("db", None)
|
||||
if db is not None:
|
||||
db.close()
|
||||
|
||||
|
||||
def init_db() -> None:
|
||||
db_path = app.config["DB_PATH"]
|
||||
db_dir = os.path.dirname(db_path)
|
||||
if db_dir:
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
|
||||
with sqlite3.connect(db_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS guests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
attending INTEGER,
|
||||
plus_one INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TEXT NOT NULL
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS uploads (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
filename TEXT NOT NULL,
|
||||
uploaded_by INTEGER NOT NULL,
|
||||
uploaded_at TEXT NOT NULL,
|
||||
FOREIGN KEY(uploaded_by) REFERENCES guests(id)
|
||||
)
|
||||
"""
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
|
||||
def login_required(view):
|
||||
@wraps(view)
|
||||
def wrapped(*args, **kwargs):
|
||||
if "guest_id" not in session:
|
||||
return redirect(url_for("landing"))
|
||||
return view(*args, **kwargs)
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
def is_allowed_file(filename: str) -> bool:
|
||||
return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
|
||||
|
||||
|
||||
def upsert_guest(name: str) -> int:
|
||||
now = datetime.utcnow().isoformat()
|
||||
db = get_db()
|
||||
row = db.execute("SELECT id FROM guests WHERE name = ?", (name,)).fetchone()
|
||||
if row:
|
||||
return int(row["id"])
|
||||
|
||||
cursor = db.execute(
|
||||
"INSERT INTO guests (name, created_at) VALUES (?, ?)",
|
||||
(name, now),
|
||||
)
|
||||
db.commit()
|
||||
return int(cursor.lastrowid)
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
|
||||
@app.post("/api/rsvp")
|
||||
def create_rsvp():
|
||||
data = request.get_json(force=True, silent=True) or {}
|
||||
name = (data.get("name") or "").strip()
|
||||
attending = data.get("attending")
|
||||
plus_one = data.get("plus_one", 0)
|
||||
|
||||
@app.get("/")
|
||||
def landing():
|
||||
if "guest_id" in session:
|
||||
return redirect(url_for("dashboard"))
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@app.post("/login")
|
||||
def login():
|
||||
name = (request.form.get("name") or "").strip()
|
||||
event_password = request.form.get("event_password") or ""
|
||||
|
||||
if not name:
|
||||
return jsonify({"error": "name is required"}), 400
|
||||
if attending not in (0, 1, True, False):
|
||||
return jsonify({"error": "attending must be 0/1"}), 400
|
||||
flash("Bitte Namen eingeben.")
|
||||
return redirect(url_for("landing"))
|
||||
|
||||
attending_int = 1 if attending in (1, True) else 0
|
||||
plus_one_int = 1 if plus_one in (1, True) else 0
|
||||
if event_password != app.config["EVENT_PASSWORD"]:
|
||||
flash("Ungültiges Event-Passwort.")
|
||||
return redirect(url_for("landing"))
|
||||
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"INSERT INTO rsvps (name, attending, plus_one, created_at) VALUES (?, ?, ?, ?)",
|
||||
(name, attending_int, plus_one_int, datetime.utcnow().isoformat())
|
||||
guest_id = upsert_guest(name)
|
||||
session["guest_id"] = guest_id
|
||||
session["guest_name"] = name
|
||||
return redirect(url_for("dashboard"))
|
||||
|
||||
|
||||
@app.post("/logout")
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for("landing"))
|
||||
|
||||
|
||||
@app.post("/lang/<code>")
|
||||
def set_lang(code: str):
|
||||
if code in TEXTS:
|
||||
session["lang"] = code
|
||||
return redirect(request.referrer or url_for("landing"))
|
||||
|
||||
|
||||
@app.get("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template("dashboard.html")
|
||||
|
||||
|
||||
@app.route("/rsvp", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def rsvp():
|
||||
db = get_db()
|
||||
|
||||
if request.method == "POST":
|
||||
attending_raw = request.form.get("attending")
|
||||
plus_one = 1 if request.form.get("plus_one") == "on" else 0
|
||||
|
||||
if attending_raw not in {"yes", "no"}:
|
||||
flash("Bitte eine RSVP-Auswahl treffen.")
|
||||
return redirect(url_for("rsvp"))
|
||||
|
||||
attending = 1 if attending_raw == "yes" else 0
|
||||
if not attending:
|
||||
plus_one = 0
|
||||
|
||||
db.execute(
|
||||
"UPDATE guests SET attending = ?, plus_one = ? WHERE id = ?",
|
||||
(attending, plus_one, session["guest_id"]),
|
||||
)
|
||||
conn.commit()
|
||||
db.commit()
|
||||
flash("RSVP gespeichert.")
|
||||
return redirect(url_for("rsvp"))
|
||||
|
||||
return jsonify({"ok": True})
|
||||
guest = db.execute(
|
||||
"SELECT attending, plus_one FROM guests WHERE id = ?",
|
||||
(session["guest_id"],),
|
||||
).fetchone()
|
||||
|
||||
@app.get("/api/rsvps")
|
||||
def list_rsvps():
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, name, attending, plus_one, created_at FROM rsvps ORDER BY id DESC"
|
||||
).fetchall()
|
||||
return jsonify([dict(r) for r in rows])
|
||||
return render_template("rsvp.html", guest=guest)
|
||||
|
||||
|
||||
@app.route("/upload", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def upload():
|
||||
if request.method == "POST":
|
||||
file = request.files.get("photo")
|
||||
if file is None or file.filename == "":
|
||||
flash("Bitte eine Bilddatei auswählen.")
|
||||
return redirect(url_for("upload"))
|
||||
|
||||
if not is_allowed_file(file.filename):
|
||||
flash("Nur JPG/JPEG/PNG sind erlaubt.")
|
||||
return redirect(url_for("upload"))
|
||||
|
||||
safe_name = secure_filename(file.filename)
|
||||
ext = safe_name.rsplit(".", 1)[1].lower()
|
||||
stored_name = f"{uuid.uuid4().hex}.{ext}"
|
||||
|
||||
upload_dir = app.config["UPLOAD_FOLDER"]
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
file.save(os.path.join(upload_dir, stored_name))
|
||||
|
||||
db = get_db()
|
||||
db.execute(
|
||||
"INSERT INTO uploads (filename, uploaded_by, uploaded_at) VALUES (?, ?, ?)",
|
||||
(stored_name, session["guest_id"], datetime.utcnow().isoformat()),
|
||||
)
|
||||
db.commit()
|
||||
|
||||
flash("Upload erfolgreich.")
|
||||
return redirect(url_for("gallery"))
|
||||
|
||||
return render_template("upload.html")
|
||||
|
||||
|
||||
@app.get("/gallery")
|
||||
@login_required
|
||||
def gallery():
|
||||
db = get_db()
|
||||
images = db.execute(
|
||||
"""
|
||||
SELECT uploads.filename, uploads.uploaded_at, guests.name AS uploaded_by
|
||||
FROM uploads
|
||||
JOIN guests ON guests.id = uploads.uploaded_by
|
||||
ORDER BY uploads.id DESC
|
||||
"""
|
||||
).fetchall()
|
||||
return render_template("gallery.html", images=images)
|
||||
|
||||
|
||||
@app.get("/uploads/<path:filename>")
|
||||
@login_required
|
||||
def serve_upload(filename: str):
|
||||
return send_from_directory(app.config["UPLOAD_FOLDER"], filename)
|
||||
|
||||
|
||||
@app.get("/info/<page>")
|
||||
@login_required
|
||||
def info(page: str):
|
||||
allowed = {"schedule", "hotels", "taxi", "location"}
|
||||
if page not in allowed:
|
||||
return redirect(url_for("dashboard"))
|
||||
return render_template("info.html", page=page)
|
||||
|
||||
|
||||
init_db()
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_db()
|
||||
app.run(host="0.0.0.0", port=5000)
|
||||
app.run(host="0.0.0.0", port=8000)
|
||||
|
||||
Reference in New Issue
Block a user