mirror of
https://github.com/sartography/uva-covid19-testing-communicator.git
synced 2025-02-23 20:38:13 +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-marshmallow = "*"
|
||||||
flask-migrate = "*"
|
flask-migrate = "*"
|
||||||
flask-restful = "*"
|
flask-restful = "*"
|
||||||
|
flask-wtf = "*"
|
||||||
|
flask-table = "*"
|
||||||
marshmallow = "*"
|
marshmallow = "*"
|
||||||
marshmallow-enum = "*"
|
marshmallow-enum = "*"
|
||||||
marshmallow-sqlalchemy = "*"
|
marshmallow-sqlalchemy = "*"
|
||||||
|
107
Pipfile.lock
generated
107
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "d21912b50ed403a1436f4eb9feed3facedea30b4c9e887620d7e8359e1cdba88"
|
"sha256": "cab43a07d915cc528153009278d8402c8350e6d2df0e1f629dc514d402c639d6"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -38,6 +38,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==20.2.0"
|
"version": "==20.2.0"
|
||||||
},
|
},
|
||||||
|
"babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
|
||||||
|
"sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
|
||||||
|
],
|
||||||
|
"version": "==2.8.0"
|
||||||
|
},
|
||||||
"bcrypt": {
|
"bcrypt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29",
|
"sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29",
|
||||||
@ -193,30 +200,30 @@
|
|||||||
},
|
},
|
||||||
"cryptography": {
|
"cryptography": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a",
|
"sha256:21b47c59fcb1c36f1113f3709d37935368e34815ea1d7073862e92f810dc7499",
|
||||||
"sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed",
|
"sha256:451cdf60be4dafb6a3b78802006a020e6cd709c22d240f94f7a0696240a17154",
|
||||||
"sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36",
|
"sha256:4549b137d8cbe3c2eadfa56c0c858b78acbeff956bd461e40000b2164d9167c6",
|
||||||
"sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08",
|
"sha256:48ee615a779ffa749d7d50c291761dc921d93d7cf203dca2db663b4f193f0e49",
|
||||||
"sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237",
|
"sha256:559d622aef2a2dff98a892eef321433ba5bc55b2485220a8ca289c1ecc2bd54f",
|
||||||
"sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618",
|
"sha256:5d52c72449bb02dd45a773a203196e6d4fae34e158769c896012401f33064396",
|
||||||
"sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f",
|
"sha256:65beb15e7f9c16e15934569d29fb4def74ea1469d8781f6b3507ab896d6d8719",
|
||||||
"sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695",
|
"sha256:680da076cad81cdf5ffcac50c477b6790be81768d30f9da9e01960c4b18a66db",
|
||||||
"sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c",
|
"sha256:762bc5a0df03c51ee3f09c621e1cee64e3a079a2b5020de82f1613873d79ee70",
|
||||||
"sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10",
|
"sha256:89aceb31cd5f9fc2449fe8cf3810797ca52b65f1489002d58fe190bfb265c536",
|
||||||
"sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c",
|
"sha256:983c0c3de4cb9fcba68fd3f45ed846eb86a2a8b8d8bc5bb18364c4d00b3c61fe",
|
||||||
"sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1",
|
"sha256:99d4984aabd4c7182050bca76176ce2dbc9fa9748afe583a7865c12954d714ba",
|
||||||
"sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e",
|
"sha256:9d9fc6a16357965d282dd4ab6531013935425d0dc4950df2e0cf2a1b1ac1017d",
|
||||||
"sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f",
|
"sha256:a7597ffc67987b37b12e09c029bd1dc43965f75d328076ae85721b84046e9ca7",
|
||||||
"sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791",
|
"sha256:ab010e461bb6b444eaf7f8c813bb716be2d78ab786103f9608ffd37a4bd7d490",
|
||||||
"sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0",
|
"sha256:b12e715c10a13ca1bd27fbceed9adc8c5ff640f8e1f7ea76416352de703523c8",
|
||||||
"sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af",
|
"sha256:b2bded09c578d19e08bd2c5bb8fed7f103e089752c9cf7ca7ca7de522326e921",
|
||||||
"sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8",
|
"sha256:b372026ebf32fe2523159f27d9f0e9f485092e43b00a5adacf732192a70ba118",
|
||||||
"sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761",
|
"sha256:cb179acdd4ae1e4a5a160d80b87841b3d0e0be84af46c7bb2cd7ece57a39c4ba",
|
||||||
"sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716",
|
"sha256:e97a3b627e3cb63c415a16245d6cef2139cca18bb1183d1b9375a1c14e83f3b3",
|
||||||
"sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32",
|
"sha256:f0e099fc4cc697450c3dd4031791559692dd941a95254cb9aeded66a7aa8b9bc",
|
||||||
"sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"
|
"sha256:f99317a0fa2e49917689b8cf977510addcfaaab769b3f899b9c481bbd76730c2"
|
||||||
],
|
],
|
||||||
"version": "==3.1"
|
"version": "==3.1.1"
|
||||||
},
|
},
|
||||||
"docxtpl": {
|
"docxtpl": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@ -241,6 +248,13 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.5.6"
|
"version": "==1.5.6"
|
||||||
},
|
},
|
||||||
|
"flask-babel": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468",
|
||||||
|
"sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d"
|
||||||
|
],
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
"flask-bcrypt": {
|
"flask-bcrypt": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"
|
"sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"
|
||||||
@ -294,6 +308,21 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.4.4"
|
"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": {
|
"globus-sdk": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:883a862ddd17b0f4868ec55d6697a64c13d91c41b9fa5103198d2140053abac2",
|
"sha256:883a862ddd17b0f4868ec55d6697a64c13d91c41b9fa5103198d2140053abac2",
|
||||||
@ -400,6 +429,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.10"
|
"version": "==2.10"
|
||||||
},
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||||
|
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==1.7.0"
|
||||||
|
},
|
||||||
"inflection": {
|
"inflection": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||||
@ -818,6 +855,13 @@
|
|||||||
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
|
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
|
||||||
],
|
],
|
||||||
"version": "==2.3.3"
|
"version": "==2.3.3"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||||
|
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||||
|
],
|
||||||
|
"version": "==3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"develop": {
|
"develop": {
|
||||||
@ -868,6 +912,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.3"
|
"version": "==5.3"
|
||||||
},
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83",
|
||||||
|
"sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==1.7.0"
|
||||||
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||||
@ -939,6 +991,13 @@
|
|||||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||||
],
|
],
|
||||||
"version": "==0.10.1"
|
"version": "==0.10.1"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||||
|
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||||
|
],
|
||||||
|
"version": "==3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ import os
|
|||||||
|
|
||||||
import connexion
|
import connexion
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
from flask import render_template, request
|
||||||
from flask_cors import CORS
|
from flask_cors import CORS
|
||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from flask_marshmallow import Marshmallow
|
from flask_marshmallow import Marshmallow
|
||||||
@ -10,7 +11,6 @@ from flask_migrate import Migrate
|
|||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||||
|
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
# API, fully defined in api.yml
|
# API, fully defined in api.yml
|
||||||
@ -36,6 +36,8 @@ ma = Marshmallow(app)
|
|||||||
|
|
||||||
from communicator import models
|
from communicator import models
|
||||||
from communicator import api
|
from communicator import api
|
||||||
|
from communicator import forms
|
||||||
|
|
||||||
connexion_app.add_api('api.yml', base_path='/v1.0')
|
connexion_app.add_api('api.yml', base_path='/v1.0')
|
||||||
|
|
||||||
# Convert list of allowed origins to list of regexes
|
# Convert list of allowed origins to list of regexes
|
||||||
@ -50,6 +52,38 @@ if app.config['SENTRY_ENVIRONMENT']:
|
|||||||
integrations=[FlaskIntegration()]
|
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
|
# Access tokens
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
def globus_token():
|
def globus_token():
|
||||||
@ -57,18 +91,21 @@ def globus_token():
|
|||||||
ivy_service = IvyService()
|
ivy_service = IvyService()
|
||||||
ivy_service.get_access_token()
|
ivy_service.get_access_token()
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
def list_files():
|
def list_files():
|
||||||
from communicator.services.ivy_service import IvyService
|
from communicator.services.ivy_service import IvyService
|
||||||
ivy_service = IvyService()
|
ivy_service = IvyService()
|
||||||
ivy_service.list_files()
|
ivy_service.list_files()
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
def transfer():
|
def transfer():
|
||||||
from communicator.services.ivy_service import IvyService
|
from communicator.services.ivy_service import IvyService
|
||||||
ivy_service = IvyService()
|
ivy_service = IvyService()
|
||||||
ivy_service.request_transfer()
|
ivy_service.request_transfer()
|
||||||
|
|
||||||
|
|
||||||
@app.cli.command()
|
@app.cli.command()
|
||||||
def delete():
|
def delete():
|
||||||
from communicator.services.ivy_service import IvyService
|
from communicator.services.ivy_service import IvyService
|
||||||
|
@ -14,7 +14,7 @@ def status():
|
|||||||
|
|
||||||
|
|
||||||
def update_data():
|
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."""
|
read in from the firecloud database."""
|
||||||
fb_service = FirebaseService()
|
fb_service = FirebaseService()
|
||||||
ivy_service = IvyService()
|
ivy_service = IvyService()
|
||||||
@ -22,6 +22,7 @@ def update_data():
|
|||||||
samples = fb_service.get_samples()
|
samples = fb_service.get_samples()
|
||||||
samples.extend(ivy_service.load_directory())
|
samples.extend(ivy_service.load_directory())
|
||||||
SampleService().add_or_update_records(samples)
|
SampleService().add_or_update_records(samples)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
def notify_by_email():
|
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?
|
in_ivy = db.Column(db.Boolean, default=False) # Has this record come in from the IVY?
|
||||||
email_notified = db.Column(db.Boolean, default=False)
|
email_notified = db.Column(db.Boolean, default=False)
|
||||||
text_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",
|
notifications = db.relationship(Notification, back_populates="sample",
|
||||||
cascade="all, delete, delete-orphan",
|
cascade="all, delete, delete-orphan",
|
||||||
order_by=Notification.date)
|
order_by=Notification.date)
|
||||||
|
@ -18,7 +18,6 @@ class FirebaseService(object):
|
|||||||
self.db = firestore.Client(project="uva-covid19-testing-kiosk",
|
self.db = firestore.Client(project="uva-covid19-testing-kiosk",
|
||||||
credentials= credentials)
|
credentials= credentials)
|
||||||
|
|
||||||
|
|
||||||
def get_samples(self):
|
def get_samples(self):
|
||||||
# Then query for documents
|
# Then query for documents
|
||||||
fb_samples = self.db.collection(u'samples')
|
fb_samples = self.db.collection(u'samples')
|
||||||
|
@ -3,8 +3,9 @@ import csv
|
|||||||
import globus_sdk
|
import globus_sdk
|
||||||
from dateutil import parser
|
from dateutil import parser
|
||||||
|
|
||||||
from communicator import app
|
from communicator import app, db
|
||||||
from communicator.errors import CommError
|
from communicator.errors import CommError
|
||||||
|
from communicator.models.ivy_file import IvyFile
|
||||||
from communicator.models.sample import Sample
|
from communicator.models.sample import Sample
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
@ -30,8 +31,10 @@ class IvyService(object):
|
|||||||
def load_directory(self):
|
def load_directory(self):
|
||||||
onlyfiles = [f for f in listdir(self.path) if isfile(join(self.path, f))]
|
onlyfiles = [f for f in listdir(self.path) if isfile(join(self.path, f))]
|
||||||
samples = []
|
samples = []
|
||||||
for file in onlyfiles:
|
for file_name in onlyfiles:
|
||||||
samples.extend(IvyService.samples_from_ivy_file(join(self.path, file)))
|
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
|
return samples
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -5,21 +5,88 @@ from email.mime.multipart import MIMEMultipart
|
|||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
|
|
||||||
from flask import render_template
|
from flask import render_template
|
||||||
from flask_mail import Message
|
from twilio.rest import Client
|
||||||
|
|
||||||
from communicator import db, mail, app
|
from communicator import app
|
||||||
from communicator.errors import ApiError, CommError
|
from communicator.errors import CommError
|
||||||
|
|
||||||
TEST_MESSAGES = []
|
TEST_MESSAGES = []
|
||||||
|
|
||||||
|
|
||||||
class NotificationService(object):
|
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):
|
def __init__(self, app):
|
||||||
self.app = app
|
self.app = app
|
||||||
self.sender = app.config['MAIL_SENDER']
|
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'])
|
print("Server:" + self.app.config['MAIL_SERVER'])
|
||||||
server = smtplib.SMTP(host=self.app.config['MAIL_SERVER'],
|
server = smtplib.SMTP(host=self.app.config['MAIL_SERVER'],
|
||||||
port=self.app.config['MAIL_PORT'],
|
port=self.app.config['MAIL_PORT'],
|
||||||
@ -32,30 +99,11 @@ class NotificationService(object):
|
|||||||
self.app.config['MAIL_PASSWORD'])
|
self.app.config['MAIL_PASSWORD'])
|
||||||
return server
|
return server
|
||||||
|
|
||||||
def tracking_code(self):
|
def _get_twilio_client(self):
|
||||||
return str(uuid.uuid4())[:16]
|
return Client(self.app.config['TWILIO_SID'],
|
||||||
|
self.app.config['TWILIO_TOKEN'])
|
||||||
|
|
||||||
def send_result_email(self, sample):
|
def _send_email(self, subject, recipients, text_body, html_body, sender=None, ical=None):
|
||||||
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 = MIMEMultipart('related')
|
||||||
msgRoot.set_charset('utf8')
|
msgRoot.set_charset('utf8')
|
||||||
|
|
||||||
@ -89,11 +137,8 @@ class NotificationService(object):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
server = self.email_server()
|
self.email_server.sendmail(sender, recipients, msgRoot.as_bytes())
|
||||||
server.sendmail(sender, recipients, msgRoot.as_bytes())
|
|
||||||
server.quit()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
app.logger.error('An exception happened in EmailService', exc_info=True)
|
app.logger.error('An exception happened in EmailService', exc_info=True)
|
||||||
app.logger.error(str(e))
|
app.logger.error(str(e))
|
||||||
raise CommError(5000, f"failed to send email to {', '.join(recipients)}", 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_TRANSFER_AT = environ.get('GLOBUS_TRANSFER_AT')
|
||||||
GLOBUS_IVY_ENDPOINT = environ.get('GLOBUS_IVY_ENDPOINT')
|
GLOBUS_IVY_ENDPOINT = environ.get('GLOBUS_IVY_ENDPOINT')
|
||||||
GLOBUS_DTN_ENDPOINT = environ.get('GLOBUS_DTN_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