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 = "*"
|
||||
gunicorn = "*"
|
||||
twilio = "*"
|
||||
flask-paginate = "*"
|
||||
flask-assets = "*"
|
||||
pyscss = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.8"
|
||||
|
61
Pipfile.lock
generated
61
Pipfile.lock
generated
@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "ab12e64675592a61635bac5221044eb2d23bc232b4db303cae88e5cb8f4f2251"
|
||||
"sha256": "490cbcef70664a97232f019f8c20c17cea8035fac4904aa2e997c2f288832b87"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@ -248,6 +248,14 @@
|
||||
"index": "pypi",
|
||||
"version": "==1.5.6"
|
||||
},
|
||||
"flask-assets": {
|
||||
"hashes": [
|
||||
"sha256:1dfdea35e40744d46aada72831f7613d67bf38e8b20ccaaa9e91fdc37aa3b8c2",
|
||||
"sha256:2845bd3b479be9db8556801e7ebc2746ce2d9edb4e7b64a1c786ecbfc1e5867b"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.0"
|
||||
},
|
||||
"flask-babel": {
|
||||
"hashes": [
|
||||
"sha256:e6820a052a8d344e178cdd36dd4bb8aea09b4bda3d5f9fa9f008df2c7f2f5468",
|
||||
@ -293,6 +301,13 @@
|
||||
"index": "pypi",
|
||||
"version": "==2.5.3"
|
||||
},
|
||||
"flask-paginate": {
|
||||
"hashes": [
|
||||
"sha256:4d5c746e4f7b639a9bb72fac0c2452d58e5297b88d6ecda3340153a59453d581"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"flask-restful": {
|
||||
"hashes": [
|
||||
"sha256:5ea9a5991abf2cb69b4aac19793faac6c032300505b325687d7c305ffaa76915",
|
||||
@ -429,6 +444,14 @@
|
||||
],
|
||||
"version": "==2.10"
|
||||
},
|
||||
"importlib-metadata": {
|
||||
"hashes": [
|
||||
"sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
|
||||
"sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
|
||||
],
|
||||
"markers": "python_version < '3.8'",
|
||||
"version": "==2.0.0"
|
||||
},
|
||||
"inflection": {
|
||||
"hashes": [
|
||||
"sha256:1a29730d366e996aaacffb2f1f1cb9593dc38e2ddd30c91250c6dde09ea9b417",
|
||||
@ -668,6 +691,13 @@
|
||||
],
|
||||
"version": "==0.17.3"
|
||||
},
|
||||
"pyscss": {
|
||||
"hashes": [
|
||||
"sha256:f1df571569021a23941a538eb154405dde80bed35dc1ea7c5f3e18e0144746bf"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.7"
|
||||
},
|
||||
"python-box": {
|
||||
"hashes": [
|
||||
"sha256:b7a6f3edd2f71e2475d93163b6465f637a2714b155acafef17408b06e55282b3",
|
||||
@ -826,6 +856,13 @@
|
||||
],
|
||||
"version": "==1.4.4"
|
||||
},
|
||||
"webassets": {
|
||||
"hashes": [
|
||||
"sha256:167132337677c8cedc9705090f6d48da3fb262c8e0b2773b29f3352f050181cd",
|
||||
"sha256:a31a55147752ba1b3dc07dee0ad8c8efff274464e08bbdb88c1fd59ffd552724"
|
||||
],
|
||||
"version": "==2.0"
|
||||
},
|
||||
"webob": {
|
||||
"hashes": [
|
||||
"sha256:a3c89a8e9ba0aeb17382836cdb73c516d0ecf6630ec40ec28288f3ed459ce87b",
|
||||
@ -854,6 +891,13 @@
|
||||
"sha256:81195de0ac94fbc8368abbaf9197b88c4f3ffd6c2719b5bf5fc9da744f3d829c"
|
||||
],
|
||||
"version": "==2.3.3"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
@ -904,6 +948,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",
|
||||
@ -975,6 +1027,13 @@
|
||||
"sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"
|
||||
],
|
||||
"version": "==0.10.1"
|
||||
},
|
||||
"zipp": {
|
||||
"hashes": [
|
||||
"sha256:43f4fa8d8bb313e65d8323a3952ef8756bf40f9a5c3ea7334be23ee4ec8278b6",
|
||||
"sha256:b52f22895f4cfce194bc8172f3819ee8de7540aa6d873535a8668b730b8b411f"
|
||||
],
|
||||
"version": "==3.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,15 @@ import os
|
||||
import connexion
|
||||
import sentry_sdk
|
||||
from flask import render_template, request
|
||||
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,6 +37,27 @@ 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
|
||||
@ -58,18 +82,28 @@ BASE_HREF = app.config['APPLICATION_ROOT'].strip('/')
|
||||
|
||||
@app.route('/', methods=['GET', 'POST'])
|
||||
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():
|
||||
form = forms.InvitationForm(request.form)
|
||||
action = BASE_HREF + "/invitation"
|
||||
title = "Send invitation to students"
|
||||
|
||||
|
||||
if request.method == 'POST':
|
||||
from communicator.services.notification_service import NotificationService
|
||||
with NotificationService(app) as ns:
|
||||
|
@ -6,8 +6,6 @@ info:
|
||||
name: MIT
|
||||
servers:
|
||||
- url: http://localhost:5000/v1.0
|
||||
security:
|
||||
- jwt: ['secret']
|
||||
paths:
|
||||
/status:
|
||||
get:
|
||||
@ -45,10 +43,44 @@ 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"
|
||||
components:
|
||||
schemas:
|
||||
Status:
|
||||
properties:
|
||||
status:
|
||||
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():
|
||||
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():
|
||||
"""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?
|
||||
email_notified = db.Column(db.Boolean, default=False)
|
||||
text_notified = db.Column(db.Boolean, default=False)
|
||||
ivy_file_name = db.Column(db.String)
|
||||
notifications = db.relationship(Notification, back_populates="sample",
|
||||
cascade="all, delete, delete-orphan",
|
||||
order_by=Notification.date)
|
||||
|
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 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>
|
||||
<p>Just making sure this works ok.</p>
|
||||
|
||||
<h3>Records to be processed</h3>
|
||||
{{ pagination.info }}
|
||||
{{ pagination.links }}
|
||||
{{ table }}
|
||||
{{ pagination.links }}
|
||||
</body>
|
||||
</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