Skip to content
This repository was archived by the owner on Dec 8, 2022. It is now read-only.

Commit e876b54

Browse files
committed
code challenge: setup daily reminder emails
1 parent a0bc097 commit e876b54

File tree

7 files changed

+449
-58
lines changed

7 files changed

+449
-58
lines changed

CodeChallenge/api/eb.py

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
from hmac import compare_digest
2-
3-
import requests
4-
from flask import Blueprint, request, current_app, render_template
5-
from flask_mail import Message
1+
from flask import Blueprint, current_app
62

73
from .. import core
8-
from ..mail import mail
94
from ..mailgun import mg_send
105
from ..models import Users
116

@@ -18,42 +13,6 @@ def eb_health_check():
1813
return "OK", 200
1914

2015

21-
# POST request from an AWS Lambda function once per day
22-
# any daily tasks should be placed here
23-
@bp.route("/worker", methods=["POST"])
24-
def worker():
25-
try:
26-
password = request.json["password"]
27-
except (TypeError, KeyError):
28-
return "", 400
29-
30-
if not compare_digest(password, current_app.config["WORKER_PASSWORD"]):
31-
return "", 401
32-
33-
# send daily reminder emails only while challenge is active, up until the first day of the final challenge
34-
if 1 <= core.day_number() <= core.max_rank():
35-
msg = Message(
36-
"New code challenge question is unlocked!",
37-
sender=current_app.config["MAIL_DEFAULT_SENDER"],
38-
recipients=[current_app.config["MG_LIST"]],
39-
)
40-
41-
msg.html = render_template(
42-
"challenge_daily_email.html",
43-
name="%recipient_fname%",
44-
external_url=current_app.config["EXTERNAL_URL"],
45-
)
46-
msg.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"}
47-
48-
mail.send(msg)
49-
50-
elif core.challenge_ended():
51-
# TODO: email everyone individually how many votes they have
52-
pass
53-
54-
return "", 200
55-
56-
5716
@bp.route("/teacher/progress", methods=["POST"])
5817
def teacher_progress():
5918
"""Send daily emails to teachers of their student's progress."""
@@ -64,3 +23,11 @@ def teacher_progress():
6423
mg_send([teacher.parent_email], "Code Challenge Student Progress", body)
6524

6625
return "", 200
26+
27+
28+
@bp.route("/daily", methods=["POST"])
29+
def daily_email():
30+
if current_app.config["DAILY_EMAILS"] and 1 <= core.day_number() <= core.max_rank():
31+
# Users.fire_daily_reminder()
32+
return "OK", 200
33+
return "Challenge not active", 200

CodeChallenge/cli/email.py

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
from flask import Blueprint, current_app, render_template
55
from flask_mail import Message
66
from jinja2 import Environment, FileSystemLoader, meta
7-
7+
from CodeChallenge.mailgun import mg_list_delete, mg_bulk_add, mg_create_list
8+
from CodeChallenge.models import Users
89
from ..mail import mail
910

1011
bp = Blueprint("emailcli", __name__, cli_group="email")
@@ -24,9 +25,11 @@ def get_template_variables(filename):
2425
@click.argument("subject")
2526
@click.argument("template")
2627
def email_send(to, subject, template):
27-
msg = Message(subject=subject,
28-
recipients=[to],
29-
sender=current_app.config["MAIL_DEFAULT_SENDER"])
28+
msg = Message(
29+
subject=subject,
30+
recipients=[to],
31+
sender=current_app.config["MAIL_DEFAULT_SENDER"],
32+
)
3033
msg.extra_headers = {"List-Unsubscribe": "%unsubscribe_email%"}
3134

3235
filled_vars = {}
@@ -42,3 +45,44 @@ def email_send(to, subject, template):
4245
mail.send(msg)
4346

4447
click.secho("message sent", fg="green")
48+
49+
50+
@bp.cli.command("build-list")
51+
def build_mg_list():
52+
list_name = current_app.config["MG_LIST"]
53+
54+
if list_name is None:
55+
click.secho("MG_LIST not set in config", fg="red", err=True)
56+
return
57+
58+
page = 0
59+
size = 500
60+
61+
click.secho(f"(re)creating list: {list_name}", fg="blue")
62+
63+
try:
64+
mg_list_delete(list_name)
65+
mg_create_list(list_name)
66+
except Exception as e:
67+
click.secho(f"list recreation failed: {str(e)}", err=True, fg="red")
68+
return
69+
70+
while True:
71+
offset = page * size
72+
click.secho(f"loading page {page} offset {offset}", fg="blue")
73+
74+
click.secho(f"generating recipient-variables", fg="blue")
75+
members = []
76+
for user in Users.query.limit(size).offset(offset).all():
77+
members.extend(user.mg_member())
78+
79+
click.secho(f"uploading batch to Mailgun")
80+
click.secho(str(mg_bulk_add(list_name, members)), fg="magenta")
81+
82+
if len(members) < size:
83+
click.secho("no more pages to load", fg="yellow")
84+
break
85+
86+
page += 1
87+
88+
click.secho("Done", fg="green")

