be: EmailSender Thread for pushing emails sends off the response thread
This commit is contained in:
parent
ad632dd4f9
commit
d1e2545b49
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue