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
73from 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
99import asyncpg
10+ from jinja2 import Environment , PackageLoader
11+ from sanic import response
1012
1113
1214class Oauth2 :
@@ -116,13 +118,14 @@ async def decorated_function(request, *args, **kwargs):
116118
117119
118120def 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
125127def 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