mirror of
https://github.com/sartography/uva-covid19-testing-communicator.git
synced 2025-02-23 12:28:26 +00:00
Adding a tool for sending out bulk email notifications and a simple web form for doing so.
This commit is contained in:
parent
ccb9cf1631
commit
bdee709324
2
Pipfile
2
Pipfile
@ -21,6 +21,8 @@ flask-mail = "*"
|
||||
flask-marshmallow = "*"
|
||||
flask-migrate = "*"
|
||||
flask-restful = "*"
|
||||
flask-wtf = "*"
|
||||
flask-table = "*"
|
||||
marshmallow = "*"
|
||||
marshmallow-enum = "*"
|
||||
marshmallow-sqlalchemy = "*"
|
||||
|
107
Pipfile.lock
generated
107
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "d21912b50ed403a1436f4eb9feed3facedea30b4c9e887620d7e8359e1cdba88"
|
||||
"sha256": "cab43a07d915cc528153009278d8402c8350e6d2df0e1f629dc514d402c639d6"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -38,6 +38,13 @@
|
||||
],
|
||||
"version": "==20.2.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
|
||||
"sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
|
||||
],
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29",
|
||||
@ -193,30 +200,30 @@
|
||||
},
|
||||
"cryptography": {
|
||||
"hashes": [
|
||||
"sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a",
|
||||
"sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed",
|
||||
"sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36",
|
||||
"sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08",
|
||||
"sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237",
|
||||
"sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618",
|
||||
"sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f",
|
||||
"sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695",
|
||||
"sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c",
|
||||
"sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10",
|
||||
"sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c",
|
||||
"sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1",
|
||||
"sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e",
|
||||
"sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f",
|
||||
"sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791",
|
||||
"sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0",
|
||||
"sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af",
|
||||
"sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8",
|
||||
"sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761",
|
||||
"sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716",
|
||||
"sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32",
|
||||
"sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"
|
||||
"sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499",
|
||||
"sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154",
|
||||
"sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6",
|
||||
"sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49",
|
||||
"sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f",
|
||||
"sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396",
|
||||
"sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719",
|
||||
"sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db",
|
||||
"sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70",
|
||||
"sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536",
|
||||
"sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe",
|
||||
"sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba",
|
||||
"sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d",
|
||||
"sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7",
|
||||
"sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490",
|
||||
"sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8",
|
||||
"sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921",
|
||||
"sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118",
|
||||
"sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba",
|
||||
"sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3",
|
||||
"sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc",
|
||||
"sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2"
|
||||
],
|
||||
"version": "==3.1"
|
||||
"version": "==3.1.1"
|
||||
},
|
||||
"docxtpl": {
|
||||
"hashes": [
|
||||
@ -241,6 +248,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.5.6"
|
||||
},
|
||||
"flask-babel": {
|
||||
"hashes": [
|
||||
"sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468",
|
||||
"sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d"
|
||||
],
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"flask-bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"
|
||||
@ -294,6 +308,21 @@
|
||||
],
|
||||
"version": "==2.4.4"
|
||||
},
|
||||
"flask-table": {
|
||||
"hashes": [
|
||||
"sha256:320e5756cd7252e902e03b6cd1087f2c7ebc31364341b482f41f30074d10a770"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.5.0"
|
||||
},
|
||||
"flask-wtf": {
|
||||
"hashes": [
|
||||
"sha256:57b3faf6fe5d6168bda0c36b0df1d05770f8e205e18332d0376ddb954d17aef2",
|
||||
"sha256:d417e3a0008b5ba583da1763e4db0f55a1269d9dd91dcc3eb3c026d3c5dbd720"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.14.3"
|
||||
},
|
||||
"globus-sdk": {
|
||||
"hashes": [
|
||||
"sha256:883a862ddd17b0f4868ec55d6697a64c13d91c41b9fa5103198d2140053abac2",
|
||||
@ -400,6 +429,14 @@
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
@ -818,6 +855,13 @@
|
||||
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
|
||||
],
|
||||
"version": "==2.3.3"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -868,6 +912,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==5.3"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==1.7.0"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||
@ -939,6 +991,13 @@
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ import os
|
||||
|
||||
import connexion
|
||||
import sentry_sdk
|
||||
from flask import render_template, request
|
||||
from flask_cors import CORS
|
||||
from flask_mail import Mail
|
||||
from flask_marshmallow import Marshmallow
|
||||
@ -10,7 +11,6 @@ from flask_migrate import Migrate
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
# API, fully defined in api.yml
|
||||
@ -36,6 +36,8 @@ ma = Marshmallow(app)
|
||||
|
||||
from communicator import models
|
||||
from communicator import api
|
||||
from communicator import forms
|
||||
|
||||
connexion_app.add_api('api.yml', base_path='/v1.0')
|
||||
|
||||
# Convert list of allowed origins to list of regexes
|
||||
@ -50,6 +52,38 @@ if app.config['SENTRY_ENVIRONMENT']:
|
||||
integrations=[FlaskIntegration()]
|
||||
)
|
||||
|
||||
### HTML Pages
|
||||
BASE_HREF = app.config['APPLICATION_ROOT'].strip('/')
|
||||
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
def index():
|
||||
# display results
|
||||
return render_template(
|
||||
'index.html',
|
||||
base_href=BASE_HREF
|
||||
)
|
||||
|
||||
|
||||
@app.route('/invitation', methods=['GET', 'POST'])
|
||||
def send_invitation():
|
||||
form = forms.InvitationForm(request.form)
|
||||
action = BASE_HREF + "/invitation"
|
||||
title = "Send invitation to students"
|
||||
if request.method == 'POST':
|
||||
from communicator.services.notification_service import NotificationService
|
||||
with NotificationService(app) as ns:
|
||||
ns.send_invitations(form.date.data, form.location.data, form.emails.data)
|
||||
return render_template(
|
||||
'form.html',
|
||||
form=form,
|
||||
action=action,
|
||||
title=title,
|
||||
description_map={},
|
||||
base_href=BASE_HREF
|
||||
)
|
||||
|
||||
|
||||
# Access tokens
|
||||
@app.cli.command()
|
||||
def globus_token():
|
||||
@ -57,18 +91,21 @@ def globus_token():
|
||||
ivy_service = IvyService()
|
||||
ivy_service.get_access_token()
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def list_files():
|
||||
from communicator.services.ivy_service import IvyService
|
||||
ivy_service = IvyService()
|
||||
ivy_service.list_files()
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def transfer():
|
||||
from communicator.services.ivy_service import IvyService
|
||||
ivy_service = IvyService()
|
||||
ivy_service.request_transfer()
|
||||
|
||||
|
||||
@app.cli.command()
|
||||
def delete():
|
||||
from communicator.services.ivy_service import IvyService
|
||||
|
@ -14,7 +14,7 @@ def status():
|
||||
|
||||
|
||||
def update_data():
|
||||
"""Updates the database based on local files placed by IVY and recoreds
|
||||
"""Updates the database based on local files placed by IVY and records
|
||||
read in from the firecloud database."""
|
||||
fb_service = FirebaseService()
|
||||
ivy_service = IvyService()
|
||||
@ -22,6 +22,7 @@ def update_data():
|
||||
samples = fb_service.get_samples()
|
||||
samples.extend(ivy_service.load_directory())
|
||||
SampleService().add_or_update_records(samples)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def notify_by_email():
|
||||
|
10
communicator/forms.py
Normal file
10
communicator/forms.py
Normal file
@ -0,0 +1,10 @@
|
||||
from flask_table import Table, Col, LinkCol, BoolCol, DatetimeCol, NestedTableCol
|
||||
from flask_wtf import FlaskForm
|
||||
from wtforms import SelectMultipleField, StringField, BooleanField, SelectField, validators, HiddenField, TextAreaField
|
||||
from wtforms.widgets import TextArea
|
||||
|
||||
|
||||
class InvitationForm(FlaskForm):
|
||||
location = StringField('Location (ex. Newcomb Hall, South Meeting Room)', [validators.DataRequired()])
|
||||
date = StringField('Date (ex. Monday, September 23 from 10:00 am-5:00 pm ', [validators.DataRequired()])
|
||||
emails = TextAreaField('Emails (newline delimited)', render_kw={'rows': 50, 'cols': 50})
|
8
communicator/models/ivy_file.py
Normal file
8
communicator/models/ivy_file.py
Normal file
@ -0,0 +1,8 @@
|
||||
from sqlalchemy import func
|
||||
|
||||
from communicator import db
|
||||
|
||||
|
||||
class IvyFile(db.Model):
|
||||
file_name = db.Column(db.String, primary_key=True)
|
||||
date_added = db.Column(db.DateTime(timezone=True), default=func.now())
|
@ -14,6 +14,7 @@ class Sample(db.Model):
|
||||
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)
|
||||
ivy_file_name = db.Column(db.String)
|
||||
notifications = db.relationship(Notification, back_populates="sample",
|
||||
cascade="all, delete, delete-orphan",
|
||||
order_by=Notification.date)
|
||||
|
@ -18,7 +18,6 @@ class FirebaseService(object):
|
||||
self.db = firestore.Client(project="uva-covid19-testing-kiosk",
|
||||
credentials= credentials)
|
||||
|
||||
|
||||
def get_samples(self):
|
||||
# Then query for documents
|
||||
fb_samples = self.db.collection(u'samples')
|
||||
|
@ -3,8 +3,9 @@ import csv
|
||||
import globus_sdk
|
||||
from dateutil import parser
|
||||
|
||||
from communicator import app
|
||||
from communicator import app, db
|
||||
from communicator.errors import CommError
|
||||
from communicator.models.ivy_file import IvyFile
|
||||
from communicator.models.sample import Sample
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
@ -30,8 +31,10 @@ class IvyService(object):
|
||||
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)))
|
||||
for file_name in onlyfiles:
|
||||
samples = IvyService.samples_from_ivy_file(join(self.path, file_name))
|
||||
ivy_file = IvyFile(file_name=file_name)
|
||||
db.session.add(ivy_file)
|
||||
return samples
|
||||
|
||||
@staticmethod
|
||||
|
@ -5,21 +5,88 @@ from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from flask import render_template
|
||||
from flask_mail import Message
|
||||
from twilio.rest import Client
|
||||
|
||||
from communicator import db, mail, app
|
||||
from communicator.errors import ApiError, CommError
|
||||
from communicator import app
|
||||
from communicator.errors import CommError
|
||||
|
||||
TEST_MESSAGES = []
|
||||
|
||||
|
||||
class NotificationService(object):
|
||||
"""Provides common tools for working with an Email"""
|
||||
"""Provides common tools for working with email and text messages, please use this
|
||||
using the "with" syntax, to assure connections are properly closed.
|
||||
ex:
|
||||
|
||||
with NotificationService() as notifier:
|
||||
notifier.send_result_email(sample)
|
||||
notifier.send_result_text(sample)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
self.sender = app.config['MAIL_SENDER']
|
||||
|
||||
def email_server(self):
|
||||
def __enter__(self):
|
||||
self.email_server = self._get_email_server()
|
||||
self.twilio_client = self._get_twilio_client()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.email_server.close()
|
||||
# No way to close the twilio client that I can see.
|
||||
|
||||
def get_link(self, sample):
|
||||
return f"https://besafe.virginia.edu/result-demo?code={sample.result_code}"
|
||||
|
||||
def send_result_sms(self, sample):
|
||||
link = self.get_link(sample)
|
||||
message = self.twilio_client.messages.create(
|
||||
to=sample.phone,
|
||||
from_=self.app.config['TWILIO_NUMBER'],
|
||||
body=f"You have an important notification from UVA Be Safe, please visit: {link}")
|
||||
print(message.sid)
|
||||
|
||||
def send_result_email(self, sample):
|
||||
link = self.get_link(sample)
|
||||
subject = "UVA: BE SAFE Notification"
|
||||
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_invitations(self, date, location, email_string):
|
||||
emails = email_string.splitlines()
|
||||
for email in emails:
|
||||
subject = "UVA: BE SAFE - Appointment"
|
||||
tracking_code = self._tracking_code()
|
||||
text_body = render_template("invitation_email.txt",
|
||||
date=date,
|
||||
location=location,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
html_body = render_template("invitation_email.html",
|
||||
date=date,
|
||||
location=location,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
self._send_email(subject, recipients=[email], text_body=text_body, html_body=html_body)
|
||||
|
||||
def _tracking_code(self):
|
||||
return str(uuid.uuid4())[:16]
|
||||
|
||||
def _get_email_server(self):
|
||||
print("Server:" + self.app.config['MAIL_SERVER'])
|
||||
server = smtplib.SMTP(host=self.app.config['MAIL_SERVER'],
|
||||
port=self.app.config['MAIL_PORT'],
|
||||
@ -32,30 +99,11 @@ class NotificationService(object):
|
||||
self.app.config['MAIL_PASSWORD'])
|
||||
return server
|
||||
|
||||
def tracking_code(self):
|
||||
return str(uuid.uuid4())[:16]
|
||||
def _get_twilio_client(self):
|
||||
return Client(self.app.config['TWILIO_SID'],
|
||||
self.app.config['TWILIO_TOKEN'])
|
||||
|
||||
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):
|
||||
def _send_email(self, subject, recipients, text_body, html_body, sender=None, ical=None):
|
||||
msgRoot = MIMEMultipart('related')
|
||||
msgRoot.set_charset('utf8')
|
||||
|
||||
@ -89,11 +137,8 @@ class NotificationService(object):
|
||||
return
|
||||
|
||||
try:
|
||||
server = self.email_server()
|
||||
server.sendmail(sender, recipients, msgRoot.as_bytes())
|
||||
server.quit()
|
||||
self.email_server.sendmail(sender, recipients, msgRoot.as_bytes())
|
||||
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)
|
||||
|
||||
|
38
communicator/templates/form.html
Normal file
38
communicator/templates/form.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Be SAFE Notification System</title>
|
||||
<base href="/">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://use.typekit.net/kwp6dli.css">
|
||||
</head>
|
||||
<style type="text/css">
|
||||
input {
|
||||
width: 500px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ details|safe }}</p>
|
||||
<form action="{{ action }}" method="post">
|
||||
|
||||
{{ form.csrf_token() }}
|
||||
|
||||
{% for field in form if field.name != "csrf_token" %}
|
||||
<div class="form-field {{ field.widget.input_type }}">
|
||||
<div class="form-field-label">{{ field.label() }}:</div>
|
||||
<div class="form-field-input">{{ field }}</div>
|
||||
<div class="form-field-help">{{ description_map[field.name] }}</div>
|
||||
{% for error in field.errors %}
|
||||
<div class="form-field-error">{{ error }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<button class="btn btn-primary" type="submit">Submit</button>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-default">Cancel</a>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
14
communicator/templates/index.html
Normal file
14
communicator/templates/index.html
Normal file
@ -0,0 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>UVA Be Safe Communicator</title>
|
||||
<base href="/">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://use.typekit.net/kwp6dli.css">
|
||||
<link rel="shortcut icon" href="{{ base_href + url_for('static', filename='favicon.ico') }}">
|
||||
</head>
|
||||
<body>
|
||||
<h2>UVA Be Safe Communicator</h2>
|
||||
<p>Just making sure this works ok.</p>
|
||||
</body>
|
||||
</html>
|
39
communicator/templates/invitation_email.html
Normal file
39
communicator/templates/invitation_email.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base_email.html" %}
|
||||
{% block content %}
|
||||
<p style="font-family: sans-serif; font-size: 24px; font-weight: bold; margin: 0; Margin-bottom: 15px;">
|
||||
This is your invitation from the University of Virginia Be SAFE System.
|
||||
</p>
|
||||
|
||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
|
||||
To help keep our classes in-person, and to help keep our community safe, we are launching Be SAFE*
|
||||
(Screening Asymptomatic Finding Effort). Be SAFE is a quick and easy coronavirus screening
|
||||
program that will rely on saliva samples to detect the virus, with the goal of identifying silent
|
||||
carriers before they have the opportunity to transmit the coronavirus. The saliva screenings are
|
||||
mandatory and free of charge. The screenings will be done at multiple locations around Grounds and
|
||||
only take about 2-3 minutes. The ultimate goal is to test all asymptomatic students who are coming
|
||||
on Grounds periodically throughout the semester and to get them results within hours (rather than days).
|
||||
</p>
|
||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
|
||||
Your next screening test is scheduled at:
|
||||
</p>
|
||||
<p style="font-family: sans-serif; font-size: 24px; font-weight: bold; margin: 0; Margin-bottom: 15px;">
|
||||
{{location}}
|
||||
</p>
|
||||
<p style="font-family: sans-serif; font-size: 24px; font-weight: bold; margin: 0; Margin-bottom: 15px;">
|
||||
{{date}}
|
||||
</p>
|
||||
|
||||
<p style="font-family: sans-serif; font-size: 24px; font-weight: bold; margin: 0; Margin-bottom: 15px;">
|
||||
You will need to bring your student ID card with you.
|
||||
</p>
|
||||
|
||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
|
||||
For more information please visit https://besafe.virginia.edu/
|
||||
</p>
|
||||
<p style="font-family: sans-serif; font-size: 14px; font-weight: normal; margin: 0; Margin-bottom: 15px;">
|
||||
Questions? Email us at asksafe@virginia.edu
|
||||
</p>
|
||||
|
||||
|
||||
|
||||
{% endblock %}
|
22
communicator/templates/invitation_email.txt
Normal file
22
communicator/templates/invitation_email.txt
Normal file
@ -0,0 +1,22 @@
|
||||
This is your invitation from the University of Virginia Be SAFE System.
|
||||
-----------------------------------------------------------------------
|
||||
|
||||
To help keep our classes in-person, and to help keep our community safe, we are launching Be SAFE*
|
||||
(Screening Asymptomatic Finding Effort). Be SAFE is a quick and easy coronavirus screening
|
||||
program that will rely on saliva samples to detect the virus, with the goal of identifying silent
|
||||
carriers before they have the opportunity to transmit the coronavirus. The saliva screenings are
|
||||
mandatory and free of charge. The screenings will be done at multiple locations around Grounds and
|
||||
only take about 2-3 minutes. The ultimate goal is to test all asymptomatic students who are coming
|
||||
on Grounds periodically throughout the semester and to get them results within hours (rather than days).
|
||||
|
||||
==========================================
|
||||
Your next screening test is scheduled at:
|
||||
{{location}}
|
||||
{{date}}
|
||||
==========================================
|
||||
|
||||
You will need to bring your student ID card with you.
|
||||
|
||||
For more information please visit https://besafe.virginia.edu/
|
||||
Questions? Email us at asksafe@virginia.edu
|
||||
|
@ -57,3 +57,8 @@ GLOBUS_TRANSFER_RT = environ.get('GLOBUS_TRANSFER_RT')
|
||||
GLOBUS_TRANSFER_AT = environ.get('GLOBUS_TRANSFER_AT')
|
||||
GLOBUS_IVY_ENDPOINT = environ.get('GLOBUS_IVY_ENDPOINT')
|
||||
GLOBUS_DTN_ENDPOINT = environ.get('GLOBUS_DTN_ENDPOINT')
|
||||
|
||||
# Twilio SMS Messages
|
||||
TWILIO_SID = environ.get('TWILIO_SID')
|
||||
TWILIO_TOKEN = environ.get('TWILIO_TOKEN')
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user