Prep for pull request

This commit is contained in:
Nile Walker 2020-12-18 10:12:15 -05:00
parent 4cb3fec40b
commit 7b414b1f15
6 changed files with 169 additions and 89 deletions

View File

@ -1,3 +1,4 @@
import csv import csv
import io import io
import json import json
@ -5,6 +6,7 @@ import json
import logging import logging
import os import os
from datetime import datetime from datetime import datetime
from datetime import date
from functools import wraps from functools import wraps
import connexion import connexion
@ -23,8 +25,6 @@ from webassets import Bundle
from flask_executor import Executor from flask_executor import Executor
import numpy as np import numpy as np
import matplotlib.pyplot as plt
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
# API, fully defined in api.yml # API, fully defined in api.yml
@ -49,6 +49,7 @@ else:
def render_errors(exception): def render_errors(exception):
return Response(json.dumps({"error": str(exception)}), status=500, mimetype="application/json") return Response(json.dumps({"error": str(exception)}), status=500, mimetype="application/json")
connexion_app.add_error_handler(Exception, render_errors) connexion_app.add_error_handler(Exception, render_errors)
@ -80,12 +81,10 @@ scss = Bundle(
output='argon.css' output='argon.css'
) )
assets.register('app_scss', scss) assets.register('app_scss', scss)
import random
from communicator import models
from communicator import api
from communicator import forms from communicator import forms
import random from communicator import api
# from communicator import scheduler from communicator import models
connexion_app.add_api('api.yml', base_path='/v1.0') connexion_app.add_api('api.yml', base_path='/v1.0')
@ -105,7 +104,6 @@ if app.config['ENABLE_SENTRY']:
# HTML Pages # HTML Pages
BASE_HREF = app.config['APPLICATION_ROOT'].strip('/') BASE_HREF = app.config['APPLICATION_ROOT'].strip('/')
def superuser(f): def superuser(f):
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
@ -118,6 +116,11 @@ def superuser(f):
return f(*args, **kwargs) return f(*args, **kwargs)
return decorated_function return decorated_function
@app.errorhandler(404)
@superuser
def page_not_found(e):
# note that we set the 404 status explicitly
return render_template('pages/404.html')
@app.route('/', methods=['GET', 'POST']) @app.route('/', methods=['GET', 'POST'])
@superuser @superuser
@ -130,17 +133,14 @@ def index():
form = forms.SearchForm(request.form) form = forms.SearchForm(request.form)
action = BASE_HREF + "/" action = BASE_HREF + "/"
samples = db.session.query(Sample).order_by(Sample.date.desc()) samples = db.session.query(Sample).order_by(Sample.date.desc())
if request.method == "POST" or request.args.get('cancel') == 'true': if request.method == "POST" or request.args.get('cancel') == 'true':
session["index_filter"] = {} # Clear out the session if it is invalid. session["index_filter"] = {} # Clear out the session if it is invalid.
if form.validate(): if form.validate():
session["index_filter"] = {} session["index_filter"] = {}
if form.startDate.data: if form.startDate.data:
session["index_filter"]["start_date"] = form.startDate.data session["index_filter"]["start_date"] = form.startDate.data
else:
from datetime import date
session["index_filter"]["start_date"] = date.today()
if form.endDate.data: if form.endDate.data:
session["index_filter"]["end_date"] = form.endDate.data session["index_filter"]["end_date"] = form.endDate.data
if form.studentId.data: if form.studentId.data:
@ -150,30 +150,48 @@ def index():
if form.email.data: if form.email.data:
session["index_filter"]["email"] = form.email.data session["index_filter"]["email"] = form.email.data
if form.download.data: if form.download.data:
download = True download = True
# # Store previous form submission settings in the session, so they are preseved through pagination. # # Store previous form submission settings in the session, so they are preseved through pagination.
filtered_samples = samples
if "index_filter" in session: if "index_filter" in session:
filters = session["index_filter"] filters = session["index_filter"]
try: try:
if "start_date" in filters: if "start_date" in filters:
samples = samples.filter(Sample.date >= filters["start_date"]) filtered_samples = filtered_samples.filter(Sample.date >= filters["start_date"])
else:
filtered_samples = filtered_samples.filter(Sample.date >= date.today())
if "end_date" in filters: if "end_date" in filters:
samples = samples.filter(Sample.date <= filters["end_date"]) filtered_samples = filtered_samples.filter(Sample.date <= filters["end_date"])
if "student_id" in filters: if "student_id" in filters:
samples = samples.filter( filtered_samples = filtered_samples.filter(
Sample.student_id.in_(filters["student_id"].split())) Sample.student_id.in_(filters["student_id"].split()))
if "location" in filters: if "location" in filters:
samples = samples.filter( filtered_samples = filtered_samples.filter(
Sample.location.in_(filters["location"].split())) Sample.location.in_(filters["location"].split()))
if "email" in filters: if "email" in filters:
samples = samples.filter( filtered_samples = filtered_samples.filter(
Sample.email.ilike(filters["email"] + "%")) Sample.email.ilike(filters["email"] + "%"))
except Exception as e: except Exception as e:
logging.error( logging.error("Encountered an error building filters, so clearing. " + str(e))
"Encountered an error building filters, so clearing. " + e)
session["index_filter"] = {} session["index_filter"] = {}
else:
# Default to Todays Results
filtered_samples = filtered_samples.filter(Sample.date >= date.today())
############# Daily Total #######################
from datetime import date, timedelta
stats = dict()
stats["today"] = samples.filter(Sample.date >= date.today()).count()
############# Last 2 Week Average ###############
today = date.today()
counts = [] # ! Could be calculated in a single pass since data is sorted
for i in range(14):
days_back_start = timedelta(i)
days_back_stop = timedelta(i + 1)
temp = samples.filter(Sample.date <= today - days_back_start)
temp = temp.filter(Sample.date >= today - days_back_stop)
counts.append(temp.count())
stats["weeks"] = sum(counts)/len(counts)
#################################################
# display results # display results
if download: if download:
csv = __make_csv(samples) csv = __make_csv(samples)
@ -186,60 +204,86 @@ def index():
table = SampleTable(samples.paginate(page, 10, error_out=False).items) table = SampleTable(samples.paginate(page, 10, error_out=False).items)
chart_data = {"datasets": []}
# Get Active Locations Info # Get Active Locations Info
active_stations = ["10", "20", "30", "40", "50", "60"] active_stations = ["10", "20", "30", "40", "50", "60"]
# https://stackoverflow.com/questions/19442224/getting-information-for-bins-in-matplotlib-histogram-function
# Seperate Data by location and station
# Seperate Data
location_data = dict() location_data = dict()
sample_times = dict() sample_times = dict()
active_stations = set()
for entry in samples: for entry in filtered_samples:
loc_code = str(entry.location)[:2] loc_code = str(entry.location)[:2]
stat_code= str(entry.location)[2:]
active_stations.add(stat_code)
if loc_code not in location_data: if loc_code not in location_data:
location_data[loc_code] = [entry] location_data[loc_code] = [entry]
sample_times[loc_code] = [entry.date.timestamp()] sample_times[loc_code] = [entry.date.timestamp()]
logging.info(entry.date)
else: else:
location_data[loc_code].append(entry) location_data[loc_code].append(entry)
sample_times[loc_code].append(entry.date.timestamp()) sample_times[loc_code].append(entry.date.timestamp())
# Analysis # Analysis
i = 0 station_charts = []
location_chart = {"datasets": []}
for loc_code in location_data.keys(): for loc_code in location_data.keys():
data_dict = dict({ #################################################
############# Build histogram ###################
color = [hash(loc_code), 128, (hash(loc_code) % 256 + 128) % 256]
single_hist = dict({
"label": loc_code, "label": loc_code,
"borderColor": f'rgba(255,{i*50},{i*20},.7)', "borderColor": f'rgba({color[0]},{color[1]},{color[2]},.7)',
"pointBorderColor": f'rgba(255,{i*50},{i*20},1)', "pointBorderColor": f'rgba({color[0]},{color[1]},{color[2]},1)',
"borderWidth": 10, "borderWidth": 8,
"data": [], "data": [],
}) })
# https://stackoverflow.com/questions/19442224/getting-information-for-bins-in-matplotlib-histogram-function
hist, bin_edges = np.histogram(np.array(sample_times[loc_code]))#, dtype = np.int64)) hist, bin_edges = np.histogram(np.array(sample_times[loc_code]))
#bin_edges = [datetime.fromtimestamp(date) for date in bin_edges] bins = [bin_edges[i]+(bin_edges[i+1]-bin_edges[i]) /
bins = [bin_edges[i]+(bin_edges[i+1]-bin_edges[i])/2 for i in range(len(bin_edges)-1)] 2 for i in range(len(bin_edges)-1)]
for cnt, time in zip(hist, bins):
for cnt, date in zip(hist,bins): single_hist["data"].append({
data_dict["data"].append({ "x": datetime.utcfromtimestamp(time), "y": int(cnt)
"x": datetime.utcfromtimestamp(date), "y": int(cnt)
}) })
location_chart["datasets"].append(single_hist)
###### Build Rolling Averaging Graph ##############
chart_data["datasets"].append(data_dict) #################################################
i += 1 ############## Build Station Graph ##############
# Check for Unresponsive station_lines = []
for loc_code in active_stations: # Read Data by station
if loc_code not in location_data: i = 0
chart_data["datasets"].append({ for stat_code in active_stations:
"label": loc_code, filtered_entries = [_entry for _entry in location_data[loc_code] if str(_entry.location)[2:] == stat_code] # ! Inefficient but works for rn
"borderColor": f'rgba(128,128,128,.7)', if len(filtered_entries) == 0: continue
"pointBorderColor": f'rgba(128,128,128,1)', station_line = {"label": stat_code,
"borderWidth": 10, "borderColor": f'rgba(50,255,255,.7)',
"data": [{ "pointBorderColor": f'rgba(50,255,255,1)',
"x": session["index_filter"]["start_date"], "y": i "borderWidth": 10,
}, ], "data": [
}) {"x": filtered_entries[0].date, "y": i}, {"x": filtered_entries[-1].date, "y": i},
i += 1 ],
}
i += 1
station_lines.append(station_line)
station_charts.append({"datasets": station_lines, "labels" : []})
#################################################
# # Check for Unresponsive
# for loc_code in active_stations:
# if loc_code not in location_data:
# location_dict["datasets"].append({
# "label": loc_code,
# "borderColor": f'rgba(128,128,128,.7)',
# "pointBorderColor": f'rgba(128,128,128,1)',
# "borderWidth": 10,
# "data": [{
# "x": session["index_filter"]["start_date"], "y": i
# }, ],
# })
# i += 1
return render_template('layouts/default.html', return render_template('layouts/default.html',
base_href=BASE_HREF, base_href=BASE_HREF,
content=render_template( content=render_template(
@ -248,8 +292,9 @@ def index():
table=table, table=table,
action=action, action=action,
pagination=pagination, pagination=pagination,
description_map={}, location_data=location_chart,
chart_data=chart_data station_data=station_charts,
stats = stats
)) ))

