be: EmailSender Thread for pushing emails sends off the response thread

This commit is contained in:
Aaron 2019-03-12 20:12:07 -05:00
parent ad632dd4f9
commit d1e2545b49
No known key found for this signature in database
GPG Key ID: 3B5B7597106F0A0E
4 changed files with 77 additions and 37 deletions

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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,