From 3dc091d04aa247388eccca63054e4b159c03bb2a Mon Sep 17 00:00:00 2001 From: team3 Date: Wed, 9 Sep 2026 18:15:10 +0200 Subject: [PATCH] update --- backend/app/features.py | 20 ++------ backend/app/main.py | 12 +++-- backend/app/suggestions.py | 72 ++++------------------------ backend/app/users.py | 57 ++++++++++++++++++++++ backend/app/watched.py | 38 +++++---------- backend/app/wishlist.py | 30 +++--------- frontend/src/App.vue | 3 +- frontend/src/components/WheelTab.vue | 26 ++-------- 8 files changed, 103 insertions(+), 155 deletions(-) diff --git a/backend/app/features.py b/backend/app/features.py index 008ddd5..6680381 100644 --- a/backend/app/features.py +++ b/backend/app/features.py @@ -6,6 +6,7 @@ Jeder darf aufschreiben, was er sich wünscht. Alle sehen alle Wünsche, from datetime import datetime, timezone +from . import users from .db import connect from .users import besitzer, gehoert_mir @@ -13,6 +14,7 @@ SCHEMA = """ CREATE TABLE IF NOT EXISTS features ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, + user_id INTEGER, text TEXT NOT NULL, created_at TEXT NOT NULL ); @@ -21,23 +23,10 @@ CREATE TABLE IF NOT EXISTS features ( MAX_LAENGE = 500 -def _spalte_user_id(conn, tabelle): - """Nachrüsten und aus der Sitzung füllen — einmalig je Datenbank.""" - spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")} - if "user_id" in spalten: - return - conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER") - conn.execute( - f"UPDATE {tabelle} SET user_id = (" - " SELECT user_id FROM user_sessions s WHERE s.session_id = " - f" {tabelle}.session_id)" - ) - - def init(): with connect() as conn: conn.executescript(SCHEMA) - _spalte_user_id(conn, "features") + users.user_id_nachruesten(conn, "features") def alle(session_id): @@ -45,8 +34,7 @@ def alle(session_id): with connect() as conn: rows = conn.execute( "SELECT f.*, u.name FROM features f " - "LEFT JOIN user_sessions s ON s.session_id = f.session_id " - "LEFT JOIN users u ON u.id = s.user_id " + "LEFT JOIN users u ON u.id = f.user_id " "ORDER BY f.created_at DESC" ).fetchall() ich = besitzer(session_id) diff --git a/backend/app/main.py b/backend/app/main.py index c56699a..cb1cd19 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -80,9 +80,9 @@ async def guarded_sync(): @app.on_event("startup") async def startup(): db.init() + users.init() suggestions.init() watched.init() - users.init() wishlist.init() features.init() scheduler.add_job(guarded_sync, "cron", hour=4, minute=30, id="daily-sync") @@ -294,13 +294,15 @@ def letzter(): def liste_vorschlaege(response: Response, filmabend_sid: str | None = Cookie(None)): sid = sitzung(response, filmabend_sid) eintraege = suggestions.alle() + ich = users.besitzer(sid) return { "items": [ - {k: v for k, v in e.items() if k != "session_id"} for e in eintraege + {k: v for k, v in e.items() if k not in ("session_id", "user_id")} + for e in eintraege ], # Damit die Oberfläche den eigenen Vorschlag hervorheben kann "mine": next( - (e["id"] for e in eintraege if e["session_id"] == sid), + (e["id"] for e in eintraege if users.gehoert_mir(e, sid, ich)), None, ), } @@ -317,7 +319,9 @@ async def vorschlagen( # Ein zweiter Vorschlag ersetzt den ersten, statt abgelehnt zu werden eintrag = suggestions.ersetzen(sid, movie, users.name_von(sid)) events.senden("suggestions") - return {k: v for k, v in eintrag.items() if k != "session_id"} + return { + k: v for k, v in eintrag.items() if k not in ("session_id", "user_id") + } @app.delete("/api/suggestions") diff --git a/backend/app/suggestions.py b/backend/app/suggestions.py index 0135230..124de96 100644 --- a/backend/app/suggestions.py +++ b/backend/app/suggestions.py @@ -14,13 +14,15 @@ import sqlite3 import uuid from datetime import datetime, timezone +from . import users from .db import connect -from .users import besitzer +from .users import WEM, besitzer SCHEMA = """ CREATE TABLE IF NOT EXISTS suggestions ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, + user_id INTEGER, movie_id INTEGER NOT NULL, title TEXT NOT NULL, year INTEGER, @@ -38,67 +40,11 @@ COOKIE = "filmabend_sid" DREH_DAUER = 4.2 -# Ein Vorschlag je Person. Früher stand das UNIQUE auf der Sitzung — dann -# blockierte der Vorschlag des einen Namens den des nächsten, sobald beide -# denselben Browser benutzten. -INDIZES = """ -CREATE UNIQUE INDEX IF NOT EXISTS suggestions_je_benutzer - ON suggestions (user_id) WHERE user_id IS NOT NULL; -CREATE UNIQUE INDEX IF NOT EXISTS suggestions_je_sitzung - ON suggestions (session_id) WHERE user_id IS NULL; -""" - - -def _unique_loesen(conn): - """Das alte UNIQUE steckt in der Spaltendefinition — Tabelle neu bauen.""" - sql = conn.execute( - "SELECT sql FROM sqlite_master WHERE type='table' AND name='suggestions'" - ).fetchone() - if not sql or "UNIQUE" not in sql["sql"]: - return - conn.executescript( - """ - ALTER TABLE suggestions RENAME TO suggestions_alt; - CREATE TABLE suggestions ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT NOT NULL, - user_id INTEGER, - movie_id INTEGER NOT NULL, - title TEXT NOT NULL, - year INTEGER, - poster_path TEXT, - name TEXT, - created_at TEXT NOT NULL - ); - INSERT INTO suggestions - (id, session_id, user_id, movie_id, title, year, poster_path, - name, created_at) - SELECT id, session_id, user_id, movie_id, title, year, poster_path, - name, created_at FROM suggestions_alt; - DROP TABLE suggestions_alt; - """ - ) - - -def _spalte_user_id(conn, tabelle): - """Nachrüsten und aus der Sitzung füllen — einmalig je Datenbank.""" - spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")} - if "user_id" in spalten: - return - conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER") - conn.execute( - f"UPDATE {tabelle} SET user_id = (" - " SELECT user_id FROM user_sessions s WHERE s.session_id = " - f" {tabelle}.session_id)" - ) - - def init(): with connect() as conn: conn.executescript(SCHEMA) - _spalte_user_id(conn, "suggestions") - _unique_loesen(conn) - conn.executescript(INDIZES) + # Ein Vorschlag je Person + users.je_person(conn, "suggestions", SCHEMA) def neue_session(): @@ -123,10 +69,6 @@ def alle(): ] -# Mit Namen zählt der Name, ohne Namen die Sitzung -WEM = "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))" - - def von_session(session_id): """Der eigene Vorschlag — auch aus einem anderen Browser.""" ich = besitzer(session_id) @@ -191,7 +133,9 @@ def ziehen(): "winkel": winkel, "dauer": DREH_DAUER, "gewinner": { - k: v for k, v in eintraege[treffer].items() if k != "session_id" + k: v + for k, v in eintraege[treffer].items() + if k not in ("session_id", "user_id") }, "anzahl": len(eintraege), } diff --git a/backend/app/users.py b/backend/app/users.py index bab57d6..ec683f2 100644 --- a/backend/app/users.py +++ b/backend/app/users.py @@ -128,6 +128,63 @@ def gehoert_mir(zeile, session_id, user_id): return zeile["session_id"] == session_id +# Dasselbe als WHERE-Klausel. Parameter: (user_id, user_id, session_id) +WEM = "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))" + + +def user_id_nachruesten(conn, tabelle): + """Spalte ergänzen und aus der Sitzung füllen — einmalig je Datenbank.""" + spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")} + if "user_id" in spalten: + return + conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER") + conn.execute( + f"UPDATE {tabelle} SET user_id = (" + " SELECT user_id FROM user_sessions s WHERE s.session_id = " + f" {tabelle}.session_id)" + ) + + +def je_person(conn, tabelle, schema, *schluessel): + """Eindeutig je Person statt je Sitzung. + + Früher stand das UNIQUE auf der Sitzung — dann blockierte der Eintrag + des einen Namens den des nächsten, sobald beide denselben Browser + benutzten. Mit Namen zählt jetzt der Name, ohne Namen die Sitzung. + + Das alte UNIQUE steckt in der Spaltendefinition und lässt sich nicht + ändern — die Tabelle wird dafür nach `schema` neu gebaut. Hat ein Name + aus mehreren Browsern doppelt eingetragen, bleibt der jüngste Eintrag. + """ + user_id_nachruesten(conn, tabelle) + alt = conn.execute( + "SELECT sql FROM sqlite_master WHERE type='table' AND name=?", (tabelle,) + ).fetchone() + if alt and "UNIQUE" in alt["sql"]: + spalten = ", ".join( + r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})") + ) + conn.executescript( + f"ALTER TABLE {tabelle} RENAME TO {tabelle}_alt;" + f"{schema}" + f"INSERT INTO {tabelle} ({spalten}) " + f"SELECT {spalten} FROM {tabelle}_alt;" + f"DROP TABLE {tabelle}_alt;" + ) + mit_name = ", ".join((*schluessel, "user_id")) + ohne_name = ", ".join((*schluessel, "session_id")) + conn.execute( + f"DELETE FROM {tabelle} WHERE user_id IS NOT NULL AND id NOT IN " + f"(SELECT MAX(id) FROM {tabelle} GROUP BY {mit_name})" + ) + conn.executescript( + f"CREATE UNIQUE INDEX IF NOT EXISTS {tabelle}_je_benutzer " + f"ON {tabelle} ({mit_name}) WHERE user_id IS NOT NULL;" + f"CREATE UNIQUE INDEX IF NOT EXISTS {tabelle}_je_sitzung " + f"ON {tabelle} ({ohne_name}) WHERE user_id IS NULL;" + ) + + def name_von(session_id): """Nur der Name, für Vorschläge und Wertungen.""" benutzer = aktuell(session_id) diff --git a/backend/app/watched.py b/backend/app/watched.py index b1658d7..1c32319 100644 --- a/backend/app/watched.py +++ b/backend/app/watched.py @@ -10,8 +10,9 @@ auch wenn der Film später aus dem Katalog fällt. from datetime import datetime, timezone +from . import users from .db import connect -from .users import besitzer, gehoert_mir +from .users import WEM, besitzer, gehoert_mir SCHEMA = """ CREATE TABLE IF NOT EXISTS watched ( @@ -23,38 +24,28 @@ CREATE TABLE IF NOT EXISTS watched ( watched_at TEXT NOT NULL, hidden INTEGER NOT NULL DEFAULT 0 ); +""" +RATINGS = """ CREATE TABLE IF NOT EXISTS ratings ( id INTEGER PRIMARY KEY AUTOINCREMENT, watched_id INTEGER NOT NULL, session_id TEXT NOT NULL, + user_id INTEGER, stars INTEGER NOT NULL, name TEXT, - created_at TEXT NOT NULL, - UNIQUE (watched_id, session_id) + created_at TEXT NOT NULL ); """ MAX_STERNE = 5 -def _spalte_user_id(conn, tabelle): - """Nachrüsten und aus der Sitzung füllen — einmalig je Datenbank.""" - spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")} - if "user_id" in spalten: - return - conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER") - conn.execute( - f"UPDATE {tabelle} SET user_id = (" - " SELECT user_id FROM user_sessions s WHERE s.session_id = " - f" {tabelle}.session_id)" - ) - - def init(): with connect() as conn: - conn.executescript(SCHEMA) - _spalte_user_id(conn, "ratings") + conn.executescript(SCHEMA + RATINGS) + # Eine Stimme je Person und Film + users.je_person(conn, "ratings", RATINGS, "watched_id") # Die Tabelle stand vor der Spalte schon in bestehenden Datenbanken spalten = {r["name"] for r in conn.execute("PRAGMA table_info(watched)")} if "hidden" not in spalten: @@ -165,21 +156,16 @@ def bewerten(watched_id, session_id, stars, name=None): ).fetchone() if not da: return None - # Eine Stimme je Person — die eigene alte fliegt vorher raus. - # Mit Namen zählt der Name, ohne Namen die Sitzung. + # Eine Stimme je Person — die eigene alte fliegt vorher raus ich = besitzer(session_id) conn.execute( - "DELETE FROM ratings WHERE watched_id = ? AND " - "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))", + f"DELETE FROM ratings WHERE watched_id = ? AND {WEM}", (watched_id, ich, ich, session_id), ) conn.execute( "INSERT INTO ratings " "(watched_id, session_id, user_id, stars, name, created_at) " - "VALUES (?, ?, ?, ?, ?, ?) " - "ON CONFLICT (watched_id, session_id) DO UPDATE SET " - "user_id = excluded.user_id, stars = excluded.stars, " - "name = excluded.name, created_at = excluded.created_at", + "VALUES (?, ?, ?, ?, ?, ?)", ( watched_id, session_id, diff --git a/backend/app/wishlist.py b/backend/app/wishlist.py index 33a1f87..62d5a53 100644 --- a/backend/app/wishlist.py +++ b/backend/app/wishlist.py @@ -12,40 +12,29 @@ import json import sqlite3 from datetime import datetime, timezone +from . import users from .db import connect -from .users import besitzer, gehoert_mir +from .users import WEM, besitzer, gehoert_mir SCHEMA = """ CREATE TABLE IF NOT EXISTS wishlist ( id INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, + user_id INTEGER, movie_id INTEGER NOT NULL, title TEXT NOT NULL, year INTEGER, poster_path TEXT, - created_at TEXT NOT NULL, - UNIQUE (session_id, movie_id) + created_at TEXT NOT NULL ); """ -def _spalte_user_id(conn, tabelle): - """Nachrüsten und aus der Sitzung füllen — einmalig je Datenbank.""" - spalten = {r[1] for r in conn.execute(f"PRAGMA table_info({tabelle})")} - if "user_id" in spalten: - return - conn.execute(f"ALTER TABLE {tabelle} ADD COLUMN user_id INTEGER") - conn.execute( - f"UPDATE {tabelle} SET user_id = (" - " SELECT user_id FROM user_sessions s WHERE s.session_id = " - f" {tabelle}.session_id)" - ) - - def init(): with connect() as conn: conn.executescript(SCHEMA) - _spalte_user_id(conn, "wishlist") + # Jeder Film einmal je Person + users.je_person(conn, "wishlist", SCHEMA, "movie_id") def alle(session_id): @@ -55,8 +44,7 @@ def alle(session_id): # Genres kommen aus dem Katalog. Ein Film, der nie darin stand, # hat keine — das Rad behandelt ihn dann als "nicht Horror". "SELECT w.*, u.name, m.genres FROM wishlist w " - "LEFT JOIN user_sessions s ON s.session_id = w.session_id " - "LEFT JOIN users u ON u.id = s.user_id " + "LEFT JOIN users u ON u.id = w.user_id " "LEFT JOIN movies m ON m.id = w.movie_id " "ORDER BY w.created_at DESC" ).fetchall() @@ -98,10 +86,6 @@ def merken(session_id, movie): return alle(session_id) -# Mit Namen zählt der Name, ohne Namen die Sitzung -WEM = "(user_id = ? OR (? IS NULL AND user_id IS NULL AND session_id = ?))" - - def vergessen(session_id, movie_id): ich = besitzer(session_id) with connect() as conn: diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c9f21b9..5d955ef 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -694,7 +694,8 @@ onUnmounted(() => { v-else-if="activeTab === 'wheel'" ref="rad" :host="host" - :ich="ich" + :merkliste="merkliste" + :mine="meinVorschlag?.id ?? null" @open="selected = $event" @reset="meinVorschlag = null" /> diff --git a/frontend/src/components/WheelTab.vue b/frontend/src/components/WheelTab.vue index 64ec8d9..aa1f82e 100644 --- a/frontend/src/components/WheelTab.vue +++ b/frontend/src/components/WheelTab.vue @@ -1,17 +1,17 @@