Skip to content

Commit de9c098

Browse files
committed
actaully make the emails send at 7 am every day
1 parent 2d21039 commit de9c098

5 files changed

Lines changed: 166 additions & 90 deletions

File tree

.env.example

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ DB_USERNAME = # Postgresql Username
77
DB_PASSWORD = # Postgresql Password
88
DB_HOST = # Database host IP
99
DB_NAME = # Database name
10-
BRAWLSTATS_TOKEN = # API Key of the Brawl Stars API
10+
BRAWLSTATS_TOKEN = # API Key of the Brawl Stars API
11+
NOREPLY_EMAIL = # Actual gmail email
12+
CUSTOM_EMAIL = # Custom domain email to use
13+
NOREPLY_APP_PASSWORD = # 2fa app password

core/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from core.routes import root
77
from core.listeners import listeners
8+
from core.utils import handle_daily_emails
89

910

1011
def create_app(config_class=Config):
@@ -20,5 +21,6 @@ def create_app(config_class=Config):
2021
app.blueprint(listeners)
2122

2223
Session(app) # sanic_session
24+
app.add_task(handle_daily_emails)
2325

2426
return app

core/config.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,5 @@ class Config:
1818
DB_HOST = os.getenv('DB_HOST')
1919
BRAWLSTATS_TOKEN = os.getenv('BRAWLSTATS_TOKEN')
2020
NOREPLY_EMAIL = os.getenv('NOREPLY_EMAIL')
21-
EMAIL_APP_PASSWORD = os.getenv('NOREPLY_APP_PASSWORD')
21+
CUSTOM_EMAIL = os.getenv('CUSTOM_EMAIL')
22+
EMAIL_APP_PASSWORD = os.getenv('NOREPLY_APP_PASSWORD')

core/routes.py

Lines changed: 10 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
from sanic import Blueprint, response
1111
from sanic.exceptions import abort
1212

13-
from core.utils import add_message, disable_xss, login_required, open_db_connection, render_template
14-
from core.utils import daterange, thisweek
13+
from core.utils import (add_message, disable_xss, get_school_week,
14+
login_required, open_db_connection, render_template)
1515

1616
root = Blueprint('root')
1717

@@ -273,77 +273,7 @@ async def schoolweek(request, requested_date_str):
273273
if not first_day <= requested_date <= date(2021, 6, 11):
274274
abort(404, message=f'Requested URL {request.path} not found')
275275

276-
no_school = [
277-
date(2020, 9, 28), # Yom Kippur
278-
date(2020, 10, 12), # Columbus day
279-
date(2020, 11, 3), # Election day
280-
date(2020, 11, 11), # Veteran's day
281-
*daterange(date(2020, 11, 25), date(2020, 11, 27)), # Thanksgiving break
282-
*daterange(date(2020, 12, 24), date(2021, 1, 1)) # Holiday break
283-
]
284-
special_days = [
285-
date(2020, 10, 2)
286-
]
287-
288-
all_days = []
289-
day_map = {
290-
1: 'A',
291-
0: 'B'
292-
}
293-
294-
next_friday = thisweek(requested_date)[-1]
295-
elapsed_dates = daterange(first_day, next_friday)
296-
mondays = [d for d in elapsed_dates if d.weekday() == 0 and d not in no_school]
297-
cohort_day = 'maroon'
298-
299-
for d in elapsed_dates:
300-
if d in no_school:
301-
continue
302-
dow = d.weekday()
303-
if dow in (5, 6):
304-
continue
305-
if dow in (1, 3):
306-
cohort_day = 'maroon'
307-
elif dow in (2, 4):
308-
cohort_day = 'gray'
309-
elif dow == 0:
310-
# Mondays
311-
if d not in mondays:
312-
continue
313-
if mondays.index(d) % 2 == 0:
314-
cohort_day = 'maroon'
315-
else:
316-
cohort_day = 'gray'
317-
318-
try:
319-
prev_day = [day for day in all_days if day['cohort'] == cohort_day][-1]['day']
320-
except IndexError:
321-
# We are adding 1 for the first day of each cohort so we "start" with a B (0) day
322-
prev_day = 0
323-
324-
if d in special_days:
325-
# Skip a day
326-
all_days.append({
327-
'cohort': cohort_day,
328-
'date': d,
329-
'day': prev_day + 2})
330-
else:
331-
all_days.append({
332-
'cohort': cohort_day,
333-
'date': d,
334-
'day': prev_day + 1})
335-
336-
337-
week = thisweek(requested_date)
338-
339-
week_fmt = []
340-
for day in week:
341-
try:
342-
day_info = list(filter(lambda d: d['date'] == day, all_days))[0]
343-
except IndexError:
344-
week_fmt.append(f"{day.strftime('%a %m/%d')}<br>NO SCHOOL")
345-
else:
346-
week_fmt.append(f"{day.strftime('%a %m/%d')}<br>{day_info['cohort'].title()} {day_map[day_info['day'] % 2]} day")
276+
week_fmt = await get_school_week(requested_date, first_day, week=True)
347277