CodeChallenge/config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class DefaultConfig:
3434
SLACK_SIGNING_SECRET = os.getenv("SLACK_SIGNING_SECRET")
3535
SLACK_OAUTH_TOKEN = os.getenv("SLACK_OAUTH_TOKEN")
3636
SLACK_CHANNEL = os.getenv("SLACK_CHANNEL")
37+
DAILY_EMAILS = False
3738

3839
# no trailing /
3940
EXTERNAL_URL = "https://challenge.codewizardshq.com"
@@ -79,6 +80,7 @@ class ProductionConfig(DefaultConfig):
7980
MG_LIST = os.getenv("MG_LIST")
8081
ANSWER_ATTEMPT_LIMIT = "5 per 1 minutes"
8182
VOTING_DISABLED = True
83+
DAILY_EMAILS = os.getenv("DAILY_EMAILS") != ""
8284

8385

8486
class DevelopmentConfig(ProductionConfig):
@@ -96,6 +98,7 @@ class DevelopmentConfig(ProductionConfig):
9698
MAIL_SUPPRESS_SEND = False
9799
TESTING = True
98100
TEST_EMAIL_RECIPIENT = "sam@codewizardshq.com"
101+
MG_LIST = "codechallenge-test@school.codewizardshq.com"
99102

100103
@property
101104
def DIST_DIR(self):

CodeChallenge/mailgun.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ def mg_list_add(email_address, name, data=None):
3333
return r
3434

3535

36+
def mg_list_delete(list_address: str):
37+
"""Delete the given Mailing List name from Mailgun."""
38+
response = requests.delete(
39+
f"https://api.mailgun.net/v3/lists/{list_address}",
40+
auth=__auth(),
41+
)
42+
43+
if not response.ok and response.status_code != 404:
44+
response.raise_for_status()
45+
46+
3647
def mg_validate(email_address):
3748
r = requests.get(
3849
"https://api.mailgun.net/v4/address/validate",
@@ -135,3 +146,23 @@ def mg_lists():
135146
response.raise_for_status()
136147

137148
return response.json()["items"]
149+
150+
151+
def mg_bulk_add(list_name: str, users: list):
152+
response = requests.post(
153+
f"https://api.mailgun.net/v3/lists/{list_name}/members.json",
154+
auth=__auth(),
155+
data={"upsert": True, "members": json.dumps(users)},
156+
)
157+
response.raise_for_status()
158+
return response.json()
159+
160+
161+
def mg_create_list(list_name: str):
162+
response = requests.post(
163+
"https://api.mailgun.net/v3/lists",
164+
auth=__auth(),
165+
data={"address": list_name, "description": "Code Challenge Participants"},
166+
)
167+
response.raise_for_status()
168+
return response.json()

CodeChallenge/models.py

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ def send_confirmation_email(self, password=None):
315315
password=password,
316316
)
317317

318-
def _mg_vars(self):
318+
def mg_recipient_vars(self):
319319
return dict(
320320
codeChallengeUsername=self.username,
321321
studentEmail=self.student_email,
@@ -325,21 +325,33 @@ def _mg_vars(self):
325325
parentFirstName=self.parent_first_name,
326326
parentLastName=self.parent_last_name,
327327
parentName=f"{self.parent_first_name} {self.parent_last_name}",
328-
userId=self.id,
329-
studentDOB=self.dob,
330-
type="",
331328
)
332329

333330
def add_to_mailing_list(self, list_name: str):
334-
for i, addr in enumerate(self._mail_recipients()):
335-
mg_vars = self._mg_vars()
336-
337-
if i == 0:
338-
mg_vars["type"] = "parent"
339-
else:
340-
mg_vars["type"] = "student"
331+
for addr in self._mail_recipients():
332+
mg_list_add(addr, list_name, self.mg_recipient_vars())
333+
334+
def mg_member(self) -> list[dict]:
335+
mg_vars = self.mg_recipient_vars()
336+
337+
members = [
338+
{
339+
"name": self.parent_first_name,
340+
"address": self.parent_email,
341+
"vars": mg_vars,
342+
}
343+
]
344+
345+
if self.student_email:
346+
members.append(
347+
{
348+
"name": self.student_first_name,
349+
"address": self.student_email,
350+
"vars": mg_vars,
351+
}
352+
)
341353

342-
mg_list_add(addr, list_name, mg_vars)
354+
return members
343355

344356
def generate_password(self):
345357
"""Generate a random password. Set's the user's password to the generated_students value,
@@ -457,6 +469,14 @@ def to_csv(self) -> Tuple[str, ...]:
457469
def lookup_teacher(cls, email: str):
458470
return cls.query.filter_by(is_teacher=True, parent_email=email).first()
459471

472+
@classmethod
473+
def fire_daily_reminder(cls):
474+
mg_send(
475+
[current_app.config["MG_LIST"]],
476+
"New code challenge question is unlocked!",
477+
render_template("challenge_daily.html"),
478+
)
479+
460480
def render_progress_report(self):
461481
assert self.is_teacher
462482
students = Users.query.filter_by(teacher_id=self.id).all()

0 commit comments

Comments
 (0)