Basic email configuration
This commit is contained in:
parent
f2d809d9d9
commit
091450857c
|
@ -21,6 +21,30 @@ paths:
|
|||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Status"
|
||||
/update_data:
|
||||
get:
|
||||
operationId: communicator.api.admin.update_data
|
||||
summary: Checks the local file system and firecloud for data and loads it into the db.
|
||||
security: [] # Disable security for this endpoint only.
|
||||
responses:
|
||||
'200':
|
||||
description: Status indicator that the app is up and alive.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
/notify_by_email:
|
||||
get:
|
||||
operationId: communicator.api.admin.notify_by_email
|
||||
summary: when called, reviews all samples, and sends out any pending notifications.
|
||||
security: [] # Disable security for this endpoint only.
|
||||
responses:
|
||||
'200':
|
||||
description: Just returns a 200 if it was successful.
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
components:
|
||||
schemas:
|
||||
|
|
|
@ -1,3 +1,44 @@
|
|||
from operator import or_
|
||||
|
||||
from communicator import db, app
|
||||
from communicator.errors import ApiError, CommError
|
||||
from communicator.models import Sample
|
||||
from communicator.services.firebase_service import FirebaseService
|
||||
from communicator.services.ivy_service import IvyService
|
||||
from communicator.services.notification_service import NotificationService
|
||||
from communicator.services.sample_service import SampleService
|
||||
|
||||
|
||||
def status():
|
||||
return {"status":"good"}
|
||||
|
||||
|
||||
def update_data():
|
||||
"""Updates the database based on local files placed by IVY and recoreds
|
||||
read in from the firecloud database."""
|
||||
fb_service = FirebaseService()
|
||||
ivy_service = IvyService()
|
||||
|
||||
samples = fb_service.get_samples()
|
||||
samples.extend(ivy_service.load_directory())
|
||||
SampleService().add_or_update_records(samples)
|
||||
|
||||
|
||||
def notify_by_email():
|
||||
"""Sends out notifications via email"""
|
||||
|
||||
samples = db.session.query(Sample)\
|
||||
.filter(Sample.result_code != None)\
|
||||
.filter(Sample.email_notified == False).all()
|
||||
notifier = NotificationService(app)
|
||||
for sample in samples:
|
||||
try:
|
||||
notifier.send_result_email(sample)
|
||||
sample.email_notified = True
|
||||
except CommError as ce:
|
||||
print("Error")
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
from communicator import db
|
||||
|
||||
|
||||
class Notification(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date = db.Column(db.DateTime)
|
||||
type = db.Column(db.String) # Either 'email' or 'text'
|
||||
successful = db.Column(db.Boolean)
|
||||
error_message = db.Column(db.String)
|
||||
sample_barcode = db.Column(db.String, db.ForeignKey('sample.barcode'), nullable=False)
|
||||
sample = db.relationship("Sample")
|
|
@ -1,4 +1,5 @@
|
|||
from communicator import db
|
||||
from communicator.models.notification import Notification
|
||||
|
||||
|
||||
class Sample(db.Model):
|
||||
|
@ -9,9 +10,13 @@ class Sample(db.Model):
|
|||
phone = db.Column(db.String)
|
||||
email = db.Column(db.String)
|
||||
result_code = db.Column(db.String)
|
||||
notified = db.Column(db.Boolean, default=False)
|
||||
in_firebase = db.Column(db.Boolean, default=False) # Does this record exist in Firebase?
|
||||
in_ivy = db.Column(db.Boolean, default=False) # Has this record come in from the IVY?
|
||||
email_notified = db.Column(db.Boolean, default=False)
|
||||
text_notified = db.Column(db.Boolean, default=False)
|
||||
notifications = db.relationship(Notification, back_populates="sample",
|
||||
cascade="all, delete, delete-orphan",
|
||||
order_by=Notification.date)
|
||||
|
||||
def merge(self, sample):
|
||||
if sample.phone:
|
||||
|
|
|
@ -16,7 +16,10 @@ class FirebaseService(object):
|
|||
|
||||
def get_samples(self):
|
||||
# Then query for documents
|
||||
samples = self.db.collection(u'samples')
|
||||
fb_samples = self.db.collection(u'samples')
|
||||
samples = []
|
||||
for s in fb_samples.stream():
|
||||
samples.append(FirebaseService.record_to_sample(s))
|
||||
return samples
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -2,13 +2,26 @@ import csv
|
|||
|
||||
from dateutil import parser
|
||||
|
||||
from communicator import app
|
||||
from communicator.errors import CommError
|
||||
from communicator.models.sample import Sample
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
|
||||
|
||||
class IvyService(object):
|
||||
"""Opens files uploaded to the server from IVY and imports them into the database. """
|
||||
|
||||
def __init__(self):
|
||||
self.path = app.config['IVY_IMPORT_DIR']
|
||||
|
||||
def load_directory(self):
|
||||
onlyfiles = [f for f in listdir(self.path) if isfile(join(self.path, f))]
|
||||
samples = []
|
||||
for file in onlyfiles:
|
||||
samples.extend(IvyService.samples_from_ivy_file(join(self.path, file)))
|
||||
return samples
|
||||
|
||||
@staticmethod
|
||||
def samples_from_ivy_file(file_name):
|
||||
rows = []
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
import smtplib
|
||||
import uuid
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from flask import render_template
|
||||
from flask_mail import Message
|
||||
|
||||
from communicator import db, mail, app
|
||||
from communicator.errors import ApiError, CommError
|
||||
|
||||
TEST_MESSAGES = []
|
||||
|
||||
class NotificationService(object):
|
||||
"""Provides common tools for working with an Email"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.sender = app.config['MAIL_SENDER']
|
||||
|
||||
def email_server(self):
|
||||
server = smtplib.SMTP(host=self.app.config['MAIL_SERVER'],
|
||||
port=self.app.config['MAIL_PORT'],
|
||||
timeout=self.app.config['MAIL_TIMEOUT'])
|
||||
server.ehlo()
|
||||
if self.app.config['MAIL_USE_TLS']:
|
||||
server.starttls()
|
||||
if self.app.config['MAIL_USERNAME']:
|
||||
server.login(self.app.config['MAIL_USERNAME'],
|
||||
self.app.config['MAIL_PASSWORD'])
|
||||
return server
|
||||
|
||||
def tracking_code(self):
|
||||
return str(uuid.uuid4())[:16]
|
||||
|
||||
def send_result_email(self, sample):
|
||||
subject = "UVA: BE SAFE Notification"
|
||||
link = f"https://besafe.virginia.edu/result-demo?code={sample.result_code}"
|
||||
tracking_code = self.tracking_code()
|
||||
text_body = render_template("result_email.txt",
|
||||
link=link,
|
||||
sample=sample,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
html_body = render_template("result_email.html",
|
||||
link=link,
|
||||
sample=sample,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
self.send_email(subject, recipients=[sample.email], text_body=text_body, html_body=html_body)
|
||||
|
||||
return tracking_code
|
||||
|
||||
|
||||
|
||||
def send_email(self, subject, recipients, text_body, html_body, sender=None, ical=None):
|
||||
msgRoot = MIMEMultipart('related')
|
||||
msgRoot.set_charset('utf8')
|
||||
|
||||
if sender is None:
|
||||
sender = self.sender
|
||||
|
||||
msgRoot['Subject'] = Header(subject.encode('utf-8'), 'utf-8').encode()
|
||||
msgRoot['From'] = sender
|
||||
msgRoot['To'] = ', '.join(recipients)
|
||||
msgRoot.preamble = 'This is a multi-part message in MIME format.'
|
||||
|
||||
msgAlternative = MIMEMultipart('alternative')
|
||||
msgRoot.attach(msgAlternative)
|
||||
|
||||
part1 = MIMEText(text_body, 'plain', _charset='UTF-8')
|
||||
part2 = MIMEText(html_body, 'html', _charset='UTF-8')
|
||||
|
||||
msgAlternative.attach(part1)
|
||||
msgAlternative.attach(part2)
|
||||
|
||||
# Leaving this on here, just in case we need it later.
|
||||
if ical:
|
||||
ical_atch = MIMEText(ical.decode("utf-8"),'calendar')
|
||||
ical_atch.add_header('Filename','event.ics')
|
||||
ical_atch.add_header('Content-Disposition','attachment; filename=event.ics')
|
||||
msgRoot.attach(ical_atch)
|
||||
|
||||
if 'TESTING' in self.app.config and self.app.config['TESTING']:
|
||||
print("TEST: Recording Emails, not sending - %s - to:%s" % (subject, recipients))
|
||||
TEST_MESSAGES.append(msgRoot)
|
||||
return
|
||||
|
||||
try:
|
||||
server = self.email_server()
|
||||
server.sendmail(sender, recipients, msgRoot.as_bytes())
|
||||
server.quit()
|
||||
except Exception as e:
|
||||
app.logger.error('An exception happened in EmailService', exc_info=True)
|
||||
app.logger.error(str(e))
|
||||
raise CommError(5000, f"failed to send email to {', '.join(recipients)}", e)
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||||
<title>UVA BE SAFE</title>
|
||||
<style>
|
||||
/* -------------------------------------
|
||||
INLINED WITH htmlemail.io/inline
|
||||
------------------------------------- */
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #34495e !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #34495e !important;
|
||||
border-color: #34495e !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="" style="background-color: #f6f6f6; font-family: sans-serif; -webkit-font-smoothing: antialiased; font-size: 14px; line-height: 1.4; margin: 0; padding: 0; -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" class="body" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background-color: #f6f6f6;">
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
<td class="container" style="font-family: sans-serif; font-size: 14px; vertical-align: top; display: block; Margin: 0 auto; max-width: 580px; padding: 10px; width: 580px;">
|
||||
<div class="content" style="box-sizing: border-box; display: block; Margin: 0 auto; max-width: 580px; padding: 10px;">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<span class="preheader" style="color: transparent; display: none; height: 0; max-height: 0; max-width: 0; opacity: 0; overflow: hidden; mso-hide: all; visibility: hidden; width: 0;">Autism DRIVE Portal: Resources for Translational Medicine in Virginia</span>
|
||||
<table class="main" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%; background: #ffffff; border-radius: 3px;">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper" style="font-family: sans-serif; font-size: 14px; vertical-align: top; box-sizing: border-box; padding: 20px;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;">
|
||||
<img style="margin:0; padding:0 0 30px 0" src="{{logo_url}}" alt="Autism DRIVE"/>
|
||||
{% block content %}{% endblock %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer" style="clear: both; Margin-top: 10px; text-align: center; width: 100%;">
|
||||
<table border="0" cellpadding="0" cellspacing="0" style="border-collapse: separate; mso-table-lspace: 0pt; mso-table-rspace: 0pt; width: 100%;">
|
||||
<tr>
|
||||
<td class="content-block" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
<span class="apple-link" style="color: #999999; font-size: 12px; text-align: center;">
|
||||
You received this email from UVA's BE SAFE system.
|
||||
Please note that email is not a secure form of communication and should not be used to discuss any confidential matters, including personal health information, given its confidentiality cannot be assured.
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="content-block powered-by" style="font-family: sans-serif; vertical-align: top; padding-bottom: 10px; padding-top: 10px; font-size: 12px; color: #999999; text-align: center;">
|
||||
Powered by <a href=" {{ site_url }}" style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">BE SAFE</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
</div>
|
||||
</td>
|
||||
<td style="font-family: sans-serif; font-size: 14px; vertical-align: top;"> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
|
@ -38,8 +38,6 @@ GITHUB_REPO = environ.get('GITHUB_REPO', None)
|
|||
TARGET_BRANCH = environ.get('TARGET_BRANCH', None)
|
||||
|
||||
# Email configuration
|
||||
DEFAULT_SENDER = 'askresearch@virginia.edu'
|
||||
FALLBACK_EMAILS = ['askresearch@virginia.edu', 'sartographysupport@googlegroups.com']
|
||||
MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True)
|
||||
MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io')
|
||||
MAIL_PORT = environ.get('MAIL_PORT', default=2525)
|
||||
|
@ -47,14 +45,8 @@ MAIL_USE_SSL = environ.get('MAIL_USE_SSL', default=False)
|
|||
MAIL_USE_TLS = environ.get('MAIL_USE_TLS', default=False)
|
||||
MAIL_USERNAME = environ.get('MAIL_USERNAME', default='')
|
||||
MAIL_PASSWORD = environ.get('MAIL_PASSWORD', default='')
|
||||
MAIL_SENDER = 'askresearch@virginia.edu'
|
||||
|
||||
# Ivy Directory
|
||||
IVY_IMPORT_DIR = os.path.join(basedir, '..', 'example_ivy_data')
|
||||
|
||||
# Firebase connection
|
||||
FIREBASE = {
|
||||
"apiKey": "AIzaSyCZHvaAQJKGiU1McxqgbrH-_KPV92JofUA",
|
||||
"authDomain": "uva-covid19-testing-kiosk.firebaseapp.com",
|
||||
"databaseURL": "https://uva-covid19-testing-kiosk.firebaseio.com",
|
||||
"storageBucket": "uva-covid19-testing-kiosk.appspot.com",
|
||||
"projectId": "uva-covid19-testing-kiosk",
|
||||
"messagingSenderId": "452622162774",
|
||||
"appId": "1:452622162774:web:f2b513f3c1765fc9b954f7"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
Student ID|Student Cellphone|Student Email|Test Date Time|Test Kiosk Loc|Test Result Code
|
||||
987654321|555/555-5555|rkc7h@virginia.edu|202009030809|4321|8726520277
|
||||
987654322|555/555-5556|testpositive@virginia.edu|202009060919|4321|8269722523
|
||||
987655321|555/555-5558|testnegetive@virginia.edu|202009070719|4321|1142270225
|
||||
000000111|555/555-5555|rkc7h@virginia.edu|202009091449|4321|8726520277
|
||||
000000222|555/555-5556|testpositive@virginia.edu|202009091449|4321|8269722523
|
||||
000000333|555/555-5558|testnegetive@virginia.edu|202009091449|4321|1142270225
|
|
|
@ -1,6 +1,9 @@
|
|||
# Set environment variable to testing before loading.
|
||||
# IMPORTANT - Environment must be loaded before app, models, etc....
|
||||
import base64
|
||||
import os
|
||||
import quopri
|
||||
import re
|
||||
import unittest
|
||||
os.environ["TESTING"] = "true"
|
||||
|
||||
|
@ -47,3 +50,19 @@ class BaseTest(unittest.TestCase):
|
|||
def tearDown(self):
|
||||
db.session.query(Sample).delete()
|
||||
db.session.commit()
|
||||
|
||||
def decode(self, encoded_words):
|
||||
"""
|
||||
Useful for checking the content of email messages
|
||||
(which we store in an array for testing)
|
||||
"""
|
||||
encoded_word_regex = r'=\?{1}(.+)\?{1}([b|q])\?{1}(.+)\?{1}='
|
||||
charset, encoding, encoded_text = re.match(encoded_word_regex,
|
||||
encoded_words).groups()
|
||||
if encoding == 'b':
|
||||
byte_string = base64.b64decode(encoded_text)
|
||||
elif encoding == 'q':
|
||||
byte_string = quopri.decodestring(encoded_text)
|
||||
text = byte_string.decode(charset)
|
||||
text = text.replace("_", " ")
|
||||
return text
|
|
@ -8,6 +8,4 @@ class FirebaseServiceTest(BaseTest):
|
|||
def test_get_samples(self):
|
||||
service = FirebaseService()
|
||||
samples = service.get_samples()
|
||||
for sample in samples.stream():
|
||||
print(u'{} => {}'.format(sample.id, sample.to_dict()))
|
||||
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
from tests.base_test import BaseTest
|
||||
|
||||
|
||||
from communicator import app
|
||||
from communicator.models import Sample
|
||||
from communicator.services.notification_service import TEST_MESSAGES, NotificationService
|
||||
|
||||
|
||||
class TestNotificationService(BaseTest):
|
||||
|
||||
def test_send_notification(self):
|
||||
message_count = len(TEST_MESSAGES)
|
||||
notifier = NotificationService(app)
|
||||
sample = Sample(email="dan@stauntonmakerspace.com", result_code="1234")
|
||||
notifier.send_result_email(sample)
|
||||
self.assertEqual(len(TEST_MESSAGES), message_count + 1)
|
||||
self.assertEqual("UVA: BE SAFE Notification", self.decode(TEST_MESSAGES[-1]['subject']))
|
Loading…
Reference in New Issue