Skip to content

Commit 97541a3

Browse files
authored
Merge pull request #43 from Women-in-Computing-at-RIT/feature/confirmations
Feature/confirmations
2 parents 71eea74 + 4e189dd commit 97541a3

18 files changed

Lines changed: 682 additions & 22 deletions

api/.DS_Store

0 Bytes
Binary file not shown.

api/src/controller/confirmation.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import logging
2+
3+
from flask_restful import Resource, reqparse
4+
from flask import request
5+
from data.application import createApplication, updateApplication
6+
from data.users import getUserByAuthID, getUserIdFromAuthID
7+
from utils.authentication import authenticate
8+
from data.permissions import canUpdateApplicationStatus
9+
from data.email import sendConfirmedEmail, CONFIRMED, ACCEPTED
10+
11+
logger = logging.getLogger("Confirmation")
12+
13+
14+
class Confirmation(Resource):
15+
PATH = '/user/application/confirm'
16+
17+
def post(self):
18+
"""
19+
Update user application status to CONFIRMED
20+
:return:
21+
"""
22+
authenticationPayload = authenticate(request.headers)
23+
if authenticationPayload is None:
24+
return {"message": "Authorization Header Failure"}, 401
25+
auth0_id = authenticationPayload['sub']
26+
27+
userData = getUserByAuthID(auth0_id)
28+
if userData is None:
29+
return {"message": "Internal Server Error"}, 500
30+
if len(userData.keys()) == 0:
31+
return {"message": "Could Not Find User"}, 400
32+
if not userData['status'] == ACCEPTED:
33+
if userData['status'] == CONFIRMED:
34+
return {"message": "User Already Confirmed"}
35+
return {"message": "User Status Failed"}, 400
36+
37+
# update status by retrieving and re-saving application
38+
applicationUpdated = updateApplication(applicationId=userData['application_id'],
39+
status=CONFIRMED,
40+
major=userData['major'],
41+
levelOfStudy=userData['level_of_study'],
42+
age=userData['age'],
43+
shirtSize=userData['shirt_size'],
44+
hasAttendedWiCHacks=userData['has_attended_wichacks'],
45+
hasAttendedHackathons=userData['has_attended_hackathons'],
46+
university=userData['university'],
47+
gender=userData['gender'],
48+
busRider=userData['bus_rider'],
49+
busStop=userData['bus_stop'],
50+
dietaryRestrictions=userData['dietary_restrictions'],
51+
specialAccommodations=userData['special_accommodations'],
52+
affirmedAgreements=userData['affirmed_agreements'],
53+
isVirtual=userData['is_virtual'],
54+
mlhEmailsAllowed=userData['allowMlhEmails']
55+
)
56+
if applicationUpdated is None:
57+
return {"message": "Internal Server Error"}, 500
58+
elif not applicationUpdated:
59+
return {"message": "Application Update Failure"}, 400
60+
61+
sendConfirmedEmail(getUserIdFromAuthID(auth_id=auth0_id))
62+
return {"message": "Application Updated"}, 200

api/src/controller/email.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from flask_restful import Resource, reqparse
2+
from flask import request
3+
4+
from data.users import getUserEmailsWithFilter
5+
from utils.authentication import authenticate, getAuthToken
6+
from data.permissions import canSendEmails
7+
from data.email import sendGroupEmail
8+
9+
10+
class Email(Resource):
11+
PATH = '/email'
12+
13+
def post(self):
14+
parser = reqparse.RequestParser()
15+
parser.add_argument('body', type=str, required=True)
16+
parser.add_argument('subjectLine', type=str, required=True)
17+
parser.add_argument('recipientStatusFilter', type=str, required=True, action="append")
18+
args = parser.parse_args()
19+
20+
authenticationPayload = authenticate(request.headers)
21+
if authenticationPayload is None:
22+
return {"message": "Must be logged in"}, 401
23+
auth0_id = authenticationPayload['sub']
24+
25+
permissions = canSendEmails(auth0_id)
26+
if permissions is None:
27+
return {"message": "Internal Server Error"}, 500
28+
if not permissions:
29+
return {"message": "Permission Denied"}, 403
30+
31+
userEmails = getUserEmailsWithFilter(args['recipientStatusFilter'])
32+
33+
if len(userEmails) == 0:
34+
# no recipients
35+
return {"message": "No Recipients"}, 400
36+
37+
failedEmailList, didSucceed = sendGroupEmail(emailAddresses=userEmails, subject=args['subjectLine'], content=args['body'])
38+
if didSucceed:
39+
return {"message": "Email Successfully Sent"}
40+
else:
41+
return {"message": "Email Failed", "failedEmailAddresses": failedEmailList}, 500

