"""SuS-Profil-Routen: Login, Cockpit (progressiv), Check-In, Lektion-Status, Paarung. Etappe 1: Login-Flow. Etappe 2: Cockpit progressiv (V1), Check-In (V3 + E-021), Lektion-Status, Paarung. Etappe 6: Projekt-Workflow. Etappe 7: Druckauftraege + Slots. """ from datetime import date from flask import ( Blueprint, render_template, request, redirect, url_for, session, flash, abort, jsonify ) from database import get_db from services.auth import ( pin_pruefen, ist_gesperrt, fehlversuch_loggen, lockout_zuruecksetzen, login_required, ) from services.cockpit_status import ( get_cockpit_status, get_andere_profile, ONBOARDING_LEKTIONEN, ) from tiere import tier_emoji, tier_name bp = Blueprint("profil", __name__) # =========================================================================== # Login-Flow (Etappe 1, unveraendert) # =========================================================================== @bp.route("/login") def login(): conn = get_db() profile = conn.execute( "SELECT id, vorname, tier FROM profil WHERE aktiv = 1 " "ORDER BY vorname COLLATE NOCASE, tier" ).fetchall() conn.close() return render_template("login.html", profile=profile) @bp.route("/login/", methods=["GET", "POST"]) def login_pin(profil_id): conn = get_db() profil = conn.execute( "SELECT id, vorname, tier, pin_hash, aktiv FROM profil WHERE id = ?", (profil_id,) ).fetchone() conn.close() if not profil or not profil["aktiv"]: flash("Profil nicht gefunden.", "error") return redirect(url_for("profil.login")) gesperrt, sek_rest = ist_gesperrt(profil_id) if gesperrt: min_rest = sek_rest // 60 + 1 return render_template( "login_pin.html", profil=profil, gesperrt=True, min_rest=min_rest ) if request.method == "POST": eingabe_pin = (request.form.get("pin") or "").strip() if pin_pruefen(eingabe_pin, profil["pin_hash"]): lockout_zuruecksetzen(profil_id) session.clear() session["profil_id"] = profil["id"] session["vorname"] = profil["vorname"] session["tier"] = profil["tier"] return redirect(url_for("profil.cockpit")) else: fehlversuch_loggen(profil_id) flash("Falsche PIN. Versuch's nochmal.", "error") return render_template("login_pin.html", profil=profil, gesperrt=False) @bp.route("/logout") def logout(): session.clear() flash("Du bist ausgeloggt.", "success") return redirect(url_for("oeffentlich.index")) # =========================================================================== # Cockpit (Etappe 2: progressives Layout V1) # =========================================================================== @bp.route("/cockpit") @login_required def cockpit(): status = get_cockpit_status(session["profil_id"]) andere_profile = [] if not status["paarung"]: andere_profile = get_andere_profile(session["profil_id"]) return render_template( "cockpit.html", status=status, andere_profile=andere_profile, vorname=session.get("vorname"), tier=session.get("tier"), ) # =========================================================================== # Check-In (V3 + E-021) # =========================================================================== @bp.route("/cockpit/check-in", methods=["POST"]) @login_required def check_in(): modus = (request.form.get("modus") or "").strip() freitext = (request.form.get("freitext") or "").strip() or None # Modus validieren — einfach, kein striktes ENUM erlaubte_modi = { "weiter_wie_bisher", "neues_projekt", "onboarding", "drucken", "helfen", "sonstiges" } if modus not in erlaubte_modi: flash("Ungueltige Auswahl.", "error") return redirect(url_for("profil.cockpit")) conn = get_db() conn.execute( "INSERT OR REPLACE INTO check_in (profil_id, besuch_datum, modus, freitext) " "VALUES (?, ?, ?, ?)", (session["profil_id"], date.today().isoformat(), modus, freitext) ) conn.commit() conn.close() return redirect(url_for("profil.cockpit")) @bp.route("/cockpit/notiz//gelesen", methods=["POST"]) @login_required def notiz_gelesen(notiz_id): """Lehrer-Notiz als gelesen markieren (V6 + V5).""" conn = get_db() conn.execute( "UPDATE lehrer_notiz SET gelesen_am = CURRENT_TIMESTAMP " "WHERE id = ? AND profil_id = ? AND gelesen_am IS NULL", (notiz_id, session["profil_id"]) ) conn.commit() conn.close() return redirect(url_for("profil.cockpit")) # =========================================================================== # Onboarding-Lektion-Status (selbst-zertifiziert, E-019) # =========================================================================== @bp.route("/cockpit/lektion//status", methods=["POST"]) @login_required def lektion_status_setzen(nummer): """Status-Wechsel: offen -> laeuft -> fertig (oder zurueck via 'reset').""" neuer_status = (request.form.get("status") or "").strip() erlaubte = {"offen", "laeuft", "fertig"} if neuer_status not in erlaubte: flash("Ungueltiger Status.", "error") return redirect(url_for("profil.cockpit")) conn = get_db() # Pruefe ob Lektion existiert + onboarding ist (nicht jede Lektion soll selbst-zertifizierbar sein) lektion = conn.execute( "SELECT nummer FROM lektion WHERE nummer = ? AND aktiv = 1", (nummer,) ).fetchone() if not lektion: conn.close() flash("Lektion nicht gefunden.", "error") return redirect(url_for("profil.cockpit")) conn.execute( "INSERT INTO profil_lektion_status (profil_id, lektion_nummer, status) " "VALUES (?, ?, ?) " "ON CONFLICT(profil_id, lektion_nummer) DO UPDATE SET " " status = excluded.status, geaendert_am = CURRENT_TIMESTAMP", (session["profil_id"], nummer, neuer_status) ) conn.commit() conn.close() if neuer_status == "fertig": flash(f"L{nummer} als fertig markiert. 👍", "success") return redirect(url_for("profil.cockpit")) # =========================================================================== # Paarung (E-002): anfragen + bestaetigen + ablehnen + aufloesen # =========================================================================== # # Mechanik: # - Anfrage wird als profil_anfrage-Eintrag gespeichert (mit Status 'wartet'). # - Adressat sieht im Cockpit ein gelbes Banner "Mia moechte mit dir zusammenarbeiten — [Ja] [Nein]". # - Bei [Ja]: paarung-Eintrag wird angelegt (UNIQUE-Constraint verhindert Doppel-Paarungen). # - Bei [Nein]: profil_anfrage geht auf 'abgelehnt'. # - Bei Aufloesung: paarung.aktiv = 0. # # Tabelle profil_anfrage wird hier on-demand angelegt, weil wir das nicht # initial im Schema hatten. Das ist OK fuer Etappe 2 — falls die Paarung # spaeter komplexer wird, migrieren wir. def _ensure_anfrage_tabelle(): """Idempotent — legt profil_anfrage an, falls noch nicht da.""" conn = get_db() conn.execute(""" CREATE TABLE IF NOT EXISTS profil_anfrage ( id INTEGER PRIMARY KEY AUTOINCREMENT, von_profil_id INTEGER NOT NULL, an_profil_id INTEGER NOT NULL, status TEXT DEFAULT 'wartet', erstellt_am DATETIME DEFAULT CURRENT_TIMESTAMP, beantwortet_am DATETIME, FOREIGN KEY (von_profil_id) REFERENCES profil(id) ON DELETE CASCADE, FOREIGN KEY (an_profil_id) REFERENCES profil(id) ON DELETE CASCADE ) """) conn.commit() conn.close() _ensure_anfrage_tabelle() @bp.route("/cockpit/paarung/anfragen/", methods=["POST"]) @login_required def paarung_anfragen(andere_profil_id): von = session["profil_id"] if von == andere_profil_id: flash("Du kannst dich nicht mit dir selbst paaren.", "error") return redirect(url_for("profil.cockpit")) conn = get_db() # Pruefen: bin ich oder die andere Person schon in einer aktiven Paarung? schon_gepaart = conn.execute( "SELECT id FROM paarung WHERE aktiv = 1 AND ? IN (profil_a, profil_b)", (von,) ).fetchone() if schon_gepaart: conn.close() flash("Du bist schon in einer aktiven Paarung — loese sie erst auf.", "error") return redirect(url_for("profil.cockpit")) andere_gepaart = conn.execute( "SELECT id FROM paarung WHERE aktiv = 1 AND ? IN (profil_a, profil_b)", (andere_profil_id,) ).fetchone() if andere_gepaart: conn.close() flash("Die andere Person ist schon in einer Paarung.", "error") return redirect(url_for("profil.cockpit")) # Bereits offene Anfrage in die Richtung? schon_angefragt = conn.execute( "SELECT id FROM profil_anfrage WHERE status = 'wartet' " "AND ((von_profil_id = ? AND an_profil_id = ?) OR (von_profil_id = ? AND an_profil_id = ?))", (von, andere_profil_id, andere_profil_id, von) ).fetchone() if schon_angefragt: conn.close() flash("Es laeuft schon eine Anfrage zwischen euch.", "error") return redirect(url_for("profil.cockpit")) conn.execute( "INSERT INTO profil_anfrage (von_profil_id, an_profil_id) VALUES (?, ?)", (von, andere_profil_id) ) conn.commit() conn.close() flash("Anfrage geschickt. Die andere Person muss sie bestaetigen.", "success") return redirect(url_for("profil.cockpit")) @bp.route("/cockpit/paarung/anfrage//", methods=["POST"]) @login_required def paarung_anfrage_beantworten(anfrage_id, aktion): if aktion not in ("annehmen", "ablehnen"): abort(400) conn = get_db() anfrage = conn.execute( "SELECT id, von_profil_id, an_profil_id, status FROM profil_anfrage WHERE id = ?", (anfrage_id,) ).fetchone() if not anfrage or anfrage["an_profil_id"] != session["profil_id"] or anfrage["status"] != "wartet": conn.close() flash("Anfrage nicht gefunden oder schon beantwortet.", "error") return redirect(url_for("profil.cockpit")) if aktion == "annehmen": a, b = sorted([anfrage["von_profil_id"], anfrage["an_profil_id"]]) conn.execute( "INSERT INTO paarung (profil_a, profil_b, aktiv) VALUES (?, ?, 1)", (a, b) ) flash("Paarung gebildet — viel Erfolg zusammen!", "success") else: flash("Anfrage abgelehnt.", "success") conn.execute( "UPDATE profil_anfrage SET status = ?, beantwortet_am = CURRENT_TIMESTAMP WHERE id = ?", ("angenommen" if aktion == "annehmen" else "abgelehnt", anfrage_id) ) conn.commit() conn.close() return redirect(url_for("profil.cockpit")) @bp.route("/cockpit/paarung/aufloesen", methods=["POST"]) @login_required def paarung_aufloesen(): conn = get_db() conn.execute( "UPDATE paarung SET aktiv = 0 WHERE aktiv = 1 AND ? IN (profil_a, profil_b)", (session["profil_id"],) ) conn.commit() conn.close() flash("Paarung aufgeloest.", "success") return redirect(url_for("profil.cockpit")) # =========================================================================== # Stubs fuer spaetere Etappen # =========================================================================== @bp.route("/cockpit/slots") @login_required def slots(): abort(501) # Etappe 6/7 @bp.route("/cockpit/druckauftraege") @login_required def druckauftraege(): abort(501) # Etappe 7