Skip to content

Commit

Permalink
Better Email service and regular reminder email functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
benymng authored Apr 14, 2024
1 parent 8e98de5 commit 7e2449f
Show file tree
Hide file tree
Showing 26 changed files with 601 additions and 73 deletions.
23 changes: 21 additions & 2 deletions backend/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
from .graphql import schema as graphql_schema


from flask_apscheduler import APScheduler


def create_app(config_name):
# configure Flask logger
dictConfig(
{
"version": 1,
Expand Down Expand Up @@ -56,6 +58,7 @@ def create_app(config_name):
re.compile(r"^https:\/\/uw-blueprint-starter-code--pr.*\.web\.app$"),
]
app.config["CORS_SUPPORTS_CREDENTIALS"] = True
app.config["SCHEDULER_API_ENABLED"] = True
CORS(app)

firebase_admin.initialize_app(
Expand Down Expand Up @@ -85,6 +88,22 @@ def create_app(config_name):
from . import models, graphql

models.init_app(app)
graphql.init_app(app)
services = graphql.init_app(app)

scheduler = APScheduler()
scheduler.init_app(app)

# checks every hour for meal requests that were either yesterday or today and sends an email to the donor and requestor
@scheduler.task(
"interval", id="daily_job", seconds=60 * 60 * 24, misfire_grace_time=900
)
def dailyJob():
try:
with scheduler.app.app_context():
services["reminder_email_service"].send_regularly_scheduled_emails()
except Exception as e:
print("Error in Scheduled Task!", e)

scheduler.start()

return app
55 changes: 34 additions & 21 deletions backend/app/graphql/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import graphene
import os
from app.services.implementations.reminder_email_service import ReminderEmailService

from flask import current_app

Expand Down Expand Up @@ -53,28 +54,33 @@ class RootMutation(
)


def init_email_service(app):
print("Initializing email service")
if app.config["TESTING"]:
print("Using mock email service in testings!")
services["email_service"] = MockEmailService(
logger=current_app.logger,
credentials={},
sender_email=os.getenv("MAILER_USER"),
display_name="Feeding Canadian Kids",
)
else:
services["email_service"] = EmailService(
logger=current_app.logger,
credentials={
"refresh_token": os.getenv("MAILER_REFRESH_TOKEN"),
"token_uri": "https://oauth2.googleapis.com/token",
"client_id": os.getenv("MAILER_CLIENT_ID"),
"client_secret": os.getenv("MAILER_CLIENT_SECRET"),
},
sender_email=os.getenv("MAILER_USER"),
display_name="Feeding Canadian Kids",
)


def init_app(app):
with app.app_context():
if app.config["TESTING"]:
print("Using mock email service in testings!")
services["email_service"] = MockEmailService(
logger=current_app.logger,
credentials={},
sender_email=os.getenv("MAILER_USER"),
display_name="Feeding Canadian Kids",
)
else:
services["email_service"] = EmailService(
logger=current_app.logger,
credentials={
"refresh_token": os.getenv("MAILER_REFRESH_TOKEN"),
"token_uri": "https://oauth2.googleapis.com/token",
"client_id": os.getenv("MAILER_CLIENT_ID"),
"client_secret": os.getenv("MAILER_CLIENT_SECRET"),
},
sender_email=os.getenv("MAILER_USER"),
display_name="Feeding Canadian Kids",
)
init_email_service(app)
services["onsite_contact_service"] = OnsiteContactService(
logger=current_app.logger
)
Expand All @@ -90,4 +96,11 @@ def init_app(app):
services["onboarding_request_service"] = OnboardingRequestService(
logger=current_app.logger, email_service=services["email_service"]
)
services["meal_request_service"] = MealRequestService(logger=current_app.logger)
services["meal_request_service"] = MealRequestService(
logger=current_app.logger, email_service=services["email_service"]
)
services["reminder_email_service"] = ReminderEmailService(
logger=current_app.logger, email_service=services["email_service"]
)

return services
4 changes: 4 additions & 0 deletions backend/app/graphql/services.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from typing import TypedDict


from ..services.interfaces.onsite_contact_service import IOnsiteContactService
from ..services.interfaces.user_service import IUserService
from ..services.interfaces.auth_service import IAuthService
from ..services.interfaces.email_service import IEmailService
from ..services.interfaces.onboarding_request_service import IOnboardingRequestService
from ..services.interfaces.meal_request_service import IMealRequestService
from ..services.interfaces.reminder_email_service import IReminderEmailService

"""
Global services for GraphQL that will be initialized with
Expand All @@ -20,6 +22,7 @@ class ServicesObject(TypedDict):
onboarding_request_service: IOnboardingRequestService
meal_request_service: IMealRequestService
onsite_contact_service: IOnsiteContactService
reminder_email_service: IReminderEmailService


services: ServicesObject = {
Expand All @@ -29,4 +32,5 @@ class ServicesObject(TypedDict):
"onboarding_request_service": None,
"meal_request_service": None,
"onsite_contact_service": None,
"reminder_email_service": None,
} # type: ignore
52 changes: 13 additions & 39 deletions backend/app/services/implementations/auth_service.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from app.services.implementations.email_service import EmailService
import firebase_admin.auth

from ..interfaces.auth_service import IAuthService
Expand Down Expand Up @@ -115,18 +116,9 @@ def forgot_password(self, email):
set_password_link = "{url}/{ObjectID}/reset-password".format(
url=url, ObjectID=user.id
)

email_body = """
Hello,
<br><br>
We have received your reset password request.
Please reset your password using the following link.
<br><br>
<a href="{reset_link}">Reset Password</a>
""".format(
reset_link=set_password_link
)

email_body = EmailService.read_email_template(
"email_templates/reset_password.html"
).format(reset_link=set_password_link)
self.email_service.send_email(email, "FCK Reset Password Link", email_body)

except Exception as e:
Expand Down Expand Up @@ -175,17 +167,9 @@ def send_email_verification_link(self, email):
verification_link = firebase_admin.auth.generate_email_verification_link(
email
)
email_body = """
Hello,
<br><br>
Please click the following link to verify your email and
activate your account.
<strong>This link is only valid for 1 hour.</strong>
<br><br>
<a href={verification_link}>Verify email</a>
""".format(
verification_link=verification_link
)
email_body = EmailService.read_email_template(
"email_templates/verification_email.html"
).format(verification_link=verification_link)
self.email_service.send_email(email, "Verify your email", email_body)
except Exception as e:
self.logger.error(
Expand All @@ -209,16 +193,9 @@ def send_onboarding_request_approve_email(self, objectID, email):
url=url, ObjectID=objectID
)

email_body = """
Hello,
<br><br>
We have received your onboarding request and it has been approved.
Please set your password using the following link.
<br><br>
<a href="{reset_link}">Reset Password</a>
""".format(
reset_link=set_password_link
)
email_body = EmailService.read_email_template(
"email_templates/onboarding_request_approved.html"
).format(set_password_link=set_password_link)

self.email_service.send_email(
email, "Onboarding request approved. Set Password", email_body
Expand All @@ -240,12 +217,9 @@ def send_onboarding_request_rejected_email(self, email):
raise Exception(error_message)

try:
email_body = """
Hello,
<br><br>
This is a notification that your onboarding request has been rejected.
<br><br>
"""
email_body = EmailService.EmailService.read_email_template(
"email_templates/onboarding_request_rejected.html"
)
self.email_service.send_email(
email, "Onboarding request rejected", email_body
)
Expand Down
6 changes: 6 additions & 0 deletions backend/app/services/implementations/email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ class EmailService(IEmailService):
EmailService implementation for handling email related functionality
"""

@staticmethod
def read_email_template(file_path: str):
with open(file_path, "r") as file:
return file.read()

def __init__(self, logger, credentials, sender_email, display_name=None):
"""
Create an instance of EmailService
Expand All @@ -24,6 +29,7 @@ def __init__(self, logger, credentials, sender_email, display_name=None):
:param display_name: the sender's display name, defaults to None
:type display_name: str, optional
"""

self.logger = logger
creds = Credentials(None, **credentials)
self.service = build("gmail", "v1", credentials=creds)
Expand Down
65 changes: 64 additions & 1 deletion backend/app/services/implementations/meal_request_service.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import List
from app.services.interfaces.email_service import IEmailService
from app.services.implementations.email_service import EmailService
from ...models.meal_request import MealInfo, MealRequest
from ..interfaces.meal_request_service import IMealRequestService
from datetime import datetime
Expand All @@ -11,8 +13,9 @@


class MealRequestService(IMealRequestService):
def __init__(self, logger):
def __init__(self, logger, email_service: IEmailService):
self.logger = logger
self.email_service = email_service

def create_meal_request(
self,
Expand Down Expand Up @@ -125,6 +128,13 @@ def commit_to_meal_request(
raise Exception(
f'meal request "{meal_request_id}" is not open for commitment'
)
meal_requestor_id = meal_request.requestor.id
meal_requestor = User.objects(id=meal_requestor_id).first()

self.send_donor_commit_email(meal_request, donor.info.email)
self.send_requestor_commit_email(
meal_request, meal_requestor.info.email
)

meal_request.donation_info = DonationInfo(
donor=donor,
Expand Down Expand Up @@ -324,3 +334,56 @@ def get_meal_requests_by_ids(self, ids: str) -> List[MealRequestDTO]:
]

return meal_request_dtos

def send_donor_commit_email(self, meal_request, email):
if not self.email_service:
error_message = """
Attempted to call committed_to_meal_request but this
instance of AuthService does not have an EmailService instance
"""
self.logger.error(error_message)
raise Exception(error_message)

try:
email_body = EmailService.read_email_template(
"email_templates/committed_to_meal_request.html"
).format(
dropoff_location=meal_request.drop_off_location,
dropoff_time=meal_request.drop_off_datetime,
num_meals=meal_request.meal_info.portions,
)
self.email_service.send_email(
email, "Thank you for committing to a meal request!", email_body
)

except Exception as e:
self.logger.error(
f"Failed to send committed to meal request email for user {meal_request.id if meal_request else ''} {email}"
)
raise e

def send_requestor_commit_email(self, meal_request, email):
if not self.email_service:
error_message = """
Attempted to call meal_request_success but this
instance of AuthService does not have an EmailService instance
"""
self.logger.error(error_message)
raise Exception(error_message)

try:
email_body = EmailService.read_email_template(
"email_templates/meal_request_success.html"
).format(
dropoff_location=meal_request.drop_off_location,
dropoff_time=meal_request.drop_off_datetime,
num_meals=meal_request.meal_info.portions,
)
self.email_service.send_email(
email, "Your meal request has been fulfilled!", email_body
)
except Exception as e:
self.logger.error(
f"Failed to send committed to meal request email for user {meal_request.id if meal_request else ''} {email}"
)
raise e
3 changes: 3 additions & 0 deletions backend/app/services/implementations/mock_email_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def send_email(self, to, subject, body):
"subject": subject,
"body": body,
}
print(
f"MockEmailService: Sent email to {message['to']} from {message['from_']} with subject '{message['subject']}'"
)
self.emails_sent.append(message)

def get_last_email_sent(self):
Expand Down
Loading

0 comments on commit 7e2449f

Please sign in to comment.