api/src/controller/emailPreset.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from flask_restful import Resource, reqparse
2+
from flask import request
3+
4+
from data.users import getUserEmailsWithFilter
5+
from utils.authentication import authenticate, getAuthToken
6+
from data.permissions import canSendEmails
7+
from data.email import sendPresetEmail
8+
9+
10+
class EmailPreset(Resource):
11+
PATH = '/email/preset/<emailName>'
12+
13+
def post(self, emailName):
14+
authenticationPayload = authenticate(request.headers)
15+
if authenticationPayload is None:
16+
return {"message": "Must be logged in"}, 401
17+
auth0_id = authenticationPayload['sub']
18+
19+
permissions = canSendEmails(auth0_id)
20+
if permissions is None:
21+
return {"message": "Internal Server Error"}, 500
22+
if not permissions:
23+
return {"message": "Permission Denied"}, 403
24+
25+
failedEmailList, didSucceed = sendPresetEmail(emailName)
26+
if didSucceed:
27+
return {"message": "Email Successfully Sent"}
28+
else:
29+
return {"message": "Email Failed", "failedEmailAddresses": failedEmailList}, 500

api/src/data/email.py

Lines changed: 73 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,49 @@
11
import os
22

3-
from data.users import getUserByAuthID, getUserByUserID
3+
from data.users import getUserByAuthID, getUserByUserID, getUserEmailsWithFilter
44
from data.emailTemplates.applied import getAppliedEmail, getAppliedSubjectLine
55
from data.emailTemplates.accepted import getAcceptedEmail, getAcceptedSubjectLine
66
from data.emailTemplates.rejected import getRejectedEmail, getRejectedSubjectLine
7-
from data.emailTemplates.confirmed import getConfirmedEmail, getConfirmedSubjectLine
7+
from data.emailTemplates.confirmed import getConfirmedEmail, getConfirmedSubjectLine, getRequestConfirmedEmail, \
8+
getRequestConfirmedSubjectLine
89
import logging
910
from sendgrid import SendGridAPIClient
10-
from sendgrid.helpers.mail import Mail
11-
from typing import Union, List
11+
from sendgrid.helpers.mail import Mail, Personalization, Bcc
12+
from typing import Union, List, Tuple
1213
from utils.aws import getSendgridAPIKey
1314

1415
logger = logging.getLogger("email")
1516
WICHACKS_EMAIL = "organizers@wichacks.io"
1617

