From c53ace7eba7100bd5c689c3099fb20c79252ac67 Mon Sep 17 00:00:00 2001 From: Lukas Martin Date: Thu, 14 Nov 2024 23:11:57 +0100 Subject: [PATCH] [LM] added recursive matching and email sending --- .idea/vcs.xml | 4 +- participants.csv | 21 +++--- wichtel_matcher2.py | 168 ++++++++++++++++++++++++++++++++++++++++++++ wichteln_matcher.py | 100 +++++++++++++++++++------- 4 files changed, 255 insertions(+), 38 deletions(-) create mode 100644 wichtel_matcher2.py diff --git a/.idea/vcs.xml b/.idea/vcs.xml index d843f34..35eb1dd 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,4 +1,6 @@ - + + + \ No newline at end of file diff --git a/participants.csv b/participants.csv index 1f6baf4..6b3e7fe 100644 --- a/participants.csv +++ b/participants.csv @@ -1,11 +1,12 @@ id,name,household,email,last_match_id -0,Lukas,Martin,contact@lukasmartin.eu,4 -1,Jan,Martin,Null,3 -2,Ulrike,Martin,Null,5 -3,Petra,Scheib,Null,2 -4,Dietmar,Scheib,Null,1 -5,Lisa,Scheib,Null,8 -6,Andreas,Scheib,Null,0 -7,Annette,Simon,Null,6 -8,Julia,Simon,Null,3 -9,Marc,Simon,Null,6 +0,Lukas,Martin,contact@lukasmartin.eu,10 +1,Jan,Martin,lukas.martin155@outlook.de,4 +2,Ulrike,Martin,Null,7 +3,Petra,Scheib,Null,0 +4,Dietmar,Scheib,Null,9 +5,Lisa,Scheib,Null,1 +6,Andreas,Scheib,Null,8 +7,Mara,Scheib,Null,2 +8,Annette,Simon,Null,6 +9,Julia,Simon,Null,5 +10,Marc,Simon,Null,3 diff --git a/wichtel_matcher2.py b/wichtel_matcher2.py new file mode 100644 index 0000000..43c2546 --- /dev/null +++ b/wichtel_matcher2.py @@ -0,0 +1,168 @@ +import csv +import smtplib +from email.mime.text import MIMEText +from random import shuffle + +# 1. Load participants from CSV, now including id and last_match_id +def load_participants(filename): + participants = [] + with open(filename, 'r') as file: + reader = csv.DictReader(file) + for row in reader: + participants.append({ + 'id': int(row['id']), + 'name': row['name'], + 'household': row['household'], + 'email': row['email'], + 'last_match_id': int(row['last_match_id']) if row['last_match_id'] else None + }) + return participants + +# 2. Backtracking algorithm to find valid matches +def find_matches(givers, receivers, matches, used_receivers): + if len(matches) == len(givers): + return True # Found a complete set of matches + + giver = givers[len(matches)] # Process the next giver + + for receiver in receivers: + # Check constraints: different person, different household, no last-year match, no reciprocal match + if receiver['id'] not in used_receivers and \ + giver['id'] != receiver['id'] and \ + giver['household'] != receiver['household'] and \ + giver['last_match_id'] != receiver['id'] and \ + matches.get(receiver['id']) != giver['id']: + + # Assign receiver to giver + matches[giver['id']] = receiver + used_receivers.add(receiver['id']) + + # Recurse with the next giver + if find_matches(givers, receivers, matches, used_receivers): + return True # Solution found + + # Backtrack: remove the match and try another receiver + del matches[giver['id']] + used_receivers.remove(receiver['id']) + + return False # No valid match found, backtrack + +# 3. Wrapper function to initialize and run the backtracking +def generate_complete_matches(participants): + givers = participants.copy() + receivers = participants.copy() + shuffle(givers) # Shuffle for variety + shuffle(receivers) # Shuffle for variety + + matches = {} + used_receivers = set() + + # Start backtracking + if find_matches(givers, receivers, matches, used_receivers): + return matches + else: + print("Could not find a complete set of matches.") + return None + +# 4. Confirm matches interactively +def confirm_matches(participants, matches): + print("Proposed Matches:") + for giver_id, receiver in matches.items(): + giver = next(p for p in participants if p['id'] == giver_id) + print(f"{giver['name']} ({giver['email']}) will give a gift to {receiver['name']} ({receiver['email']})") + response = input("Approve this match? (yes/no): ").strip().lower() + if response != 'yes': + print("Match not approved. Restarting the matching process...\n") + return False # Signal to restart if any match is disapproved + return True + +# 5. Final confirmation to save matches +def final_confirmation(participants, matches): + print("\nFinal Match List:") + for giver_id, receiver in matches.items(): + giver = next(p for p in participants if p['id'] == giver_id) + print(f"{giver['name']} will give a gift to {receiver['name']}") + + # Final save prompt + response = input("\nDo you want to save this match list? (yes to save, no to restart): ").strip().lower() + return response == 'yes' + +# 6. Update last matches in the CSV file +def update_last_match_in_file(participants, matches, filename): + for giver_id, receiver in matches.items(): + for participant in participants: + if participant['id'] == giver_id: + participant['last_match_id'] = receiver['id'] + + with open(filename, 'w', newline='') as file: + fieldnames = ['id', 'name', 'household', 'email', 'last_match_id'] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + for participant in participants: + writer.writerow({ + 'id': participant['id'], + 'name': participant['name'], + 'household': participant['household'], + 'email': participant['email'], + 'last_match_id': participant['last_match_id'] + }) + print("Matches saved to 'participants.csv'.") + +# 7. Email sending function +# 7. Email sending function with improved formatting +def send_emails(matches, participants, sender_email, smtp_server, smtp_port, login, password): + for giver_id, receiver in matches.items(): + giver = next(p for p in participants if p['id'] == giver_id) + + # Compose email + subject = "Dein Wichtel-Match für dieses Jahr" + message = ( + f"Hallo liebe/r {giver['name']},\n\n" + f"Dein Match für das diesjährige Wichteln ist {receiver['name']}.\n\n" + "Mit freundlichen Grüßen,\n" + "Dein Wichtelsystem\n\n" + "--Bitte beachtet, dass ungefähr für 25€ gewichtelt wird--" + ) + + # Prepare email + msg = MIMEText(message, 'plain') + msg['Subject'] = subject + msg['From'] = sender_email + msg['To'] = giver['email'] + + # Send email + try: + with smtplib.SMTP(smtp_server, smtp_port) as server: + server.starttls() # Secure the connection + server.login(login, password) + server.sendmail(sender_email, giver['email'], msg.as_string()) + print(f"Email sent to {giver['name']} at {giver['email']}") + except Exception as e: + print(f"Error sending email to {giver['name']} ({giver['email']}): {e}") + + +# 8. Main function to run the match process +def main(): + participants = load_participants('participants.csv') + while True: + matches = generate_complete_matches(participants) + if matches and confirm_matches(participants, matches): + if final_confirmation(participants, matches): + update_last_match_in_file(participants, matches, 'participants.csv') + + # Send emails after saving the matches + sender_email = "weihnachtswichteln0@gmail.com" # Update to your email + smtp_server = "smtp.gmail.com" # Update to your SMTP server + smtp_port = 587 # Typically 587 for TLS + login = "weihnachtswichteln0@gmail.com" # SMTP login + password = "" # SMTP password + + send_emails(matches, participants, sender_email, smtp_server, smtp_port, login, password) + break + else: + print("\nRestarting the matching process...\n") + else: + print("No complete match found, restarting...\n") + +if __name__ == '__main__': + main() diff --git a/wichteln_matcher.py b/wichteln_matcher.py index b3b4fbc..2584763 100644 --- a/wichteln_matcher.py +++ b/wichteln_matcher.py @@ -16,31 +16,76 @@ def load_participants(filename): }) return participants -# 2. Match participants with conditions, avoiding self-gift and reciprocal matches -def match_participants(participants): - shuffled = participants.copy() - shuffle(shuffled) # Randomize list +# 2. Generate potential matches, ensuring a complete set +def generate_complete_matches(participants): + max_attempts = 100 # Limit to avoid infinite loops + attempts = 0 - matches = {} - for giver in participants: - for receiver in shuffled: - if giver['id'] != receiver['id'] and \ - giver['household'] != receiver['household'] and \ - giver['last_match_id'] != receiver['id'] and \ - matches.get(receiver['id']) != giver['id']: # Avoid reciprocal match - matches[giver['id']] = receiver - shuffled.remove(receiver) + while attempts < max_attempts: + # Shuffle both givers and receivers lists + givers = participants.copy() + receivers = participants.copy() + shuffle(givers) + shuffle(receivers) + + matches = {} + used_receivers = set() # Track receivers that have already been matched + + for giver in givers: + match_found = False + for receiver in receivers: + if receiver['id'] not in used_receivers and \ + giver['id'] != receiver['id'] and \ + giver['household'] != receiver['household'] and \ + giver['last_match_id'] != receiver['id'] and \ + matches.get(receiver['id']) != giver['id']: + matches[giver['id']] = receiver + used_receivers.add(receiver['id']) # Mark this receiver as used + match_found = True + break + + if not match_found: + # Incomplete match, restart the process + attempts += 1 break - return matches + else: + # Complete match set found + return matches + + print("Could not find a complete match set after several attempts. Restarting the entire process.") + return None + -# 3. Update participants with this year's matches +# 3. Confirm matches interactively +def confirm_matches(participants, matches): + print("Proposed Matches:") + for giver_id, receiver in matches.items(): + giver = next(p for p in participants if p['id'] == giver_id) + print(f"{giver['name']} ({giver['email']}) will give a gift to {receiver['name']} ({receiver['email']})") + response = input("Approve this match? (yes/no): ").strip().lower() + if response != 'yes': + print("Match not approved. Restarting the matching process...\n") + return False # Signal to restart if any match is disapproved + return True + +# 4. Final confirmation to save matches +def final_confirmation(participants, matches): + print("\nFinal Match List:") + for giver_id, receiver in matches.items(): + giver = next(p for p in participants if p['id'] == giver_id) + print(f"{giver['name']} will give a gift to {receiver['name']}") + + # Final save prompt + response = input("\nDo you want to save this match list? (yes to save, no to restart): ").strip().lower() + return response == 'yes' + +# 5. Update last matches in the CSV file def update_last_match_in_file(participants, matches, filename): for giver_id, receiver in matches.items(): for participant in participants: if participant['id'] == giver_id: participant['last_match_id'] = receiver['id'] - # Write updated participants back to CSV with open(filename, 'w', newline='') as file: fieldnames = ['id', 'name', 'household', 'email', 'last_match_id'] writer = csv.DictWriter(file, fieldnames=fieldnames) @@ -53,20 +98,21 @@ def update_last_match_in_file(participants, matches, filename): 'email': participant['email'], 'last_match_id': participant['last_match_id'] }) + print("Matches saved to 'participants.csv'.") -# 4. Main function to run the match and update the file +# 6. Main function to run the match process def main(): participants = load_participants('participants.csv') - matches = match_participants(participants) - - # Print and update matches - for giver_id, receiver in matches.items(): - giver = next(p for p in participants if p['id'] == giver_id) - print(f"{giver['name']} ({giver['email']}) will give a gift to {receiver['name']} ({receiver['email']})") - - # Update last matches in the CSV file - update_last_match_in_file(participants, matches, 'participants.csv') - print("Participants file updated with this year's matches.") + while True: + matches = generate_complete_matches(participants) + if matches and confirm_matches(participants, matches): + if final_confirmation(participants, matches): + update_last_match_in_file(participants, matches, 'participants.csv') + break + else: + print("\nRestarting the matching process...\n") + else: + print("No complete match found, restarting...\n") if __name__ == '__main__': main()