From d1e2545b49a88efccf44a93bee4c3835777a41c8 Mon Sep 17 00:00:00 2001 From: Aaron Date: Tue, 12 Mar 2019 20:12:07 -0500 Subject: [PATCH] be: EmailSender Thread for pushing emails sends off the response thread --- backend/grant/email/send.py | 64 +++++++++++++++++++++++--------- backend/grant/proposal/models.py | 20 ++++++---- backend/grant/proposal/views.py | 6 ++- backend/grant/task/jobs.py | 24 +++++++----- 4 files changed, 77 insertions(+), 37 deletions(-) diff --git a/backend/grant/email/send.py b/backend/grant/email/send.py index d56b79ff..ece870fb 100644 --- a/backend/grant/email/send.py +++ b/backend/grant/email/send.py @@ -1,4 +1,6 @@ import sendgrid +import time +from threading import Thread from flask import render_template, Markup, current_app from grant.settings import SENDGRID_API_KEY, SENDGRID_DEFAULT_FROM, UI from grant.utils.misc import make_url @@ -317,11 +319,10 @@ def generate_email(type, email_args, user=None): UI=UI, ) - template_args = { **default_template_args } + template_args = {**default_template_args} if user: template_args['unsubscribe_url'] = make_url('/email/unsubscribe?code={}'.format(user.email_verification.code)) - html = render_template( 'emails/template.html', args={ @@ -349,8 +350,14 @@ def generate_email(type, email_args, user=None): def send_email(to, type, email_args): + mail = make_envelope(to, type, email_args) + if mail: + sendgrid_send(mail) + + +def make_envelope(to, type, email_args): if current_app and current_app.config.get("TESTING"): - return + return None from grant.user.models import User user = User.get_by_email(to) @@ -360,24 +367,47 @@ def send_email(to, type, email_args): sub = info['subscription'] if user and not is_subscribed(user.settings.email_subscriptions, sub): print(f'Ignoring send_email to {to} of type {type} because user is unsubscribed.') - return + return None + email = generate_email(type, email_args, user) + + mail = Mail( + from_email=Email(SENDGRID_DEFAULT_FROM), + to_email=Email(to), + subject=email['info']['subject'], + ) + mail.add_content(Content('text/plain', email['text'])) + mail.add_content(Content('text/html', email['html'])) + + mail.___type = type + mail.___to = to + + return mail + + +def sendgrid_send(mail): try: - email = generate_email(type, email_args, user) sg = sendgrid.SendGridAPIClient(apikey=SENDGRID_API_KEY) - - mail = Mail( - from_email=Email(SENDGRID_DEFAULT_FROM), - to_email=Email(to), - subject=email['info']['subject'], - ) - mail.add_content(Content('text/plain', email['text'])) - mail.add_content(Content('text/html', email['html'])) - res = sg.client.mail.send.post(request_body=mail.get()) - print('Just sent an email to %s of type %s, response code: %s' % (to, type, res.status_code)) + print('Just sent an email to %s of type %s, response code: %s' % (mail.___to, mail.___type, res.status_code)) except HTTPError as e: - print('An HTTP error occured while sending an email to %s - %s: %s' % (to, e.__class__.__name__, e)) + print('An HTTP error occured while sending an email to %s - %s: %s' % (mail.___to, e.__class__.__name__, e)) print(e.body) except Exception as e: - print('An unknown error occured while sending an email to %s - %s: %s' % (to, e.__class__.__name__, e)) + print('An unknown error occured while sending an email to %s - %s: %s' % (mail.___to, e.__class__.__name__, e)) + + +class EmailSender(Thread): + def __init__(self): + Thread.__init__(self) + self.envelopes = [] + + def add(self, to, type, email_args): + env = make_envelope(to, type, email_args) + if env: + self.envelopes.append(env) + + def run(self): + # time.sleep(5) + for envelope in self.envelopes: + sendgrid_send(envelope) diff --git a/backend/grant/proposal/models.py b/backend/grant/proposal/models.py index de2d984c..936e3f28 100644 --- a/backend/grant/proposal/models.py +++ b/backend/grant/proposal/models.py @@ -6,7 +6,7 @@ from decimal import Decimal from marshmallow import post_dump from grant.comment.models import Comment -from grant.email.send import send_email +from grant.email.send import send_email, EmailSender from grant.extensions import ma, db from grant.utils.exceptions import ValidationException from grant.utils.misc import dt_to_unix, make_url, gen_random_id @@ -507,19 +507,23 @@ class Proposal(db.Model): self.stage = ProposalStage.CANCELED db.session.add(self) db.session.flush() + # Send emails to team & contributors + email_sender = EmailSender() for u in self.team: - send_email(u.email_address, 'proposal_canceled', { + email_sender.add(u.email_address, 'proposal_canceled', { 'proposal': self, 'support_url': make_url('/contact'), }) for c in self.contributions: - send_email(c.user.email_address, 'contribution_proposal_canceled', { - 'contribution': c, - 'proposal': self, - 'refund_address': c.user.settings.refund_address, - 'account_settings_url': make_url('/profile/settings?tab=account') - }) + if c.user: + email_sender.add(c.user.email_address, 'contribution_proposal_canceled', { + 'contribution': c, + 'proposal': self, + 'refund_address': c.user.settings.refund_address, + 'account_settings_url': make_url('/profile/settings?tab=account') + }) + email_sender.start() @hybrid_property def contributed(self): diff --git a/backend/grant/proposal/views.py b/backend/grant/proposal/views.py index de00e6e3..9421063f 100644 --- a/backend/grant/proposal/views.py +++ b/backend/grant/proposal/views.py @@ -6,7 +6,7 @@ from marshmallow import fields, validate from sqlalchemy import or_ from grant.comment.models import Comment, comment_schema, comments_schema -from grant.email.send import send_email +from grant.email.send import send_email, EmailSender from grant.milestone.models import Milestone from grant.parser import body, query, paginated_fields from grant.rfp.models import RFP @@ -366,14 +366,16 @@ def post_proposal_update(proposal_id, title, content): db.session.commit() # Send email to all contributors (even if contribution failed) + email_sender = EmailSender() contributions = ProposalContribution.query.filter_by(proposal_id=proposal_id).all() for c in contributions: if c.user: - send_email(c.user.email_address, 'contribution_update', { + email_sender.add(c.user.email_address, 'contribution_update', { 'proposal': g.current_proposal, 'proposal_update': update, 'update_url': make_url(f'/proposals/{proposal_id}?tab=updates&update={update.id}'), }) + email_sender.start() dumped_update = proposal_update_schema.dump(update) return dumped_update, 201 diff --git a/backend/grant/task/jobs.py b/backend/grant/task/jobs.py index e7d17b89..690c34d2 100644 --- a/backend/grant/task/jobs.py +++ b/backend/grant/task/jobs.py @@ -1,7 +1,7 @@ from datetime import datetime, timedelta from grant.extensions import db -from grant.email.send import send_email +from grant.email.send import send_email, EmailSender from grant.utils.enums import ProposalStage, ContributionStatus from grant.utils.misc import make_url @@ -42,12 +42,12 @@ class ProposalDeadline: def __init__(self, proposal): self.proposal = proposal - + def blobify(self): return { "proposal_id": self.proposal.id, } - + def make_task(self): from .models import Task task = Task( @@ -66,37 +66,40 @@ class ProposalDeadline: # If it was deleted, canceled, or successful, just noop out if not proposal or proposal.is_funded or proposal.stage != ProposalStage.FUNDING_REQUIRED: return - + # Otherwise, mark it as failed and inform everyone proposal.stage = ProposalStage.FAILED db.session.add(proposal) db.session.commit() - # TODO: Bulk-send emails instead of one per email + # Send emails to team & contributors + email_sender = EmailSender() for u in proposal.team: - send_email(u.email_address, 'proposal_failed', { + email_sender.add(u.email_address, 'proposal_failed', { 'proposal': proposal, }) for c in proposal.contributions: if c.user: - send_email(c.user.email_address, 'contribution_proposal_failed', { + email_sender.add(c.user.email_address, 'contribution_proposal_failed', { 'contribution': c, 'proposal': proposal, 'refund_address': c.user.settings.refund_address, 'account_settings_url': make_url('/profile/settings?tab=account') }) + email_sender.start() + class ContributionExpired: JOB_TYPE = 3 def __init__(self, contribution): self.contribution = contribution - + def blobify(self): return { "contribution_id": self.contribution.id, } - + def make_task(self): from .models import Task task = Task( @@ -115,7 +118,7 @@ class ContributionExpired: # If it's missing or not pending, noop out if not contribution or contribution.status != ContributionStatus.PENDING: return - + # Otherwise, inform the user (if not anonymous) if contribution.user: send_email(contribution.user.email_address, 'contribution_expired', { @@ -126,6 +129,7 @@ class ContributionExpired: 'proposal_url': make_url(f'/proposals/{contribution.proposal.id}'), }) + JOBS = { 1: ProposalReminder.process_task, 2: ProposalDeadline.process_task,