mirror of
https://github.com/sartography/uva-covid19-testing-communicator.git
synced 2025-02-23 20:38:13 +00:00
Merge branch 'master' into feature-scheduler
This commit is contained in:
commit
586e5c9cf3
@ -21,7 +21,6 @@ install:
|
||||
env:
|
||||
global:
|
||||
- TESTING=true
|
||||
- PB_ENABLED=false
|
||||
- SQLALCHEMY_DATABASE_URI="postgresql://postgres:@localhost:5432/communicator_test"
|
||||
|
||||
script:
|
||||
|
33
Dockerfile
33
Dockerfile
@ -1,23 +1,20 @@
|
||||
FROM python:3.8-slim
|
||||
|
||||
WORKDIR /app
|
||||
COPY Pipfile Pipfile.lock /app/
|
||||
|
||||
RUN set -xe \
|
||||
pip install pipenv \
|
||||
&& apt-get update -q \
|
||||
&& apt-get install -y -q \
|
||||
gcc python3-dev libssl-dev \
|
||||
curl postgresql-client git-core \
|
||||
gunicorn3 postgresql-client
|
||||
|
||||
RUN set -xe \
|
||||
pipenv install --dev \
|
||||
&& apt-get remove -y gcc python3-dev libssl-dev \
|
||||
&& apt-get autoremove -y \
|
||||
&& apt-get clean -y \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& useradd _gunicorn --no-create-home --user-group
|
||||
RUN apt-get update -q \
|
||||
&& apt-get install -y -q \
|
||||
gcc \
|
||||
libssl-dev \
|
||||
curl \
|
||||
postgresql-client \
|
||||
gunicorn3
|
||||
RUN useradd _gunicorn --no-create-home --user-group
|
||||
|
||||
COPY . /app/
|
||||
WORKDIR /app
|
||||
|
||||
RUN chmod +x "/app/docker_run.sh" \
|
||||
&& pip install pipenv \
|
||||
&& pipenv install --dev --deploy --system --ignore-pipfile
|
||||
|
||||
ENTRYPOINT ["/app/docker_run.sh"]
|
||||
|
||||
|
8
Pipfile
8
Pipfile
@ -21,18 +21,24 @@ flask-mail = "*"
|
||||
flask-marshmallow = "*"
|
||||
flask-migrate = "*"
|
||||
flask-restful = "*"
|
||||
flask-wtf = "*"
|
||||
flask-table = "*"
|
||||
marshmallow = "*"
|
||||
marshmallow-enum = "*"
|
||||
marshmallow-sqlalchemy = "*"
|
||||
sentry-sdk = {extras = ["flask"],version = "==0.14.4"}
|
||||
swagger-ui-bundle = "*"
|
||||
spiffworkflow = {git = "https://github.com/sartography/SpiffWorkflow.git",ref = "master"}
|
||||
webtest = "*"
|
||||
python-box = "*"
|
||||
psycopg2-binary = "*"
|
||||
google-cloud-firestore = "*"
|
||||
globus-sdk = "*"
|
||||
apscheduler = "*"
|
||||
gunicorn = "*"
|
||||
twilio = "*"
|
||||
flask-paginate = "*"
|
||||
flask-assets = "*"
|
||||
pyscss = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
153
Pipfile.lock
generated
153
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "11bbdfff553eecca38caef64677e8e9fb43705ea35b83f220c663cb8396d1cd6"
|
||||
"sha256": "490cbcef70664a97232f019f8c20c17cea8035fac4904aa2e997c2f288832b87"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -55,6 +55,13 @@
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==20.2.0"
|
||||
},
|
||||
"babel": {
|
||||
"hashes": [
|
||||
"sha256:1aac2ae2d0d8ea368fa90906567f5c08463d98ade155c0c4bfedd6a0f7160e38",
|
||||
"sha256:d670ea0b10f8b723672d3a6abeb87b565b244da220d76b4dba1b66269ec152d4"
|
||||
],
|
||||
"version": "==2.8.0"
|
||||
},
|
||||
"bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:5b93c1726e50a93a033c36e5ca7fdcd29a5c7395af50a6892f5d9e7c6cfbfb29",
|
||||
@ -236,30 +243,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": [
|
||||
@ -284,6 +291,21 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.5.6"
|
||||
},
|
||||
"flask-assets": {
|
||||
"hashes": [
|
||||
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
|
||||
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0"
|
||||
},
|
||||
"flask-babel": {
|
||||
"hashes": [
|
||||
"sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468",
|
||||
"sha256:f9faf45cdb2e1a32ea2ec14403587d4295108f35017a7821a2b1acb8cfd9257d"
|
||||
],
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"flask-bcrypt": {
|
||||
"hashes": [
|
||||
"sha256:d71c8585b2ee1c62024392ebdbc447438564e2c8c02b4e57b56a4cafd8d13c5f"
|
||||
@ -322,6 +344,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.5.3"
|
||||
},
|
||||
"flask-paginate": {
|
||||
"hashes": [
|
||||
"sha256:4d5c746e4f7b639a9bb72fac0c2452d58e5297b88d6ecda3340153a59453d581"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"flask-restful": {
|
||||
"hashes": [
|
||||
"sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915",
|
||||
@ -344,6 +373,20 @@
|
||||
],
|
||||
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==0.18.2"
|
||||
"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": [
|
||||
@ -440,6 +483,14 @@
|
||||
],
|
||||
"version": "==1.32.0"
|
||||
},
|
||||
"gunicorn": {
|
||||
"hashes": [
|
||||
"sha256:1904bb2b8a43658807108d59c3f3d56c2b6121a701161de0ddf9ad140073c626",
|
||||
"sha256:cd4a810dd51bf497552cf3f863b575dabd73d6ad6a91075b65936b151cbf4f9c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==20.0.4"
|
||||
},
|
||||
"idna": {
|
||||
"hashes": [
|
||||
"sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
|
||||
@ -448,6 +499,14 @@
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
|
||||
"version": "==2.10"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
|
||||
"sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
@ -725,6 +784,13 @@
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"pyscss": {
|
||||
"hashes": [
|
||||
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.7"
|
||||
},
|
||||
"python-box": {
|
||||
"hashes": [
|
||||
"sha256:b7a6f3edd2f71e2475d93163b6465f637a2714b155acafef17408b06e55282b3",
|
||||
@ -829,10 +895,6 @@
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==2.0.1"
|
||||
},
|
||||
"spiffworkflow": {
|
||||
"git": "https://github.com/sartography/SpiffWorkflow.git",
|
||||
"ref": "cfd1b994ac3bcc7f1d6459b00ab7455b76b942bc"
|
||||
},
|
||||
"sqlalchemy": {
|
||||
"hashes": [
|
||||
"sha256:072766c3bd09294d716b2d114d46ffc5ccf8ea0b714a4e1c48253014b771c6bb",
|
||||
@ -885,6 +947,12 @@
|
||||
"sha256:e2cb6c6b5b604af38597403e9852872d7f534962ae2954c7f35efcb1ccacf4a4"
|
||||
],
|
||||
"version": "==2.1"
|
||||
"twilio": {
|
||||
"hashes": [
|
||||
"sha256:df1cf8f7e62fbe4d412e66204ee7f948cd31ff18173ff4690475834174eedaf0"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.45.3"
|
||||
},
|
||||
"urllib3": {
|
||||
"hashes": [
|
||||
@ -910,6 +978,13 @@
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"webassets": {
|
||||
"hashes": [
|
||||
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
|
||||
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
|
||||
],
|
||||
"version": "==2.0"
|
||||
},
|
||||
"webob": {
|
||||
"hashes": [
|
||||
"sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b",
|
||||
@ -940,6 +1015,13 @@
|
||||
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
|
||||
],
|
||||
"version": "==2.3.3"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -991,6 +1073,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==5.3"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
|
||||
"sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||
@ -1068,6 +1158,13 @@
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,13 +3,16 @@ import os
|
||||
|
||||
import connexion
|
||||
import sentry_sdk
|
||||
from flask import render_template, request, redirect, url_for
|
||||
from flask_assets import Environment
|
||||
from flask_cors import CORS
|
||||
from flask_mail import Mail
|
||||
from flask_marshmallow import Marshmallow
|
||||
from flask_migrate import Migrate
|
||||
from flask_paginate import Pagination, get_page_parameter
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||
|
||||
from webassets import Bundle
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
@ -34,8 +37,31 @@ db = SQLAlchemy(app)
|
||||
migrate = Migrate(app, db)
|
||||
ma = Marshmallow(app)
|
||||
|
||||
# Asset management
|
||||
url_map = app.url_map
|
||||
try:
|
||||
for rule in url_map.iter_rules('static'):
|
||||
url_map._rules.remove(rule)
|
||||
except ValueError:
|
||||
# no static view was created yet
|
||||
pass
|
||||
app.add_url_rule(
|
||||
app.static_url_path + '/<path:filename>',
|
||||
endpoint='static', view_func=app.send_static_file)
|
||||
assets = Environment(app)
|
||||
assets.init_app(app)
|
||||
assets.url = app.static_url_path
|
||||
scss = Bundle(
|
||||
'scss/app.scss',
|
||||
filters='pyscss',
|
||||
output='app.css'
|
||||
)
|
||||
assets.register('app_scss', scss)
|
||||
|
||||
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 +76,88 @@ if app.config['SENTRY_ENVIRONMENT']:
|
||||
integrations=[FlaskIntegration()]
|
||||
)
|
||||
|
||||
### HTML Pages
|
||||
BASE_HREF = app.config['APPLICATION_ROOT'].strip('/')
|
||||
|
||||
|
||||
@app.route('/', methods=['GET'])
|
||||
def index():
|
||||
from communicator.models import Sample
|
||||
from communicator.tables import SampleTable
|
||||
# display results
|
||||
page = request.args.get(get_page_parameter(), type=int, default=1)
|
||||
samples = db.session.query(Sample).order_by(Sample.date.desc())
|
||||
pagination = Pagination(page=page, total=samples.count(), search=False, record_name='samples')
|
||||
|
||||
table = SampleTable(samples.paginate(page,10,error_out=False).items)
|
||||
return render_template(
|
||||
'index.html',
|
||||
table=table,
|
||||
pagination=pagination,
|
||||
base_href=BASE_HREF
|
||||
)
|
||||
|
||||
@app.route('/invitation', methods=['GET', 'POST'])
|
||||
def send_invitation():
|
||||
from communicator.models.invitation import Invitation
|
||||
from communicator.tables import InvitationTable
|
||||
|
||||
form = forms.InvitationForm(request.form)
|
||||
action = BASE_HREF + "/invitation"
|
||||
title = "Send invitation to students"
|
||||
if request.method == 'POST' and form.validate():
|
||||
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 redirect(url_for('send_invitation'))
|
||||
# display results
|
||||
page = request.args.get(get_page_parameter(), type=int, default=1)
|
||||
invites = db.session.query(Invitation).order_by(Invitation.date_sent.desc())
|
||||
pagination = Pagination(page=page, total=invites.count(), search=False, record_name='samples')
|
||||
|
||||
table = InvitationTable(invites.paginate(page,10,error_out=False).items)
|
||||
|
||||
return render_template(
|
||||
'form.html',
|
||||
form=form,
|
||||
table=table,
|
||||
pagination=pagination,
|
||||
action=action,
|
||||
title=title,
|
||||
description_map={},
|
||||
base_href=BASE_HREF
|
||||
)
|
||||
|
||||
@app.route('/imported_files', methods=['GET'])
|
||||
def list_imported_files_from_ivy():
|
||||
from communicator.models.ivy_file import IvyFile
|
||||
from communicator.tables import IvyFileTable
|
||||
|
||||
# display results
|
||||
page = request.args.get(get_page_parameter(), type=int, default=1)
|
||||
files = db.session.query(IvyFile).order_by(IvyFile.date_added.desc())
|
||||
pagination = Pagination(page=page, total=files.count(), search=False, record_name='samples')
|
||||
|
||||
table = IvyFileTable(files.paginate(page, 10, error_out=False).items)
|
||||
return render_template(
|
||||
'imported_files.html',
|
||||
table=table,
|
||||
pagination=pagination,
|
||||
base_href=BASE_HREF
|
||||
)
|
||||
|
||||
@app.route('/sso')
|
||||
def sso():
|
||||
response = ""
|
||||
response += "<h1>Headers</h1>"
|
||||
response += "<ul>"
|
||||
for k, v in request.headers:
|
||||
response += "<li><b>%s</b> %s</li>\n" % (k, v)
|
||||
response += "<h1>Environment</h1>"
|
||||
for k, v in request.environ:
|
||||
response += "<li><b>%s</b> %s</li>\n" % (k, v)
|
||||
return response
|
||||
|
||||
# Access tokens
|
||||
@app.cli.command()
|
||||
def globus_token():
|
||||
@ -57,18 +165,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
|
||||
|
@ -6,8 +6,6 @@ info:
|
||||
name: MIT
|
||||
servers:
|
||||
- url: http://localhost:5000/v1.0
|
||||
security:
|
||||
- jwt: ['secret']
|
||||
paths:
|
||||
/status:
|
||||
get:
|
||||
@ -45,10 +43,52 @@ paths:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/sample:
|
||||
post:
|
||||
operationId: communicator.api.admin.add_sample
|
||||
summary: Creates a new sample
|
||||
tags:
|
||||
- Samples
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Sample'
|
||||
responses:
|
||||
'200':
|
||||
description: Sample created successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Sample"
|
||||
delete:
|
||||
operationId: communicator.api.admin.clear_samples
|
||||
summary: Removes the given samples completely.
|
||||
tags:
|
||||
- Studies
|
||||
responses:
|
||||
'204':
|
||||
description: All Samples removed.
|
||||
components:
|
||||
schemas:
|
||||
Status:
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
type: string
|
||||
Sample:
|
||||
properties:
|
||||
barcode:
|
||||
type: string
|
||||
example: "000000111-202009091449-4321"
|
||||
student_id:
|
||||
type: string
|
||||
example: "000000111"
|
||||
date:
|
||||
type: string
|
||||
format: date_time
|
||||
example: "2019-12-25T09:12:33.001Z"
|
||||
location:
|
||||
type: string
|
||||
example: "0001"
|
||||
|
@ -12,16 +12,25 @@ from communicator.services.sample_service import SampleService
|
||||
def status():
|
||||
return {"status":"good"}
|
||||
|
||||
def add_sample(body):
|
||||
sample = Sample(barcode=body['barcode'],
|
||||
student_id=body['student_id'],
|
||||
date=body['date'],
|
||||
location=body['location'])
|
||||
db.session.add(sample)
|
||||
db.session.commit()
|
||||
|
||||
def clear_samples():
|
||||
db.session.query(Sample).delete()
|
||||
db.session.commit()
|
||||
|
||||
def update_data():
|
||||
"""Updates the database based on local files placed by IVY and recoreds
|
||||
read in from the firecloud database."""
|
||||
fb_service = FirebaseService()
|
||||
"""Updates the database based on local files placed by IVY. No longer attempts
|
||||
to pull files from the Firebase service."""
|
||||
ivy_service = IvyService()
|
||||
|
||||
samples = fb_service.get_samples()
|
||||
samples.extend(ivy_service.load_directory())
|
||||
samples = ivy_service.load_directory()
|
||||
SampleService().add_or_update_records(samples)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def notify_by_email():
|
||||
|
20
communicator/forms.py
Normal file
20
communicator/forms.py
Normal file
@ -0,0 +1,20 @@
|
||||
import re
|
||||
|
||||
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, \
|
||||
ValidationError
|
||||
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': 10, 'cols': 50})
|
||||
|
||||
def validate_emails(form, field):
|
||||
all_emails = field.data.splitlines()
|
||||
EMAIL_REGEX = re.compile('^[a-z0-9]+[._a-z0-9]+[@]\w+[.]\w{2,3}$')
|
||||
for email in all_emails:
|
||||
if not re.search(EMAIL_REGEX, email):
|
||||
raise ValidationError(f'Invalid email \'{email}\', Emails must each be on a seperate line.')
|
11
communicator/models/invitation.py
Normal file
11
communicator/models/invitation.py
Normal file
@ -0,0 +1,11 @@
|
||||
from sqlalchemy import func
|
||||
|
||||
from communicator import db
|
||||
|
||||
|
||||
class Invitation(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
date_sent = db.Column(db.DateTime(timezone=True), default=func.now())
|
||||
location = db.Column(db.String)
|
||||
date = db.Column(db.String)
|
||||
total_recipients = db.Column(db.Integer)
|
9
communicator/models/ivy_file.py
Normal file
9
communicator/models/ivy_file.py
Normal file
@ -0,0 +1,9 @@
|
||||
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())
|
||||
sample_count = db.Column(db.Integer)
|
@ -1,7 +1,5 @@
|
||||
import json
|
||||
import os
|
||||
|
||||
from google.auth.credentials import Credentials
|
||||
from google.cloud import firestore
|
||||
from google.oauth2 import service_account
|
||||
|
||||
@ -18,7 +16,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')
|
||||
|
@ -1,10 +1,12 @@
|
||||
import csv
|
||||
from datetime import datetime
|
||||
|
||||
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
|
||||
@ -22,16 +24,20 @@ class IvyService(object):
|
||||
self.GLOBUS_IVY_ENDPOINT = app.config['GLOBUS_IVY_ENDPOINT']
|
||||
self.GLOBUS_DTN_ENDPOINT = app.config['GLOBUS_DTN_ENDPOINT']
|
||||
|
||||
print("Client ID:" + self.GLOBUS_CLIENT_ID)
|
||||
|
||||
self.client = globus_sdk.NativeAppAuthClient(self.GLOBUS_CLIENT_ID)
|
||||
self.client.oauth2_start_flow(refresh_tokens=True)
|
||||
|
||||
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 = db.session.query(IvyFile).filter(IvyFile.file_name == file_name).first()
|
||||
if not ivy_file:
|
||||
ivy_file = IvyFile(file_name=file_name, sample_count=len(samples))
|
||||
else:
|
||||
ivy_file.date_added = datetime.now()
|
||||
ivy_file.sample_count = len(samples)
|
||||
db.session.add(ivy_file)
|
||||
db.session.commit()
|
||||
return samples
|
||||
|
||||
@staticmethod
|
||||
@ -102,6 +108,9 @@ class IvyService(object):
|
||||
|
||||
|
||||
def get_transfer_client(self):
|
||||
self.client = globus_sdk.NativeAppAuthClient(self.GLOBUS_CLIENT_ID)
|
||||
self.client.oauth2_start_flow(refresh_tokens=True)
|
||||
|
||||
authorizer = globus_sdk.RefreshTokenAuthorizer(
|
||||
self.GLOBUS_TRANSFER_RT, self.client, access_token=self.GLOBUS_TRANSFER_AT, expires_at=self.EXPIRES_AT)
|
||||
tc = globus_sdk.TransferClient(authorizer=authorizer)
|
||||
|
@ -3,24 +3,105 @@ import uuid
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
import re
|
||||
|
||||
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, db
|
||||
from communicator.errors import CommError
|
||||
from communicator.models.invitation import Invitation
|
||||
|
||||
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']
|
||||
self.BASE_HREF = app.config['APPLICATION_ROOT'].strip('/')
|
||||
|
||||
def __enter__(self):
|
||||
if 'TESTING' in self.app.config and self.app.config['TESTING']:
|
||||
return 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):
|
||||
if 'TESTING' in self.app.config and self.app.config['TESTING']:
|
||||
return
|
||||
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,
|
||||
base_url = self.BASE_HREF,
|
||||
sample=sample,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
html_body = render_template("result_email.html",
|
||||
link=link,
|
||||
base_url = self.BASE_HREF,
|
||||
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()
|
||||
subject = "UVA: BE SAFE - Appointment"
|
||||
tracking_code = self._tracking_code()
|
||||
text_body = render_template("invitation_email.txt",
|
||||
date=date,
|
||||
location=location,
|
||||
base_url = self.BASE_HREF,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
html_body = render_template("invitation_email.html",
|
||||
date=date,
|
||||
location=location,
|
||||
base_url = self.BASE_HREF,
|
||||
tracking_code=tracking_code)
|
||||
|
||||
self._send_email(subject, recipients=[self.sender], bcc=emails, text_body=text_body, html_body=html_body)
|
||||
|
||||
invitation_log = Invitation(location=location, date=date, total_recipients=len(emails))
|
||||
db.session.add(invitation_log)
|
||||
db.session.commit()
|
||||
|
||||
def _tracking_code(self):
|
||||
return str(uuid.uuid4())[:16]
|
||||
|
||||
def _get_email_server(self):
|
||||
|
||||
def 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'],
|
||||
timeout=self.app.config['MAIL_TIMEOUT'])
|
||||
@ -32,30 +113,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, bcc=[], sender=None, ical=None):
|
||||
msgRoot = MIMEMultipart('related')
|
||||
msgRoot.set_charset('utf8')
|
||||
|
||||
@ -78,9 +140,9 @@ class NotificationService(object):
|
||||
|
||||
# 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')
|
||||
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']:
|
||||
@ -88,12 +150,11 @@ class NotificationService(object):
|
||||
TEST_MESSAGES.append(msgRoot)
|
||||
return
|
||||
|
||||
all_recipients = recipients + bcc
|
||||
|
||||
try:
|
||||
server = self.email_server()
|
||||
server.sendmail(sender, recipients, msgRoot.as_bytes())
|
||||
server.quit()
|
||||
self.email_server.sendmail(sender, all_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)
|
||||
|
||||
|
201
communicator/static/app.css
Normal file
201
communicator/static/app.css
Normal file
@ -0,0 +1,201 @@
|
||||
.mat-icon {
|
||||
font-family: 'Material Icons', sans-serif;
|
||||
font-size: 24px; }
|
||||
|
||||
.text-center {
|
||||
text-align: center; }
|
||||
|
||||
html, body {
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 16px; }
|
||||
|
||||
table {
|
||||
border: 1px solid #cacaca;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-collapse: collapse; }
|
||||
table th, table td {
|
||||
padding: 0.5em; }
|
||||
table td, table.blueTable th {
|
||||
border: 1px solid #cacaca; }
|
||||
table tbody td {
|
||||
font-size: 14px; }
|
||||
table tr:nth-child(even) {
|
||||
background: #ededed; }
|
||||
table thead {
|
||||
background-color: #495e9d; }
|
||||
table thead th {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
border-left: 1px solid #cacaca; }
|
||||
table thead th:first-child {
|
||||
border-left: none; }
|
||||
table tfoot {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: #cacaca; }
|
||||
table tfoot td {
|
||||
font-size: 16px; }
|
||||
table tfoot .links {
|
||||
text-align: right; }
|
||||
table tfoot .links a {
|
||||
display: inline-block;
|
||||
background: #495e9d;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px; }
|
||||
|
||||
.btn {
|
||||
font-size: 16px;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
border: none; }
|
||||
.btn:hover {
|
||||
text-decoration: none; }
|
||||
.btn.btn-icon {
|
||||
font-family: 'Material Icons', sans-serif;
|
||||
font-size: 24px;
|
||||
border: none; }
|
||||
.btn.btn-icon.btn-default {
|
||||
color: #4e4e4e;
|
||||
background-color: transparent;
|
||||
border: none; }
|
||||
.btn.btn-icon.btn-default:hover {
|
||||
color: #373737;
|
||||
background-color: transparent; }
|
||||
.btn.btn-icon.btn-primary {
|
||||
color: #232D4B;
|
||||
background-color: transparent; }
|
||||
.btn.btn-icon.btn-primary:hover {
|
||||
color: #191f34;
|
||||
background-color: transparent; }
|
||||
.btn.btn-icon.btn-accent {
|
||||
color: #E57200;
|
||||
background-color: transparent; }
|
||||
.btn.btn-icon.btn-accent:hover {
|
||||
color: #a05000;
|
||||
background-color: transparent; }
|
||||
.btn.btn-icon.btn-warn {
|
||||
color: #DF1E43;
|
||||
background-color: transparent; }
|
||||
.btn.btn-icon.btn-warn:hover {
|
||||
color: #9c152f;
|
||||
background-color: transparent; }
|
||||
.btn.btn-default {
|
||||
color: #373737;
|
||||
background-color: white;
|
||||
border: 1px solid #cacaca; }
|
||||
.btn.btn-default:hover {
|
||||
background-color: #ededed; }
|
||||
.btn.btn-primary {
|
||||
background-color: #232D4B; }
|
||||
.btn.btn-primary:hover {
|
||||
background-color: #191f34; }
|
||||
.btn.btn-warn {
|
||||
background-color: #DF1E43; }
|
||||
.btn.btn-warn:hover {
|
||||
background-color: #9c152f; }
|
||||
.btn.btn-accent {
|
||||
background-color: #E57200; }
|
||||
.btn.btn-accent:hover {
|
||||
background-color: #a05000; }
|
||||
|
||||
select.multi {
|
||||
height: 600px; }
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
max-width: 100vw;
|
||||
margin-bottom: 40px;
|
||||
padding: 2em; }
|
||||
.form-field.hidden {
|
||||
display: none; }
|
||||
.form-field:nth-child(even) {
|
||||
background-color: #ededed; }
|
||||
.form-field .form-field-label, .form-field .form-field-help,
|
||||
.form-field .form-field-input {
|
||||
width: 30%;
|
||||
text-align: left;
|
||||
margin-right: 24px; }
|
||||
.form-field .form-field-label {
|
||||
font-weight: bold;
|
||||
text-align: right; }
|
||||
.form-field .form-field-input input {
|
||||
width: 100%;
|
||||
text-align: left; }
|
||||
.form-field .form-field-input input[type=checkbox] {
|
||||
width: 24px; }
|
||||
.form-field .form-field-help {
|
||||
font-style: italic; }
|
||||
.form-field .form-field-error {
|
||||
color: #DF1E43; }
|
||||
|
||||
.alert {
|
||||
padding: 20px;
|
||||
background-color: #4e4e4e;
|
||||
color: white;
|
||||
margin-bottom: 15px;
|
||||
opacity: 1;
|
||||
border-radius: 5px;
|
||||
transform: scale3d(1, 1, 1);
|
||||
transition: all 0.5s ease-in-out; }
|
||||
.alert.warn {
|
||||
background-color: #DF1E43; }
|
||||
.alert.success {
|
||||
background-color: #64B343; }
|
||||
.alert.info {
|
||||
background-color: #cacaca;
|
||||
color: black; }
|
||||
.alert .btn-close {
|
||||
margin-left: 15px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
font-size: 22px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s; }
|
||||
.alert .btn-close:hover {
|
||||
color: black; }
|
||||
.alert.fade-out {
|
||||
opacity: 0; }
|
||||
.alert.shrink {
|
||||
transform: scale3d(0, 0, 0);
|
||||
padding: 0;
|
||||
margin: 0; }
|
||||
|
||||
.highlight {
|
||||
font-weight: bolder;
|
||||
font-style: italic; }
|
||||
|
||||
.pagination-page-info {
|
||||
padding: 0.6em;
|
||||
padding-left: 0;
|
||||
width: 40em;
|
||||
margin: 0.5em;
|
||||
margin-left: 0;
|
||||
font-size: 12px; }
|
||||
|
||||
.pagination-page-info b {
|
||||
color: black;
|
||||
background: #6aa6ed;
|
||||
padding-left: 2px;
|
||||
padding: 0.1em 0.25em;
|
||||
font-size: 150%; }
|
||||
|
||||
.pagination ul li {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
border: 1px solid black;
|
||||
text-align: center; }
|
||||
.pagination ul li.active {
|
||||
background-color: #ededed; }
|
334
communicator/static/scss/app.scss
Normal file
334
communicator/static/scss/app.scss
Normal file
@ -0,0 +1,334 @@
|
||||
// COLOR PALETTE
|
||||
|
||||
// gray
|
||||
$color-gray: #4e4e4e;
|
||||
$color-gray-light-2: scale-color($color-gray, $lightness: +90%);
|
||||
$color-gray-light-1: scale-color($color-gray, $lightness: +70%);
|
||||
$color-gray-light: $color-gray-light-1;
|
||||
$color-gray-dark: scale-color($color-gray, $lightness: -30%);
|
||||
|
||||
// primary (UVA "Jefferson Blue")
|
||||
$color-primary: #232D4B;
|
||||
$color-primary-light: scale-color($color-primary, $lightness: +30%);
|
||||
$color-primary-dark: scale-color($color-primary, $lightness: -30%);
|
||||
|
||||
// accent (UVA "Rotunda Orange")
|
||||
$color-accent: #E57200;
|
||||
$color-accent-light: scale-color($color-accent, $lightness: +30%);
|
||||
$color-accent-dark: scale-color($color-accent, $lightness: -30%);
|
||||
|
||||
// warn (UVA "Emergency Red")
|
||||
$color-warn: #DF1E43;
|
||||
$color-warn-light: scale-color($color-warn, $lightness: +30%);
|
||||
$color-warn-dark: scale-color($color-warn, $lightness: -30%);
|
||||
|
||||
// success (Green)
|
||||
$color-success: #64B343;
|
||||
$color-success-light: scale-color($color-success, $lightness: +30%);
|
||||
$color-success-dark: scale-color($color-success, $lightness: -30%);
|
||||
|
||||
$font-size-default: 16px;
|
||||
$font-size-lg: 24px;
|
||||
$font-size-md: 16px;
|
||||
$font-size-sm: 14px;
|
||||
|
||||
@mixin mat-icon {
|
||||
font-family: 'Material Icons', sans-serif;
|
||||
font-size: $font-size-lg;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
@include mat-icon;
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
html, body {
|
||||
padding: 1em;
|
||||
margin: 0;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
|
||||
table {
|
||||
border: 1px solid $color-gray-light;
|
||||
background-color: white;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-collapse: collapse;
|
||||
|
||||
th, td {
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
td, &.blueTable th {
|
||||
border: 1px solid $color-gray-light;
|
||||
}
|
||||
|
||||
tbody td {
|
||||
font-size: $font-size-sm;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background: $color-gray-light-2;
|
||||
}
|
||||
|
||||
thead {
|
||||
background-color: $color-primary-light;
|
||||
|
||||
th {
|
||||
font-size: $font-size-default;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
border-left: 1px solid $color-gray-light;
|
||||
}
|
||||
}
|
||||
|
||||
thead th:first-child {
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
tfoot {
|
||||
font-size: $font-size-default;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
background-color: $color-gray-light;
|
||||
|
||||
td {
|
||||
font-size: $font-size-default;
|
||||
}
|
||||
|
||||
.links {
|
||||
text-align: right;
|
||||
|
||||
a {
|
||||
display: inline-block;
|
||||
background: $color-primary-light;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
font-size: $font-size-default;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 5px;
|
||||
text-decoration: none;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&.btn-icon {
|
||||
@include mat-icon;
|
||||
border: none;
|
||||
|
||||
&.btn-default {
|
||||
color: $color-gray;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
color: $color-gray-dark;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
color: $color-primary;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: $color-primary-dark;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-accent {
|
||||
color: $color-accent;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: $color-accent-dark;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-warn {
|
||||
color: $color-warn;
|
||||
background-color: transparent;
|
||||
|
||||
&:hover {
|
||||
color: $color-warn-dark;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-default {
|
||||
color: $color-gray-dark;
|
||||
background-color: white;
|
||||
border: 1px solid $color-gray-light;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-gray-light-2;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-primary {
|
||||
background-color: $color-primary;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-primary-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-warn {
|
||||
background-color: $color-warn;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-warn-dark;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-accent {
|
||||
background-color: $color-accent;
|
||||
|
||||
&:hover {
|
||||
background-color: $color-accent-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
select.multi {
|
||||
height: 600px;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
max-width: 100vw;
|
||||
margin-bottom: 40px;
|
||||
padding: 2em;
|
||||
|
||||
&.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:nth-child(even) {
|
||||
background-color: $color-gray-light-2;
|
||||
}
|
||||
|
||||
.form-field-label,
|
||||
.form-field-help,
|
||||
.form-field-input {
|
||||
width: 30%;
|
||||
text-align: left;
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
.form-field-label {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.form-field-input input {
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&[type=checkbox] {
|
||||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-field-help {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.form-field-error {
|
||||
color: $color-warn;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
padding: 20px;
|
||||
background-color: $color-gray;
|
||||
color: white;
|
||||
margin-bottom: 15px;
|
||||
opacity: 1;
|
||||
border-radius: 5px;
|
||||
transform: scale3d(1, 1, 1);
|
||||
transition: all 0.5s ease-in-out;
|
||||
|
||||
&.warn { background-color: $color-warn; }
|
||||
&.success { background-color: $color-success; }
|
||||
&.info { background-color: $color-gray-light; color: black; }
|
||||
|
||||
.btn-close {
|
||||
margin-left: 15px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
float: right;
|
||||
font-size: 22px;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
transition: 0.3s;
|
||||
|
||||
&:hover {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
|
||||
&.fade-out {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&.shrink {
|
||||
transform: scale3d(0, 0, 0);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.highlight {
|
||||
font-weight: bolder;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.pagination-page-info {
|
||||
padding: .6em;
|
||||
padding-left: 0;
|
||||
width: 40em;
|
||||
margin: .5em;
|
||||
margin-left: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
.pagination-page-info b {
|
||||
color: black;
|
||||
background: #6aa6ed;
|
||||
padding-left: 2px;
|
||||
padding: .1em .25em;
|
||||
font-size: 150%;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
ul {
|
||||
li {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 20px;
|
||||
border: 1px solid black;
|
||||
text-align: center;
|
||||
}
|
||||
li.active {
|
||||
background-color: $color-gray-light-2;
|
||||
}
|
||||
}
|
||||
}
|
BIN
communicator/static/uva_logo.png
Normal file
BIN
communicator/static/uva_logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
29
communicator/tables.py
Normal file
29
communicator/tables.py
Normal file
@ -0,0 +1,29 @@
|
||||
from flask_table import Table, Col, DatetimeCol, BoolCol
|
||||
|
||||
|
||||
class SampleTable(Table):
|
||||
def sort_url(self, col_id, reverse=False):
|
||||
pass
|
||||
barcode = Col('Barcode')
|
||||
student_id = Col('Student Id')
|
||||
date = DatetimeCol('Date', "medium")
|
||||
location = Col('Location')
|
||||
email_notified = BoolCol('Emailed?')
|
||||
text_notified = BoolCol('Texted?')
|
||||
|
||||
|
||||
class IvyFileTable(Table):
|
||||
def sort_url(self, col_id, reverse=False):
|
||||
pass
|
||||
file_name = Col('File Name')
|
||||
date_added = DatetimeCol('Date', "medium")
|
||||
sample_count = Col('Total Records')
|
||||
|
||||
|
||||
class InvitationTable(Table):
|
||||
def sort_url(self, col_id, reverse=False):
|
||||
pass
|
||||
date_sent = DatetimeCol('Date Sent', "medium")
|
||||
location = Col('Location')
|
||||
date = Col('Date')
|
||||
total_recipients = Col('# Recipients')
|
@ -103,7 +103,7 @@
|
||||
<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; background-color:#232d4b; padding: 12px">
|
||||
<img src="https://besafe.virginia.edu/themes/custom/de_theme/logo.svg">
|
||||
<img src="{{base_url + '/images/uva_logo.png'}}" alt="University of Virginia"/>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@ -132,7 +132,7 @@
|
||||
</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>.
|
||||
Powered by <div style="color: #999999; font-size: 12px; text-align: center; text-decoration: none;">BE SAFE</div>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
52
communicator/templates/form.html
Normal file
52
communicator/templates/form.html
Normal file
@ -0,0 +1,52 @@
|
||||
<!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">
|
||||
<link rel="shortcut icon" href="{{ base_href + url_for('static', filename='favicon.ico') }}">
|
||||
{% assets 'app_scss' %}
|
||||
<link href="{{ base_href + ASSET_URL }}" rel="stylesheet" type="text/css">
|
||||
{% endassets %}
|
||||
<link rel="shortcut icon" href="{{ base_href + url_for('static', filename='favicon.ico') }}">
|
||||
</head>
|
||||
<style type="text/css">
|
||||
input {
|
||||
width: 500px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
</style>
|
||||
<body>
|
||||
<a href="{{base_href}}"><< Home</a>
|
||||
|
||||
<h3>Previous Entries</h3>
|
||||
{{ pagination.info }}
|
||||
{{ pagination.links }}
|
||||
{{ table }}
|
||||
{{ pagination.links }}
|
||||
|
||||
<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>
|
25
communicator/templates/imported_files.html
Normal file
25
communicator/templates/imported_files.html
Normal file
@ -0,0 +1,25 @@
|
||||
<!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') }}">
|
||||
{% assets 'app_scss' %}
|
||||
<link href="{{ base_href + ASSET_URL }}" rel="stylesheet" type="text/css">
|
||||
{% endassets %}
|
||||
<link rel="shortcut icon" href="{{ base_href + url_for('static', filename='favicon.ico') }}">
|
||||
</head>
|
||||
<body>
|
||||
<a href="{{base_href}}"><< Home</a>
|
||||
|
||||
<h2>UVA Be Safe Communicator</h2>
|
||||
|
||||
<h3>The following files were imported from IVY</h3>
|
||||
{{ pagination.info }}
|
||||
{{ pagination.links }}
|
||||
{{ table }}
|
||||
{{ pagination.links }}
|
||||
</body>
|
||||
</html>
|
24
communicator/templates/index.html
Normal file
24
communicator/templates/index.html
Normal file
@ -0,0 +1,24 @@
|
||||
<!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') }}">
|
||||
{% assets 'app_scss' %}
|
||||
<link href="{{ base_href + ASSET_URL }}" rel="stylesheet" type="text/css">
|
||||
{% endassets %}
|
||||
<link rel="shortcut icon" href="{{ base_href + url_for('static', filename='favicon.ico') }}">
|
||||
</head>
|
||||
<body>
|
||||
<h2>UVA Be Safe Communicator</h2>
|
||||
<a href="{{base_href + '/invitation'}}">Send Invitations</a> | <a href="{{base_href + '/imported_files'}}">View imported files</a>
|
||||
|
||||
<h3>Records to be processed</h3>
|
||||
{{ pagination.info }}
|
||||
{{ pagination.links }}
|
||||
{{ table }}
|
||||
{{ pagination.links }}
|
||||
</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
|
||||
|
@ -39,17 +39,18 @@ GITHUB_REPO = environ.get('GITHUB_REPO', None)
|
||||
TARGET_BRANCH = environ.get('TARGET_BRANCH', None)
|
||||
|
||||
# Email configuration
|
||||
MAIL_DEBUG = environ.get('MAIL_DEBUG', default=True)
|
||||
MAIL_DEBUG = environ.get('MAIL_DEBUG', default="false") == "true"
|
||||
MAIL_SERVER = environ.get('MAIL_SERVER', default='smtp.mailtrap.io')
|
||||
MAIL_PORT = environ.get('MAIL_PORT', default=2525)
|
||||
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='xxx')
|
||||
MAIL_PASSWORD = environ.get('MAIL_PASSWORD', default='yyy')
|
||||
MAIL_USE_SSL = environ.get('MAIL_USE_SSL', default="false") == "true"
|
||||
MAIL_USE_TLS = environ.get('MAIL_USE_TLS', default="false") == "true"
|
||||
MAIL_USERNAME = environ.get('MAIL_USERNAME', default='')
|
||||
MAIL_PASSWORD = environ.get('MAIL_PASSWORD', default='')
|
||||
MAIL_SENDER = 'askresearch@virginia.edu'
|
||||
MAIL_TIMEOUT = 10
|
||||
|
||||
# Ivy Directory
|
||||
IVY_IMPORT_DIR = os.path.join(basedir, '..', 'example_ivy_data')
|
||||
IVY_IMPORT_DIR = environ.get('IVY_IMPORT_DIR', default='')
|
||||
|
||||
# Globus endpoint connections
|
||||
GLOBUS_CLIENT_ID = environ.get('GLOBUS_CLIENT_ID')
|
||||
@ -57,3 +58,12 @@ 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')
|
||||
TWILIO_NUMBER = environ.get('TWILIO_NUMBER')
|
||||
|
||||
# Firestore configuration
|
||||
FIRESTORE_JSON = environ.get('FIRESTORE_JSON')
|
||||
|
||||
|
@ -19,15 +19,8 @@ function branch_to_tag () {
|
||||
if [ "$1" == "master" ]; then echo "latest"; else echo "$1" ; fi
|
||||
}
|
||||
|
||||
function branch_to_deploy_group() {
|
||||
echo "covid";
|
||||
# if [[ $1 =~ ^(rrt\/.*)$ ]]; then echo "rrt"; else echo "crconnect" ; fi
|
||||
}
|
||||
|
||||
DOCKER_TAG=$(branch_to_tag "$TRAVIS_BRANCH")
|
||||
|
||||
DEPLOY_GROUP=$(branch_to_deploy_group "$TRAVIS_BRANCH")
|
||||
|
||||
echo "DOCKER_REPO = $DOCKER_REPO"
|
||||
echo "DOCKER_TAG = $DOCKER_TAG"
|
||||
|
||||
|
@ -19,6 +19,12 @@ if [ "$RESET_DB" = "true" ]; then
|
||||
fi
|
||||
|
||||
# THIS MUST BE THE LAST COMMAND!
|
||||
if [ -z "$PORT0" ]
|
||||
then
|
||||
echo "$PORT0 is not set, setting to 5000"
|
||||
PORT0=5000
|
||||
fi
|
||||
|
||||
if [ "$APPLICATION_ROOT" = "/" ]; then
|
||||
pipenv run gunicorn --bind 0.0.0.0:$PORT0 wsgi:app
|
||||
else
|
||||
|
35
migrations/versions/3525dcf128cb_.py
Normal file
35
migrations/versions/3525dcf128cb_.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 3525dcf128cb
|
||||
Revises: 39a845579587
|
||||
Create Date: 2020-09-23 16:23:09.383030
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '3525dcf128cb'
|
||||
down_revision = '39a845579587'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('invitation',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('date_sent', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.Column('location', sa.String(), nullable=True),
|
||||
sa.Column('date', sa.String(), nullable=True),
|
||||
sa.Column('total_recipients', sa.Integer(), nullable=True),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('invitation')
|
||||
# ### end Alembic commands ###
|
28
migrations/versions/39a845579587_.py
Normal file
28
migrations/versions/39a845579587_.py
Normal file
@ -0,0 +1,28 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: 39a845579587
|
||||
Revises: fa4b41e0bfe6
|
||||
Create Date: 2020-09-23 15:50:44.987745
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '39a845579587'
|
||||
down_revision = 'fa4b41e0bfe6'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column('ivy_file', sa.Column('sample_count', sa.Integer(), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column('ivy_file', 'sample_count')
|
||||
# ### end Alembic commands ###
|
32
migrations/versions/fa4b41e0bfe6_.py
Normal file
32
migrations/versions/fa4b41e0bfe6_.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""empty message
|
||||
|
||||
Revision ID: fa4b41e0bfe6
|
||||
Revises: d904b7b3c1c0
|
||||
Create Date: 2020-09-23 14:05:50.766928
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'fa4b41e0bfe6'
|
||||
down_revision = 'd904b7b3c1c0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('ivy_file',
|
||||
sa.Column('file_name', sa.String(), nullable=False),
|
||||
sa.Column('date_added', sa.DateTime(timezone=True), nullable=True),
|
||||
sa.PrimaryKeyConstraint('file_name')
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_table('ivy_file')
|
||||
# ### end Alembic commands ###
|
2
tests/data/import_directory/file1.csv
Normal file
2
tests/data/import_directory/file1.csv
Normal file
@ -0,0 +1,2 @@
|
||||
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
|
|
3
tests/data/import_directory/file2.csv
Normal file
3
tests/data/import_directory/file2.csv
Normal file
@ -0,0 +1,3 @@
|
||||
Student ID|Student Cellphone|Student Email|Test Date Time|Test Kiosk Loc|Test Result Code
|
||||
987655321|555/555-5558|testnegetive@virginia.edu|202009070719|4321|1142270225
|
||||
|
|
3
tests/data/import_directory/file3.csv
Normal file
3
tests/data/import_directory/file3.csv
Normal file
@ -0,0 +1,3 @@
|
||||
Student ID|Student Cellphone|Student Email|Test Date Time|Test Kiosk Loc|Test Result Code
|
||||
000000222|555/555-5556|testpositive@virginia.edu|202009091449|4321|8269722523
|
||||
000000333|555/555-5558|testnegetive@virginia.edu|202009091449|4321|1142270225
|
|
4
tests/data/import_directory/file4.csv
Normal file
4
tests/data/import_directory/file4.csv
Normal file
@ -0,0 +1,4 @@
|
||||
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
|
||||
|
|
@ -1,9 +1,11 @@
|
||||
from tests.base_test import BaseTest
|
||||
import os
|
||||
import unittest
|
||||
import globus_sdk
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
from communicator import app
|
||||
from communicator.models.ivy_file import IvyFile
|
||||
|
||||
from communicator import app, db
|
||||
|
||||
from communicator.errors import CommError
|
||||
from communicator.services.ivy_service import IvyService
|
||||
@ -22,5 +24,11 @@ class IvyServiceTest(BaseTest):
|
||||
ivy_incorrect_file = os.path.join(app.root_path, '..', 'tests', 'data', 'incorrect.csv')
|
||||
IvyService.samples_from_ivy_file(ivy_incorrect_file)
|
||||
|
||||
def test_load_directory(self):
|
||||
self.assertEquals(0, db.session.query(IvyFile).count())
|
||||
app.config['IVY_IMPORT_DIR'] = os.path.join(app.root_path, '..', 'tests', 'data', 'import_directory')
|
||||
records = IvyService().load_directory()
|
||||
files = db.session.query(IvyFile).all()
|
||||
self.assertEquals(4, len(files))
|
||||
|
||||
|
||||
|
@ -10,8 +10,9 @@ 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)
|
||||
with NotificationService(app) as notifier:
|
||||
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']))
|
||||
self.assertEqual("UVA: BE SAFE Notification", self.decode(TEST_MESSAGES[-1]['subject']))
|
||||
|
||||
|
27
tests/services/test_sample_endpoints.py
Normal file
27
tests/services/test_sample_endpoints.py
Normal file
@ -0,0 +1,27 @@
|
||||
from tests.base_test import BaseTest
|
||||
|
||||
import json
|
||||
from communicator.models import Sample
|
||||
from communicator import db
|
||||
|
||||
|
||||
class TestSampleEndpoint(BaseTest):
|
||||
|
||||
def test_create_sample(self):
|
||||
|
||||
sample_json = {"barcode": "000000111-202009091449-4321",
|
||||
"location": "4321",
|
||||
"date": "2020-09-09T14:49:00+0000",
|
||||
"student_id": "000000111"}
|
||||
|
||||
# Test add sample
|
||||
samples = db.session.query(Sample).all()
|
||||
self.assertEquals(0, len(samples))
|
||||
|
||||
rv = self.app.post('/v1.0/sample',
|
||||
content_type="application/json",
|
||||
data=json.dumps(sample_json))
|
||||
|
||||
samples = db.session.query(Sample).all()
|
||||
self.assertEquals(1, len(samples))
|
||||
|
Loading…
x
Reference in New Issue
Block a user