Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,41 @@ The markdown should be rendered to HTML before sending; the VSCode markdown prev

## How to send emails

### Within a competition cycle

During a competition cycle the emails can be sent in one of two ways.
Since we offer teams the option of having their secondary contact(s) CCd into emails, we are unable to use GMail's mail merge feature.

For either of these routes you will need to have configured your SR GMail account to be able to send from `teams@`.

#### Personalised emails
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also do this with an extra spreadsheet that includes all the primary contacts and the secondary contacts. This list should be largely static.

Then we can remain within email clients.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requiring a script to send emails feels overkill to me, and is definitely a barrier for some people (yes, GitHub may also be a barrier, but a very different kind).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first step being having to set up a Python virtual environment would make it a massive barrier for a lot of people and I don't think is a reasonable expectation for our volunteers to have to know/be able to do this. GitHub can be a barrier too, but much easier to overcome imo

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for these comments. I appreciate the desire to ensure our processes are accessible and consider various alternatives. However the purpose of this PR is not to propose a new process, but rather to document one which has been in place for the substantial majority of this competition year.

While alternatives may be possible (though the requirements are more complicated than might be expected and the alternatives here aren't easily workable), a number of options were considered and this route chosen as the simplest given the volunteers currently responsible & involved. Documenting the process as it stands does not prevent it being changed in future, though it does immediately make it more accessible than the status quo (namely: no documentation).

Even if the process here isn't optimal, it strikes me as far better that it is documented than that it remain undocumented.


1. Set up a Python virtual environment and `pip install -r scripts/requirements.txt`
1. Export as a CSV the "Combined" tab of the current year's "Teams Organisation (internal)" spreadsheet
1. Pre-process that file using `./scripts/process-export.py $EXPORTED.csv > $EXPORTED.processed.csv`
1. Send the email using `./scripts/send.py`:
1. Create a fresh "app password" for your account for use by the script; set that via ` export SMTP_PASSWORD=...`
1. Set your username via `export SMTP_USERNAME=...`
1. Dry-run the send via the `--dry-run`, check that the printed output is what you expect
1. You may also want to dummy send the email to yourself; this requires creating a dummy CSV with equivalent columns to the processed file and then sending the email. Be sure you're sending to the right file when doing this!
1. Send the actual email. For 30 teams this takes a couple of minutes as the script rate-limits itself to avoid hitting GMail sending limits.
1. Delete from your account the app password you created in the earlier step

#### Mailshots

When no personalisation is needed, the email can be sent as a mailshot.
This involves:

* Rendering the email markdown yourself (VSCode's preview tab does this well for example)
* Identifying the recipient email addresses yourself (don't forget to include the secondary contacts for teams requesting that)
* Composing the email in your own SR GMail account
* Setting the email to be sent from `teams@`
* Setting the email as `To` `teams@`
* Putting all the recipient emails in the `BCC` field
* Copy/pasting the rendered email body into the email

### Outside a competition cycle

Emails should be sent using the MailChimp account.

When sending emails to 'All Teams', please send to both the _"Website - Potential Team Leaders"_ and _"Potential Team Leaders"_ lists.
82 changes: 82 additions & 0 deletions scripts/process-export.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#!/usr/bin/env python3

"""
Script for processing an exported CSV from the "Combined" tab of the current
year's "Teams Organisation (internal)" spreadsheet for use in emailing teams.
Outputs to standard output, typical usage is to redirect that to a file.
"""

import argparse
import csv
import re
import sys
from typing import TextIO


def pascal_case(text: str) -> str:
if text.isupper():
return text
return re.sub(r'\W+', '', text.title())


def main(input_csv: TextIO, in_place: bool) -> None:
rows = list(csv.reader(input_csv))

super_header_row = rows.pop(0)
header_row = rows.pop(0)

try:
supervisor_offset = super_header_row.index('Supervisor')
secondary_contact_offset = super_header_row.index('Secondary Contact')
secondary_contact_end = super_header_row.index('Kit Info')
except ValueError as e:
raise ValueError(f"Badly formed input file: {e}") from e

status_col = header_row.index('Status')

# Update in place to ensure the new values are written back
header_row = [pascal_case(x) for x in header_row]

for idx, value in enumerate(header_row):
if idx < supervisor_offset or idx > secondary_contact_end:
continue
if idx < secondary_contact_offset:
header_row[idx] = 'Primary' + value
else:
header_row[idx] = 'Secondary' + value

rows = [x for x in rows if x[status_col] not in ('Dropped Out', '')]

if in_place:
input_csv.seek(0)
input_csv.truncate()
output = input_csv
else:
output = sys.stdout

csv.writer(output).writerow(header_row)
csv.writer(output).writerows(rows)


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
'input_csv',
help="CSV file to process",
type=argparse.FileType(mode='r+'),
)
parser.add_argument(
'--in-place',
action='store_true',
default=False,
help="Update the file in place (discouraged)",
)
return parser.parse_args()


if __name__ == '__main__':
main(**(parse_args().__dict__))
16 changes: 15 additions & 1 deletion scripts/send.py
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
#!/usr/bin/env python3

"""
Script for sending emails to teams.

This supports:
- rendering the markdown to HTML
- CCing secondary contacts where asked
- personalising the email content based on Python format string style placeholders
"""

import argparse
import csv
import dataclasses
Expand Down Expand Up @@ -129,7 +140,10 @@ def main(args: argparse.Namespace) -> None:


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument('template', type=Path)
parser.add_argument('teams_csv', type=Path)
parser.add_argument(
Expand Down