18+
ACCEPTED = "ACCEPTED"
19+
REJECTED = "REJECTED"
20+
CONFIRMED = "CONFIRMED"
21+
22+
23+
def sendPresetEmail(emailName) -> Tuple[List[str], bool]:
24+
"""
25+
Send preset email
26+
:param emailName:
27+
:return:
28+
"""
29+
if emailName == "requestConfirmation":
30+
return sendRequestConfirmedEmail()
31+
32+
# default behavior mimic crash
33+
return None, False
34+
35+
1736
def sendEmailByStatus(userID, status) -> bool:
18-
if status == "ACCEPTED":
37+
if status == ACCEPTED:
1938
return sendAcceptedEmail(userID)
20-
elif status == "REJECTED":
39+
elif status == REJECTED:
2140
return sendRejectedEmail(userID)
22-
elif status == "CONFIRMED":
41+
elif status == CONFIRMED:
2342
return sendConfirmedEmail(userID)
2443
else:
2544
logger.error("Application Status Change, Status not recognized: %s for user %s", status, userID)
45+
46+
2647
def sendAppliedEmail(auth0ID) -> bool:
2748
"""
2849
Wrapper of send email for applied email
@@ -39,7 +60,7 @@ def sendAppliedEmail(auth0ID) -> bool:
3960
return False
4061
messageContent = getAppliedEmail(firstName, lastName)
4162
subjectLine = getAppliedSubjectLine()
42-
return sendEmail(emailAddresses=emailAddress, subject=subjectLine, content=messageContent)
63+
return sendEmailToSingleRecipient(emailAddress=emailAddress, subject=subjectLine, content=messageContent)
4364

4465

4566
def sendConfirmedEmail(userId) -> bool:
@@ -53,9 +74,6 @@ def sendConfirmedEmail(userId) -> bool:
5374
:param auth0ID:
5475
:return: bool success
5576
"""
56-
logger.error("Confirmation Email Not Enabled")
57-
return False # REMOVE BEFORE ENABLING
58-
5977
userData = getUserByUserID(userId)
6078
emailAddress = userData.get("email", None)
6179
firstName = userData.get("first_name", None)
@@ -66,7 +84,24 @@ def sendConfirmedEmail(userId) -> bool:
6684
return False
6785
messageContent = getConfirmedEmail(firstName, lastName)
6886
subjectLine = getConfirmedSubjectLine()
69-
return sendEmail(emailAddresses=emailAddress, subject=subjectLine, content=messageContent)
87+
return sendEmailToSingleRecipient(emailAddress=emailAddress, subject=subjectLine, content=messageContent)
88+
89+
90+
def sendRequestConfirmedEmail() -> Tuple[List[str], bool]:
91+
"""
92+
Wrapper of send email for requesting confirmation email
93+
:param: applicationStatusFilterList list of application statuses that will receive the email, default is email goes to nobody
94+
:return: list of failed email addresses
95+
"""
96+
"""
97+
Wrapper of send email for applied email
98+
:return: bool success
99+
"""
100+
userEmails = getUserEmailsWithFilter([ACCEPTED])
101+
messageContent = getRequestConfirmedEmail()
102+
subjectLine = getRequestConfirmedSubjectLine()
103+
104+
return sendGroupEmail(emailAddresses=userEmails, subject=subjectLine, content=messageContent)
70105

71106

72107
def sendAcceptedEmail(userId) -> bool:
@@ -85,7 +120,7 @@ def sendAcceptedEmail(userId) -> bool:
85120
return False
86121
messageContent = getAcceptedEmail(firstName, lastName)
87122
subjectLine = getAcceptedSubjectLine()
88-
return sendEmail(emailAddresses=emailAddress, subject=subjectLine, content=messageContent)
123+
return sendEmailToSingleRecipient(emailAddress=emailAddress, subject=subjectLine, content=messageContent)
89124

90125

91126
def sendRejectedEmail(userId) -> bool:
@@ -104,21 +139,21 @@ def sendRejectedEmail(userId) -> bool:
104139
return False
105140
messageContent = getRejectedEmail(firstName, lastName)
106141
subjectLine = getRejectedSubjectLine()
107-
return sendEmail(emailAddresses=emailAddress, subject=subjectLine, content=messageContent)
142+
return sendEmailToSingleRecipient(emailAddress=emailAddress, subject=subjectLine, content=messageContent)
108143

109144

110-
def sendEmail(emailAddresses: Union[List[str], str], subject, content) -> bool:
145+
def sendEmailToSingleRecipient(emailAddress: str, subject, content) -> bool:
111146
"""
112-
Send emailType to user based on id
147+
Send email with subject line and content to email address
113148
:param subject: subject line of the email
114-
:param emailAddresses: single or list of string email addresses to send email to
149+
:param emailAddress: single email address to send email to
115150
:param content: HTML to send
116151
:return:
117152
"""
118153

119154
message = Mail(
120155
from_email=WICHACKS_EMAIL,
121-
to_emails=emailAddresses,
156+
to_emails=emailAddress,
122157
subject=subject,
123158
html_content=content
124159
)
@@ -132,6 +167,26 @@ def sendEmail(emailAddresses: Union[List[str], str], subject, content) -> bool:
132167
return True
133168

