270 lines
9.1 KiB
Python
270 lines
9.1 KiB
Python
"""Die Runde: wer mitmacht, und wer gerade wer ist.
|
|
|
|
Der Host pflegt die Namensliste. Alle anderen wählen sich einen davon aus.
|
|
|
|
Wer nicht will, dass ein anderer in seinem Namen bewertet, hinterlegt in
|
|
der Verwaltung einen Anmeldefilm. Dann muss man ihn beim Wechsel erst in
|
|
der Suche finden und anklicken. Kein Passwort im Klartext, nichts zu
|
|
merken außer einem Film — und für einen Filmabend unter Freunden genau die
|
|
richtige Hürde. Wer keinen Film hinterlegt, ist weiter offen für alle.
|
|
|
|
Die Zuordnung hängt an derselben Cookie-Sitzung wie Vorschläge und
|
|
Wertungen. Damit steht der Name serverseitig fest und kommt nicht mehr aus
|
|
einem Textfeld, in das jeder schreiben kann, was er will.
|
|
"""
|
|
|
|
import sqlite3
|
|
from datetime import datetime, timezone
|
|
|
|
from . import db
|
|
from .db import connect
|
|
|
|
SCHEMA = """
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
name TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS user_sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
user_id INTEGER NOT NULL,
|
|
seit TEXT NOT NULL
|
|
);
|
|
|
|
-- Wer den Hostmodus eingeschaltet hat. Der Host führt durch den Abend und
|
|
-- wechselt die Namen frei, ohne jedes Mal den Anmeldefilm zu suchen.
|
|
CREATE TABLE IF NOT EXISTS host_sessions (
|
|
session_id TEXT PRIMARY KEY,
|
|
seit TEXT NOT NULL
|
|
);
|
|
"""
|
|
|
|
|
|
def init():
|
|
with connect() as conn:
|
|
conn.executescript(SCHEMA)
|
|
# Der Anmeldefilm kam später dazu
|
|
spalten = {r[1] for r in conn.execute("PRAGMA table_info(users)")}
|
|
if "auth_movie_id" not in spalten:
|
|
conn.execute("ALTER TABLE users ADD COLUMN auth_movie_id INTEGER")
|
|
|
|
|
|
def _oeffentlich(row):
|
|
"""Der hinterlegte Film verlässt den Server nie — sonst stünde das
|
|
Passwort im Netzwerk-Tab. Nach draußen geht nur, ob einer gesetzt ist."""
|
|
benutzer = dict(row)
|
|
benutzer["geschuetzt"] = benutzer.pop("auth_movie_id", None) is not None
|
|
return benutzer
|
|
|
|
|
|
def alle():
|
|
with connect() as conn:
|
|
rows = conn.execute("SELECT * FROM users ORDER BY name COLLATE NOCASE").fetchall()
|
|
return [_oeffentlich(r) for r in rows]
|
|
|
|
|
|
def anlegen(name):
|
|
"""Gibt None zurück, wenn der Name schon vergeben ist."""
|
|
name = (name or "").strip()[:40]
|
|
if not name:
|
|
return None
|
|
try:
|
|
with connect() as conn:
|
|
conn.execute(
|
|
"INSERT INTO users (name, created_at) VALUES (?, ?)",
|
|
(name, datetime.now(timezone.utc).isoformat(timespec="seconds")),
|
|
)
|
|
except sqlite3.IntegrityError:
|
|
return None
|
|
return von_name(name)
|
|
|
|
|
|
def von_name(name):
|
|
with connect() as conn:
|
|
row = conn.execute("SELECT * FROM users WHERE name = ?", (name,)).fetchone()
|
|
return _oeffentlich(row) if row else None
|
|
|
|
|
|
def schutz_setzen(user_id, movie_id):
|
|
"""Anmeldefilm hinterlegen oder mit None wieder freigeben."""
|
|
with connect() as conn:
|
|
return conn.execute(
|
|
"UPDATE users SET auth_movie_id = ? WHERE id = ?", (movie_id, user_id)
|
|
).rowcount
|
|
|
|
|
|
def schutz_von(user_id):
|
|
"""Die hinterlegte Film-Kennung, oder None bei offenem Namen."""
|
|
with connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT auth_movie_id FROM users WHERE id = ?", (user_id,)
|
|
).fetchone()
|
|
return row["auth_movie_id"] if row else None
|
|
|
|
|
|
def entfernen(user_id):
|
|
with connect() as conn:
|
|
conn.execute("DELETE FROM user_sessions WHERE user_id = ?", (user_id,))
|
|
return conn.execute("DELETE FROM users WHERE id = ?", (user_id,)).rowcount
|
|
|
|
|
|
def waehlen(session_id, user_id):
|
|
"""Gibt None zurück, wenn es den Namen nicht gibt."""
|
|
with connect() as conn:
|
|
da = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
|
if not da:
|
|
return None
|
|
conn.execute(
|
|
"INSERT INTO user_sessions (session_id, user_id, seit) VALUES (?, ?, ?) "
|
|
"ON CONFLICT (session_id) DO UPDATE SET "
|
|
"user_id = excluded.user_id, seit = excluded.seit",
|
|
(
|
|
session_id,
|
|
user_id,
|
|
datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
),
|
|
)
|
|
return _oeffentlich(da)
|
|
|
|
|
|
def abwaehlen(session_id):
|
|
with connect() as conn:
|
|
return conn.execute(
|
|
"DELETE FROM user_sessions WHERE session_id = ?", (session_id,)
|
|
).rowcount
|
|
|
|
|
|
def aktuell(session_id):
|
|
"""Der Benutzer dieser Sitzung, oder None."""
|
|
with connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT u.* FROM users u "
|
|
"JOIN user_sessions s ON s.user_id = u.id "
|
|
"WHERE s.session_id = ?",
|
|
(session_id,),
|
|
).fetchone()
|
|
return _oeffentlich(row) if row else None
|
|
|
|
|
|
# Der Host-Film: das Passwort für den Hostmodus, wie der Anmeldefilm
|
|
# für einen Namen. Steht in der meta-Tabelle, gesetzt aus der Verwaltung.
|
|
def host_film_setzen(movie_id):
|
|
with connect() as conn:
|
|
db.set_meta(conn, "host_film", str(movie_id) if movie_id else "")
|
|
|
|
|
|
def host_film():
|
|
wert = db.get_meta("host_film")
|
|
return int(wert) if wert else None
|
|
|
|
|
|
def ist_host(session_id):
|
|
with connect() as conn:
|
|
return bool(
|
|
conn.execute(
|
|
"SELECT 1 FROM host_sessions WHERE session_id = ?", (session_id,)
|
|
).fetchone()
|
|
)
|
|
|
|
|
|
def host_setzen(session_id, an):
|
|
"""Den Hostmodus merkt sich auch der Server — sonst könnte er den
|
|
Anmeldefilm nicht überspringen."""
|
|
with connect() as conn:
|
|
if an:
|
|
conn.execute(
|
|
"INSERT OR REPLACE INTO host_sessions (session_id, seit) VALUES (?, ?)",
|
|
(session_id, datetime.now(timezone.utc).isoformat(timespec="seconds")),
|
|
)
|
|
else:
|
|
conn.execute(
|
|
"DELETE FROM host_sessions WHERE session_id = ?", (session_id,)
|
|
)
|
|
return an
|
|
|
|
|
|
def besitzer(session_id):
|
|
"""Die user_id dieser Sitzung, oder None ohne gewählten Namen.
|
|
|
|
Sammlungen, Vorschläge und Wertungen werden damit beschriftet. Ein
|
|
Namenswechsel im selben Browser lässt die alten Einträge dann in Ruhe —
|
|
sie gehören weiter dem Namen, unter dem sie entstanden sind.
|
|
"""
|
|
with connect() as conn:
|
|
row = conn.execute(
|
|
"SELECT user_id FROM user_sessions WHERE session_id = ?",
|
|
(session_id,),
|
|
).fetchone()
|
|
return row["user_id"] if row else None
|
|
|
|
|
|
def gehoert_mir(zeile, session_id, user_id):
|
|
"""Ein Eintrag gehört mir, wenn der Name passt — sonst die Sitzung."""
|
|
if zeile["user_id"] is not None or user_id is not None:
|
|
return zeile["user_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)
|
|
return benutzer["name"] if benutzer else None
|