mirror of
https://github.com/sartography/uva-covid19-testing-communicator.git
synced 2025-02-23 12:28:26 +00:00
Adding an endpoint to create new sample records, and an index page
for displaying all samples in a paginated view.
This commit is contained in:
parent
ab07ea783d
commit
f0132df1e5
3
Pipfile
3
Pipfile
@ -35,6 +35,9 @@ google-cloud-firestore = "*"
|
|||||||
globus-sdk = "*"
|
globus-sdk = "*"
|
||||||
gunicorn = "*"
|
gunicorn = "*"
|
||||||
twilio = "*"
|
twilio = "*"
|
||||||
|
flask-paginate = "*"
|
||||||
|
flask-assets = "*"
|
||||||
|
pyscss = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.8"
|
python_version = "3.8"
|
||||||
|
61
Pipfile.lock
generated
61
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "ab12e64675592a61635bac5221044eb2d23bc232b4db303cae88e5cb8f4f2251"
|
"sha256": "490cbcef70664a97232f019f8c20c17cea8035fac4904aa2e997c2f288832b87"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@ -248,6 +248,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==1.5.6"
|
"version": "==1.5.6"
|
||||||
},
|
},
|
||||||
|
"flask-assets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
|
||||||
|
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==2.0"
|
||||||
|
},
|
||||||
"flask-babel": {
|
"flask-babel": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468",
|
"sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468",
|
||||||
@ -293,6 +301,13 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.5.3"
|
"version": "==2.5.3"
|
||||||
},
|
},
|
||||||
|
"flask-paginate": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4d5c746e4f7b639a9bb72fac0c2452d58e5297b88d6ecda3340153a59453d581"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.7.0"
|
||||||
|
},
|
||||||
"flask-restful": {
|
"flask-restful": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915",
|
"sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915",
|
||||||
@ -429,6 +444,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==2.10"
|
"version": "==2.10"
|
||||||
},
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
|
||||||
|
"sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
"inflection": {
|
"inflection": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||||
@ -668,6 +691,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.17.3"
|
"version": "==0.17.3"
|
||||||
},
|
},
|
||||||
|
"pyscss": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==1.3.7"
|
||||||
|
},
|
||||||
"python-box": {
|
"python-box": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:b7a6f3edd2f71e2475d93163b6465f637a2714b155acafef17408b06e55282b3",
|
"sha256:b7a6f3edd2f71e2475d93163b6465f637a2714b155acafef17408b06e55282b3",
|
||||||
@ -826,6 +856,13 @@
|
|||||||
],
|
],
|
||||||
"version": "==1.4.4"
|
"version": "==1.4.4"
|
||||||
},
|
},
|
||||||
|
"webassets": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
|
||||||
|
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
|
||||||
|
],
|
||||||
|
"version": "==2.0"
|
||||||
|
},
|
||||||
"webob": {
|
"webob": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b",
|
"sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b",
|
||||||
@ -854,6 +891,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": {
|
||||||
@ -904,6 +948,14 @@
|
|||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==5.3"
|
"version": "==5.3"
|
||||||
},
|
},
|
||||||
|
"importlib-metadata": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
|
||||||
|
"sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
|
||||||
|
],
|
||||||
|
"markers": "python_version < '3.8'",
|
||||||
|
"version": "==2.0.0"
|
||||||
|
},
|
||||||
"iniconfig": {
|
"iniconfig": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
"sha256:80cf40c597eb564e86346103f609d74efce0f6b4d4f30ec8ce9e2c26411ba437",
|
||||||
@ -975,6 +1027,13 @@
|
|||||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||||
],
|
],
|
||||||
"version": "==0.10.1"
|
"version": "==0.10.1"
|
||||||
|
},
|
||||||
|
"zipp": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||||
|
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||||
|
],
|
||||||
|
"version": "==3.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,12 +4,15 @@ import os
|
|||||||
import connexion
|
import connexion
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
from flask import render_template, request
|
from flask import render_template, request
|
||||||
|
from flask_assets import Environment
|
||||||
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
|
||||||
from flask_migrate import Migrate
|
from flask_migrate import Migrate
|
||||||
|
from flask_paginate import Pagination, get_page_parameter
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from sentry_sdk.integrations.flask import FlaskIntegration
|
from sentry_sdk.integrations.flask import FlaskIntegration
|
||||||
|
from webassets import Bundle
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
@ -34,6 +37,27 @@ db = SQLAlchemy(app)
|
|||||||
migrate = Migrate(app, db)
|
migrate = Migrate(app, db)
|
||||||
ma = Marshmallow(app)
|
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 models
|
||||||
from communicator import api
|
from communicator import api
|
||||||
from communicator import forms
|
from communicator import forms
|
||||||
@ -58,18 +82,28 @@ BASE_HREF = app.config['APPLICATION_ROOT'].strip('/')
|
|||||||
|
|
||||||
@app.route('/', methods=['GET', 'POST'])
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
def index():
|
def index():
|
||||||
|
from communicator.models import Sample
|
||||||
|
from communicator.tables import SampleTable
|
||||||
# display results
|
# 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(
|
return render_template(
|
||||||
'index.html',
|
'index.html',
|
||||||
|
table=table,
|
||||||
|
pagination=pagination,
|
||||||
base_href=BASE_HREF
|
base_href=BASE_HREF
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/invitation', methods=['GET', 'POST'])
|
@app.route('/invitation', methods=['GET', 'POST'])
|
||||||
def send_invitation():
|
def send_invitation():
|
||||||
form = forms.InvitationForm(request.form)
|
form = forms.InvitationForm(request.form)
|
||||||
action = BASE_HREF + "/invitation"
|
action = BASE_HREF + "/invitation"
|
||||||
title = "Send invitation to students"
|
title = "Send invitation to students"
|
||||||
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
from communicator.services.notification_service import NotificationService
|
from communicator.services.notification_service import NotificationService
|
||||||
with NotificationService(app) as ns:
|
with NotificationService(app) as ns:
|
||||||
|
@ -6,8 +6,6 @@ info:
|
|||||||
name: MIT
|
name: MIT
|
||||||
servers:
|
servers:
|
||||||
- url: http://localhost:5000/v1.0
|
- url: http://localhost:5000/v1.0
|
||||||
security:
|
|
||||||
- jwt: ['secret']
|
|
||||||
paths:
|
paths:
|
||||||
/status:
|
/status:
|
||||||
get:
|
get:
|
||||||
@ -45,10 +43,44 @@ paths:
|
|||||||
text/plain:
|
text/plain:
|
||||||
schema:
|
schema:
|
||||||
type: string
|
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"
|
||||||
components:
|
components:
|
||||||
schemas:
|
schemas:
|
||||||
Status:
|
Status:
|
||||||
properties:
|
properties:
|
||||||
status:
|
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,6 +12,13 @@ from communicator.services.sample_service import SampleService
|
|||||||
def status():
|
def status():
|
||||||
return {"status":"good"}
|
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 update_data():
|
def update_data():
|
||||||
"""Updates the database based on local files placed by IVY and records
|
"""Updates the database based on local files placed by IVY and records
|
||||||
|
@ -14,7 +14,6 @@ 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)
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
12
communicator/tables.py
Normal file
12
communicator/tables.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
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?')
|
@ -6,9 +6,18 @@
|
|||||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://use.typekit.net/kwp6dli.css">
|
<link rel="stylesheet" href="https://use.typekit.net/kwp6dli.css">
|
||||||
<link rel="shortcut icon" href="{{ base_href + url_for('static', filename='favicon.ico') }}">
|
<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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h2>UVA Be Safe Communicator</h2>
|
<h2>UVA Be Safe Communicator</h2>
|
||||||
<p>Just making sure this works ok.</p>
|
|
||||||
|
<h3>Records to be processed</h3>
|
||||||
|
{{ pagination.info }}
|
||||||
|
{{ pagination.links }}
|
||||||
|
{{ table }}
|
||||||
|
{{ pagination.links }}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
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