Limit Contribution Related Emails (#360)

* BE: limit contribution emails to CONFIRMED & one per user + add app context to EmailSender

* BE: handle EmailSender setup and start using flask request context
This commit is contained in:
AMStrix 2019-03-14 11:46:09 -05:00 committed by William O'Beirne
parent 0069de7fc4
commit 46aa7cf6cf
7 changed files with 85 additions and 73 deletions

View File

@ -4,7 +4,7 @@ import sentry_sdk
import logging
import traceback
from animal_case import animalify
from flask import Flask, Response, jsonify, request
from flask import Flask, Response, jsonify, request, g
from flask_cors import CORS
from flask_security import SQLAlchemyUserDatastore
from flask_sslify import SSLify
@ -32,6 +32,13 @@ def create_app(config_objects=["grant.settings"]):
app = Flask(__name__.split(".")[0])
app.response_class = JSONResponse
@app.after_request
def send_emails(response):
if 'email_sender' in g:
# starting email sender
g.email_sender.start()
return response
# Return validation errors
@app.errorhandler(ValidationException)
def handle_validation_error(err):

View File

@ -7,7 +7,7 @@ from grant.settings import SENDGRID_API_KEY, SENDGRID_DEFAULT_FROM, SENDGRID_DEF
from grant.settings import SENDGRID_API_KEY, SENDGRID_DEFAULT_FROM, UI, E2E_TESTING
import sendgrid
from threading import Thread
from flask import render_template, Markup, current_app
from flask import render_template, Markup, current_app, g
default_template_args = {
@ -351,9 +351,9 @@ 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)
if 'email_sender' not in g:
g.email_sender = EmailSender(current_app._get_current_object())
g.email_sender.add(to, type, email_args)
def make_envelope(to, type, email_args):
@ -385,7 +385,7 @@ def make_envelope(to, type, email_args):
return mail
def sendgrid_send(mail):
def sendgrid_send(mail, app=current_app):
to = mail.___to
type = mail.___type
try:
@ -393,27 +393,28 @@ def sendgrid_send(mail):
if E2E_TESTING:
from grant.e2e import views
views.last_email = mail.get()
current_app.logger.info(f'Just set last_email for e2e to pickup, to: {to}, type: {type}')
app.logger.info(f'Just set last_email for e2e to pickup, to: {to}, type: {type}')
else:
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))
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.debug(e.body)
app.logger.info('An HTTP error occured while sending an email to %s - %s: %s' %
(to, e.__class__.__name__, e))
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.debug(e)
app.logger.info('An unknown error occured while sending an email to %s - %s: %s' %
(to, e.__class__.__name__, e))
app.logger.debug(e)
capture_exception(e)
class EmailSender(Thread):
def __init__(self):
def __init__(self, app):
Thread.__init__(self)
self.envelopes = []
self.app = app
def add(self, to, type, email_args):
env = make_envelope(to, type, email_args)
@ -422,4 +423,4 @@ class EmailSender(Thread):
def run(self):
for envelope in self.envelopes:
sendgrid_send(envelope)
sendgrid_send(envelope, self.app)

View File

