diff --git a/backend/app/main.py b/backend/app/main.py
index e692370..8780299 100644
--- a/backend/app/main.py
+++ b/backend/app/main.py
@@ -13,6 +13,7 @@ from fastapi.staticfiles import StaticFiles
from . import (
backup,
+ punkte,
db,
events,
features,
@@ -364,8 +365,9 @@ def mitglied(sid):
return ich
-# Der letzte Dreh, damit eine Seite mitten in der Animation nachziehen kann
-letzter_dreh = {"ergebnis": None, "start": 0.0}
+# Der letzte Dreh, damit eine Seite mitten in der Animation nachziehen
+# kann — samt Anwesenheit der Runde, die beim Eintragen am Abend landet
+letzter_dreh = {"ergebnis": None, "start": 0.0, "teilnehmer": []}
@app.get("/api/events")
@@ -394,6 +396,7 @@ def drehen():
letzter_dreh["ergebnis"] = ergebnis
letzter_dreh["start"] = time.monotonic()
+ letzter_dreh["teilnehmer"] = suggestions.teilnehmer()
events.senden("spin", ergebnis)
return {"started": True, **ergebnis}
@@ -418,6 +421,7 @@ def liste_vorschlaege(response: Response, filmabend_sid: str | None = Cookie(Non
sid = sitzung(response, filmabend_sid)
eintraege = suggestions.alle()
ich = users.besitzer(sid)
+ dabei = suggestions.dabei_alle()
return {
"items": [
{k: v for k, v in e.items() if k not in ("session_id", "user_id")}
@@ -428,9 +432,22 @@ def liste_vorschlaege(response: Response, filmabend_sid: str | None = Cookie(Non
(e["id"] for e in eintraege if users.gehoert_mir(e, sid, ich)),
None,
),
+ # Wer ohne Vorschlag dabei ist
+ "dabei": [d["name"] or "Anonym" for d in dabei],
+ "dabei_mine": any(users.gehoert_mir(d, sid, ich) for d in dabei),
}
+@app.post("/api/dabei")
+def dabei(response: Response, filmabend_sid: str | None = Cookie(None)):
+ """Dabei ohne Vorschlag — an oder wieder aus."""
+ sid = sitzung(response, filmabend_sid)
+ mitglied(sid)
+ steht = suggestions.dabei_umschalten(sid)
+ events.senden("suggestions")
+ return {"dabei": steht}
+
+
@app.post("/api/suggestions")
async def vorschlagen(
response: Response,
@@ -595,8 +612,10 @@ def vergessen(
@app.get("/api/users")
def runde(response: Response, filmabend_sid: str | None = Cookie(None)):
sid = sitzung(response, filmabend_sid)
+ staende = punkte.alle()
+ leer = punkte.stand(0)
return {
- "items": users.alle(),
+ "items": [{**u, **staende.get(u["id"], leer)} for u in users.alle()],
"mine": users.aktuell(sid),
"host": users.ist_host(sid),
}
@@ -739,17 +758,45 @@ def name_waehlen(
@app.get("/api/watched")
def liste_gesehen(response: Response, filmabend_sid: str | None = Cookie(None)):
sid = sitzung(response, filmabend_sid)
- return watched.alle(sid)
+ return watched.alle(sid, users.ist_host(sid))
@app.post("/api/watched")
async def als_gesehen(movie_id: int = Body(..., embed=True)):
- """Der Host trägt ein, was am Abend lief."""
+ """Der Host trägt ein, was am Abend lief. Ist es der gezogene Film,
+ hängt die Runde vom Rad als Anwesenheit am Abend."""
eintrag = watched.markieren(await film_holen(movie_id))
+ gezogen = letzter_dreh["ergebnis"]
+ if gezogen and gezogen["gewinner"]["movie_id"] == movie_id and letzter_dreh["teilnehmer"]:
+ watched.dabei_setzen(eintrag["id"], letzter_dreh["teilnehmer"])
events.senden("watched")
return eintrag
+@app.post("/api/watched/{watched_id}/dabei")
+def dabei_aendern(watched_id: int, user_ids: list[int] = Body(..., embed=True)):
+ """Der Host berichtigt, wer dabei war — etwa wer spät kam."""
+ watched.dabei_setzen(watched_id, user_ids)
+ events.senden("watched")
+ return {"dabei": user_ids}
+
+
+@app.post("/api/watched/hearts")
+def herz_setzen(
+ response: Response,
+ note_id: int = Body(..., embed=True),
+ filmabend_sid: str | None = Cookie(None),
+):
+ """Ein Klick setzt das Herz, der nächste nimmt es zurück."""
+ sid = sitzung(response, filmabend_sid)
+ mitglied(sid)
+ steht = watched.herz(sid, note_id)
+ if steht is None:
+ raise HTTPException(status_code=404, detail="Kommentar nicht gefunden")
+ events.senden("watched")
+ return {"hearted": steht}
+
+
@app.delete("/api/watched/{watched_id}")
def gesehen_entfernen(watched_id: int):
entfernt = watched.entfernen(watched_id)
diff --git a/backend/app/punkte.py b/backend/app/punkte.py
new file mode 100644
index 0000000..749d40c
--- /dev/null
+++ b/backend/app/punkte.py
@@ -0,0 +1,75 @@
+"""Erfahrung und Level — berechnet, nicht gebucht.
+
+Punkte sind eine Formel über dem, was gerade in der Datenbank steht:
+wer dabei war, bewertet, kommentiert, geantwortet und ein Herz gegeben
+hat. Nimmt jemand seine Wertung zurück, sind die Punkte weg. Es gibt
+kein Konto, das man pflegen oder austricksen könnte.
+
+Je Film zählt jede Art nur einmal. Level nach Dreieckszahlen: Level L
+ist erreicht, sobald L·(L−1)/2 Punkte da sind — anfangs schnell, dann
+stetig langsamer, aber immer in Reichweite.
+"""
+
+from math import isqrt
+
+from .db import connect
+
+SCHAUEN = 10
+BEWERTEN = 3
+KOMMENTAR = 5
+ANTWORT = 2
+HERZ = 1
+
+ZAEHLUNGEN = (
+ (SCHAUEN, "SELECT user_id, COUNT(*) AS n FROM watched_dabei GROUP BY user_id"),
+ (
+ BEWERTEN,
+ "SELECT user_id, COUNT(DISTINCT watched_id) AS n FROM ratings "
+ "WHERE user_id IS NOT NULL GROUP BY user_id",
+ ),
+ (
+ KOMMENTAR,
+ "SELECT user_id, COUNT(DISTINCT watched_id) AS n FROM watched_notes "
+ "WHERE user_id IS NOT NULL AND parent_id IS NULL GROUP BY user_id",
+ ),
+ (
+ ANTWORT,
+ "SELECT user_id, COUNT(DISTINCT watched_id) AS n FROM watched_notes "
+ "WHERE user_id IS NOT NULL AND parent_id IS NOT NULL GROUP BY user_id",
+ ),
+ (
+ HERZ,
+ "SELECT h.user_id, COUNT(DISTINCT n.watched_id) AS n FROM watched_hearts h "
+ "JOIN watched_notes n ON n.id = h.note_id "
+ "WHERE h.user_id IS NOT NULL GROUP BY h.user_id",
+ ),
+)
+
+
+def level(punkte):
+ """Größtes L mit L·(L−1)/2 ≤ Punkte. Null Punkte sind Level 1."""
+ stufe = (1 + isqrt(1 + 8 * punkte)) // 2
+ while stufe * (stufe - 1) // 2 > punkte:
+ stufe -= 1
+ return max(1, stufe)
+
+
+def stand(punkte):
+ stufe = level(punkte)
+ return {
+ "punkte": punkte,
+ "level": stufe,
+ # Wo das aktuelle Level begann und wo das nächste anfängt — für den Balken
+ "von": stufe * (stufe - 1) // 2,
+ "bis": stufe * (stufe + 1) // 2,
+ }
+
+
+def alle():
+ """Punktestand je user_id."""
+ summen = {}
+ with connect() as conn:
+ for wert, sql in ZAEHLUNGEN:
+ for row in conn.execute(sql):
+ summen[row["user_id"]] = summen.get(row["user_id"], 0) + wert * row["n"]
+ return {user_id: stand(p) for user_id, p in summen.items()}
diff --git a/backend/app/suggestions.py b/backend/app/suggestions.py
index c19a9bb..78d1c67 100644
--- a/backend/app/suggestions.py
+++ b/backend/app/suggestions.py
@@ -32,6 +32,17 @@ CREATE TABLE IF NOT EXISTS suggestions (
);
"""
+# Wer am Abend da ist, aber nichts vorschlagen will. Zusammen mit den
+# Vorschlagenden ergibt das die Anwesenheit — die zählt beim Dreh.
+DABEI = """
+CREATE TABLE IF NOT EXISTS dabei (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ session_id TEXT NOT NULL,
+ user_id INTEGER,
+ created_at TEXT NOT NULL
+);
+"""
+
COOKIE = "filmabend_sid"
# So lange läuft die Animation im Browser. Der Server merkt sich das
@@ -54,9 +65,10 @@ BRUCH_ANTEIL = 0.5
def init():
with connect() as conn:
- conn.executescript(SCHEMA)
+ conn.executescript(SCHEMA + DABEI)
# Ein Vorschlag je Person
users.je_person(conn, "suggestions", SCHEMA)
+ users.je_person(conn, "dabei", DABEI)
def neue_session():
@@ -196,4 +208,40 @@ def loeschen(session_id):
def alles_loeschen():
"""Host setzt die Runde zurück."""
with connect() as conn:
+ conn.execute("DELETE FROM dabei")
return conn.execute("DELETE FROM suggestions").rowcount
+
+
+def dabei_umschalten(session_id):
+ """Dabei ohne Vorschlag — der zweite Klick nimmt es zurück.
+ Gibt zurück, ob es jetzt gesetzt ist."""
+ ich = besitzer(session_id)
+ with connect() as conn:
+ weg = conn.execute(
+ f"DELETE FROM dabei WHERE {WEM}", (ich, ich, session_id)
+ ).rowcount
+ if weg:
+ return False
+ conn.execute(
+ "INSERT INTO dabei (session_id, user_id, created_at) VALUES (?, ?, ?)",
+ (session_id, ich, datetime.now(timezone.utc).isoformat(timespec="seconds")),
+ )
+ return True
+
+
+def dabei_alle():
+ """Wer sich ohne Vorschlag eingetragen hat — Namen und Kennungen."""
+ with connect() as conn:
+ rows = conn.execute(
+ "SELECT d.*, u.name FROM dabei d LEFT JOIN users u ON u.id = d.user_id "
+ "ORDER BY d.created_at ASC"
+ ).fetchall()
+ return [dict(r) for r in rows]
+
+
+def teilnehmer():
+ """user_ids aller Anwesenden: Vorschlagende und Eingetragene."""
+ with connect() as conn:
+ ids = {r["user_id"] for r in conn.execute("SELECT user_id FROM suggestions")}
+ ids |= {r["user_id"] for r in conn.execute("SELECT user_id FROM dabei")}
+ return sorted(i for i in ids if i is not None)
diff --git a/backend/app/watched.py b/backend/app/watched.py
index 337c4b3..336c64b 100644
--- a/backend/app/watched.py
+++ b/backend/app/watched.py
@@ -56,6 +56,28 @@ CREATE TABLE IF NOT EXISTS watched_notes (
);
"""
+# Ein Herz je Person und Kommentar: "sehe ich auch so". Alle sehen die
+# Zahl, wer es gegeben hat, sieht nur der Host.
+HEARTS = """
+CREATE TABLE IF NOT EXISTS watched_hearts (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ note_id INTEGER NOT NULL,
+ session_id TEXT NOT NULL,
+ user_id INTEGER,
+ created_at TEXT NOT NULL
+);
+"""
+
+# Wer am Abend dabei war — kommt aus der Runde am Rad, sobald der Host
+# den gezogenen Film einträgt. Daraus rechnen sich die Punkte.
+DABEI = """
+CREATE TABLE IF NOT EXISTS watched_dabei (
+ watched_id INTEGER NOT NULL,
+ user_id INTEGER NOT NULL,
+ PRIMARY KEY (watched_id, user_id)
+);
+"""
+
MAX_STERNE = 5
# Manche schreiben eine ganze Rezension — rund 600 Wörter passen hinein
MAX_KOMMENTAR = 4000
@@ -66,11 +88,10 @@ AUS_KATALOG = ("backdrop_path", "images", "runtime", "genres")
def init():
with connect() as conn:
- conn.executescript(SCHEMA + RATINGS + NOTES)
- # Eine Stimme je Person und Film
+ conn.executescript(SCHEMA + RATINGS + NOTES + HEARTS + DABEI)
+ # Eine Stimme je Person und Film, ein Herz je Person und Kommentar
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")
+ users.je_person(conn, "watched_hearts", HEARTS, "note_id")
# 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)")}
@@ -106,9 +127,12 @@ def _jetzt():
return datetime.now(timezone.utc).isoformat(timespec="seconds")
-def alle(session_id):
- """Zuletzt geschaut zuerst, mit Schnitt, eigener Stimme und
- Kommentaren. Die laufende Nummer zählt vom ersten Abend."""
+def alle(session_id, host=False):
+ """Zuletzt geschaut zuerst, mit Schnitt, eigener Stimme, Kommentaren
+ und wer dabei war. Die laufende Nummer zählt vom ersten Abend.
+
+ Wer wie gewertet und wer ein Herz gegeben hat, sieht nur der Host —
+ für die Runde bleibt die Wertung anonym, das nimmt Hemmung."""
felder = ", ".join(f"m.{f}" for f in AUS_KATALOG)
with connect() as conn:
filme = conn.execute(
@@ -125,10 +149,26 @@ def alle(session_id):
"LEFT JOIN users u ON u.id = n.user_id "
"ORDER BY n.created_at ASC"
).fetchall()
+ herzen = conn.execute(
+ "SELECT h.note_id, h.session_id, h.user_id, u.name FROM watched_hearts h "
+ "LEFT JOIN users u ON u.id = h.user_id ORDER BY h.created_at ASC"
+ ).fetchall()
+ anwesend = conn.execute(
+ "SELECT d.watched_id, u.id, u.name FROM watched_dabei d "
+ "JOIN users u ON u.id = d.user_id ORDER BY u.name COLLATE NOCASE"
+ ).fetchall()
ich = besitzer(session_id)
+ herz_je_notiz = {}
+ for h in herzen:
+ herz_je_notiz.setdefault(h["note_id"], []).append(h)
+ dabei_je_film = {}
+ for d in anwesend:
+ dabei_je_film.setdefault(d["watched_id"], []).append({"id": d["id"], "name": d["name"]})
+
def notiz(n):
+ meine_herzen = herz_je_notiz.get(n["id"], [])
return {
"id": n["id"],
"text": n["text"],
@@ -136,6 +176,9 @@ def alle(session_id):
"created_at": n["created_at"],
"mine": gehoert_mir(n, session_id, ich),
"replies": [],
+ "hearts": len(meine_herzen),
+ "hearted": any(gehoert_mir(h, session_id, ich) for h in meine_herzen),
+ "hearted_by": [h["name"] or "Anonym" for h in meine_herzen] if host else [],
}
# Die Kommentare eines Films als Baum: jede Antwort hängt sich an
@@ -176,7 +219,10 @@ def alle(session_id):
"mine": gehoert_mir(s, session_id, ich),
}
for s in eigene
- ],
+ ]
+ if host
+ else [],
+ "dabei": dabei_je_film.get(film["id"], []),
"notes": kommentare.get(film["id"], []),
"average": round(sum(werte) / len(werte), 1) if werte else None,
"count": len(werte),
@@ -207,9 +253,31 @@ def markieren(movie):
return dict(row)
-def entfernen(watched_id):
- """Samt Wertungen und Kommentaren."""
+def dabei_setzen(watched_id, user_ids):
+ """Die Anwesenheit eines Abends — ersetzt die bisherige Liste."""
with connect() as conn:
+ conn.execute("DELETE FROM watched_dabei WHERE watched_id = ?", (watched_id,))
+ conn.executemany(
+ "INSERT OR IGNORE INTO watched_dabei (watched_id, user_id) VALUES (?, ?)",
+ [(watched_id, i) for i in user_ids],
+ )
+
+
+def _herzen_weg(conn, note_ids):
+ conn.executemany(
+ "DELETE FROM watched_hearts WHERE note_id = ?", [(i,) for i in note_ids]
+ )
+
+
+def entfernen(watched_id):
+ """Samt Wertungen, Kommentaren, Herzen und Anwesenheit."""
+ with connect() as conn:
+ notizen = [
+ r["id"]
+ for r in conn.execute("SELECT id FROM watched_notes WHERE watched_id = ?", (watched_id,))
+ ]
+ _herzen_weg(conn, notizen)
+ conn.execute("DELETE FROM watched_dabei WHERE watched_id = ?", (watched_id,))
conn.execute("DELETE FROM watched_notes WHERE watched_id = ?", (watched_id,))
conn.execute("DELETE FROM ratings WHERE watched_id = ?", (watched_id,))
return conn.execute(
@@ -326,12 +394,35 @@ def anmerkung_loeschen(note_id, session_id, host):
if not host and not gehoert_mir(zeile, session_id, ich):
return 0
alle_ids = [note_id, *_nachkommen(conn, note_id)]
+ _herzen_weg(conn, alle_ids)
conn.executemany(
"DELETE FROM watched_notes WHERE id = ?", [(i,) for i in alle_ids]
)
return len(alle_ids)
+def herz(session_id, note_id):
+ """Herz setzen oder zurücknehmen. None, wenn es den Kommentar nicht
+ gibt, sonst ob das Herz jetzt steht."""
+ ich = besitzer(session_id)
+ with connect() as conn:
+ weg = conn.execute(
+ f"DELETE FROM watched_hearts WHERE note_id = ? AND {WEM}",
+ (note_id, ich, ich, session_id),
+ ).rowcount
+ if weg:
+ return False
+ da = conn.execute("SELECT 1 FROM watched_notes WHERE id = ?", (note_id,)).fetchone()
+ if not da:
+ return None
+ conn.execute(
+ "INSERT INTO watched_hearts (note_id, session_id, user_id, created_at) "
+ "VALUES (?, ?, ?, ?)",
+ (note_id, session_id, ich, _jetzt()),
+ )
+ return True
+
+
def ids():
"""movie_ids der geschauten Filme, für die Detailansicht."""
with connect() as conn:
diff --git a/backend/horror.db.bak b/backend/horror.db.bak
index 15175a6..9add1dc 100644
Binary files a/backend/horror.db.bak and b/backend/horror.db.bak differ
diff --git a/backend/tests/test_gaestebuch.py b/backend/tests/test_gaestebuch.py
index 705e0f4..86dd382 100644
--- a/backend/tests/test_gaestebuch.py
+++ b/backend/tests/test_gaestebuch.py
@@ -9,8 +9,7 @@ def abend(client, film, movie_id=1):
def wertung(client, watched_id, stars=4):
client.post(f"/api/watched/{watched_id}/rating", json={"stars": stars})
- film = next(f for f in client.get("/api/watched").json() if f["id"] == watched_id)
- return next(r for r in film["ratings"] if r["mine"])
+ return next(f for f in client.get("/api/watched").json() if f["id"] == watched_id)
def film_von(client, watched_id):
@@ -103,8 +102,9 @@ def test_wertungen_und_kommentare_sind_unabhaengig(client, film):
wertung(client, watched_id, 5)
kommentar(client, watched_id, "Diskussion.")
f = film_von(client, watched_id)
- assert f["ratings"][0]["stars"] == 5
- assert "text" not in f["ratings"][0]
+ assert f["average"] == 5.0 and f["mine"] == 5
+ # Die Namen an den Wertungen sieht nur der Host
+ assert f["ratings"] == []
assert [n["text"] for n in f["notes"]] == ["Diskussion."]
diff --git a/backend/tests/test_punkte.py b/backend/tests/test_punkte.py
new file mode 100644
index 0000000..e6f27c5
--- /dev/null
+++ b/backend/tests/test_punkte.py
@@ -0,0 +1,106 @@
+"""Erfahrung und Level: berechnet aus dem Bestand, nicht gebucht."""
+
+from app import punkte
+
+
+def abend(client, film, movie_id=1):
+ return client.post("/api/watched", json={"movie_id": film(movie_id)}).json()["id"]
+
+
+def stand(client, name):
+ return next(u for u in client.get("/api/users").json()["items"] if u["name"] == name)
+
+
+def test_level_nach_dreieckszahlen():
+ assert punkte.level(0) == 1
+ assert punkte.level(1) == 2
+ assert punkte.level(2) == 2
+ assert punkte.level(3) == 3
+ assert punkte.level(20) == 6
+ assert punkte.level(21) == 7
+ assert punkte.level(1081) == 47
+ assert punkte.stand(21) == {"punkte": 21, "level": 7, "von": 21, "bis": 28}
+
+
+def test_punkte_kommen_und_gehen_mit_dem_bestand(client, client2, film):
+ watched_id = abend(client, film)
+ assert stand(client, "Anna")["punkte"] == 0
+
+ client.post(f"/api/watched/{watched_id}/rating", json={"stars": 4})
+ assert stand(client, "Anna")["punkte"] == punkte.BEWERTEN
+
+ oben = client.post(f"/api/watched/{watched_id}/notes", json={"text": "gut"}).json()["id"]
+ client.post(f"/api/watched/{watched_id}/notes", json={"text": "nochmal"})
+ # Zwei Kommentare, einer zählt
+ assert stand(client, "Anna")["punkte"] == punkte.BEWERTEN + punkte.KOMMENTAR
+
+ client2.post(f"/api/watched/{watched_id}/notes", json={"text": "finde ich nicht", "parent_id": oben})
+ client2.post("/api/watched/hearts", json={"note_id": oben})
+ assert stand(client2, "Ben")["punkte"] == punkte.ANTWORT + punkte.HERZ
+
+ # Wertung weg, Punkte weg — nichts wird gebucht
+ client.post(f"/api/watched/{watched_id}/rating", json={"stars": 4})
+ assert stand(client, "Anna")["level"] == punkte.level(punkte.BEWERTEN + punkte.KOMMENTAR)
+ client.delete(f"/api/watched/{watched_id}")
+ assert stand(client, "Anna")["punkte"] == 0
+ assert stand(client2, "Ben")["punkte"] == 0
+
+
+def test_anwesenheit_kommt_vom_rad(client, client2, zuschauer, hostmodus, film):
+ """Wer vorschlägt oder sich einträgt, war dabei — sobald der Host den
+ gezogenen Film einträgt."""
+ from app import main
+
+ client.post("/api/suggestions", json={"movie_id": film(1)})
+ assert client2.post("/api/dabei").json()["dabei"] is True
+ liste = client2.get("/api/suggestions").json()
+ assert liste["dabei"] == ["Ben"] and liste["dabei_mine"] is True
+
+ hostmodus(zuschauer)
+ assert zuschauer.post("/api/spin").json()["started"] is True
+ main.letzter_dreh["start"] = 0.0 # Animation vorbei
+ watched_id = zuschauer.post("/api/watched", json={"movie_id": film(1)}).json()["id"]
+
+ f = next(x for x in client.get("/api/watched").json() if x["id"] == watched_id)
+ assert sorted(d["name"] for d in f["dabei"]) == ["Anna", "Ben"]
+ assert stand(client, "Anna")["punkte"] == punkte.SCHAUEN
+ assert stand(client, "Ben")["level"] == punkte.level(punkte.SCHAUEN)
+
+ # Der Host berichtigt: Ben kam doch nicht
+ anna = stand(client, "Anna")["id"]
+ zuschauer.post(f"/api/watched/{watched_id}/dabei", json={"user_ids": [anna]})
+ f = next(x for x in client.get("/api/watched").json() if x["id"] == watched_id)
+ assert [d["name"] for d in f["dabei"]] == ["Anna"]
+
+ # Reset leert auch die Eingetragenen
+ zuschauer.delete("/api/suggestions/alle")
+ assert client2.get("/api/suggestions").json()["dabei"] == []
+ assert client2.post("/api/dabei").json()["dabei"] is True
+ assert client2.post("/api/dabei").json()["dabei"] is False
+
+
+def test_wertungen_und_herzen_sieht_nur_der_host(client, client2, zuschauer, hostmodus, film):
+ watched_id = abend(client, film)
+ client.post(f"/api/watched/{watched_id}/rating", json={"stars": 5})
+ note_id = client.post(f"/api/watched/{watched_id}/notes", json={"text": "x"}).json()["id"]
+ client2.post("/api/watched/hearts", json={"note_id": note_id})
+
+ def film_von(c):
+ return next(x for x in c.get("/api/watched").json() if x["id"] == watched_id)
+
+ # Ben sieht Schnitt und Herzzahl, aber keine Namen
+ f = film_von(client2)
+ assert f["ratings"] == [] and f["average"] == 5.0 and f["count"] == 1
+ assert f["notes"][0]["hearts"] == 1 and f["notes"][0]["hearted"] is True
+ assert f["notes"][0]["hearted_by"] == []
+ # Anna sieht ihre eigenen Sterne trotzdem
+ assert film_von(client)["mine"] == 5
+ # Der Host sieht alles
+ hostmodus(zuschauer)
+ f = film_von(zuschauer)
+ assert f["ratings"][0]["name"] == "Anna"
+ assert f["notes"][0]["hearted_by"] == ["Ben"]
+ # Herz zurück, Zähler runter; Zuschauer ohne Namen darf kein Herz geben
+ assert client2.post("/api/watched/hearts", json={"note_id": note_id}).json()["hearted"] is False
+ assert film_von(client)["notes"][0]["hearts"] == 0
+ assert client.post("/api/watched/hearts", json={"note_id": 999}).status_code == 404
diff --git a/backend/tests/test_watched.py b/backend/tests/test_watched.py
index d9852b6..9ebd58f 100644
--- a/backend/tests/test_watched.py
+++ b/backend/tests/test_watched.py
@@ -16,7 +16,8 @@ def test_wertung_ist_nur_sterne(client, film):
assert eintrag["mine"] == 4
assert eintrag["average"] == 4.0
assert eintrag["count"] == 1
- assert "text" not in eintrag["ratings"][0]
+ # Wer wie gewertet hat, sieht nur der Host
+ assert eintrag["ratings"] == []
assert "mine_text" not in eintrag
@@ -58,7 +59,8 @@ def test_fremde_wertung_ist_nicht_meine(client, client2, film):
[eintrag] = client2.get("/api/watched").json()
assert eintrag["mine"] is None
assert eintrag["count"] == 1
- assert eintrag["ratings"][0]["mine"] is False
+ assert eintrag["ratings"] == []
+ assert eintrag["count"] == 1
client2.post(f"/api/watched/{wid}/rating", json={"stars": 2})
[eintrag] = client2.get("/api/watched").json()
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 1207843..ed820c9 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -1,5 +1,5 @@
+
+
+
+ {{ stufe }}{{ name || 'Anonym' }}
+
+
+
+
diff --git a/frontend/src/components/SammlungFilter.vue b/frontend/src/components/SammlungFilter.vue
index 2f67ecc..1b781b0 100644
--- a/frontend/src/components/SammlungFilter.vue
+++ b/frontend/src/components/SammlungFilter.vue
@@ -3,6 +3,7 @@
// Filterleiste im Katalog, nur mit dem, was hier zählt: wessen Sammlung,
// Horror oder nicht, und wie groß die Karten sind.
import { computed } from 'vue'
+import PersonName from './PersonName.vue'
const props = defineProps({
// [{ name, anzahl }] — wer überhaupt etwas gesammelt hat
@@ -104,7 +105,7 @@ function zuruecksetzen() {
:class="{ on: gewaehlt.includes(person.name) }"
@click="umschalten(person.name)"
>
- {{ person.name }}
+
Noch nichts vorgeschlagen.
+ Dabei ohne Vorschlag
+
+