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:
parent
0069de7fc4
commit
46aa7cf6cf
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
Loading…
Reference in New Issue