From f80b8c58269931e782db3652a8012bd677d712aa Mon Sep 17 00:00:00 2001 From: team3 Date: Tue, 29 Sep 2026 00:54:58 +0200 Subject: [PATCH] update --- backend/app/main.py | 57 +++- backend/app/punkte.py | 75 ++++++ backend/app/suggestions.py | 50 +++- backend/app/watched.py | 111 +++++++- backend/horror.db.bak | Bin 22646784 -> 22671360 bytes backend/tests/test_gaestebuch.py | 8 +- backend/tests/test_punkte.py | 106 ++++++++ backend/tests/test_watched.py | 6 +- frontend/src/App.vue | 8 +- frontend/src/components/Abend.vue | 128 ++++++--- frontend/src/components/Kommentar.vue | 75 +++++- frontend/src/components/NamensWahl.vue | 282 +++++++++++++++++++- frontend/src/components/PersonName.vue | 54 ++++ frontend/src/components/SammlungFilter.vue | 3 +- frontend/src/components/VerwaltungTab.vue | 3 +- frontend/src/components/VorschlagsListe.vue | 69 ++++- frontend/src/components/WatchedTab.vue | 10 + frontend/src/components/WheelTab.vue | 21 +- frontend/src/components/WuenscheTab.vue | 5 +- frontend/src/composables/useMeins.js | 3 + 20 files changed, 997 insertions(+), 77 deletions(-) create mode 100644 backend/app/punkte.py create mode 100644 backend/tests/test_punkte.py create mode 100644 frontend/src/components/PersonName.vue 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 15175a680f6208d4ed6124fc3cc417b76fe5bdb9..9add1dcb51b6467aff07c8247ca2b74a48e999cc 100644 GIT binary patch delta 14959 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%0A(2a%=o^znn}^zk>t zbsM^H{1CcSQ1AHH(-#k+(-#NPg^NSzatkeAP=?VU3r@h$%fqNO^s)|pulK+;9fwq! ziLII~Kiu+`Et<`r?!i4#E$TkC=|`I$(LU3AzWYnP2RaUI{pe$d^r)j%`+5+8J29cr zqfF=ZJAhhW^>?y&K)dV6KK04*<|E$-q8&}=H-kuOMSJu}+o4vCl7Q;%%@@8FME5n% zfWYb2c@UTaXRKdOZU%BQlADR#%;aVvw-Iu)lA8^IyLuR1YO1%XTAF7k(SP@~5IB_` zCpxxwrJLH(BWQoeb6x3;p*q=>+M!kX?`*#0IfC|iqHrR7EkOu$lH?xgsp=3p%M3g( z3DHL&jcY!vp8Bvx^I@~~Hl6OUPM6val^tx_C!wrGe6>JA$%hs~C@th5DW z?NoOE{RxnT+wJ7`7IM3T+>VmlD7o2DE8%sZ(@&x9`(Jt0f#BvOHy63N$<0Gxs zI*F2nmS%JYp1(h!X=%<)!nX?-Cz0mj6PjnYq5dmlc`k@v?R)Sc_yx zoI$re>}JAt$D|uVzU?ujg3N*D3hahAH7|TMh&DG@{x*oT&9`>zQQs|r;JDu&oM(LQ zdFH@cAs!e#unO@QpE%fa@bJOr{a*^A8?J8rn0`%~HZkzf$-=GLT?Y@U-?i&1P1770 zC238z{cGE`e^gicvN@>Lqaz(K_01{J@TTVHK-;NXq&I1I4enB(90WteRcVvQGTbqs zS8$9V)mGt5bM)_ECtv^Cx6JeuSWA+fJkV@_#NIcRZ_(}=hT8O3*9IO}3DN*!ySlsD z)!ndO-P-Fb>)Q$=dh_5(<9_X~UAxrpbke@l675P{SM`)WYYssVZOtZ~3){DX=(gsI z(8udr&o2kjFOYik3$3rgc<=3Mo!;$1UsGF8GPh}W9X+T%8K>%AJ(gFD!qqPZnpFRR z;nkJU9{MZ9NjI)8!W@7t*PQ%v5FN1Yd$V?z9omHdxPSq_T8>n*HW*jG893W+RQCss zuF1b%t1ppO&tB|B549e;7(}W2TN-8_w$$Ur5IWvX|6gj;)2dCaPglm!=hSV`Al4<& zx-NVWxwIEnLP(=~LUS%m_9R2^5DHevzd+E80-`a6SF!08XmR)lVx}j^e z-Q!`Dnr>-y6f1P|NCP!p45N#%i=(8ba}iJo^|a7v7s?aT83pb|s32`d`*mHQgO&I<~4mrap)sgAD%W7vD^} z^z@d-$(`LQf4BOm+JCgUayf*UnEa#HP0VS(|3;aYt5M%_y-N@ zb?qT4oZLn`a^re?L5LWxxsTd^zLI$VG2VT3#XtazBkhWFC;Mqfu3fKqT7949Nt=GCk`_vc01&A!s{~63PQ+!VQ^X7fK-;9`BuCB&l zJ#1X<^*c`X(rRpK-|8awcfOkY;}J~lPu?JW^O{yh4jXK(%+I3eBNI<(`sc|89i2s& zhFj`Dm>BmHv*_|L>b!7n77`MjsC!n`ea4Q5Zra$lr#kMR{p~Dj&Hi={Jq8rxVu;%J zFo41{8e;1ytq5#`Z;uC{fwr{=U;tbw(gx2RhIbnHU(t^Ozv!bFK>>}R5|O6J^m|GZ I{_yMn0Kr*3{{R30 delta 5876 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 p&vGV*+(8Dq!kt-vACv`y&7V 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) {

  • - {{ u.name }} +
    + +

    Vorschläge {{ items.length }}

    Noch nichts vorgeschlagen.

      @@ -98,15 +117,63 @@ async function wuerfeln() { {{ item.genres.join(' · ') }} - {{ item.name || 'Anonym' }} +
    + +

    + Dabei ohne Vorschlag + + + +