Compare commits
9 Commits
e0d1cf968c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 23c4276afb | |||
| 95f0dea5b3 | |||
| 0f2df17584 | |||
| 8934f58292 | |||
| 2f3f1d9a13 | |||
| 541b947609 | |||
| 598bae34ba | |||
| 0d9f5264d4 | |||
| 1d376d6b56 |
+1
-1
@@ -160,4 +160,4 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
config/env.py
|
src/env.py
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
|
|
||||||
# Random Christmas Bot
|
# Random Christmas Bot
|
||||||
|
|
||||||
|
This Python project automates the process of organizing a Secret Santa event. It randomly assigns each participant one or two recipients and ensures participants do not receive the same recipients from the previous years. The results are sent via email using an SMTP relay, and participant data (names, emails, and previous draw results) are managed in a CSV file.
|
||||||
This Python project automates the process of organizing a Secret Santa event. It randomly assigns each participant one or two recipients and ensures participants do not receive the same recipients from the previous year. The results are sent via email using an SMTP relay, and participant data (names, emails, and previous draw results) are managed in a CSV file.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Randomly assign one or two recipients for each participant.
|
- Randomly assign one or two recipients for each participant.
|
||||||
- Ensure participants do not receive the same recipients as last year.
|
- Ensure participants do not receive the same recipients as the last `n` years.
|
||||||
- Sends personalized emails with draw results to participants.
|
- Sends personalized emails with draw results to participants.
|
||||||
- Stores participant data (names, emails, and draw results) in a CSV file.
|
- Stores participant data (names, emails, and draw results) in a CSV file.
|
||||||
- Modular structure for better code maintenance.
|
- Modular structure for better code maintenance.
|
||||||
@@ -15,33 +15,34 @@ This Python project automates the process of organizing a Secret Santa event. It
|
|||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
secret-santa/
|
Random-Christmas-Bot/
|
||||||
│
|
|
||||||
├── config/
|
|
||||||
│ └── env.py # Configuration settings (SMTP, file paths, etc.)
|
|
||||||
│
|
│
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── draw.py # Logic for drawing names
|
│ ├── draw.py # Logic for drawing names
|
||||||
│ ├── emailer.py # Email sending functionality
|
│ ├── emailer.py # Email sending functionality
|
||||||
│ ├── file_io.py # File handling (CSV reading/writing)
|
│ ├── file_io.py # File handling (CSV reading/writing)
|
||||||
│ ├── main.py # Main program logic
|
│ ├── main.py # Main program logic
|
||||||
│ └── utils.py # Utility functions (date, time handling)
|
│ ├── utils.py # Utility functions (date, time handling)
|
||||||
|
│ └── env.py # Configuration settings (SMTP, file paths, etc.)
|
||||||
│
|
│
|
||||||
└── README.md # Project readme
|
└── README.md # Project readme
|
||||||
```
|
```
|
||||||
|
|
||||||
### `config/env.py`
|
### `src/env.py`
|
||||||
|
|
||||||
The configuration file contains SMTP settings, file paths, and customizable parameters for the draw.
|
The configuration file contains SMTP settings, file paths, and customizable parameters for the draw.
|
||||||
|
|
||||||
Example `env.py`:
|
Example `env.py`:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
|
# Configuration file with SMTP settings and CSV path settings
|
||||||
SMTP_SERVER = "smtp.example.com"
|
SMTP_SERVER = "smtp.example.com"
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
SENDER_EMAIL = "santa@example.com"
|
SENDER_EMAIL = "santa@example.com"
|
||||||
CSV_FILE_PATH = "secret_santa_DB.csv"
|
CSV_PATH = r"path\to\your\csv\files" # Path to the CSV files
|
||||||
DRAW_PER_PERSON = 2 # Choose 1 or 2 recipients per person
|
CSV_PREFIX = r"secret_santa_DB" # Prefix for CSV files
|
||||||
|
HISTORY_YEARS = 2 # Number of past years to consider in the draw
|
||||||
|
DRAW_PER_PERSON = 2 # Number of recipients per person
|
||||||
|
|
||||||
# Email content
|
# Email content
|
||||||
EMAIL_SUBJECT = "Secret Santa {year} Draw"
|
EMAIL_SUBJECT = "Secret Santa {year} Draw"
|
||||||
@@ -68,44 +69,57 @@ This email was sent automatically, please do not reply.
|
|||||||
|
|
||||||
1. Clone the repository or download the script files.
|
1. Clone the repository or download the script files.
|
||||||
2. Ensure you have Python installed on your system. If not, download and install Python from [here](https://www.python.org/downloads/).
|
2. Ensure you have Python installed on your system. If not, download and install Python from [here](https://www.python.org/downloads/).
|
||||||
3. Set up the `env.py` file in the `config/` directory, adjusting the SMTP settings, CSV file path, and draw parameters as needed.
|
3. Set up the `env.py` file in the `src/` directory, adjusting the SMTP settings, CSV file path, and draw parameters as needed.
|
||||||
|
|
||||||
Example structure of the CSV file:
|
|
||||||
|
|
||||||
|
Example structure of the CSV file with the following columns:
|
||||||
```csv
|
```csv
|
||||||
Name,Email,Last_Year_Recipient_1,Last_Year_Recipient_2
|
Name,Email,Last_Year_Recipient_1,Last_Year_Recipient_2
|
||||||
|
```
|
||||||
|
|
||||||
|
```csv
|
||||||
Alice,alice@example.com,Bob,Charlie
|
Alice,alice@example.com,Bob,Charlie
|
||||||
Bob,bob@example.com,Alice,David
|
Bob,bob@example.com,Alice,David
|
||||||
Charlie,charlie@example.com,David,Alice
|
Charlie,charlie@example.com,David,Alice
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Ensure the CSV file is in the correct location as specified in `env.py`.
|
4. Name your CSVs using a consistent naming convention `[prefix]_20xx.csv` as the program will retrieve those using the prefix set in `env.py`.
|
||||||
|
|
||||||
|
5. Ensure the CSV file is in the correct location as specified in `env.py`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
1. Run the main script by executing:
|
1. Create a CSV file for the current year draw with the participants' information (Name and email).
|
||||||
|
Like this:
|
||||||
|
```csv
|
||||||
|
Alice,alice@example.com
|
||||||
|
Bob,bob@example.com
|
||||||
|
Charlie,charlie@example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Run the main script by executing:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python src/main.py
|
python src/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
2. The script will:
|
3. The script will:
|
||||||
- Load participant data from the CSV file.
|
- Load participant data from the CSV file.
|
||||||
- Perform the Secret Santa draw based on the configuration (1 or 2 recipients).
|
- Perform the Secret Santa draw based on the configuration (1 or 2 recipients).
|
||||||
- Send an email to each participant with the names of their gift recipients.
|
- Send an email to each participant with the names of their gift recipients.
|
||||||
- Save the updated draw results back to the CSV file.
|
- Save the updated draw results back to the CSV file.
|
||||||
|
|
||||||
3. If any errors occur, they will be displayed in the console, and you can retry or debug as needed.
|
4. If any errors occur, they will be displayed in the console, and you can retry or debug as needed.
|
||||||
|
|
||||||
## Customization
|
## Customization
|
||||||
|
|
||||||
- **Number of recipients**: Modify `DRAW_PER_PERSON` in `env.py` to choose whether participants receive one or two recipients.
|
- **Number of recipients**: Modify `DRAW_PER_PERSON` in `env.py` to choose whether participants receive one or two recipients.
|
||||||
- **Email content**: Customize the email subject and body in `env.py` using placeholders like `{name}` for the participant's name and `{draws}` for their recipients.
|
- **Email content**: Customize the email subject and body in `env.py` using placeholders like `{name}` for the participant's name and `{draws}` for their recipients.
|
||||||
- **CSV file location**: Adjust the `CSV_FILE_PATH` in `env.py` if you prefer a different directory for the participant data.
|
- **CSV file location**: Adjust the `CSV_PATH` in `env.py` if you prefer a different directory for the participant data.
|
||||||
|
- **Number of historical years**: Change `HISTORY_YEARS` in `env.py` to set how many previous years of draws should be considered.
|
||||||
|
|
||||||
## File Descriptions
|
## File Descriptions
|
||||||
|
|
||||||
- **`draw.py`**: Contains the logic for performing the Secret Santa draw, ensuring no repeat recipients from last year.
|
- **`draw.py`**: Contains the logic for performing the Secret Santa draw, ensuring no repeat recipients from the last years.
|
||||||
- **`emailer.py`**: Handles email sending via the SMTP server.
|
- **`emailer.py`**: Handles email sending via the SMTP server.
|
||||||
- **`file_io.py`**: Responsible for reading and writing the participant data from/to the CSV file.
|
- **`file_io.py`**: Responsible for reading and writing the participant data from/to the CSV file.
|
||||||
- **`main.py`**: The main program that ties everything together and coordinates the draw and email sending.
|
- **`main.py`**: The main program that ties everything together and coordinates the draw and email sending.
|
||||||
@@ -123,4 +137,4 @@ This project is licensed under the MIT License. You are free to modify and distr
|
|||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Contributions are welcome! Feel free to open issues or submit pull requests to improve this project.
|
Contributions are welcome! Feel free to open issues or submit pull requests to improve this project.
|
||||||
|
|||||||
+38
-27
@@ -1,40 +1,51 @@
|
|||||||
from random import choice
|
from random import choice
|
||||||
|
|
||||||
def draw_names(previous_draw, draws_per_person):
|
def draw_names(current_participants, history_data, draws_per_person, max_attempts=5):
|
||||||
"""
|
"""
|
||||||
Perform the Secret Santa draw considering past draws.
|
Perform the Secret Santa draw considering past years' draws.
|
||||||
:param previous_draw: Last year's draw data (list of participants and recipients).
|
:param current_participants: This year's participant list.
|
||||||
|
:param history_data: Historical draw data to avoid repeats.
|
||||||
:param draws_per_person: Number of people each participant should give gifts to.
|
:param draws_per_person: Number of people each participant should give gifts to.
|
||||||
|
:param max_attempts: Maximum number of retry attempts before loosening exclusions. by default : 5.
|
||||||
:return: The new draw results.
|
:return: The new draw results.
|
||||||
"""
|
"""
|
||||||
participants = [a[0] for a in previous_draw] # Get participant names
|
participants = [p[0] for p in current_participants] # Get participant names
|
||||||
already_drawn = [] # Track who has been drawn
|
already_drawn = [] # Track who has been drawn
|
||||||
new_draw = [] # Store new draw results
|
new_draw = [] # Store new draw results
|
||||||
|
|
||||||
for i in range(len(participants)):
|
for attempt in range(max_attempts):
|
||||||
last_year_r1 = previous_draw[i][2]
|
new_draw.clear()
|
||||||
last_year_r2 = previous_draw[i][3]
|
already_drawn.clear()
|
||||||
giver = previous_draw[i][0]
|
success = True # Track if draw is successful in this attempt
|
||||||
email = previous_draw[i][1]
|
|
||||||
|
|
||||||
available_participants = participants.copy()
|
for i, giver in enumerate(participants):
|
||||||
try:
|
email = current_participants[i][1]
|
||||||
available_participants.remove(giver)
|
# Collect previous recipients to avoid drawing the same person again
|
||||||
available_participants.remove(last_year_r1)
|
previous_recipients = {recipient for record in history_data if record[0] == giver for recipient in record[2:]}
|
||||||
available_participants.remove(last_year_r2)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
new_recipients = []
|
# Create a set of available participants excluding the giver and previous recipients
|
||||||
while len(new_recipients) < draws_per_person:
|
available_participants = set(participants) - {giver} - previous_recipients
|
||||||
selected = choice(available_participants)
|
new_recipients = []
|
||||||
if already_drawn.count(selected) >= draws_per_person:
|
|
||||||
available_participants.remove(selected)
|
|
||||||
else:
|
|
||||||
new_recipients.append(selected)
|
|
||||||
already_drawn.append(selected)
|
|
||||||
available_participants.remove(selected)
|
|
||||||
|
|
||||||
new_draw.append([giver, email] + new_recipients)
|
# Ensure there are enough available participants for the draw
|
||||||
|
if len(available_participants) < draws_per_person:
|
||||||
|
success = False
|
||||||
|
break
|
||||||
|
|
||||||
return new_draw
|
# Select recipients for the current giver
|
||||||
|
while len(new_recipients) < draws_per_person:
|
||||||
|
selected = choice(list(available_participants))
|
||||||
|
if already_drawn.count(selected) < draws_per_person:
|
||||||
|
new_recipients.append(selected)
|
||||||
|
already_drawn.append(selected)
|
||||||
|
available_participants.discard(selected)
|
||||||
|
|
||||||
|
new_draw.append([giver, email] + new_recipients)
|
||||||
|
|
||||||
|
# If the draw was successful, break out of attempts
|
||||||
|
if success:
|
||||||
|
return new_draw
|
||||||
|
print(f"Attempt {attempt + 1} failed, retrying...")
|
||||||
|
|
||||||
|
# If all attempts fail, raise an error
|
||||||
|
raise ValueError("Unable to complete a valid draw after maximum attempts.")
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from config.env import SMTP_SERVER, SMTP_PORT, SENDER_EMAIL
|
from env import SMTP_SERVER, SMTP_PORT, SENDER_EMAIL
|
||||||
|
|
||||||
def send_email(receiver_email, subject, msg):
|
def send_email(receiver_email, subject, msg):
|
||||||
"""
|
"""
|
||||||
|
|||||||
+6
-3
@@ -1,8 +1,11 @@
|
|||||||
|
# Configuration file with SMTP settings and CSV path settings
|
||||||
SMTP_SERVER = "smtp.example.com"
|
SMTP_SERVER = "smtp.example.com"
|
||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
SENDER_EMAIL = "santa@example.com"
|
SENDER_EMAIL = "santa@example.com"
|
||||||
CSV_FILE_PATH = r"secret_santa_DB.csv"
|
CSV_PATH = r"path\to\your\csv\files" # Path to the CSV files
|
||||||
DRAW_PER_PERSON = 2 # Choose 1 or 2 recipients per person
|
CSV_PREFIX = r"secret_santa_DB" # Prefix for CSV files
|
||||||
|
HISTORY_YEARS = 2 # Number of past years to consider in the draw
|
||||||
|
DRAW_PER_PERSON = 2 # Number of recipients per person
|
||||||
|
|
||||||
# Email content
|
# Email content
|
||||||
EMAIL_SUBJECT = "Secret Santa {year} Draw"
|
EMAIL_SUBJECT = "Secret Santa {year} Draw"
|
||||||
@@ -15,4 +18,4 @@ Feel free to use your imagination and make their Christmas magical!
|
|||||||
Merry Christmas!
|
Merry Christmas!
|
||||||
|
|
||||||
This email was sent automatically, please do not reply.
|
This email was sent automatically, please do not reply.
|
||||||
"""
|
"""
|
||||||
|
|||||||
+52
-8
@@ -1,13 +1,57 @@
|
|||||||
import csv
|
import csv
|
||||||
|
from datetime import date
|
||||||
|
from env import CSV_PREFIX, CSV_PATH
|
||||||
|
|
||||||
def open_csv(file_name):
|
def load_history(years):
|
||||||
"""Open the CSV file and return its contents as a list of rows"""
|
"""
|
||||||
with open(file_name, "r", encoding='utf-8') as file:
|
Load participant data from multiple CSV files based on the specified history years.
|
||||||
reader = csv.reader(file)
|
:param years: Number of years of history to load.
|
||||||
return list(reader)
|
:return: A list of all historical draw data.
|
||||||
|
"""
|
||||||
|
current_year = date.today().year
|
||||||
|
history_data = []
|
||||||
|
|
||||||
def save_csv(data, file_name):
|
# Load data for each of the past specified years
|
||||||
"""Save the updated draw results to the CSV file"""
|
for i in range(1, years + 1):
|
||||||
|
|
||||||
|
try:
|
||||||
|
year = current_year - i
|
||||||
|
file_name = f"{CSV_PATH}/{CSV_PREFIX}_{year}.csv"
|
||||||
|
with open(file_name, "r", encoding='utf-8') as file:
|
||||||
|
reader = csv.reader(file)
|
||||||
|
history_data.extend(list(reader)) # Add each year's data
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"No historical file found for year {year}, skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
return history_data
|
||||||
|
|
||||||
|
def load_participants():
|
||||||
|
"""
|
||||||
|
Load participant data from the current year CSV file.
|
||||||
|
:return: A list of all historical draw data.
|
||||||
|
"""
|
||||||
|
current_year = date.today().year
|
||||||
|
participants_data = []
|
||||||
|
try:
|
||||||
|
file_name = f"{CSV_PATH}/{CSV_PREFIX}_{current_year}.csv"
|
||||||
|
print(file_name)
|
||||||
|
with open(file_name, "r", encoding='utf-8') as file:
|
||||||
|
reader = csv.reader(file)
|
||||||
|
participants_data.extend(list(reader)) # Add each year's data
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"No file found for year {current_year}, skipping.")
|
||||||
|
|
||||||
|
|
||||||
|
return participants_data
|
||||||
|
|
||||||
|
def save_csv(data, year):
|
||||||
|
"""
|
||||||
|
Save the new draw results to a CSV file named with the current year.
|
||||||
|
:param data: New draw data.
|
||||||
|
:param year: The current year for naming the file.
|
||||||
|
"""
|
||||||
|
file_name = f"{CSV_PATH}/{CSV_PREFIX}_{year}.csv"
|
||||||
with open(file_name, 'w', newline='', encoding='utf-8') as file:
|
with open(file_name, 'w', newline='', encoding='utf-8') as file:
|
||||||
writer = csv.writer(file)
|
writer = csv.writer(file)
|
||||||
writer.writerows(data)
|
writer.writerows(data)
|
||||||
+14
-14
@@ -1,8 +1,8 @@
|
|||||||
from config.env import CSV_FILE_PATH, DRAW_PER_PERSON, EMAIL_SUBJECT, EMAIL_BODY
|
from env import DRAW_PER_PERSON, HISTORY_YEARS, EMAIL_SUBJECT, EMAIL_BODY
|
||||||
from src.file_io import open_csv, save_csv
|
from file_io import load_history, save_csv, load_participants
|
||||||
from src.draw import draw_names
|
from draw import draw_names
|
||||||
from src.emailer import send_email
|
from emailer import send_email
|
||||||
from src.utils import get_current_time
|
from utils import get_current_time
|
||||||
from datetime import date
|
from datetime import date
|
||||||
|
|
||||||
def send_all_emails(new_draw):
|
def send_all_emails(new_draw):
|
||||||
@@ -11,29 +11,29 @@ def send_all_emails(new_draw):
|
|||||||
for participant in new_draw:
|
for participant in new_draw:
|
||||||
name = participant[0]
|
name = participant[0]
|
||||||
receiver_email = participant[1]
|
receiver_email = participant[1]
|
||||||
draws = ", ".join(participant[2:]) # List of recipients
|
draws = ", ".join(participant[2:])
|
||||||
message = EMAIL_BODY.format(name=name, draws=draws)
|
message = EMAIL_BODY.format(name=name, draws=draws)
|
||||||
subject = EMAIL_SUBJECT.format(year=current_year)
|
subject = EMAIL_SUBJECT.format(year=current_year)
|
||||||
send_email(receiver_email, subject, message) # Send the email
|
send_email(receiver_email, subject, message)
|
||||||
print(f"Email sent to {name} ({receiver_email})")
|
print(f"Email sent to {name} ({receiver_email})")
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
# Load previous draw data
|
# Load data from historical CSVs and this year's participants
|
||||||
old_draw = open_csv(CSV_FILE_PATH)
|
history_data = load_history(HISTORY_YEARS)
|
||||||
|
|
||||||
# Perform new draw
|
current_year = date.today().year
|
||||||
new_draw = draw_names(old_draw, DRAW_PER_PERSON)
|
current_participants = load_participants() # Load participants data
|
||||||
|
|
||||||
|
# Perform the draw
|
||||||
|
new_draw = draw_names(current_participants, history_data, DRAW_PER_PERSON)
|
||||||
# Send emails to participants
|
# Send emails to participants
|
||||||
send_all_emails(new_draw)
|
send_all_emails(new_draw)
|
||||||
|
|
||||||
# Save new draw results
|
# Save new draw results
|
||||||
save_csv(new_draw, CSV_FILE_PATH)
|
save_csv(new_draw, current_year)
|
||||||
|
|
||||||
# Output completion time
|
|
||||||
print(f"Process completed at {get_current_time()[1]} on {get_current_time()[0]}")
|
print(f"Process completed at {get_current_time()[1]} on {get_current_time()[0]}")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error occurred: {e}")
|
print(f"Error occurred: {e}")
|
||||||
# Retry or handle errors
|
|
||||||
|
|||||||
Reference in New Issue
Block a user