Merge pull request #343 from grant-project/batch-emails

Batch (read threaded) emails
This commit is contained in:
AMStrix 2019-03-13 15:19:24 -05:00 committed by GitHub
commit 25a71ced1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 32 deletions

View File

@ -1,4 +1,5 @@
import sendgrid
from threading import Thread
from flask import render_template, Markup, current_app
from grant.settings import SENDGRID_API_KEY, SENDGRID_DEFAULT_FROM, SENDGRID_DEFAULT_FROMNAME, UI
from sentry_sdk import capture_exception
@ -318,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={
@ -350,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)
@ -361,27 +367,50 @@ def send_email(to, type, email_args):
sub = info['subscription']
if user and not is_subscribed(user.settings.email_subscriptions, sub):
app.logger.debug(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, SENDGRID_DEFAULT_FROMNAME),
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, SENDGRID_DEFAULT_FROMNAME),
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())
current_app.logger.info('Just sent an email to %s of type %s, response code: %s' % (to, type, res.status_code))
except HTTPError as e:
current_app.logger.info('An HTTP error occured while sending an email to %s - %s: %s' % (to, e.__class__.__name__, e))
current_app.logger.info('An HTTP error occured while sending an email to %s - %s: %s' %
(to, e.__class__.__name__, e))
current_app.logger.debug(e.body)
capture_exception(e)
except Exception as e:
current_app.logger.info('An unknown error occured while sending an email to %s - %s: %s' % (to, e.__class__.__name__, e))
current_app.logger.info('An unknown error occured while sending an email to %s - %s: %s' %
(to, e.__class__.__name__, e))
current_app.logger.debug(e)
capture_exception(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):
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
@ -289,7 +289,8 @@ class Proposal(db.Model):
try:
res = blockchain_get('/validate/address', {'address': self.payout_address})
except:
raise ValidationException("Could not validate your payout address due to an internal server error, please try again later")
raise ValidationException(
"Could not validate your payout address due to an internal server error, please try again later")
if not res['valid']:
raise ValidationException("Payout address is not a valid Zcash address")
@ -510,20 +511,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:
if c.user:
send_email(c.user.email_address, 'contribution_proposal_canceled', {
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

@ -7,7 +7,7 @@ from sqlalchemy import or_
from grant.extensions import limiter
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
@ -370,14 +370,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,