View File

@ -1,15 +1,12 @@
<!-- Core -->
<!-- Core --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script> integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script> <script src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
<!-- Optional JS --> <!-- Optional JS -->
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/moment.js/2.13.0/moment.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.bundle.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.4.0/Chart.bundle.js"></script>
<!-- Bootstrap Date-Picker Plugin --> <!-- Bootstrap Date-Picker Plugin -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js"></script> <script type="text/javascript"
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.min.css"/> src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/js/bootstrap-datepicker.min.js"></script>
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-datepicker/1.9.0/css/bootstrap-datepicker.min.css" />

View File

@ -80,7 +80,7 @@
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="/"> <a class="nav-link" href="/">
<i class="ni ni-tv-2 text-primary"></i> Daily <i class="ni ni-tv-2 text-primary"></i> Dashboard
</a> </a>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -107,11 +107,11 @@
<ul class="navbar-nav mb-md-3"> <ul class="navbar-nav mb-md-3">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="https://github.com/app-generator/flask-argon-dashboard"> <a class="nav-link" href="https://github.com/sartography/uva-covid19-testing-communicator">
<i class="ni ni-ui-04"></i> Source Code <i class="ni ni-ui-04"></i> Source Code
</a> </a>
</li> </li>
<!--
<li class="nav-item"> <li class="nav-item">
<a class="nav-link" href="https://appseed.us/admin-dashboards/flask-dashboard-argon"> <a class="nav-link" href="https://appseed.us/admin-dashboards/flask-dashboard-argon">
<i class="ni ni-book-bookmark"></i> App Information <i class="ni ni-book-bookmark"></i> App Information
@ -134,7 +134,7 @@
<a class="nav-link" href="https://demos.creative-tim.com/argon-dashboard/docs/getting-started/overview.html"> <a class="nav-link" href="https://demos.creative-tim.com/argon-dashboard/docs/getting-started/overview.html">
<i class="ni ni-books"></i> Design Docs <i class="ni ni-books"></i> Design Docs
</a> </a>
</li> </li> -->
</ul> </ul>
</div> </div>