@ -5,8 +5,9 @@ from sqlalchemy.ext.hybrid import hybrid_property
from decimal import Decimal
from marshmallow import post_dump
from flask import current_app
from grant.comment.models import Comment
from grant.email.send import send_email, EmailSender
from grant.email.send import send_email
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
@ -524,21 +525,17 @@ class Proposal(db.Model):
db.session.flush()
# Send emails to team & contributors
email_sender = EmailSender()
for u in self.team:
email_sender.add(u.email_address, 'proposal_canceled', {
send_email(u.email_address, 'proposal_canceled', {
'proposal': self,
'support_url': make_url('/contact'),
})
for c in self.contributions:
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()
for u in self.contributors:
send_email(u.email_address, 'contribution_proposal_canceled', {
'proposal': self,
'refund_address': u.settings.refund_address,
'account_settings_url': make_url('/profile/settings?tab=account')
})
@hybrid_property
def contributed(self):
@ -602,6 +599,11 @@ class Proposal(db.Model):
return self.milestones[-1] # return last one if all PAID
return None
@hybrid_property
def contributors(self):
d = {c.user.id: c.user for c in self.contributions if c.user and c.status == ContributionStatus.CONFIRMED}
return d.values()
class ProposalSchema(ma.Schema):
class Meta:

View File

@ -8,7 +8,7 @@ from sentry_sdk import capture_message
from grant.extensions import limiter
from grant.comment.models import Comment, comment_schema, comments_schema
from grant.email.send import send_email, EmailSender
from grant.email.send import send_email
from grant.milestone.models import Milestone
from grant.parser import body, query, paginated_fields
from grant.rfp.models import RFP
@ -370,17 +370,13 @@ def post_proposal_update(proposal_id, title, content):
db.session.add(update)
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:
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()
# Send email to all contributors
for u in g.current_proposal.contributors:
send_email(u.email_address, 'contribution_update', {
'proposal': g.current_proposal,
'proposal_update': update,
'update_url': make_url(f'/proposals/{proposal_id}?tab=updates&update={update.id}'),
})
dumped_update = proposal_update_schema.dump(update)
return dumped_update, 201

View File

@ -1,9 +1,10 @@
from datetime import datetime, timedelta
from grant.extensions import db
from grant.email.send import send_email, EmailSender
from grant.email.send import send_email
from grant.utils.enums import ProposalStage, ContributionStatus
from grant.utils.misc import make_url
from flask import current_app
class ProposalReminder:
@ -72,20 +73,16 @@ class ProposalDeadline:
db.session.commit()
# Send emails to team & contributors
email_sender = EmailSender()
for u in proposal.team:
email_sender.add(u.email_address, 'proposal_failed', {
send_email(u.email_address, 'proposal_failed', {
'proposal': proposal,
})
for c in proposal.contributions:
if c.user:
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()
for u in proposal.contributors:
send_email(u.email_address, 'contribution_proposal_failed', {
'proposal': proposal,
'refund_address': u.settings.refund_address,
'account_settings_url': make_url('/profile/settings?tab=account')
})
class ContributionExpired:

View File

@ -1,24 +1,33 @@
<p style="margin: 0 0 20px;">
A proposal you follow, <strong>{{ args.proposal.title }}</strong>, has
posted an update entitled "<strong>{{ args.proposal_update.title }}</strong>"
A proposal you contributed to, <strong>{{ args.proposal.title }}</strong
>, has posted an update entitled "<strong>{{
args.proposal_update.title
}}</strong
>"
</p>
<table width="100%" border="0" cellspacing="0" cellpadding="0">
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td align="center" style="border-radius: 3px;" bgcolor="{{ UI.PRIMARY }}">
<a
href="{{ args.update_url }}"
target="_blank"
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{ UI.PRIMARY }}; display: inline-block;"
>
Read the Update
</a>
</td>
</tr>
</table>
</td>
</tr>
<tr>
<td bgcolor="#ffffff" align="center" style="padding: 40px 30px 40px 30px;">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<td
align="center"
style="border-radius: 3px;"
bgcolor="{{ UI.PRIMARY }}"
>
<a
href="{{ args.update_url }}"
target="_blank"
style="font-size: 20px; font-family: Helvetica, Arial, sans-serif; color: #ffffff; text-decoration: none; color: #ffffff; text-decoration: none; padding: 20px 50px; border-radius: 4px; border: 1px solid {{
UI.PRIMARY
}}; display: inline-block;"
>
Read the Update
</a>
</td>
</tr>
</table>
</td>
</tr>
</table>

View File

@ -1,4 +1,4 @@
A proposal you follow, "{{ args.proposal.title }}", has posted an update
A proposal you contributed to, "{{ args.proposal.title }}", has posted an update
entitled "{{ args.proposal_update.title }}".
Go here to read it: {{ args.update_url }}