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}")