121 lines
4.1 KiB
Python
121 lines
4.1 KiB
Python
import random
|
|
from db import SecretSantaDB
|
|
import hashlib
|
|
|
|
class SecretSantaGenerator:
|
|
|
|
def __init__(self, imp_size: int, db: SecretSantaDB):
|
|
self.imp_size = imp_size
|
|
self.db = db
|
|
|
|
def constellation_key(self, imps: dict) -> str:
|
|
emails = sorted([data["email"].strip().lower() for data in imps.values()])
|
|
raw = "|".join(emails)
|
|
return hashlib.sha256(raw.encode("utf-8")).hexdigest()
|
|
|
|
def derangements(self, n: int) -> int:
|
|
if n == 0: return 1
|
|
if n == 1: return 0
|
|
a, b = 1, 0
|
|
for k in range(2, n + 1):
|
|
a, b = b, (k - 1) * (a + b)
|
|
return b
|
|
|
|
def pair_exists(self, cycle_id: int, imp_pairs: dict, giver: str, receiver: str, imps: dict) -> bool:
|
|
if giver == receiver:
|
|
return True
|
|
if giver in imp_pairs:
|
|
return True
|
|
if receiver in imp_pairs.values():
|
|
return True
|
|
|
|
giver_email = imps[giver]["email"]
|
|
receiver_email = imps[receiver]["email"]
|
|
|
|
if self.db.pair_used_in_cycle(cycle_id, giver_email, receiver_email):
|
|
return True
|
|
|
|
return False
|
|
|
|
def generate_imp_pairs(self, round_id: int, imps: dict, cycle_id: int):
|
|
names = list(imps.keys())
|
|
n = len(names)
|
|
|
|
emails = {name: imps[name]["email"] for name in names}
|
|
|
|
def is_allowed(giver: str, receiver: str, current: dict) -> bool:
|
|
if giver == receiver:
|
|
return False
|
|
if giver in current:
|
|
return False
|
|
if receiver in current.values():
|
|
return False
|
|
return not self.db.pair_used_in_cycle(cycle_id, emails[giver], emails[receiver])
|
|
|
|
givers = names[:]
|
|
|
|
def backtrack(i: int, current: dict) -> dict | None:
|
|
if i == n:
|
|
return current
|
|
|
|
giver = givers[i]
|
|
|
|
candidates = names[:]
|
|
random.shuffle(candidates)
|
|
|
|
for receiver in candidates:
|
|
if not is_allowed(giver, receiver, current):
|
|
continue
|
|
current[giver] = receiver
|
|
result = backtrack(i + 1, current)
|
|
if result is not None:
|
|
return result
|
|
del current[giver]
|
|
|
|
return None
|
|
|
|
pairs = backtrack(0, {})
|
|
if pairs is None:
|
|
raise RuntimeError("Keine gültige Zuordnung im aktuellen Cycle möglich. Neuer Cycle nötig.")
|
|
|
|
for giver, receiver in pairs.items():
|
|
giver_id = self.db.get_imp_id_from_name(round_id, giver)
|
|
receiver_id = self.db.get_imp_id_from_name(round_id, receiver)
|
|
self.db.add_new_pair(round_id, giver_id, receiver_id)
|
|
|
|
def create_new_round(self,round_name: str, participants: dict[str, dict]) -> tuple[int, str]:
|
|
|
|
try:
|
|
key = self.constellation_key(participants)
|
|
|
|
latest_cycle = self.db.get_latest_cycle_id(key)
|
|
|
|
max_permutations = self.derangements(len(participants))
|
|
|
|
if latest_cycle is None:
|
|
cycle_id = self.db.add_new_cycle(key)
|
|
else:
|
|
used_permutations = self.db.count_rounds_in_cycle(latest_cycle)
|
|
if used_permutations < max_permutations:
|
|
cycle_id = latest_cycle
|
|
else:
|
|
cycle_id = self.db.add_new_cycle(key)
|
|
|
|
round_id, created_at = self.db.add_new_round(round_name, key, cycle_id)
|
|
|
|
self.db.add_participants(round_id, participants)
|
|
try:
|
|
self.generate_imp_pairs(round_id, participants, cycle_id)
|
|
except RuntimeError:
|
|
cycle_id = self.db.add_new_cycle(key)
|
|
round_id, created_at = self.db.add_new_round(round_name, key, cycle_id)
|
|
self.db.add_participants(round_id, participants)
|
|
self.generate_imp_pairs(round_id, participants, cycle_id)
|
|
|
|
self.db.commit_to_db()
|
|
return round_id, created_at
|
|
|
|
except Exception as e:
|
|
self.db.connection.rollback()
|
|
raise RuntimeError(f"Fehler beim Erstellen der Runde: {e}")
|