View File

@ -11,7 +11,7 @@
<div class="col"> <div class="col">
<h5 class="card-title text-uppercase text-muted mb-0">Total Samples Today</h5> <h5 class="card-title text-uppercase text-muted mb-0">Total Samples Today</h5>
<span id="stats_traffic" class="h2 font-weight-bold mb-0">N?A</span> <span id="stats_traffic" class="h2 font-weight-bold mb-0">{{stats.today}}</span>
</div> </div>
<div class="col-auto"> <div class="col-auto">
@ -22,7 +22,7 @@
</div> </div>
<p class="mt-3 mb-0 text-muted text-sm"> <p class="mt-3 mb-0 text-muted text-sm">
<span class="text-success mr-2"><i class="fa fa-arrow-up"></i> 3.48%</span> <span class="text-success mr-2"><i class="fa fa-arrow-up"></i> 3.48%</span>
<span class="text-nowrap">Since last month</span> <span class="text-nowrap">Since Yesterday</span>
</p> </p>
</div> </div>
</div> </div>
@ -32,9 +32,9 @@
<div class="card-body"> <div class="card-body">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h5 class="card-title text-uppercase text-muted mb-0">New users</h5> <h5 class="card-title text-uppercase text-muted mb-0">2 Week Average</h5>
<span id="stats_users" class="h2 font-weight-bold mb-0">N/A</span> <span id="stats_users" class="h2 font-weight-bold mb-0">{{stats.weeks}}</span>
</div> </div>
<div class="col-auto"> <div class="col-auto">

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Be SAFE Notification System</title> <title>Be SAFE Notification System</title>
<base href="/"> <base href="/">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

