diff --git a/Pipfile b/Pipfile index 104c26d..d3f355e 100755 --- a/Pipfile +++ b/Pipfile @@ -14,10 +14,10 @@ certifi = "==2020.4.5.2" cffi = "==1.14.0" chardet = "==3.0.4" click = "==7.1.2" -cryptography = "==2.9.2" +cryptography = ">=3.2.1" cssutils = "==1.0.2" diff-match-patch = "==20181111" -httplib2 = "==0.18.1" +httplib2 = ">=0.18.1" idna = "==2.9" itsdangerous = "==0.24" kombu = "==4.6.10" @@ -33,7 +33,7 @@ python-dateutil = "==2.8.1" pytz = "==2017.2" qrcode = "==6.1" delta = {git = "https://github.com/forgeworks/quill-delta-python.git"} -requests = "==2.23.0" +requests = ">=2.23.0" retrying = "==1.3.3" six = "==1.15.0" twilio = "==5.7.0" @@ -41,24 +41,30 @@ urllib3 = "==1.25.9" vine = "==1.3.0" Cycler = "==0.10.0" Cython = "==0.29.20" -Flask = "==0.12.2" +Flask = "==1.1.2" +flask-caching = "==1.9.0" Flask-Login = "==0.4.1" Flask-Mail = "==0.9.1" Flask-QRcode = "==3.0.0" Flask-SQLAlchemy = "==2.3.2" -Flask-WTF = "==0.14.2" -Jinja2 = "==2.9.6" +Flask-WTF = "==0.14.3" +Jinja2 = ">=2.10.1" MarkupSafe = "==1.1.1" Pillow = "==7.1.2" PyJWT = "==1.7.1" PySocks = "==1.7.1" -SQLAlchemy = "==1.3.17" +SQLAlchemy = ">=1.3.0" SQLAlchemy-Utils = "==0.36.5" -Werkzeug = "==0.12.2" -WTForms = "==2.3.1" +Werkzeug = ">=1.0.1" +WTForms = ">=2.3.1" simplekv = "*" -wtforms-sqlalchemy = "*" -wtforms-alchemy = "*" +scipy = "*" +WTForms-SQLAlchemy = "*" +WTForms-Alchemy = "*" +pyOpenSSL = ">=19.1.0" +importlib-metadata = "==2.0.0" +flask-migrate = "*" +email-validator = "*" [requires] python_version = "3.6" diff --git a/run.py b/app.py similarity index 100% rename from run.py rename to app.py diff --git a/assets/fonts/OpenSans-Bold.woff b/assets/fonts/OpenSans-Bold.woff new file mode 100644 index 0000000..9cad9cd Binary files /dev/null and b/assets/fonts/OpenSans-Bold.woff differ diff --git a/assets/fonts/OpenSans.woff b/assets/fonts/OpenSans.woff new file mode 100644 index 0000000..f6b5751 Binary files /dev/null and b/assets/fonts/OpenSans.woff differ diff --git a/assets/images/medinfo_icon.png b/assets/images/medinfo_icon.png new file mode 100644 index 0000000..3c311a6 Binary files /dev/null and b/assets/images/medinfo_icon.png differ diff --git a/assets/main.css b/assets/main.css index 3424815..74bed8f 100755 --- a/assets/main.css +++ b/assets/main.css @@ -1,6 +1,7 @@ @font-face{ font-family: Neue_Helvetica; src:url("/assets/fonts/neue_helvetica_reg.woff2"); + font-display: swap; } body{ font-family: Neue_Helvetica; @@ -64,7 +65,7 @@ select { .img-circle { horizontal-align:middle; border-radius:50%; - height:150px; + height:100px; } .col-lg-4 { @@ -414,4 +415,4 @@ form tr { border-style:none none none solid; margin-left:120px; padding:20px; -} \ No newline at end of file +} diff --git a/cleanup_sr.py b/cleanup_sr.py new file mode 100644 index 0000000..f49ad15 --- /dev/null +++ b/cleanup_sr.py @@ -0,0 +1,32 @@ +from medtracker import * +from sqlalchemy import func, desc +import pandas as pd + +dbobj = models.SurveyResponse.query.all() + +objects = pd.DataFrame([i.to_dict() for i in dbobj]) +delete_count = 0 +to_delete = set() +for name,group in objects.groupby("session_id"): + if len(group)>1: + group = group.sort_values("start_time",ascending=False) + print(group) + successful = group[(group["completed"]==1)|(group["exited"]==1)] + if len(successful)>0: + keep_id = group[(group["completed"]==1)|(group["exited"]==1)].iloc[0,].name + print("keeping ID:", keep_id) + else: + keep_id = None + for ix,row in group.iterrows(): + if row.name==keep_id: continue + print("deleting ID: ",row.name) + to_delete.add(row.name) + delete_count += 1 + else: + row = group.iloc[0,] + if (row["completed"]==False) & (row["exited"]==False): + print("deleting ID: ",row.name) + to_delete.add(row.name) + delete_count += 1 + +delete_objs = [dbobj[i] for i in to_delete] \ No newline at end of file diff --git a/disable.py b/disable.py new file mode 100644 index 0000000..57fc21d --- /dev/null +++ b/disable.py @@ -0,0 +1,28 @@ +from medtracker import * +from medtracker import database +patients = models.Patient.query.all() +devices = models.Device.query.all() + +kept = 0 +discarded = 0 +patients = models.Patient.query.all() +for p in patients: + if p.deactivate==True: + continue + sr = [s.to_dict() for s in p.surveys] + df = pd.DataFrame(sr) + if len(df)>0: + if any(df.start_time > datetime.date(2020,8,1)): + kept += 1 + p.deactivate = False + db.session.add(p) + else: + discarded += 1 + p.deactivate = True + db.session.add(p) + else: + discarded += 1 + p.deactivate = True + db.session.add(p) +db.session.commit() +print(kept,discarded) diff --git a/medtracker/__init__.py b/medtracker/__init__.py index 67da3b1..2544936 100755 --- a/medtracker/__init__.py +++ b/medtracker/__init__.py @@ -1,5 +1,5 @@ from flask import * -from flask.ext.cache import Cache +from flask_caching import Cache from requests.auth import HTTPBasicAuth import random, string, pytz, sys, random, urllib.parse, datetime, os from werkzeug.utils import secure_filename @@ -8,6 +8,8 @@ from flask_login import login_user, logout_user, current_user from medtracker.config import * from flask_qrcode import QRcode +from flask_migrate import Migrate +from flask_sqlalchemy import SQLAlchemy import twilio.twiml from twilio.rest import TwilioRestClient @@ -18,6 +20,9 @@ from ftplib import FTP_TLS from flask import flash from itsdangerous import URLSafeTimedSerializer + +#from flask_debugtoolbar import DebugToolbarExtension + ts = URLSafeTimedSerializer(flask_secret_key) #Flask init @@ -32,7 +37,20 @@ app.config['SESSION_COOKIE_SECURE'] = False app.config['SECRET_KEY'] = flask_secret_key app.config['WTF_CSRF_ENABLED']=True -app.debug = False +app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600 +#app.config["DEBUG_TB_PROFILER_ENABLED"] = True +#app.config["DEBUG_TB_INTERCEPT_REDIRECTS"] = False +#app.debug = True +#toolbar = DebugToolbarExtension(app) + +app.debug=False + + +#init db +db = SQLAlchemy(app) +db_session = db.session + +migrate = Migrate(app,db,render_as_batch=True) app.config.update( #EMAIL SETTINGS @@ -54,7 +72,6 @@ client = TwilioRestClient(twilio_AccountSID, twilio_AuthToken) auth_combo=(twilio_AccountSID, twilio_AuthToken) -from medtracker.database import db_session # to make sqlalchemy DB calls from medtracker.views import * # web pages from medtracker.triggers import * diff --git a/medtracker/data/nyc_plots.py b/medtracker/data/nyc_plots.py new file mode 100644 index 0000000..ab6df11 --- /dev/null +++ b/medtracker/data/nyc_plots.py @@ -0,0 +1,180 @@ +import pandas as pd +import plotly.graph_objects as go +import numpy as np +import requests +from io import StringIO + +def prepFigure(figure, title): + figure.update_xaxes(tickformat='%a
%b %d', + tick0 = '2020-03-22', + dtick = 7 * 24 * 3600000, + rangeselector=dict( + buttons=list([ + dict(count=7, label="1w", step="day", stepmode="backward"), + dict(count=14, label="2w", step="day", stepmode="backward"), + dict(count=1, label="1m", step="month", stepmode="backward"), + dict(count=1, label="YTD", step="year", stepmode="todate"), + dict(step="all") + ]) + ) + ) + figure.update_layout(title="" + title + "", hovermode="x", + legend_orientation="h", + margin={"r":10,"l":30}) + return figure + +start_date = "2020-03-01" + +#confirmed cases +read_headers = { + "sec-ch-ua": "\"Chromium\";v=\"92\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"92\"", + "sec-ch-ua-mobile": "?0", + "upgrade-insecure-requests": "1" + } +url = "https://static.usafacts.org/public/data/covid-19/covid_confirmed_usafacts.csv" +req = requests.get(url, headers=read_headers) +data = StringIO(req.text) +confirmed_df=pd.read_csv(data) + +nyc_counties_df = confirmed_df.loc[(confirmed_df['State'] == 'NY') & (confirmed_df['countyFIPS'].isin([36005, 36061, 36081, 36085, 36047]))] +nyc_counties_df["County Name"] = [a.strip() for a in nyc_counties_df["County Name"]] +nyc_counties_df=nyc_counties_df.set_index('County Name') +nyc_counties_df=nyc_counties_df.drop(columns=['countyFIPS', 'State', 'StateFIPS']) +nyc_counties_df = nyc_counties_df.transpose() +nyc_counties_df = nyc_counties_df.rename_axis('Date') +nyc_counties_df.index =pd.to_datetime(nyc_counties_df.index) +#print(nyc_counties_df) +nyc_counties_df['Total'] = nyc_counties_df['Bronx County'] + nyc_counties_df['Kings County'] + nyc_counties_df['New York County'] + nyc_counties_df['Queens County'] + nyc_counties_df['Richmond County'] + + +#confirmed deaths +url = "https://static.usafacts.org/public/data/covid-19/covid_deaths_usafacts.csv" +req = requests.get(url, headers=read_headers) +data = StringIO(req.text) +deaths_df = pd.read_csv(data) + +nyc_counties_deaths_df = deaths_df.loc[(deaths_df['State'] == 'NY') & (deaths_df['countyFIPS'].isin([36005, 36061, 36081, 36085, 36047]))] +nyc_counties_deaths_df["County Name"] = [a.strip() for a in nyc_counties_deaths_df["County Name"]] +nyc_counties_deaths_df=nyc_counties_deaths_df.set_index('County Name') +nyc_counties_deaths_df=nyc_counties_deaths_df.drop(columns=['countyFIPS', 'State', 'StateFIPS']) + +nyc_counties_deaths_df = nyc_counties_deaths_df.transpose() +nyc_counties_deaths_df = nyc_counties_deaths_df.rename_axis('Date') +nyc_counties_deaths_df.index = pd.to_datetime(nyc_counties_deaths_df.index) + +nyc_counties_deaths_df['Total'] = nyc_counties_deaths_df['Bronx County'] + nyc_counties_deaths_df['Kings County'] + nyc_counties_deaths_df['New York County'] + nyc_counties_deaths_df['Queens County'] + nyc_counties_deaths_df['Richmond County'] + +#case fatality ratio +cfr_df = nyc_counties_deaths_df / nyc_counties_df + +#daily cases +def dailydata(dfcounty): + dfcountydaily=dfcounty.diff(axis=0)#.fillna(0) + return dfcountydaily + +DailyCases_df=dailydata(nyc_counties_df) +DailyDeaths_df=dailydata(nyc_counties_deaths_df) +DailyCases_df = DailyCases_df.loc[start_date:] +# compute 7-day exponential moving average +DailyCases_df['EMA'] = DailyCases_df['Total'].ewm(span=7).mean() + +#daily deaths +DailyDeaths_df = DailyDeaths_df.loc[start_date:] +DailyDeaths_df.replace(0.0,np.nan, inplace=True) +# compute 7-day exponential moving average +DailyDeaths_df['EMA'] = DailyDeaths_df['Total'].ewm(span=7).mean() +DailyDeaths_df = DailyDeaths_df.loc[start_date:] +DailyDeaths_df.replace(0.0,np.nan, inplace=True) +# compute 7-day exponential moving average +DailyDeaths_df['EMA'] = DailyDeaths_df['Total'].ewm(span=7).mean() + +#NYC case hospitalizations +url = "https://raw.githubusercontent.com/nychealth/coronavirus-data/master/trends/data-by-day.csv" + +nyc_case_hosp_death_df = pd.read_csv(url) +nyc_case_hosp_death_df.index =pd.to_datetime(nyc_case_hosp_death_df.iloc[:,0]) +nyc_case_hosp_death_df = nyc_case_hosp_death_df.drop(nyc_case_hosp_death_df.columns[0], axis=1) +nyc_case_hosp_death_df = nyc_case_hosp_death_df.rename_axis('Date') + +#r0 estimation +url = "https://d14wlfuexuxgcm.cloudfront.net/covid/rt.csv" +rt_df = pd.read_csv(url) +rt_df = rt_df.rename(columns={"mean":"R0_mean"}) + +#### PLOTS #### +# case fatality ratio +df2 = cfr_df.loc["2020-03-14":,] +fig2 = go.Figure() +fig2.add_scatter(x=df2.index, y=df2['Bronx County'], mode='lines', name='Bronx') +fig2.add_scatter(x=df2.index, y=df2['Kings County'], mode='lines',name='Brooklyn') +fig2.add_scatter(x=df2.index, y=df2['Queens County'], mode='lines', name='Queens') +fig2.add_scatter(x=df2.index, y=df2['New York County'], mode='lines', name='Manhattan') +fig2.add_scatter(x=df2.index, y=df2['Richmond County'], mode='lines', name='Staten Island') +fig2.add_scatter(x=df2.index, y=df2['Total'], mode='lines+markers', name='NYC') +prepFigure(fig2, title='NYC Case Fatality Rate - From USA Facts') +fig2.update_layout(yaxis_title="Percent", yaxis_tickformat=".2%") +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/cfr.json") + +#daily new cases +df2 = DailyCases_df +fig2 = go.Figure() +fig2.add_scatter(x=df2.index,y=df2['Bronx County'], mode='lines', name='Bronx') +fig2.add_scatter(x=df2.index,y=df2['Kings County'], mode='lines', name='Brooklyn') +fig2.add_scatter(x=df2.index,y=df2['New York County'], mode='lines', name='Manhattan') +fig2.add_scatter(x=df2.index,y=df2['Queens County'], mode='lines', name='Queens') +fig2.add_scatter(x=df2.index,y=df2['Richmond County'], mode='lines', name='Staten Island') +fig2.add_scatter(x=df2.index,y=df2['Total'], mode='lines+markers', name='NYC') +fig2.add_scatter(x=df2.index, y=df2['EMA'], mode='lines', line=dict(color='royalblue', width=4, dash='dot'), name='7 day EMA') +prepFigure(fig2, title="NYC Daily New Cases (Source: USA Facts)") +latest = df2.iloc[-1,:] +fig2.add_annotation(text="On "+latest.name.strftime("%m/%d")+":\n"+str(int(latest.Total)), + font=dict(family="Helvetica, Arial",size=28), + xref="paper", yref="paper", + x=1, y=1, showarrow=False) +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/daily_cases.json") + +#daily new deaths +df2 = DailyDeaths_df +#df2 = temp_df +fig2 = go.Figure() +fig2.add_scatter(x=df2.index,y=df2['Bronx County'], mode='lines', name='Bronx') +fig2.add_scatter(x=df2.index,y=df2['Kings County'], mode='lines', name='Brooklyn') +fig2.add_scatter(x=df2.index,y=df2['New York County'], mode='lines', name='Manhattan') +fig2.add_scatter(x=df2.index,y=df2['Queens County'], mode='lines', name='Queens') +fig2.add_scatter(x=df2.index,y=df2['Richmond County'], mode='lines', name='Staten Island') +fig2.add_scatter(x=df2.index,y=df2['Total'], mode='lines+markers', name='NYC') +fig2.add_scatter(x=df2.index, y=df2['EMA'], mode='lines', line=dict(color='royalblue', width=4, dash='dot'), name='7 day EMA') +prepFigure(fig2, title="NYC Daily Deaths (Source: USA Facts)") +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/daily_deaths.json") + +#hospitalizations +df2 = nyc_case_hosp_death_df +fig2 = go.Figure() +fig2.add_scatter(x=df2.index,y=df2['HOSPITALIZED_COUNT'], mode='lines', name='Hospitalizations', ) +prepFigure(fig2,title="NYC New Hospitalizations (Source: NYC DOHMH)") +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/hospitalizations.json") + +#r0 estimation +df2 = rt_df[rt_df["region"]=="NY"].sort_values("date").set_index("date") +fig2 = go.Figure() +fig2.add_scattergl(x=df2.index,y=df2.R0_mean.where(df2.R0_mean <= 1), line={'color': 'black'}) # below threshold +fig2.add_scattergl(x=df2.index,y=df2.R0_mean.where(df2.R0_mean >= 1), line={'color': 'red'}) # Above threshhgold +fig2.add_shape( #dashed line + type='line', + x0=str(df2.index.min()), + y0=1, + x1=str(df2.index.max()), + y1=1, + line=dict( + color='black', + dash="dashdot" + ) +) +prepFigure(fig2,title="R0 estimate (Source: rt.live)") +fig2.update_layout(showlegend=False) +ro_latest = str(round(list(df2["R0_mean"])[-1],2)) +fig2.add_annotation(text="Current R0:\n"+ro_latest, + font=dict(family="Helvetica, Arial",size=24), + xref="paper", yref="paper", + x=1, y=1, showarrow=False) +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/r0_estimate.json") diff --git a/medtracker/database.py b/medtracker/database.py index 7323241..ea30561 100755 --- a/medtracker/database.py +++ b/medtracker/database.py @@ -1,8 +1 @@ -from medtracker import * -from flask_sqlalchemy import SQLAlchemy -from medtracker.config import * - -db = SQLAlchemy(app) - -# import a db_session to query db from other modules -db_session = db.session +#empty diff --git a/medtracker/email_helper.py b/medtracker/email_helper.py index 3c5aaf3..594766e 100755 --- a/medtracker/email_helper.py +++ b/medtracker/email_helper.py @@ -7,7 +7,7 @@ def send_email(address, subject, html): '''send an email''' - from_email = "info@suretify.co" + from_email = config.mail_server_sender msg = Message(sender=from_email) msg.sender = from_email msg.recipients = [address] @@ -15,4 +15,4 @@ def send_email(address, subject, html): msg.html = html mail.send(msg) print("Sent email.") - return None \ No newline at end of file + return None diff --git a/medtracker/forms.py b/medtracker/forms.py index 6329765..d9b4c00 100755 --- a/medtracker/forms.py +++ b/medtracker/forms.py @@ -2,7 +2,7 @@ from flask_wtf import FlaskForm as Form from wtforms.ext.sqlalchemy.fields import * from wtforms.fields.html5 import DateField, IntegerRangeField, EmailField -from wtforms.validators import DataRequired, Length, EqualTo, InputRequired +from wtforms.validators import DataRequired, Length, EqualTo, InputRequired, Email, Regexp from wtforms.widgets.core import HTMLString, html_params, escape, HiddenInput from medtracker.models import * from flask.views import MethodView @@ -10,6 +10,7 @@ import re, json from wtforms_alchemy import ModelForm, ModelFieldList from wtforms.fields import FormField +import re class HiddenInteger(IntegerField): widget = HiddenInput() @@ -147,7 +148,8 @@ class UsernamePasswordForm(Form): class NewUserForm(Form): name = StringField('Full Name', validators=[DataRequired()]) - email = StringField('Email', validators=[DataRequired()]) + email = StringField('Email', validators=[InputRequired(), Email(message="Please provide a valid email address."), + Regexp("(\S+(@icahn.mssm.edu|@mssm.edu|@mountsinai.org)$)",message="Please enter a Mount Sinai email address",flags=re.IGNORECASE)]) password = PasswordField('Password', validators=[DataRequired(), Length(min=8, max=40), EqualTo('confirm', message='Passwords must match')]) @@ -184,21 +186,48 @@ def validate_email(self, field): raise ValidationError('Unknown email address.') class PatientForm(Form): - mrn = DisabledTextField('Patient Device ID') + mrn = DisabledTextField('Student Device ID') + fullname = StringField('Full Name', description="Please enter your name", validators=[InputRequired(), Length(min=3, max=50, message="Full name must be between 3 and 50 characters.")]) + email = StringField('School Email Address', description="Please enter a Mount Sinai email address", + validators=[InputRequired(), Email(message="Please provide a valid email address."), + Regexp("(\S+(@icahn.mssm.edu|@mssm.edu|@mountsinai.org)$)",message="Please enter a Mount Sinai email address",flags=re.IGNORECASE)]) + lifenumber = StringField('Life Number', description="e.g. 2211234", + validators=[InputRequired(), Length(min=7,max=7,message="Life number must be correct length."), + Regexp('^\d+$', message="Life number must only contain digits.")]) + phone = StringField('Phone number (optional)') + age = StringField('Age (optional)') + program = RadioField("What program are you in?", choices=PROGRAM_CHOICES) year = SelectField("What is your anticipated graduation date?",choices=[(i,i) for ix,i in enumerate(range(2021,2030))],coerce=int) location = RadioField("Where are you currently living?", choices=LOCATION_CHOICES) - fullname = StringField('Name (optional)') - age = StringField('Age (optional)') - email = StringField('Email address (optional)') - phone = StringField('Phone number (optional)') + class PatientEditForm(Form): - mrn = HiddenField('Patient Device ID') + mrn = DisabledTextField('Student Device ID') + fullname = StringField('Full Name', description="Please enter your name", validators=[InputRequired(), Length(min=3, max=50, message="Full name must be between 3 and 50 characters.")]) + email = StringField('School Email Address', description="Please enter a Mount Sinai email address", + validators=[InputRequired(), Email(message="Please provide a valid email address."), + Regexp("(\S+(@icahn.mssm.edu|@mssm.edu|@mountsinai.org)$)",message="Please enter a Mount Sinai email address",flags=re.IGNORECASE)]) + lifenumber = StringField('Life Number', description="e.g. 2211234", + validators=[InputRequired(), Length(min=7,max=7,message="Life number must be correct length."), + Regexp('^\d+$', message="Life number must only contain digits.")]) + program = RadioField("What program are you in?", choices=PROGRAM_CHOICES) year = SelectField("What is your anticipated graduation date?",choices=[(i,i) for ix,i in enumerate(range(2021,2030))],coerce=int) location = RadioField("Where are you currently living?", choices=LOCATION_CHOICES) - fullname = StringField('Name (optional)') + + phone = StringField('Phone number (optional)') age = StringField('Age (optional)') - email = StringField('Email address (optional)') - phone = StringField('Phone number (optional)') \ No newline at end of file + +class PatientSearchForm(Form): + + mrn = TextField('Student Device ID') + fullname = StringField('Full Name') + email = StringField('Email Address') + lifenumber = StringField('Life Number', description="e.g. 2211234") + phone = StringField('Phone number') + age = StringField('Age') + program = StringField("Program") + year = StringField("Graduation Year") + location = StringField("Location") + diff --git a/medtracker/models.py b/medtracker/models.py index 969ec52..31e0b25 100755 --- a/medtracker/models.py +++ b/medtracker/models.py @@ -1,5 +1,4 @@ from medtracker import * -from medtracker.database import db from medtracker.config import * from sqlalchemy_utils import EncryptedType, ChoiceType from passlib.apps import custom_app_context as pwd_context @@ -104,7 +103,7 @@ def __init__(self, **kwargs): def to_dict(self): return {col.name: getattr(self, col.name) for col in self.__table__.columns} - + class Survey(db.Model): __tablename__ = 'survey' id = db.Column(db.Integer, primary_key=True) @@ -202,7 +201,7 @@ def response(self,value): if type(value)!=list: value = [value] self._response = ";".join([str(a) for a in value]) - + def __init__(self,**kwargs): super(QuestionResponse, self).__init__(**kwargs) self.time = datetime.datetime.utcnow() @@ -210,11 +209,7 @@ def __init__(self,**kwargs): def to_dict(self): outdict = {col.name: getattr(self, col.name) for col in self.__table__.columns} outdict["response"] = self._response - outdict["question_title"] = self._question.body - outdict["question_choices"] = self._question.choices - outdict["question_type"] = self._question.kind.code - outdict["survey_title"] = self._question.survey.title - outdict["survey_id"] = self._question.survey.id + return outdict class SurveyResponse(db.Model): @@ -229,11 +224,11 @@ class SurveyResponse(db.Model): exited = db.Column(db.Boolean,default=False) completed = db.Column(db.Boolean, default=False) message = db.Column(db.Text) - responses = db.relationship("QuestionResponse",backref="parent", lazy="joined",cascade="all,delete-orphan") + responses = db.relationship("QuestionResponse",backref="parent", lazy="joined", cascade="all,delete-orphan") def __str__(self): return '%s' % self.session_id - + def __init__(self,**kwargs): super(SurveyResponse, self).__init__(**kwargs) self.start_time = datetime.datetime.utcnow() @@ -247,18 +242,18 @@ def exit(self): self.end_time = datetime.datetime.utcnow() self.exited = True self.completed = False - + def to_dict(self): return {col.name: getattr(self, col.name) for col in self.__table__.columns} class Trigger(db.Model): __tablename__ = 'trigger' - + id = db.Column(db.Integer, primary_key=True) question_id = db.Column(db.Integer, db.ForeignKey('question.id')) conditions = db.relationship("TriggerCondition",backref="trigger",cascade="all,delete-orphan") user_id = db.Column(db.Integer, db.ForeignKey('user.id')) - + yes_type = db.Column(ChoiceType(TRIGGER_KINDS)) dest_yes = db.Column(db.Integer, db.ForeignKey('question.id')) dest_yes_question = db.relationship("Question",foreign_keys=[dest_yes]) @@ -290,7 +285,7 @@ class TriggerCondition(db.Model): class Question(db.Model): __tablename__ = 'question' - + id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String) description = db.Column(db.Text) @@ -316,7 +311,7 @@ class QuestionMeta(db.Model): id = db.Column(db.Integer, primary_key=True) body = db.Column(db.String) question_id = db.Column(db.Integer, db.ForeignKey('question.id')) - + def to_dict(self): return {col.name: getattr(self, col.name) for col in self.__table__.columns} @@ -326,7 +321,6 @@ class User(db.Model): id = db.Column(db.Integer, primary_key=True) email = db.Column(EncryptedType(db.String, flask_secret_key), unique=True) - username = db.Column(EncryptedType(db.String, flask_secret_key), unique=True) name = db.Column(EncryptedType(db.String, flask_secret_key)) password_hash = db.Column(db.String(256)) active = db.Column(db.Boolean, default=False) @@ -369,15 +363,21 @@ class Patient(db.Model): __tablename__= "patients" id = db.Column(db.Integer, primary_key=True) - mrn = db.Column(EncryptedType(db.String, flask_secret_key)) + mrn = db.Column(EncryptedType(db.String, flask_secret_key), unique=True) fullname = db.Column(EncryptedType(db.String, flask_secret_key)) - age = db.Column(EncryptedType(db.String, flask_secret_key)) - phone = db.Column(EncryptedType(db.String, flask_secret_key)) email = db.Column(EncryptedType(db.String, flask_secret_key)) + lifenumber = db.Column(EncryptedType(db.String, flask_secret_key)) location = db.Column(ChoiceType(LOCATION_CHOICES)) program = db.Column(ChoiceType(PROGRAM_CHOICES)) year = db.Column(db.Integer) - user_id = db.Column(db.Integer, db.ForeignKey('user.id')) + age = db.Column(EncryptedType(db.String, flask_secret_key)) + phone = db.Column(EncryptedType(db.String, flask_secret_key)) + + google_email = db.Column(EncryptedType(db.String, flask_secret_key)) + google_token = db.Column(EncryptedType(db.String, flask_secret_key)) + + user_id = db.Column(db.Integer, db.ForeignKey('user.id')) #this is the "owner" of the student/patient record + surveys = db.relationship("SurveyResponse", backref='patient', lazy="dynamic", cascade="all, delete-orphan") responses = db.relationship("QuestionResponse", backref='patient', lazy="dynamic", cascade="all, delete-orphan") progress = db.relationship("Progress", backref='patient', lazy="dynamic", cascade="all, delete-orphan") @@ -392,8 +392,8 @@ class Device(db.Model): __tablename__= "devices" id = db.Column(db.Integer, primary_key=True) - device_id = db.Column(db.String) + device_id = db.Column(db.String, unique=True) creation_time = db.Column(db.DateTime, default=func.now()) def to_dict(self): - return {col.name: getattr(self, col.name) for col in self.__table__.columns} \ No newline at end of file + return {col.name: getattr(self, col.name) for col in self.__table__.columns} diff --git a/medtracker/templates/404.html b/medtracker/templates/404.html index 19a3f4d..ded79f2 100755 --- a/medtracker/templates/404.html +++ b/medtracker/templates/404.html @@ -9,7 +9,7 @@

Something's not right

We can't find that page. Please try a different request.

-

+

{% if message %}

Details: {{message}}{% endif %}

diff --git a/medtracker/templates/500.html b/medtracker/templates/500.html index 887e802..287ec68 100755 --- a/medtracker/templates/500.html +++ b/medtracker/templates/500.html @@ -9,7 +9,7 @@

Big oops.

Something's gone terribly wrong. We're working on it! (Error 500)

- + {% if message %}

Details:
{{message}}

{% endif %}
diff --git a/medtracker/templates/_forms_edit.html b/medtracker/templates/_forms_edit.html index 69ea09c..887ee79 100755 --- a/medtracker/templates/_forms_edit.html +++ b/medtracker/templates/_forms_edit.html @@ -41,7 +41,7 @@ {% if field.errors or field.help_text %} {% if field.errors %} - {{ field.errors|join(' ') }} + {{ field.errors|join(' ') }} {% else %} {{ field.help_text }} {% endif %} diff --git a/medtracker/templates/_render_field.html b/medtracker/templates/_render_field.html index dd11d78..cd07fc5 100755 --- a/medtracker/templates/_render_field.html +++ b/medtracker/templates/_render_field.html @@ -53,7 +53,7 @@ {% if field.errors or field.help_text %} {% if field.errors %} - {{ field.errors|join(' ') }} + {{ field.errors|join(' ') }} {% else %} {{ field.help_text }} {% endif %} diff --git a/medtracker/templates/about.html b/medtracker/templates/about.html index f95093a..ac945e1 100755 --- a/medtracker/templates/about.html +++ b/medtracker/templates/about.html @@ -11,10 +11,11 @@

Mission

What it does

ISMMS Health Check is a new lightweight web app to simplify health workflows at ISMMS - through automated follow-up of medical protocols or critical health values. We have created if-this-then-that + through automated follow-up of medical protocols for critical health values. We have created if-this-then-that style triggers for health data that takes the burden off health care providers to perform follow-up while collecting data from patients and providers automatically.

+

Developed by Ryan Neff (MD/PhD Class of 2023) on behalf of the Mount Sinai Department of Medical Education (Dr. Valerie Parkas, Dr. Beverly Forsyth, and Dr. Rainier Soriano) and Mount Sinai Student Health (Dr. Lori Zbar). Special thanks to Michelle Sainte at MedEd and everyone at Mount Sinai Information Technology (Ben Maisano, Paul Laurence, Ricardo Somarriba).

Features

diff --git a/medtracker/templates/dashboard.html b/medtracker/templates/dashboard.html index 3648b26..ee21468 100755 --- a/medtracker/templates/dashboard.html +++ b/medtracker/templates/dashboard.html @@ -89,7 +89,7 @@

Students staying home

{% for patient in patients %} - {{fmt_id(patient.mrn)}} + {{patient.fullname}} ({{fmt_id(patient.mrn)}}) {{patient.year}} {{patient.program}} {{patient.location}} diff --git a/medtracker/templates/dashboard_student.html b/medtracker/templates/dashboard_student.html index 1cc9e3b..f6cb783 100755 --- a/medtracker/templates/dashboard_student.html +++ b/medtracker/templates/dashboard_student.html @@ -43,7 +43,13 @@
Percent Positive Screenings
{% for fig in special_figs %} -
{{fig|safe}}
+
{{fig|safe}}
+ {% endfor %} +
+

NYC Region Metrics

+
+ {% for fig in special_figs_2 %} +
{{fig|safe}}
{% endfor %}
diff --git a/medtracker/templates/email_survey_complete.html b/medtracker/templates/email_survey_complete.html new file mode 100644 index 0000000..1c3cee9 --- /dev/null +++ b/medtracker/templates/email_survey_complete.html @@ -0,0 +1,354 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ + + + + + +
{{record.survey.title}} Taken
+
+

+ +
+
ISMMS Student Health Check
+
+ +
+ + + + + + +
+ +
You may attend class or work.
+ +
 
+ +
Date taken: {{ record.end_time.strftime('%B %-d, %Y') }}
+ +
+ + + + +
+ + + + +
+ See my records +
+
+
+ +
+
+ +
+ +
+ + + + + + +
+ +
+ *********************
+ COVID-19 ATTESTATION
+ Cleared for:
+ {{ record.end_time.strftime('%x %I:%M%p') }} +

+ Student:
+ {{patient.fullname}} +

+ Life Number:
+ {{patient.lifenumber}} +

+ *********************
+ +
+
+ +
+ +
+ + + + + + +
+ +
Your answers have been recorded successfully. Please save this e-mail for your records. If you believe you have received this email in error, immediately contact the Medical Education Department by email at michelle.sainte@mssm.edu.
+ +
+
+ +
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/medtracker/templates/email_survey_complete.txt b/medtracker/templates/email_survey_complete.txt new file mode 100644 index 0000000..def3fc5 --- /dev/null +++ b/medtracker/templates/email_survey_complete.txt @@ -0,0 +1,18 @@ +{{record.survey.title}} Result +-------------- + +You may attend class or work on {{ record.end_time.strftime('%B %-d, %Y') }}. + +********************* +COVID-19 ATTESTATION

+Cleared for: +{{ record.end_time.strftime('%x %I:%M%p') }} +Student: +{{patient.fullname}} +Life Number: +{{patient.lifenumber}} +********************* + + +------------- +ISMMS Student Health Check \ No newline at end of file diff --git a/medtracker/templates/email_survey_exit.html b/medtracker/templates/email_survey_exit.html new file mode 100644 index 0000000..1eebef8 --- /dev/null +++ b/medtracker/templates/email_survey_exit.html @@ -0,0 +1,327 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+ +
+ + + + + + + + + + + + + + + + +
+ + + + + + +
{{record.survey.title}} Taken
+
+

+ +
+
ISMMS Student Health Check
+
+ +
+ + + + + + +
+ +
Please stay home.
+ +
 
+ + +
 
+ +
Follow your program requirements for reporting your absence. Schedule a tele-health visit with your provider or Student Health should symptoms persist or if you have further questions.
+ +
Name: {{patient.fullname}}
+
Life Number: {{patient.lifenumber}}
+
Taken: {{ record.end_time.strftime('%B %-d, %Y') }}
+ +
+ + + + +
+ + + + +
+ See my records +
+
+
+ +
+
+ +
+ +
+ + + + + + +
+ +
Your answers have been recorded successfully. Please save this e-mail for your records. If you believe you have received this email in error, immediately contact the Medical Education Department by email at michelle.sainte@mssm.edu.
+ +
+
+ +
+
+ +
+
+ +
+ + + \ No newline at end of file diff --git a/medtracker/templates/email_survey_exit.txt b/medtracker/templates/email_survey_exit.txt new file mode 100644 index 0000000..3ec6ef3 --- /dev/null +++ b/medtracker/templates/email_survey_exit.txt @@ -0,0 +1,12 @@ +{{record.survey.title}} Result +-------------- + +Please stay home on {{ record.end_time.strftime('%B %-d, %Y') }}. + +Follow your program requirements for reporting your absence. Schedule a tele-health +visit with your provider or Student Health should symptoms persist or if you have +further questions. + + +------------- +ISMMS Student Health Check \ No newline at end of file diff --git a/medtracker/templates/form_signup_register.html b/medtracker/templates/form_signup_register.html index 71fc451..a3d7d21 100755 --- a/medtracker/templates/form_signup_register.html +++ b/medtracker/templates/form_signup_register.html @@ -1,6 +1,6 @@ {% extends 'layout.html' %} {% from '_render_field.html' import render_field %} -{% block title %}Sign up a new device{% endblock %} +{% block title %}Please register your device | ISMMS Health Check{% endblock %} {% block body %}
@@ -8,12 +8,9 @@
- {% if form.errors %} -

Errors: {{form.errors}}

- {% endif %} {% block form %}
-

Sign up a new device

+

Please register your device

In order to complete this survey, you first need to register your device to save and view your responses. This process will only happen once and take a few seconds. Clearing your browser cookies will delete your registration.

Please use the same device and browser for future visits to skip this step and track your responses.

@@ -25,7 +22,7 @@

Sign up a new device

{% if field.errors or field.help_text %} {% if field.errors %} - {{ field.errors|join(' ') }} + {{ field.errors|join(' ') }} {% else %} {{ field.help_text }} {% endif %} @@ -33,16 +30,17 @@

Sign up a new device

{% endif %} {% endfor %}

Required questions

-
    + {{form.fullname.label}}{{render_field(form.fullname)}} + {{form.email.label}}{{render_field(form.email)}} + {{form.lifenumber.label}}{{render_field(form.lifenumber)}} +
    • {{form.program.label}}{{render_field(form.program)}}
    • {{form.year.label}}{{render_field(form.year)}}
    • {{form.location.label}}{{render_field(form.location)}}
    • -
+

Optional questions

These questions are completely optional and will only be used to contact you should your symptoms change. They will be kept private and confidential. Only Mount Sinai Student Health has access to this information. It will not be shared with other Mount Sinai faculty or staff.

- {{render_field(form.email)}} {{render_field(form.phone)}} - {{render_field(form.fullname)}} {{render_field(form.age)}} diff --git a/medtracker/templates/index.html b/medtracker/templates/index.html index 469d796..b282549 100755 --- a/medtracker/templates/index.html +++ b/medtracker/templates/index.html @@ -2,7 +2,7 @@ {% block title %}Welcome | ISMMS Health Check{% endblock %} {% block body %}
- +

Welcome to the ISMMS Student Health Check

Check your symptoms for COVID-19 daily from anywhere, whether at home or on-the go. No login necessary.

@@ -10,12 +10,12 @@

Welcome to the ISMMS Student Health Check

-

How does this work?

+

How does this work?

Check your symptoms

-

Take the quick screening tool daily to screen for possible COVID-19 symptoms. No need to login and you may choose to keep your responses anonymous.

+

Take the quick screening tool daily to screen for possible COVID-19 symptoms. Coming soon: save your responses across your devices with your Mount Sinai Google Apps account.

@@ -28,5 +28,17 @@

Stop the spread

If you screen positive, stay home and protect others from the spread of the novel coronavirus.

+

Where can I find updates on COVID-19 policies?

+
+
+
+
+ +

MedInfo App

+

Built specifically for medical and graduate students at Mount Sinai, the MedInfo App has the most up-to-date information on infection prevention policy, travel registry, testing, weekly communications, clerkship resources... and more!

+
+
+
+
{% endblock %} diff --git a/medtracker/templates/layout.html b/medtracker/templates/layout.html index 2d224ae..8e6ac0c 100755 --- a/medtracker/templates/layout.html +++ b/medtracker/templates/layout.html @@ -1,8 +1,8 @@ - - + + @@ -24,6 +24,7 @@ + @@ -113,7 +114,7 @@
-

© 2020 Icahn School of Medicine at Mount Sinai.
By using this site, and/or copying, modifying, distributing, or licensing the code for this site, you agree to the terms in the license found here, which may change at any time. Check out the Github

+

© 2020-2021 Icahn School of Medicine at Mount Sinai.
By using this site, and/or copying, modifying, distributing, or licensing the code for this site, you agree to the terms in the license found here, which may change at any time. Check out the Github.

This site uses cookies to save you time at future visits and to help keep the site running smoothly. Your data is protected under applicable HIPAA and FERPA laws of the United States.

diff --git a/medtracker/templates/layout_sidebar.html b/medtracker/templates/layout_sidebar.html index 0fec37b..2fd25d9 100755 --- a/medtracker/templates/layout_sidebar.html +++ b/medtracker/templates/layout_sidebar.html @@ -107,20 +107,26 @@ Students
  • Responses
  • Settings diff --git a/medtracker/templates/patients.html b/medtracker/templates/patients.html index c4f9793..9bce1de 100755 --- a/medtracker/templates/patients.html +++ b/medtracker/templates/patients.html @@ -1,5 +1,5 @@ {% extends "layout_sidebar.html" %} -{% block title %}Patients | ISMMS Health Check {% endblock %} +{% block title %}Responded Today | ISMMS Health Check {% endblock %} {% block sidebar %}