Created script.
This commit is contained in:
parent
f3a461296c
commit
c3887eace9
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1,7 @@
|
||||
secret.py
|
||||
data.json
|
||||
output.json
|
||||
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
122
README.md
122
README.md
@ -1,2 +1,122 @@
|
||||
# secret-santa
|
||||
# README
|
||||
|
||||
This is a Python script for running a secret Santa draw.
|
||||
|
||||
This script:
|
||||
|
||||
- Ingests participant data in JSON format,
|
||||
- Automates the randomised pairing of people to buy gifts for,
|
||||
- Includes information about preferences or restrictions for gifts that participants may have,
|
||||
- Automatically emails participants via GMail SMTP (requires external set-up), and
|
||||
- Creates an output file of gift pairings for troubleshooting later.
|
||||
|
||||
The script only uses standard libraries that come bundled with Python distributions, and does not require installing any dependencies.
|
||||
|
||||
## Set Up
|
||||
|
||||
### GMail SMTP
|
||||
|
||||
This app uses GMail as an SMTP provider.
|
||||
|
||||
- This is because GMail accounts are free and easy to create,
|
||||
- The server is trusted by spam filters and is unlikely to be flagged as junk, and
|
||||
- The app can authenticate without using any external libraries.
|
||||
|
||||
This means that there will be a few steps of external set up.
|
||||
|
||||
- [ ] Create `secret.py` from a copy of `secret.py.bak`.
|
||||
- [ ] Log in to (or create) a GMail account (a standard, free account should suffice).
|
||||
- [ ] Enable two-factor authentication on the account.
|
||||
- [ ] Under the two-factor authentication section, generate an `App Password`.
|
||||
- [ ] Enter your GMail username and the generated app password in `secret.py`.
|
||||
|
||||
`secret.py`
|
||||
|
||||
```py
|
||||
USERNAME = '<username>@gmail.com'
|
||||
PASSWORD = '<app password>'
|
||||
```
|
||||
|
||||
You can get more information on [how to do this in the GMail documentation](https://support.google.com/mail/answer/185833?hl=en-GB).
|
||||
|
||||
#### Using a Different SMTP Server
|
||||
|
||||
You can set up a different SMTP provider by editing the server information in `config.py` and providing the appropriate username and password in `secret.py`.
|
||||
|
||||
### Data
|
||||
|
||||
The script ingests data as a JSON file.
|
||||
Create a file `data.json` (matching the file path in `config.py`) in the following format:
|
||||
|
||||
`data.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name0": {
|
||||
"email": "string",
|
||||
"preference": "string"
|
||||
},
|
||||
"name1": {
|
||||
"email": "string",
|
||||
"preference": "string"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> **Punctuation**: Comma after every entry, except the last entry.
|
||||
|
||||
JSON syntax is very fragile and can break if not entered correctly.
|
||||
|
||||
> [!TIP]
|
||||
> **Email Addresses**: If you have provided an `EMAIL_DOMAIN` value in `config.py`, then you do not need to add the full email address, and can just provide the username before the `@`.
|
||||
If you do provide a full email address for a participant in the data file, it will override the domain name given.
|
||||
|
||||
### Configuration
|
||||
|
||||
You can configure the app to send the draw for the secret Santa, including the greeting and the sign-off used, via the values in `config.py`.
|
||||
You can also edit the files in the `./templates` directory to customise the email message to include any additional instructions or text as needed.
|
||||
|
||||
The most important configurations, however, are the credentials in `secret.py` and the data in `data.json`
|
||||
|
||||
## Running the script
|
||||
|
||||
Because this script should work using standard libraries in Python 3, you should be able to execute the script using whatever Python 3 interpreter you have.
|
||||
|
||||
`PowerShell`
|
||||
|
||||
```powershell
|
||||
> python app.py
|
||||
```
|
||||
|
||||
`Bash`
|
||||
|
||||
```bash
|
||||
$ python3 app.py
|
||||
```
|
||||
|
||||
## The Algorithm
|
||||
|
||||
The algorithm I used for the secret santa draw is a very elegant one, as [illustrated by Prof Hannah Fry](https://www.youtube.com/watch?v=GhnCj7Fvqt0).
|
||||
|
||||
- Randomise the order of participants.
|
||||
- Generate a derangement from the randomised order by shifting the top name to the bottom of the list.
|
||||
- Match participants between the randomised list and derangement to pair who is buying for whom.
|
||||
|
||||
This ensures that:
|
||||
|
||||
- People know whom they are buying for, but do not know who else is buying for whom, and
|
||||
- Everybody has an equal chance of buying for everybody else.
|
||||
- The person running the draw also does not know who is buying for them (unless they peek behind the curtain, as it were).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If you do need to see who is buying for whom, you can check the `output.json` file, or check the sent emails from the SMTP server.
|
||||
|
||||
## Isn't This an Over-Engineered Solution for a Trivial Problem?
|
||||
|
||||
Of course it is.
|
||||
|
||||
## Why Not Use an Existing Site for This?
|
||||
|
||||
Because where is the fun in that?
|
||||
|
79
app.py
Normal file
79
app.py
Normal file
@ -0,0 +1,79 @@
|
||||
import json, os, random, smtplib, ssl
|
||||
from email.message import EmailMessage
|
||||
from string import Template
|
||||
|
||||
from config import Config as Config
|
||||
from secret import USERNAME, PASSWORD
|
||||
|
||||
context = ssl.create_default_context()
|
||||
|
||||
# Load data file
|
||||
data_path = os.path.normpath(Config.DATA)
|
||||
with open(data_path, 'r') as data_file:
|
||||
data = json.loads(data_file.read())
|
||||
|
||||
print('Parsed data file.')
|
||||
|
||||
if not data: raise Exception('Error: data file is empty.')
|
||||
|
||||
# Randomise participants.
|
||||
participants = list(data.keys())
|
||||
random.shuffle(participants)
|
||||
|
||||
# Create a derangement in the permutation by moving the first participant to the bottom
|
||||
derangement = list(participants)
|
||||
derangement.append(derangement.pop(0))
|
||||
|
||||
# Create a dictionary pairing participants and gift recipients
|
||||
buying = {}
|
||||
for index, participant in enumerate(participants):
|
||||
buying[participant] = derangement[index]
|
||||
buying = {key: value for key, value in sorted(buying.items())}
|
||||
|
||||
# Create output file
|
||||
output_path = os.path.normpath(Config.OUTPUT)
|
||||
with open(output_path, 'w') as output:
|
||||
output.write(json.dumps(buying, indent=4))
|
||||
|
||||
# Parse Templates
|
||||
plain_path = os.path.normpath('./templates/plain.txt')
|
||||
with open(plain_path, 'r') as plain_file:
|
||||
plain_template = Template(plain_file.read())
|
||||
rich_path = os.path.normpath('./templates/rich.html')
|
||||
with open(rich_path, 'r') as rich_file:
|
||||
rich_template = Template(rich_file.read())
|
||||
|
||||
def construct_message(participant, gift_recipient):
|
||||
message = EmailMessage()
|
||||
message['From'] = USERNAME
|
||||
if '@' in data[participant]['email']:
|
||||
participant_email = data[participant]['email']
|
||||
elif hasattr(Config,'EMAIL_DOMAIN') and Config.EMAIL_DOMAIN:
|
||||
participant_email = f'{data[participant]["email"]}@{Config.EMAIL_DOMAIN}'
|
||||
else:
|
||||
raise Exception(f'Error: Invalid email address for participant {participant}.')
|
||||
if hasattr(Config,'OVERRIDE_RECIPIENT') and Config.OVERRIDE_RECIPIENT and hasattr(Config,'RECIPIENT_EMAIL') and Config.RECIPIENT_EMAIL:
|
||||
message['To'] = Config.RECIPIENT_EMAIL
|
||||
else: message['To'] = participant_email
|
||||
message['Subject'] = Config.SUBJECT
|
||||
preference = 'None' if not data[gift_recipient].get('preference') else data[gift_recipient].get('preference')
|
||||
template_substitutions = {
|
||||
'subject': Config.SUBJECT,
|
||||
'greeting': Config.GREETING,
|
||||
'participant': participant.title(),
|
||||
'gift_recipient': gift_recipient.title(),
|
||||
'preference': preference,
|
||||
'sender': Config.SENDER
|
||||
}
|
||||
body_html = rich_template.substitute(template_substitutions)
|
||||
body_plain = plain_template.substitute(template_substitutions)
|
||||
message.set_content(body_plain)
|
||||
message.add_alternative(body_html, subtype='html')
|
||||
return message
|
||||
|
||||
messages = [construct_message(participant=participant, gift_recipient=gift_recipient) for participant, gift_recipient in buying.items()]
|
||||
|
||||
with smtplib.SMTP_SSL(Config.SERVER, Config.PORT, context=context) as smtp:
|
||||
smtp.login(user=USERNAME, password=PASSWORD)
|
||||
for message in messages: smtp.send_message(message)
|
||||
smtp.quit()
|
22
config.py
Normal file
22
config.py
Normal file
@ -0,0 +1,22 @@
|
||||
class Config(object):
|
||||
|
||||
# SMTP Configuration
|
||||
SERVER = 'smtp.gmail.com' # Uses GMail SMTP by default
|
||||
PORT = 465 # Default GMail SSL SMTP port
|
||||
|
||||
# Customisation
|
||||
GROUP = '' # Group/Organisation Name
|
||||
EMAIL_DOMAIN = 'bar.com' # Email domain for organisation
|
||||
SUBJECT = f'{GROUP} Secret Santa Draw' if GROUP else 'Secret Santa Draw' # Email subject line
|
||||
GREETING = 'Ho Ho Ho' # Email greeting
|
||||
SENDER = 'The Secret Santa Elf' # Email sign-off
|
||||
|
||||
# App Configs
|
||||
DATA = './data.json' # Data file
|
||||
OUTPUT = './output.json' # Output file
|
||||
|
||||
class Test(Config):
|
||||
|
||||
# Test Config
|
||||
OVERRIDE_RECIPIENT = True # Override the recipient for all emails
|
||||
RECIPIENT_EMAIL = '' # Test email address to receive all outgoing email
|
2
secret.bak.py
Normal file
2
secret.bak.py
Normal file
@ -0,0 +1,2 @@
|
||||
USERNAME = '' # GMail Username
|
||||
PASSWORD = '' # 16-character 'App Password' (without spaces).
|
15
templates/plain.txt
Normal file
15
templates/plain.txt
Normal file
@ -0,0 +1,15 @@
|
||||
$subject
|
||||
|
||||
$greeting $participant!
|
||||
|
||||
You are buying a gift for $gift_recipient.
|
||||
|
||||
That they have given the following preferences for their gift:
|
||||
|
||||
$preference
|
||||
|
||||
I hope you enjoy the secret Santa!
|
||||
|
||||
$sender
|
||||
|
||||
This is an automatically generated email. Please do not reply to this address as it will not be monitored.
|
45
templates/rich.html
Normal file
45
templates/rich.html
Normal file
@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>
|
||||
$subject
|
||||
</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>
|
||||
$subject
|
||||
</h1>
|
||||
<h2>
|
||||
$greeting $participant!
|
||||
</h2>
|
||||
<p>
|
||||
You are buying a gift for
|
||||
<strong>
|
||||
$gift_recipient
|
||||
</strong>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
That they have given the following preferences for their gift:
|
||||
<p>
|
||||
<strong>
|
||||
$preference
|
||||
</strong>
|
||||
</p>
|
||||
</p>
|
||||
<p>
|
||||
I hope you enjoy the secret Santa!
|
||||
</p>
|
||||
<p>
|
||||
$sender
|
||||
</p>
|
||||
<p>
|
||||
<strong>
|
||||
This is an automatically generated email.
|
||||
Please do not reply to this address as it will not be monitored.
|
||||
</strong>
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in New Issue
Block a user