Merge pull request #343 from grant-project/batch-emails
Batch (read threaded) emails
This commit is contained in:
commit
25a71ced1b
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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