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 @@ + + + + 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 }} + {{ person.anzahl }} diff --git a/frontend/src/components/VerwaltungTab.vue b/frontend/src/components/VerwaltungTab.vue index d8eaec5..67459dc 100644 --- a/frontend/src/components/VerwaltungTab.vue +++ b/frontend/src/components/VerwaltungTab.vue @@ -2,6 +2,7 @@ import { computed, reactive, ref } from 'vue' import FilmSuche from './FilmSuche.vue' import { posterUrl } from '../images' +import PersonName from './PersonName.vue' const props = defineProps({ items: { type: Array, default: () => [] }, @@ -87,7 +88,7 @@ async function entfernen(user) {