Neue Features>: zu viele um sie zu beschrieben :D
This commit is contained in:
202
backend/app.py
202
backend/app.py
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
import sqlite3
|
||||
import uuid
|
||||
from hmac import compare_digest
|
||||
from datetime import datetime
|
||||
from functools import wraps
|
||||
from pathlib import Path
|
||||
@@ -24,6 +25,7 @@ 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["HOST_PASSWORD"] = os.environ.get("HOST_PASSWORD", "gastgeber2026")
|
||||
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(
|
||||
@@ -36,6 +38,8 @@ app.config["GOOGLE_MAPS_EMBED_URL"] = os.environ.get(
|
||||
"GOOGLE_MAPS_EMBED_URL",
|
||||
"https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d0",
|
||||
)
|
||||
app.config["WEDDING_DATE"] = os.environ.get("WEDDING_DATE", "")
|
||||
app.config["HERO_IMAGE_FILENAME"] = os.environ.get("HERO_IMAGE_FILENAME")
|
||||
|
||||
ALLOWED_EXTENSIONS = {"jpg", "jpeg", "png"}
|
||||
|
||||
@@ -43,15 +47,18 @@ TEXTS = {
|
||||
"de": {
|
||||
"brand": "Svenja & Dominic",
|
||||
"subtitle": "Willkommen zu unserer Hochzeits-App",
|
||||
"login_note": "Passwortgeschuetzter Zugriff fuer unsere Gaeste.",
|
||||
"login": "Login",
|
||||
"name": "Dein Name",
|
||||
"event_password": "Event-Passwort",
|
||||
"login_submit": "Weiter zum Dashboard",
|
||||
"dashboard": "Dashboard",
|
||||
"login_submit": "Weiter zum Gaestebereich",
|
||||
"guest_area": "Gaestebereich",
|
||||
"hello_guest": "Hallo {name}.",
|
||||
"logout": "Abmelden",
|
||||
"rsvp": "RSVP",
|
||||
"upload": "Upload",
|
||||
"gallery": "Galerie",
|
||||
"host_area": "Gastgeberbereich",
|
||||
"info": "Infos",
|
||||
"save": "Speichern",
|
||||
"attending": "Ich komme",
|
||||
@@ -64,19 +71,60 @@ TEXTS = {
|
||||
"taxi": "Taxi",
|
||||
"location": "Location",
|
||||
"visit_location": "Zur Location-Webseite",
|
||||
"privacy": "Datenschutz",
|
||||
"imprint": "Impressum",
|
||||
"hero_headline": "Willkommen zu unserer Hochzeit",
|
||||
"hero_text": "Wir freuen uns riesig, diesen besonderen Tag mit euch zu feiern.",
|
||||
"to_guest_area": "Zum Gaestebereich",
|
||||
"schedule_text": "15:00 Trauung, 17:00 Empfang, 19:00 Dinner.",
|
||||
"hotels_text": "Empfehlungen folgen. Bitte fruehzeitig buchen.",
|
||||
"taxi_text": "Taxi-Service: 01234 / 567890 (24/7).",
|
||||
"gallery_uploaded_by": "von {name}",
|
||||
"gallery_empty": "Noch keine Bilder vorhanden.",
|
||||
"gallery_image_alt": "Upload von {name}",
|
||||
"flash_enter_name": "Bitte Namen eingeben.",
|
||||
"flash_invalid_password": "Ungueltiges Event-Passwort.",
|
||||
"flash_rsvp_select": "Bitte eine RSVP-Auswahl treffen.",
|
||||
"flash_rsvp_saved": "RSVP gespeichert.",
|
||||
"flash_select_image": "Bitte eine Bilddatei auswaehlen.",
|
||||
"flash_allowed_types": "Nur JPG/JPEG/PNG sind erlaubt.",
|
||||
"flash_upload_success": "Upload erfolgreich.",
|
||||
"flash_invalid_host_password": "Ungueltiges Gastgeber-Passwort.",
|
||||
"host_access_title": "Gastgeberbereich",
|
||||
"host_access_note": "Dieser Bereich ist nur fuer das Brautpaar vorgesehen.",
|
||||
"host_password": "Gastgeber-Passwort",
|
||||
"host_access_submit": "Adminbereich oeffnen",
|
||||
"host_stats_title": "Uebersicht",
|
||||
"total_guests": "Gaeste gesamt",
|
||||
"attending_yes": "Zusagen",
|
||||
"attending_no": "Absagen",
|
||||
"attending_open": "Noch offen",
|
||||
"plus_one_total": "Begleitpersonen",
|
||||
"host_table_name": "Name",
|
||||
"host_table_status": "RSVP",
|
||||
"host_table_plus_one": "Begleitperson",
|
||||
"status_yes": "Kommt",
|
||||
"status_no": "Kommt nicht",
|
||||
"status_open": "Offen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"legal_de_authoritative": "Bei rechtlichen Unklarheiten gilt die deutsche Fassung.",
|
||||
},
|
||||
"en": {
|
||||
"brand": "Svenja & Dominic",
|
||||
"subtitle": "Welcome to our wedding app",
|
||||
"login_note": "Password-protected access for our guests.",
|
||||
"login": "Login",
|
||||
"name": "Your name",
|
||||
"event_password": "Event password",
|
||||
"login_submit": "Open dashboard",
|
||||
"dashboard": "Dashboard",
|
||||
"login_submit": "Open guest area",
|
||||
"guest_area": "Guest Area",
|
||||
"hello_guest": "Hello {name}.",
|
||||
"logout": "Logout",
|
||||
"rsvp": "RSVP",
|
||||
"upload": "Upload",
|
||||
"gallery": "Gallery",
|
||||
"host_area": "Host Area",
|
||||
"info": "Info",
|
||||
"save": "Save",
|
||||
"attending": "I will attend",
|
||||
@@ -89,6 +137,44 @@ TEXTS = {
|
||||
"taxi": "Taxi",
|
||||
"location": "Location",
|
||||
"visit_location": "Visit location website",
|
||||
"privacy": "Privacy",
|
||||
"imprint": "Imprint",
|
||||
"hero_headline": "Welcome to our wedding",
|
||||
"hero_text": "We are so excited to celebrate this special day with you.",
|
||||
"to_guest_area": "Open guest area",
|
||||
"schedule_text": "3:00 PM ceremony, 5:00 PM reception, 7:00 PM dinner.",
|
||||
"hotels_text": "Recommendations will follow. Please book early.",
|
||||
"taxi_text": "Taxi service: 01234 / 567890 (24/7).",
|
||||
"gallery_uploaded_by": "by {name}",
|
||||
"gallery_empty": "No photos available yet.",
|
||||
"gallery_image_alt": "Uploaded by {name}",
|
||||
"flash_enter_name": "Please enter your name.",
|
||||
"flash_invalid_password": "Invalid event password.",
|
||||
"flash_rsvp_select": "Please choose an RSVP option.",
|
||||
"flash_rsvp_saved": "RSVP saved.",
|
||||
"flash_select_image": "Please select an image file.",
|
||||
"flash_allowed_types": "Only JPG/JPEG/PNG are allowed.",
|
||||
"flash_upload_success": "Upload successful.",
|
||||
"flash_invalid_host_password": "Invalid host password.",
|
||||
"host_access_title": "Host Area",
|
||||
"host_access_note": "This section is intended for the wedding hosts only.",
|
||||
"host_password": "Host password",
|
||||
"host_access_submit": "Open admin area",
|
||||
"host_stats_title": "Overview",
|
||||
"total_guests": "Total guests",
|
||||
"attending_yes": "Attending",
|
||||
"attending_no": "Declined",
|
||||
"attending_open": "Pending",
|
||||
"plus_one_total": "Plus-ones",
|
||||
"host_table_name": "Name",
|
||||
"host_table_status": "RSVP",
|
||||
"host_table_plus_one": "Plus-one",
|
||||
"status_yes": "Attending",
|
||||
"status_no": "Not attending",
|
||||
"status_open": "Pending",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"legal_de_authoritative": "In case of legal ambiguity, the German version shall prevail.",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -102,6 +188,22 @@ def t(key: str) -> str:
|
||||
return TEXTS[get_lang()].get(key, key)
|
||||
|
||||
|
||||
def get_hero_image_asset() -> str:
|
||||
assets_dir = base_dir / "static" / "assets"
|
||||
configured = app.config.get("HERO_IMAGE_FILENAME")
|
||||
|
||||
candidates = []
|
||||
if configured:
|
||||
candidates.append(configured)
|
||||
candidates.extend(["hero.jpg", "hero.jpeg", "hero.png", "image.png", "image-1.png"])
|
||||
|
||||
for filename in candidates:
|
||||
if (assets_dir / filename).is_file():
|
||||
return f"assets/{filename}"
|
||||
|
||||
return "assets/hero.jpg"
|
||||
|
||||
|
||||
@app.context_processor
|
||||
def inject_common() -> dict:
|
||||
return {
|
||||
@@ -112,6 +214,8 @@ def inject_common() -> dict:
|
||||
"location_address": app.config["LOCATION_ADDRESS"],
|
||||
"location_website_url": app.config["LOCATION_WEBSITE_URL"],
|
||||
"google_maps_embed_url": app.config["GOOGLE_MAPS_EMBED_URL"],
|
||||
"wedding_date": app.config["WEDDING_DATE"],
|
||||
"hero_image_url": url_for("static", filename=get_hero_image_asset()),
|
||||
}
|
||||
|
||||
|
||||
@@ -201,7 +305,7 @@ def health():
|
||||
@app.get("/")
|
||||
def landing():
|
||||
if "guest_id" in session:
|
||||
return redirect(url_for("dashboard"))
|
||||
return redirect(url_for("welcome"))
|
||||
return render_template("login.html")
|
||||
|
||||
|
||||
@@ -211,17 +315,18 @@ def login():
|
||||
event_password = request.form.get("event_password") or ""
|
||||
|
||||
if not name:
|
||||
flash("Bitte Namen eingeben.")
|
||||
flash(t("flash_enter_name"))
|
||||
return redirect(url_for("landing"))
|
||||
|
||||
if event_password != app.config["EVENT_PASSWORD"]:
|
||||
flash("Ungültiges Event-Passwort.")
|
||||
flash(t("flash_invalid_password"))
|
||||
return redirect(url_for("landing"))
|
||||
|
||||
guest_id = upsert_guest(name)
|
||||
session["guest_id"] = guest_id
|
||||
session["guest_name"] = name
|
||||
return redirect(url_for("dashboard"))
|
||||
session.pop("is_host", None)
|
||||
return redirect(url_for("welcome"))
|
||||
|
||||
|
||||
@app.post("/logout")
|
||||
@@ -237,10 +342,67 @@ def set_lang(code: str):
|
||||
return redirect(request.referrer or url_for("landing"))
|
||||
|
||||
|
||||
@app.get("/welcome")
|
||||
@login_required
|
||||
def welcome():
|
||||
return render_template("welcome.html")
|
||||
|
||||
|
||||
@app.get("/gaestebereich")
|
||||
@login_required
|
||||
def guest_area():
|
||||
return render_template("guest_area.html")
|
||||
|
||||
|
||||
@app.route("/gastgeberbereich", methods=["GET", "POST"])
|
||||
@login_required
|
||||
def host_area():
|
||||
if request.method == "POST":
|
||||
host_password = request.form.get("host_password") or ""
|
||||
expected = app.config.get("HOST_PASSWORD", "")
|
||||
if not expected or not compare_digest(host_password, expected):
|
||||
flash(t("flash_invalid_host_password"))
|
||||
return redirect(url_for("host_area"))
|
||||
session["is_host"] = True
|
||||
return redirect(url_for("host_area"))
|
||||
|
||||
if not session.get("is_host"):
|
||||
return render_template("host_area.html", unlocked=False)
|
||||
|
||||
db = get_db()
|
||||
stats_row = db.execute(
|
||||
"""
|
||||
SELECT
|
||||
COUNT(*) AS total_guests,
|
||||
SUM(CASE WHEN attending = 1 THEN 1 ELSE 0 END) AS attending_yes,
|
||||
SUM(CASE WHEN attending = 0 THEN 1 ELSE 0 END) AS attending_no,
|
||||
SUM(CASE WHEN attending IS NULL THEN 1 ELSE 0 END) AS attending_open,
|
||||
SUM(CASE WHEN attending = 1 AND plus_one = 1 THEN 1 ELSE 0 END) AS plus_one_total
|
||||
FROM guests
|
||||
"""
|
||||
).fetchone()
|
||||
guests = db.execute(
|
||||
"""
|
||||
SELECT name, attending, plus_one
|
||||
FROM guests
|
||||
ORDER BY name COLLATE NOCASE ASC
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
stats = {
|
||||
"total_guests": int(stats_row["total_guests"] or 0),
|
||||
"attending_yes": int(stats_row["attending_yes"] or 0),
|
||||
"attending_no": int(stats_row["attending_no"] or 0),
|
||||
"attending_open": int(stats_row["attending_open"] or 0),
|
||||
"plus_one_total": int(stats_row["plus_one_total"] or 0),
|
||||
}
|
||||
return render_template("host_area.html", unlocked=True, stats=stats, guests=guests)
|
||||
|
||||
|
||||
@app.get("/dashboard")
|
||||
@login_required
|
||||
def dashboard():
|
||||
return render_template("dashboard.html")
|
||||
return redirect(url_for("guest_area"))
|
||||
|
||||
|
||||
@app.route("/rsvp", methods=["GET", "POST"])
|
||||
@@ -253,7 +415,7 @@ def rsvp():
|
||||
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.")
|
||||
flash(t("flash_rsvp_select"))
|
||||
return redirect(url_for("rsvp"))
|
||||
|
||||
attending = 1 if attending_raw == "yes" else 0
|
||||
@@ -265,7 +427,7 @@ def rsvp():
|
||||
(attending, plus_one, session["guest_id"]),
|
||||
)
|
||||
db.commit()
|
||||
flash("RSVP gespeichert.")
|
||||
flash(t("flash_rsvp_saved"))
|
||||
return redirect(url_for("rsvp"))
|
||||
|
||||
guest = db.execute(
|
||||
@@ -282,11 +444,11 @@ def upload():
|
||||
if request.method == "POST":
|
||||
file = request.files.get("photo")
|
||||
if file is None or file.filename == "":
|
||||
flash("Bitte eine Bilddatei auswählen.")
|
||||
flash(t("flash_select_image"))
|
||||
return redirect(url_for("upload"))
|
||||
|
||||
if not is_allowed_file(file.filename):
|
||||
flash("Nur JPG/JPEG/PNG sind erlaubt.")
|
||||
flash(t("flash_allowed_types"))
|
||||
return redirect(url_for("upload"))
|
||||
|
||||
safe_name = secure_filename(file.filename)
|
||||
@@ -304,7 +466,7 @@ def upload():
|
||||
)
|
||||
db.commit()
|
||||
|
||||
flash("Upload erfolgreich.")
|
||||
flash(t("flash_upload_success"))
|
||||
return redirect(url_for("gallery"))
|
||||
|
||||
return render_template("upload.html")
|
||||
@@ -336,10 +498,20 @@ def serve_upload(filename: str):
|
||||
def info(page: str):
|
||||
allowed = {"schedule", "hotels", "taxi", "location"}
|
||||
if page not in allowed:
|
||||
return redirect(url_for("dashboard"))
|
||||
return redirect(url_for("guest_area"))
|
||||
return render_template("info.html", page=page)
|
||||
|
||||
|
||||
@app.get("/datenschutz")
|
||||
def datenschutz():
|
||||
return render_template("datenschutz.html")
|
||||
|
||||
|
||||
@app.get("/impressum")
|
||||
def impressum():
|
||||
return render_template("impressum.html")
|
||||
|
||||
|
||||
init_db()
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user