348278
return await render_template(
349279
template='schoolweek',
@@ -363,15 +293,18 @@ async def email_subscribe(request):
363293
except KeyError:
364294
return add_message(request, 'error', 'Enter an email in the field.', '/schoolweek')
365295

296+
async with open_db_connection(request.app) as conn:
297+
existing = await conn.fetchrow('SELECT * FROM mailing_list WHERE email = $1', email)
298+
if existing:
299+
return add_message(request, 'error', 'Email already subscribed.', '/schoolweek')
300+
366301
msg = EmailMessage()
367302
msg['Subject'] = 'Thank you for subscribing to GCHS Daily Updates!'
368-
# msg['Subject'] = f"GCHS Daily Email Notification ({date.today().strftime('%m/%d/%Y')})"
369-
msg['From'] = 'gchs-noreply@sharpbit.dev'
303+
msg['From'] = request.app.config.CUSTOM_EMAIL
370304
msg['To'] = email
371305
body = MIMEText(
372306
f"If this wasn't you, click <a href=\"http{'s' if not request.app.config.DEV else ''}://{request.app.config.DOMAIN}"
373307
f"/schoolweek/unsubscribe/{email}\">here</a> to unsubscribe.", 'html')
374-
# body = f"Today, {date.today().strftime('%m/%d/%Y')}, is a maroon A day"
375308
msg.set_content(body)
376309

377310
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
@@ -380,16 +313,12 @@ async def email_subscribe(request):
380313
smtp.send_message(msg)
381314

382315
async with open_db_connection(request.app) as conn:
383-
existing = await conn.fetchrow('SELECT * FROM mailing_list WHERE email = $1', email)
384-
if existing:
385-
return add_message(request, 'error', 'Email already subscribed.', '/schoolweek')
386316
await conn.execute('INSERT INTO mailing_list(email) VALUES ($1)', email)
387317

388-
# TODO: actually send the scheduled emails
389318
return add_message(request, 'success', 'Your email has been added to the mailing list.', '/schoolweek')
390319

391320
@root.get('/schoolweek/unsubscribe/<email>')
392321
async def email_unsubscribe(request, email):
393322
async with open_db_connection(request.app) as conn:
394323
await conn.execute('DELETE FROM mailing_list WHERE email = $1', email)
395-
return add_message(request, 'success', 'Your email has been removed from mailing list.', '/schoolweek')
324+
return add_message(request, 'success', 'Your email has been removed from mailing list.', '/schoolweek')

core/utils.py

Lines changed: 148 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
1-
from datetime import date, timedelta
2-
3-
from sanic import response
4-
5-
from jinja2 import Environment, PackageLoader
6-
from functools import wraps
1+
import asyncio
2+
import smtplib
73
from contextlib import asynccontextmanager
4+
from datetime import date, datetime, time as dt_time, timedelta
5+
from email.message import EmailMessage
6+
from email.mime.text import MIMEText
7+
from functools import wraps
88

99
import asyncpg
10+
from jinja2 import Environment, PackageLoader
11+
from sanic import response
1012

1113

1214
class Oauth2:
@@ -116,13 +118,14 @@ async def decorated_function(request, *args, **kwargs):
116118

117119

118120
def daterange(start_date: date, end_date: date) -> list:
119-
'''Creates a list of dates from the start date to end date, inclusive'''
121+
"""Creates a list of dates from the start date to end date, inclusive"""
120122
day_count = (end_date - start_date).days + 1
121123
return [start_date + timedelta(days=i) for i in range(day_count)]
122124

123125

124126

125127
def thisweek(today: date) -> list:
128+
"""Gets a list of dates for this week"""
126129
if 0 <= today.weekday() <= 4:
127130
# Monday to Friday
128131
monday = today - timedelta(days=today.weekday()) # Last Monday
@@ -131,3 +134,141 @@ def thisweek(today: date) -> list:
131134
friday = monday + timedelta(days=4)
132135

133136
return daterange(monday, friday)
137+
138+
139+
async def get_school_week(requested_date: date, first_day: date, week=True):
140+
no_school = [
141+
date(2020, 9, 28), # Yom Kippur
142+
date(2020, 10, 12), # Columbus day
143+
date(2020, 11, 3), # Election day
144+
date(2020, 11, 11), # Veteran's day
145+
*daterange(date(2020, 11, 25), date(2020, 11, 27)), # Thanksgiving break
146+
*daterange(date(2020, 12, 24), date(2021, 1, 1)) # Holiday break
147+
]
148+
special_days = [
149+
date(2020, 10, 2)
150+
]
151+
152+
all_days = []
153+
day_map = {
154+
1: 'A',
155+
0: 'B'
156+
}
157+
158+
next_friday = thisweek(requested_date)[-1]
159+
elapsed_dates = daterange(first_day, next_friday)
160+
mondays = [d for d in elapsed_dates if d.weekday() == 0 and d not in no_school]
161+
cohort_day = 'maroon'
162+
163+
for d in elapsed_dates:
164+
if d in no_school:
165+
continue
166+
dow = d.weekday()
167+
if dow in (5, 6):
168+
continue
169+
if dow in (1, 3):
170+
cohort_day = 'maroon'
171+
elif dow in (2, 4):
172+
cohort_day = 'gray'
173+
elif dow == 0:
174+
# Mondays
175+
if d not in mondays:
176+
continue
177+
if mondays.index(d) % 2 == 0:
178+
cohort_day = 'maroon'
179+
else:
180+
cohort_day = 'gray'
181+
182+
try:
183+
prev_day = [day for day in all_days if day['cohort'] == cohort_day][-1]['day']
184+
except IndexError:
185+
# We are adding 1 for the first day of each cohort so we "start" with a B (0) day
186+
prev_day = 0
187+
188+
if d in special_days:
189+
# Skip a day
190+
all_days.append({
191+
'cohort': cohort_day,
192+
'date': d,
193+
'day': prev_day + 2})
194+
else:
195+
all_days.append({
196+
'cohort': cohort_day,
197+
'date': d,
198+
'day': prev_day + 1})
199+
200+
201+
this_week = thisweek(requested_date)
202+
203+
if week:
204+
week_fmt = []
205+
for day in this_week:
206+
try:
207+
day_info = list(filter(lambda d: d['date'] == day, all_days))[0]
208+
except IndexError:
209+
week_fmt.append(f"{day.strftime('%a %m/%d')}<br>NO SCHOOL")
210+
else:
211+
week_fmt.append(f"{day.strftime('%a %m/%d')}<br>{day_info['cohort'].title()} {day_map[day_info['day'] % 2]} day")
212+
return week_fmt
213+
214+
try:
215+
day_info = list(filter(lambda d: d['date'] == requested_date, all_days))[0]
216+
except IndexError:
217+
# No school
218+
return None
219+
day_info['day'] = day_map[day_info['day'] % 2]
220+
return day_info
221+
222+
223+
async def handle_daily_emails(app):
224+
"""Send out an email at a specified time every weekday"""
225+
today = date.today()
226+
# Saturday/Sunday
227+
if today.weekday() in (5, 6):
228+
today = today - timedelta(days=today.weekday() - 7) # Not actually today, next monday
229+
next_email = datetime.combine(today, dt_time(7, 0, 0))
230+
231+
# Past the time today, send "tomorrow"
232+
if next_email < datetime.now():
233+
next_email = datetime.combine(today + timedelta(days=1), dt_time(7, 0, 0))
234+
235+
delta = (next_email - datetime.now()).seconds
236+
await asyncio.sleep(delta)
237+
238+
if today.weekday() in (5, 6):
239+
# Could be here if the func was called after the time on Friday
240+
return app.add_task(handle_daily_emails)
241+
242+
243+
today_info = await get_school_week(date(2020, 11, 16), date(2020, 9, 8), week=False)
244+
if today_info is None:
245+
# No school
246+
await asyncio.sleep(1)
247+
return app.add_task(handle_daily_emails)
248+
249+
async with open_db_connection(app) as conn:
250+
emails = await conn.fetch('SELECT * FROM mailing_list')
251+
252+
messages = []
253+
for email in emails:
254+
msg = EmailMessage()
255+
msg['Subject'] = f"GCHS Daily Email Notification for {date.today().strftime('%m/%d/%Y')}"
256+
msg['From'] = app.config.CUSTOM_EMAIL
257+
msg['To'] = email
258+
body = MIMEText(
259+
f"Today, {date.today().strftime('%m/%d/%Y')}, is a {today_info['cohort'].title()} {today_info['day']} day. <br><br>"
260+
f"Click <a href=\"http{'s' if not app.config.DEV else ''}://{app.config.DOMAIN}"
261+
f"/schoolweek/unsubscribe/{msg['To']}\">here</a> to unsubscribe.", 'html')
262+
263+
msg.set_content(body)
264+
messages.append(msg)
265+
266+
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
267+
smtp.login(app.config.NOREPLY_EMAIL, app.config.EMAIL_APP_PASSWORD)
268+
269+
for msg in messages:
270+
smtp.send_message(msg)
271+
272+
# Prevent it from sending twice
273+
await asyncio.sleep(1)
274+
app.add_task(handle_daily_emails)

0 commit comments

Comments
 (0)