- FastAPI + SQLite, serverseitig gerenderte Templates, mobil-zuerst - Admin-Passwort, einseitige Ausschlüsse, eingefrorene Auslosung - Teilnehmer sehen Ergebnis per Cookie; Einmal-Recovery-Links bei Verlust - Docker/podman-tauglich (Entrypoint mit Privilegien-Drop, SELinux-:z) - Unit-Tests für Auslosung, E2E-Testskript (30 Checks)
42 lines
1.3 KiB
Python
42 lines
1.3 KiB
Python
"""Auslosung: zufällige Zuordnung, bei der niemand sich selbst zieht
|
|
und einseitige Ausschlüsse respektiert werden.
|
|
|
|
Ansatz: Rejection-Sampling über zufällige Permutationen. Für typische
|
|
Gruppen (Familien/Freunde, < 30 Personen) findet sich eine gültige
|
|
Permutation fast immer in wenigen Versuchen (nur die Derangement-
|
|
Bedingung allein hat bereits Erfolgswahrscheinlichkeit ~1/e).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
|
|
MAX_ATTEMPTS = 2000
|
|
|
|
|
|
def draw_assignment(
|
|
participant_ids: list[str],
|
|
exclusions: set[tuple[str, str]],
|
|
*,
|
|
rng: random.Random | None = None,
|
|
max_attempts: int = MAX_ATTEMPTS,
|
|
) -> dict[str, str] | None:
|
|
"""Liefert {giver_id: receiver_id} oder None, wenn keine gültige
|
|
Zuordnung gefunden wurde (z. B. unerfüllbare Ausschlüsse)."""
|
|
rng = rng or random.Random()
|
|
ids = list(participant_ids)
|
|
n = len(ids)
|
|
if n < 2:
|
|
return None
|
|
|
|
for _ in range(max_attempts):
|
|
receivers = ids[:]
|
|
rng.shuffle(receivers)
|
|
if any(receivers[i] == ids[i] for i in range(n)):
|
|
continue
|
|
mapping = dict(zip(ids, receivers))
|
|
if any((giver, receiver) in exclusions for giver, receiver in mapping.items()):
|
|
continue
|
|
return mapping
|
|
return None
|