update
This commit is contained in:
@@ -686,6 +686,32 @@ async def film_holen(movie_id: int):
|
|||||||
return movie
|
return movie
|
||||||
|
|
||||||
|
|
||||||
|
# ── Info: Bezahlen und Regeln ───────────────────────────────────────────
|
||||||
|
INFO_MAX = 8000
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/api/info")
|
||||||
|
def info():
|
||||||
|
"""Ein Markdown-Text des Hosts: PayPal, IBAN, Regeln, was man wissen
|
||||||
|
muss. Solange keiner da ist, steht die Vorlage mit den Punkteregeln."""
|
||||||
|
return {"text": db.get_meta("info_text") or punkte.vorlage()}
|
||||||
|
|
||||||
|
|
||||||
|
@app.put("/api/info")
|
||||||
|
def info_setzen(
|
||||||
|
response: Response,
|
||||||
|
text: str = Body(..., embed=True),
|
||||||
|
filmabend_sid: str | None = Cookie(None),
|
||||||
|
):
|
||||||
|
sid = sitzung(response, filmabend_sid)
|
||||||
|
if not users.ist_host(sid):
|
||||||
|
raise HTTPException(status_code=403, detail="Nur für den Host")
|
||||||
|
with db.connect() as conn:
|
||||||
|
db.set_meta(conn, "info_text", text.strip()[:INFO_MAX])
|
||||||
|
events.senden("info")
|
||||||
|
return {"text": db.get_meta("info_text")}
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/features")
|
@app.get("/api/features")
|
||||||
def wuensche(response: Response, filmabend_sid: str | None = Cookie(None)):
|
def wuensche(response: Response, filmabend_sid: str | None = Cookie(None)):
|
||||||
sid = sitzung(response, filmabend_sid)
|
sid = sitzung(response, filmabend_sid)
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ KOMMENTAR = 5
|
|||||||
ANTWORT = 2
|
ANTWORT = 2
|
||||||
HERZ = 1
|
HERZ = 1
|
||||||
|
|
||||||
|
# Für den Info-Reiter: was zählt, mit der Zahl — aus denselben Konstanten
|
||||||
|
REGELN = (
|
||||||
|
("Beim Filmabend dabei", SCHAUEN),
|
||||||
|
("Film mit Sternen bewertet", BEWERTEN),
|
||||||
|
("Kommentar geschrieben", KOMMENTAR),
|
||||||
|
("Auf einen Kommentar geantwortet", ANTWORT),
|
||||||
|
("Herz vergeben", HERZ),
|
||||||
|
)
|
||||||
|
|
||||||
ZAEHLUNGEN = (
|
ZAEHLUNGEN = (
|
||||||
(SCHAUEN, "SELECT user_id, COUNT(*) AS n FROM watched_dabei GROUP BY user_id"),
|
(SCHAUEN, "SELECT user_id, COUNT(*) AS n FROM watched_dabei GROUP BY user_id"),
|
||||||
(
|
(
|
||||||
@@ -46,6 +55,17 @@ ZAEHLUNGEN = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def vorlage():
|
||||||
|
"""Der Info-Text, solange der Host keinen eigenen geschrieben hat —
|
||||||
|
die Regeln mit ihren Zahlen und die ersten Levelstufen, als Markdown."""
|
||||||
|
zeilen = ["## Erfahrung", ""]
|
||||||
|
zeilen += [f"- {was}: **+{p}**" for was, p in REGELN]
|
||||||
|
zeilen += ["", "Je Film zählt jede Art nur einmal.", "", "## Level", ""]
|
||||||
|
stufen = ", ".join(f"Level {stufe} ab {stufe * (stufe - 1) // 2}" for stufe in range(2, 8))
|
||||||
|
zeilen.append(f"{stufen} … Jedes weitere Level kostet einen Punkt mehr als das davor.")
|
||||||
|
return "\n".join(zeilen)
|
||||||
|
|
||||||
|
|
||||||
def level(punkte):
|
def level(punkte):
|
||||||
"""Größtes L mit L·(L−1)/2 ≤ Punkte. Null Punkte sind Level 1."""
|
"""Größtes L mit L·(L−1)/2 ≤ Punkte. Null Punkte sind Level 1."""
|
||||||
stufe = (1 + isqrt(1 + 8 * punkte)) // 2
|
stufe = (1 + isqrt(1 + 8 * punkte)) // 2
|
||||||
|
|||||||
22
backend/tests/test_info.py
Normal file
22
backend/tests/test_info.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""Der Info-Reiter: ein Markdown-Text des Hosts, vorbelegt mit den Regeln."""
|
||||||
|
|
||||||
|
from app import punkte
|
||||||
|
|
||||||
|
|
||||||
|
def test_vorlage_traegt_die_regeln_aus_dem_code(client):
|
||||||
|
text = client.get("/api/info").json()["text"]
|
||||||
|
assert text == punkte.vorlage()
|
||||||
|
assert f"- Beim Filmabend dabei: **+{punkte.SCHAUEN}**" in text
|
||||||
|
assert f"- Herz vergeben: **+{punkte.HERZ}**" in text
|
||||||
|
# Dreieckszahlen: Level 2 ab 1, Level 3 ab 3, Level 4 ab 6
|
||||||
|
assert "Level 2 ab 1, Level 3 ab 3, Level 4 ab 6" in text
|
||||||
|
|
||||||
|
|
||||||
|
def test_text_schreibt_nur_der_host(client, zuschauer, hostmodus):
|
||||||
|
assert client.put("/api/info", json={"text": "PayPal: x"}).status_code == 403
|
||||||
|
hostmodus(zuschauer)
|
||||||
|
assert zuschauer.put("/api/info", json={"text": " PayPal: x "}).json()["text"] == "PayPal: x"
|
||||||
|
assert client.get("/api/info").json()["text"] == "PayPal: x"
|
||||||
|
# Leer gespeichert: wieder die Vorlage
|
||||||
|
zuschauer.put("/api/info", json={"text": ""})
|
||||||
|
assert client.get("/api/info").json()["text"] == punkte.vorlage()
|
||||||
@@ -13,6 +13,7 @@ import NamensWahl from './components/NamensWahl.vue'
|
|||||||
import FilmLogin from './components/FilmLogin.vue'
|
import FilmLogin from './components/FilmLogin.vue'
|
||||||
import VerwaltungTab from './components/VerwaltungTab.vue'
|
import VerwaltungTab from './components/VerwaltungTab.vue'
|
||||||
import WuenscheTab from './components/WuenscheTab.vue'
|
import WuenscheTab from './components/WuenscheTab.vue'
|
||||||
|
import InfoTab from './components/InfoTab.vue'
|
||||||
import SearchTab from './components/SearchTab.vue'
|
import SearchTab from './components/SearchTab.vue'
|
||||||
import StreamTab from './components/StreamTab.vue'
|
import StreamTab from './components/StreamTab.vue'
|
||||||
import WatchedTab from './components/WatchedTab.vue'
|
import WatchedTab from './components/WatchedTab.vue'
|
||||||
@@ -150,6 +151,7 @@ const tabs = computed(() => [
|
|||||||
label: 'Wünsche',
|
label: 'Wünsche',
|
||||||
count: wuensche.value.filter((w) => !w.done_at).length,
|
count: wuensche.value.filter((w) => !w.done_at).length,
|
||||||
},
|
},
|
||||||
|
{ id: 'info', label: 'Info', count: null },
|
||||||
...(host.value || angemeldet.value
|
...(host.value || angemeldet.value
|
||||||
? [{ id: 'verwaltung', label: 'Verwaltung', count: null }]
|
? [{ id: 'verwaltung', label: 'Verwaltung', count: null }]
|
||||||
: []),
|
: []),
|
||||||
@@ -430,6 +432,8 @@ onMounted(load)
|
|||||||
@changed="ladeWuensche"
|
@changed="ladeWuensche"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<InfoTab v-else-if="activeTab === 'info'" :host="host" />
|
||||||
|
|
||||||
<VerwaltungTab v-else-if="activeTab === 'verwaltung'" :items="runde" :host="host" :ich="ich" />
|
<VerwaltungTab v-else-if="activeTab === 'verwaltung'" :items="runde" :host="host" :ich="ich" />
|
||||||
|
|
||||||
<StreamTab v-else-if="activeTab === 'stream'" :angemeldet="angemeldet" />
|
<StreamTab v-else-if="activeTab === 'stream'" :angemeldet="angemeldet" />
|
||||||
|
|||||||
114
frontend/src/components/InfoTab.vue
Normal file
114
frontend/src/components/InfoTab.vue
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
<script setup>
|
||||||
|
// Ein Markdown-Text, mehr nicht: PayPal, IBAN, Regeln, was man wissen
|
||||||
|
// muss. Der Host schreibt ihn hier direkt; bis dahin steht die Vorlage
|
||||||
|
// mit den Punkteregeln.
|
||||||
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
|
import { abonnieren } from '../live'
|
||||||
|
import { rendern } from '../markdown'
|
||||||
|
import MarkdownFeld from './MarkdownFeld.vue'
|
||||||
|
|
||||||
|
defineProps({ host: Boolean })
|
||||||
|
|
||||||
|
const text = ref('')
|
||||||
|
const laden = ref(true)
|
||||||
|
const bearbeitet = ref(false)
|
||||||
|
const entwurf = ref('')
|
||||||
|
const fehler = ref('')
|
||||||
|
|
||||||
|
async function laden_() {
|
||||||
|
try {
|
||||||
|
const resp = await fetch('/api/info')
|
||||||
|
if (!resp.ok) throw new Error('Infos konnten nicht geladen werden')
|
||||||
|
text.value = (await resp.json()).text
|
||||||
|
fehler.value = ''
|
||||||
|
} catch (err) {
|
||||||
|
fehler.value = err.message
|
||||||
|
} finally {
|
||||||
|
laden.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function bearbeiten() {
|
||||||
|
entwurf.value = text.value
|
||||||
|
bearbeitet.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function speichern() {
|
||||||
|
const resp = await fetch('/api/info', {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ text: entwurf.value }),
|
||||||
|
})
|
||||||
|
if (!resp.ok) {
|
||||||
|
fehler.value = 'Speichern hat nicht geklappt.'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bearbeitet.value = false
|
||||||
|
laden_()
|
||||||
|
}
|
||||||
|
|
||||||
|
let abbestellen = null
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
laden_()
|
||||||
|
abbestellen = abonnieren((nachricht) => {
|
||||||
|
if (nachricht.type === 'info') laden_()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => abbestellen?.())
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="info-tab">
|
||||||
|
<p v-if="laden" class="note">Lade …</p>
|
||||||
|
<p v-else-if="fehler" class="note error">{{ fehler }}</p>
|
||||||
|
<MarkdownFeld
|
||||||
|
v-else-if="bearbeitet"
|
||||||
|
v-model="entwurf"
|
||||||
|
platzhalter="PayPal, IBAN, Regeln, was man wissen muss …"
|
||||||
|
knopf="Speichern"
|
||||||
|
:maxlaenge="8000"
|
||||||
|
@senden="speichern"
|
||||||
|
@abbrechen="bearbeitet = false"
|
||||||
|
/>
|
||||||
|
<template v-else>
|
||||||
|
<div class="oben">
|
||||||
|
<button v-if="host" type="button" class="klein" @click="bearbeiten">Bearbeiten</button>
|
||||||
|
</div>
|
||||||
|
<div class="markdown" v-html="rendern(text)"></div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.info-tab {
|
||||||
|
max-width: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oben {
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.klein {
|
||||||
|
padding: 0.2rem 0.55rem;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.klein:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
padding: 2.5rem 0;
|
||||||
|
color: var(--muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note.error {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -35,6 +35,7 @@ const TAB_IDS = [
|
|||||||
'merkliste',
|
'merkliste',
|
||||||
'watched',
|
'watched',
|
||||||
'wuensche',
|
'wuensche',
|
||||||
|
'info',
|
||||||
'verwaltung',
|
'verwaltung',
|
||||||
'stream',
|
'stream',
|
||||||
'clips',
|
'clips',
|
||||||
|
|||||||
Reference in New Issue
Block a user