From 48c356031480332bb9594139b12af923e06fde87 Mon Sep 17 00:00:00 2001 From: Mario Corchero Date: Sun, 29 Dec 2024 16:45:18 +0100 Subject: [PATCH] Implement secret santa with backtracking Prevent flaky failures from invalid combinations. --- eas/api/secret_santa.py | 36 +++++++++++++++++++++++++ eas/api/views.py | 58 ++++++++++++----------------------------- pyproject.toml | 2 ++ 3 files changed, 54 insertions(+), 42 deletions(-) create mode 100644 eas/api/secret_santa.py create mode 100644 pyproject.toml diff --git a/eas/api/secret_santa.py b/eas/api/secret_santa.py new file mode 100644 index 0000000..e4da5d0 --- /dev/null +++ b/eas/api/secret_santa.py @@ -0,0 +1,36 @@ +import random + + +def _is_valid_assignment(source, target, exclusions): + if source == target: + return False + if (source, target) in exclusions: + return False + return True + + +def _backtrack_assignments(participants, exclusions, assignments, targets): + if not participants: + return assignments + + source = participants[0] + random.shuffle(targets) # Shuffle targets to ensure randomness + for target in targets: + if _is_valid_assignment(source, target, exclusions): + new_assignments = assignments + [(source, target)] + new_targets = targets[:] + new_targets.remove(target) + result = _backtrack_assignments( + participants[1:], exclusions, new_assignments, new_targets + ) + if result: + return result + + return None + + +def resolve_secret_santa( + participants, + exclusions, +): + return _backtrack_assignments(participants, exclusions, [], participants[:]) diff --git a/eas/api/views.py b/eas/api/views.py index d32c188..5c9023a 100644 --- a/eas/api/views.py +++ b/eas/api/views.py @@ -1,6 +1,5 @@ import datetime as dt import logging -import random import requests.exceptions from django.http import Http404 @@ -11,7 +10,7 @@ from rest_framework.exceptions import APIException, ValidationError from rest_framework.response import Response -from . import amazonsqs, instagram, models, paypal, serializers, tiktok +from . import amazonsqs, instagram, models, paypal, secret_santa, serializers, tiktok LOG = logging.getLogger(__name__) @@ -199,25 +198,6 @@ class LinkViewSet(BaseDrawViewSet): queryset = MODEL.objects.all() -def _ss_find_target(targets, exclusions): - potential_targets = set(targets) - exclusions - if not potential_targets: - return - return random.choice(list(potential_targets)) - - -def _ss_build_results(participants, exclusions_map): - results = [] - targets = list(participants) - for source in participants: - target = _ss_find_target(targets, exclusions_map[source]) - if not target: - return - targets.remove(target) - results.append((source, target)) - return results - - class SecretSantaSet( mixins.CreateModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet ): @@ -226,28 +206,22 @@ def create(self, request, *args, **kwargs): serializer = serializers.SecretSantaSerializer(data=request.data) serializer.is_valid(raise_exception=True) data = serializer.validated_data - emails_map = { - p["name"]: p["email"] for p in data["participants"] if p.get("email") - } - phones_map = { - p["name"]: p["phone_number"] - for p in data["participants"] - if p.get("phone_number") - } - exclusions_map = { - p["name"]: set(p.get("exclusions") or []) for p in data["participants"] - } - LOG.info("Using exclusion map: %r", exclusions_map) - for participant, exclusions in exclusions_map.items(): - exclusions.add(participant) - participants = {p["name"] for p in data["participants"]} - for _ in range(min(50, len(participants))): - results = _ss_build_results(participants, exclusions_map) - if results is not None: - break - else: + exclusions = [] + participants = [] + phones_map = {} + emails_map = {} + for p in data["participants"]: + participant = p["name"] + participants.append(participant) + if p.get("phone_number"): + phones_map[participant] = p["phone_number"] + if p.get("email"): + emails_map[participant] = p["email"] + for e in p.get("exclusions", []): + exclusions.append((participant, e)) + results = secret_santa.resolve_secret_santa(participants, exclusions) + if not results: raise ValidationError("Unable to match participants") - LOG.info("Sending %s secret santa emails", len(results)) draw = models.SecretSanta() draw.save() emails = [] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..1a17557 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.isort] +line_length = 88