View File

@ -9,7 +9,7 @@
<a class="h4 mb-0 text-white text-uppercase d-none d-lg-inline-block" href="./index.html">UVA Communicator <a class="h4 mb-0 text-white text-uppercase d-none d-lg-inline-block" href="./index.html">UVA Communicator
Dashboard</a> Dashboard</a>
<!-- Form --> <!-- Form -->
<form class="navbar-search navbar-search-dark form-inline mr-3 d-none d-md-flex ml-lg-auto"> <!-- <form class="navbar-search navbar-search-dark form-inline mr-3 d-none d-md-flex ml-lg-auto">
<div class="form-group mb-0"> <div class="form-group mb-0">
<div class="input-group input-group-alternative"> <div class="input-group input-group-alternative">
<div class="input-group-prepend"> <div class="input-group-prepend">
@ -18,7 +18,7 @@
<input class="form-control" placeholder="Search" type="text"> <input class="form-control" placeholder="Search" type="text">
</div> </div>
</div> </div>
</form> </form> -->
</div> </div>
</nav> </nav>
@ -35,7 +35,7 @@
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<h6 class="text-uppercase text-light ls-1 mb-1">Overview</h6> <h6 class="text-uppercase text-light ls-1 mb-1">Overview</h6>
<h2 class="text-white mb-0">Location Activity</h2> <h2 id="chart-title" class="text-white mb-0">Location Activity</h2>
</div> </div>
</div> </div>
</div> </div>
@ -51,13 +51,16 @@
<div class="col-xl-4"> <div class="col-xl-4">
<div class="card shadow"> <div class="card shadow">
<form action="{{ action }}" method="post"> <form action="{{ action }}" method="post" id="pageForm">
{{ form.csrf_token() }} {{ form.csrf_token() }}
<div class="card-header border-0"> <div class="card-header border-0">
<div class="row align-items-center"> <div class="row align-items-center">
<div class="col"> <div class="col">
<h3 class="mb-0">Search</h3> <h3 class="mb-0">Search</h3>
</div> </div>
<div class="col text-right">
<button class="btn btn-sm btn-primary">Search Today</button>
</div>
<div class="col text-right"> <div class="col text-right">
<button type="submit" class="btn btn-sm btn-primary">Run Search</button> <button type="submit" class="btn btn-sm btn-primary">Run Search</button>
</div> </div>
@ -81,7 +84,7 @@
<input name="{{field.name}}" /> <input name="{{field.name}}" />
{%else%} {%else%}
<div class="form-field-input">{{ field }}</div> <div class="form-field-input">{{ field }}</div>
<div class="form-field-help">{{ description_map[field.name] }}</div>
{% for error in field.errors %} {% for error in field.errors %}
<div class="form-field-error">{{ error }}</div> <div class="form-field-error">{{ error }}</div>
{% endfor %} {% endfor %}
@ -137,14 +140,17 @@
</div> </div>
</div> </div>
<script> <script>
var data = JSON.parse('{{ chart_data | tojson | safe}}'); var location_data = JSON.parse('{{ location_data | tojson | safe}}');
var station_data = JSON.parse('{{ station_data | tojson | safe}}');
var ctx = document.getElementById('chart-sales').getContext('2d'); var ctx = document.getElementById('chart-sales').getContext('2d');
var timeFormat = 'YYYY-MM-DD h:mm:ss.SSS'; var timeFormat = 'YYYY-MM-DD h:mm:ss.SSS';
//////////////////
var chart = new Chart(ctx, { var chart = new Chart(ctx, {
// The type of chart we want to create // The type of chart we want to create
type: 'line', type: 'line',
data: data, data: location_data,
// Configuration options go here // Configuration options go here
options: { options: {
@ -156,6 +162,7 @@
legend: { legend: {
display: true, display: true,
position: "right", position: "right",
onClick: location_legend,
labels: { labels: {
usePointStyle: true, usePointStyle: true,
fontColor: '#FFFFFF', fontColor: '#FFFFFF',
@ -185,4 +192,34 @@
} }
} }
); );
function station_legend(e, legendItem) {
chart.config.data = location_data;
chart.config.options.legend.onClick= location_legend;
$('#chart-title').text("Location Activity");
chart.update();
};
function location_legend(e, legendItem) {
$('#chart-title').text("Station Activity @ " + chart.data.datasets[legendItem.datasetIndex].label);
chart.config.data = station_data[legendItem.datasetIndex];
chart.config.options.legend.onClick = station_legend;
chart.update();
};
</script>
</form>
<script>
var auto_refresh = setInterval(
function()
{
submitform();
}, 30000);
function submitform()
{
$("#pageForm").submit();
}
</script> </script>