134169

170+
def sendGroupEmail(emailAddresses: List[str], subject, content) -> Tuple[List[str], bool]:
171+
"""
172+
Send email to group of users
173+
:param subject: subject line of the email
174+
:param emailAddresses: single or list of string email addresses to send email to
175+
:param content: HTML to send
176+
:return: list of emails that the email failed to send to
177+
"""
178+
failedEmailList = []
179+
for email in emailAddresses:
180+
emailSuccessfullySent = sendEmailToSingleRecipient(emailAddress=email, subject=subject, content=content)
181+
if not emailSuccessfullySent:
182+
failedEmailList.append(email)
183+
emailsSuccessful = True
184+
if len(failedEmailList) > 0:
185+
emailsSuccessful = False
186+
logger.error("Sending Group Email Failed. Email errors for: %s", failedEmailList)
187+
return failedEmailList, emailsSuccessful
188+
189+
135190
def getSendGridClient():
136191
"""
137192
Return Send Grid Api Client
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<body style="margin: 0; padding: 0;">
2+
<div style="font-family: Verdana, Geneva, Tahoma, sans-serif; margin: 0; padding: 0;">
3+
<div style="align-content: center;"> <!-- header -->
4+
<img alt="WiCHacks 2023 Banner" src="https://raw.githubusercontent.com/Women-in-Computing-at-RIT/WicHacker-Manager/main/assets/centered-banner.png" style="max-width: 100%; max-height: 100%;"/>
5+
</div>
6+
7+
<div style="margin: 10px"> <!-- Email Body -->
8+
<p>Hi first_name last_name,</p>
9+
<p>Thank you for confirming your attendance of WiCHacks! Keep an eye on your email in the coming days for more information including instructions for how to join our Discord server for the event, as well as how to best prepare!</p>
10+
<p>We can't wait to see you online or in person, it's going to be a great event!</p>
11+
12+
<p>Until Next Time,</p>
13+
<p>The WiCHacks Team</p>
14+
15+
<div style="background-color: #f0e1f4; border-radius: 10px; padding: 10px; margin: 10px;"> <!-- FAQ Section -->
16+
<h3 style="padding: 0%;">FAQs</h3>
17+
<h4>I still have questions! What do I do?</h4>
18+
<p>Send us an email! You can reach us at <a href="mailto:wichacks@rit.edu">wichacks@rit.edu</a></p>
19+
</div>
20+
</div>
21+
22+
<div style="color: black; background-color: lightgray; font-size: small; text-align: center; padding: 10px;"> <!-- footer -->
23+
<p>WiCHacks 2023</p>
24+
<p>Women in Computing @ Rochester Institute of Technology</p>
25+
<a href="https://wichacks.io" target="_blank" style="display: inline-block; text-decoration: none; color: #4e1560;">
26+
<div> <!-- Link to website -->
27+
<svg style="display: inline-block; vertical-align: middle;" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 512" width="20" height="20"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#4e1560" d="M0 336c0 79.5 64.5 144 144 144H512c70.7 0 128-57.3 128-128c0-61.9-44-113.6-102.4-125.4c4.1-10.7 6.4-22.4 6.4-34.6c0-53-43-96-96-96c-19.7 0-38.1 6-53.3 16.2C367 64.2 315.3 32 256 32C167.6 32 96 103.6 96 192c0 2.7 .1 5.4 .2 8.1C40.2 219.8 0 273.2 0 336z"/></svg>
28+
<p style="display: inline-block; vertical-align: middle; text-decoration: underline;">https://wichacks.io</p>
29+
</div>
30+
</a>
31+
<div> <!-- Social Medias -->
32+
<!-- Instagram Icon -->
33+
<a href="https://www.instagram.com/wichacks/" target="_blank" style="text-decoration: none;">
34+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 448 512" width="30" height="30"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#4e1560" d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/></svg>
35+
</a>
36+
<!-- Twitter Icon -->
37+
<a href="https://twitter.com/wichacks?lang=en" target="_blank" style="text-decoration: none; margin-left: 10px;">
38+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="30" height="30"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#4e1560" d="M459.37 151.716c.325 4.548.325 9.097.325 13.645 0 138.72-105.583 298.558-298.558 298.558-59.452 0-114.68-17.219-161.137-47.106 8.447.974 16.568 1.299 25.34 1.299 49.055 0 94.213-16.568 130.274-44.832-46.132-.975-84.792-31.188-98.112-72.772 6.498.974 12.995 1.624 19.818 1.624 9.421 0 18.843-1.3 27.614-3.573-48.081-9.747-84.143-51.98-84.143-102.985v-1.299c13.969 7.797 30.214 12.67 47.431 13.319-28.264-18.843-46.781-51.005-46.781-87.391 0-19.492 5.197-37.36 14.294-52.954 51.655 63.675 129.3 105.258 216.365 109.807-1.624-7.797-2.599-15.918-2.599-24.04 0-57.828 46.782-104.934 104.934-104.934 30.213 0 57.502 12.67 76.67 33.137 23.715-4.548 46.456-13.32 66.599-25.34-7.798 24.366-24.366 44.833-46.132 57.827 21.117-2.273 41.584-8.122 60.426-16.243-14.292 20.791-32.161 39.308-52.628 54.253z"/></svg>
39+
</a>
40+
<!-- Facebook Icon -->
41+
<a href="https://www.facebook.com/wic.hacks.rit/" target="_blank" style="text-decoration: none; margin-left: 10px;">
42+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="30" height="30"><!--! Font Awesome Pro 6.2.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2022 Fonticons, Inc. --><path fill="#4e1560" d="M504 256C504 119 393 8 256 8S8 119 8 256c0 123.78 90.69 226.38 209.25 245V327.69h-63V256h63v-54.64c0-62.15 37-96.48 93.67-96.48 27.14 0 55.52 4.84 55.52 4.84v61h-31.28c-30.8 0-40.41 19.12-40.41 38.73V256h68.78l-11 71.69h-57.78V501C413.31 482.38 504 379.78 504 256z"/></svg>
43+
</a>
44+
</div>
45+
</div>
46+
</div>
47+
</body>
Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,47 @@
11
def getConfirmedEmail(firstName, lastName):
2-
return f'Confirmed Email Template'
2+
try:
3+
appliedEmail = open('./src/data/emailTemplates/confirmed.html', 'r')
4+
contents = appliedEmail.read()
5+
appliedEmail.close()
6+
contents = contents.replace("first_name", firstName).replace("last_name", lastName)
7+
return contents
8+
except:
9+
return f'<div>' \
10+
f' <div>' \
11+
f' <h3>Hello {firstName} {lastName}</h3>' \
12+
f' <p>' \
13+
f' Thank you, your attendance has been confirmed.' \
14+
f' </p>' \
15+
f' </div>' \
16+
f'</div>'
317

418

519
def getConfirmedSubjectLine() -> str:
620
return "WiCHacks Confirmation"
21+
22+
23+
def getRequestConfirmedEmail():
24+
try:
25+
appliedEmail = open('./src/data/emailTemplates/requestConfirmed.html', 'r')
26+
contents = appliedEmail.read()
27+
appliedEmail.close()
28+
return contents
29+
except:
30+
return f'<div>' \
31+
f' <div>' \
32+
f' <h3>Hello!</h3>' \
33+
f' <p>' \
34+
f' As WiCHacks approaches, we ask you to confirm your attendance of WiCHacks. ' \
35+
f' This helps the organizers finalize our planning to provide the best event possible.' \
36+
f'' \
37+
f' Please login to your application homepage at apply.wichacks.io confirm your attendance' \
38+
f'' \
39+
f' Thank you,' \
40+
f' WiCHacks Organizers' \
41+
f' </p>' \
42+
f' </div>' \
43+
f'</div>'
44+
45+
46+
def getRequestConfirmedSubjectLine() -> str:
47+
return "WiCHacks RSVP"

0 commit comments

Comments
 (0)