This commit is contained in:
team3
2026-09-17 15:40:28 +02:00
parent a1684cf54f
commit ea54a86db4
30 changed files with 3680 additions and 989 deletions

View File

@@ -1,13 +1,16 @@
"""Bereits geschaute Filme und ihre Bewertungen.
"""Bereits geschaute Filme — das Buch der Abende.
Der Host trägt ein, was am Abend lief. Bewerten darf jeder, eine Stimme
je Sitzung — dieselbe Cookie-Sitzung wie bei den Vorschlägen. Der Name ist
freiwillig, ohne ihn zählt die Stimme trotzdem.
Der Host trägt ein, was am Abend lief. Jeder Film bekommt eine Seite mit
zwei Teilen. Links die Momentaufnahme: eine Wertung je Person, nur Sterne,
so wie es direkt nach dem Abspann war. Rechts die Diskussion: Kommentare,
auf die man antworten kann, so tief man will.
Titel und Poster werden mitgespeichert, damit ein Eintrag lesbar bleibt,
auch wenn der Film später aus dem Katalog fällt.
auch wenn der Film später aus dem Katalog fällt. Szenenbild und Laufzeit
kommen aus dem Katalog.
"""
import json
from datetime import datetime, timezone
from . import users
@@ -39,16 +42,54 @@ CREATE TABLE IF NOT EXISTS ratings (
);
"""
# Die Diskussion: ein Kommentar zum Abend, oder mit parent_id eine
# Antwort auf einen anderen Kommentar — beliebig tief
NOTES = """
CREATE TABLE IF NOT EXISTS watched_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
watched_id INTEGER NOT NULL,
parent_id INTEGER,
session_id TEXT NOT NULL,
user_id INTEGER,
text TEXT NOT NULL,
created_at TEXT NOT NULL
);
"""
MAX_STERNE = 5
# Manche schreiben eine ganze Rezension — rund 600 Wörter passen hinein
MAX_TEXT = 4000
MAX_KOMMENTAR = 4000
# Was das Gästebuch aus dem Katalog dazuholt
AUS_KATALOG = ("backdrop_path", "images", "runtime", "genres")
def init():
with connect() as conn:
conn.executescript(SCHEMA + RATINGS)
conn.executescript(SCHEMA + RATINGS + NOTES)
# Eine Stimme je Person und Film
users.je_person(conn, "ratings", RATINGS, "watched_id")
# Herzen an Kommentaren gab es kurz — keiner hat sie vermisst
conn.execute("DROP TABLE IF EXISTS watched_hearts")
# Kurz hingen Antworten an der Wertung statt aneinander. Daten gab
# es dazu nie, die Spalte bekommt nur ihren neuen Namen.
spalten = {r["name"] for r in conn.execute("PRAGMA table_info(watched_notes)")}
if "rating_id" in spalten:
conn.execute("ALTER TABLE watched_notes RENAME COLUMN rating_id TO parent_id")
# Eine Weile stand ein Text an der Wertung. Seit die Sterne allein
# die Wertung sind, gehört er in die Diskussion — einmalig umziehen.
alte = conn.execute(
"SELECT * FROM ratings WHERE text IS NOT NULL AND text != ''"
).fetchall()
for r in alte:
conn.execute(
"INSERT INTO watched_notes "
"(watched_id, parent_id, session_id, user_id, text, created_at) "
"VALUES (?, NULL, ?, ?, ?, ?)",
(r["watched_id"], r["session_id"], r["user_id"], r["text"], r["created_at"]),
)
if alte:
conn.execute("UPDATE ratings SET text = NULL")
# 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:
@@ -66,43 +107,80 @@ def _jetzt():
def alle(session_id):
"""Zuletzt geschaut zuerst, mit Schnitt und der eigenen Stimme."""
"""Zuletzt geschaut zuerst, mit Schnitt, eigener Stimme und
Kommentaren. Die laufende Nummer zählt vom ersten Abend."""
felder = ", ".join(f"m.{f}" for f in AUS_KATALOG)
with connect() as conn:
filme = conn.execute(
"SELECT * FROM watched ORDER BY watched_at DESC, id DESC"
f"SELECT w.*, {felder} FROM watched w "
"LEFT JOIN movies m ON m.id = w.movie_id "
"ORDER BY w.watched_at DESC, w.id DESC"
).fetchall()
stimmen = conn.execute(
"SELECT id, watched_id, session_id, user_id, stars, name, text "
"SELECT id, watched_id, session_id, user_id, stars, name, created_at "
"FROM ratings ORDER BY created_at ASC"
).fetchall()
notizen = conn.execute(
"SELECT n.*, u.name FROM watched_notes n "
"LEFT JOIN users u ON u.id = n.user_id "
"ORDER BY n.created_at ASC"
).fetchall()
ich = besitzer(session_id)
def notiz(n):
return {
"id": n["id"],
"text": n["text"],
"name": n["name"],
"created_at": n["created_at"],
"mine": gehoert_mir(n, session_id, ich),
"replies": [],
}
# Die Kommentare eines Films als Baum: jede Antwort hängt sich an
# ihren Vater, was keinen hat, steht oben. Die Liste kommt nach
# Zeit sortiert, also ist der Vater immer schon da.
knoten = {n["id"]: notiz(n) for n in notizen}
kommentare = {}
for n in notizen:
k = knoten[n["id"]]
vater = knoten.get(n["parent_id"]) if n["parent_id"] is not None else None
if vater:
vater["replies"].append(k)
else:
kommentare.setdefault(n["watched_id"], []).append(k)
je_film = {}
for s in stimmen:
je_film.setdefault(s["watched_id"], []).append(s)
out = []
for film in filme:
for nummer, film in zip(range(len(filme), 0, -1), filme):
eigene = je_film.get(film["id"], [])
werte = [s["stars"] for s in eigene]
meine = next((s for s in eigene if gehoert_mir(s, session_id, ich)), None)
daten = dict(film)
daten["images"] = json.loads(daten["images"]) if daten["images"] else []
daten["genres"] = json.loads(daten["genres"]) if daten["genres"] else []
out.append(
{
**dict(film),
**daten,
"nummer": nummer,
"ratings": [
{
"id": s["id"],
"stars": s["stars"],
"name": s["name"],
"text": s["text"],
"created_at": s["created_at"],
"mine": gehoert_mir(s, session_id, ich),
}
for s in eigene
],
"notes": kommentare.get(film["id"], []),
"average": round(sum(werte) / len(werte), 1) if werte else None,
"count": len(werte),
"mine": meine["stars"] if meine else None,
"mine_text": meine["text"] if meine else None,
}
)
return out
@@ -130,7 +208,9 @@ def markieren(movie):
def entfernen(watched_id):
"""Samt Wertungen und Kommentaren."""
with connect() as conn:
conn.execute("DELETE FROM watched_notes WHERE watched_id = ?", (watched_id,))
conn.execute("DELETE FROM ratings WHERE watched_id = ?", (watched_id,))
return conn.execute(
"DELETE FROM watched WHERE id = ?", (watched_id,)
@@ -156,15 +236,9 @@ def sichtbarkeit(watched_id, versteckt):
).rowcount
def bewerten(watched_id, session_id, stars, name=None, text=None):
"""Sterne und ein optionaler Satz dazu.
Kommt kein Text mit (None), bleibt der bisherige stehen — wer nur die
Sterne nachjustiert, verliert seinen Satz nicht. Ein leerer String
löscht ihn ausdrücklich.
Gibt None zurück, wenn der Film gar nicht in der Liste steht.
"""
def bewerten(watched_id, session_id, stars, name=None):
"""Sterne setzen. Gibt None zurück, wenn der Film nicht in der Liste
steht."""
with connect() as conn:
da = conn.execute(
"SELECT 1 FROM watched WHERE id = ?", (watched_id,)
@@ -172,14 +246,6 @@ def bewerten(watched_id, session_id, stars, name=None, text=None):
if not da:
return None
ich = besitzer(session_id)
if text is None:
alt = conn.execute(
f"SELECT text FROM ratings WHERE watched_id = ? AND {WEM}",
(watched_id, ich, ich, session_id),
).fetchone()
text = alt["text"] if alt else None
else:
text = text.strip()[:MAX_TEXT] or None
# Eine Stimme je Person — die eigene alte fliegt vorher raus
conn.execute(
f"DELETE FROM ratings WHERE watched_id = ? AND {WEM}",
@@ -187,19 +253,11 @@ def bewerten(watched_id, session_id, stars, name=None, text=None):
)
conn.execute(
"INSERT INTO ratings "
"(watched_id, session_id, user_id, stars, name, text, created_at) "
"VALUES (?, ?, ?, ?, ?, ?, ?)",
(
watched_id,
session_id,
ich,
stars,
(name or "").strip()[:40] or None,
text,
_jetzt(),
),
"(watched_id, session_id, user_id, stars, name, created_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
(watched_id, session_id, ich, stars, (name or "").strip()[:40] or None, _jetzt()),
)
return {"watched_id": watched_id, "stars": stars, "text": text}
return {"watched_id": watched_id, "stars": stars}
def stimme_loeschen(rating_id):
@@ -210,6 +268,70 @@ def stimme_loeschen(rating_id):
).rowcount
def anmerken(watched_id, session_id, text, parent_id=None):
"""Kommentar zum Abend, oder mit parent_id eine Antwort auf einen
anderen. Gibt None zurück, wenn Film oder Vater fehlen."""
text = (text or "").strip()[:MAX_KOMMENTAR]
if not text:
return None
with connect() as conn:
da = conn.execute(
"SELECT 1 FROM watched WHERE id = ?", (watched_id,)
).fetchone()
if not da:
return None
if parent_id is not None:
passt = conn.execute(
"SELECT 1 FROM watched_notes WHERE id = ? AND watched_id = ?",
(parent_id, watched_id),
).fetchone()
if not passt:
return None
cur = conn.execute(
"INSERT INTO watched_notes "
"(watched_id, parent_id, session_id, user_id, text, created_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
(watched_id, parent_id, session_id, besitzer(session_id), text, _jetzt()),
)
return cur.lastrowid
def _nachkommen(conn, note_id):
"""Alle Antworten unter einem Kommentar, beliebig tief."""
gefunden, offen = [], [note_id]
while offen:
kinder = [
r["id"]
for r in conn.execute(
f"SELECT id FROM watched_notes WHERE parent_id IN "
f"({','.join('?' * len(offen))})",
offen,
)
]
gefunden.extend(kinder)
offen = kinder
return gefunden
def anmerkung_loeschen(note_id, session_id, host):
"""Der Verfasser und der Host dürfen weg damit — samt allem, was
darunter hängt. Eine Antwort ohne ihren Anlass ergäbe keinen Sinn."""
ich = besitzer(session_id)
with connect() as conn:
zeile = conn.execute(
"SELECT * FROM watched_notes WHERE id = ?", (note_id,)
).fetchone()
if not zeile:
return 0
if not host and not gehoert_mir(zeile, session_id, ich):
return 0
alle_ids = [note_id, *_nachkommen(conn, note_id)]
conn.executemany(
"DELETE FROM watched_notes WHERE id = ?", [(i,) for i in alle_ids]
)
return len(alle_ids)
def ids():
"""movie_ids der geschauten Filme, für die Detailansicht."""
with connect() as conn: