From b79209ddfd7aba7410d01a43460ff4f701b645b7 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 16 Oct 2020 16:18:03 +0000 Subject: [PATCH 01/44] layout changes --- Pipfile | 2 ++ medtracker/templates/layout.html | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Pipfile b/Pipfile index 104c26d..b3453da 100755 --- a/Pipfile +++ b/Pipfile @@ -59,6 +59,8 @@ WTForms = "==2.3.1" simplekv = "*" wtforms-sqlalchemy = "*" wtforms-alchemy = "*" +scipy = "*" +flask-cache = "*" [requires] python_version = "3.6" diff --git a/medtracker/templates/layout.html b/medtracker/templates/layout.html index 2d224ae..b9e8887 100755 --- a/medtracker/templates/layout.html +++ b/medtracker/templates/layout.html @@ -113,7 +113,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 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. Built by Ryan Neff at ISMMS.

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.

From 60feedd5e7da807ff9e0cef9a88e666896c6b103 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 19 Oct 2020 11:00:29 -0400 Subject: [PATCH 02/44] fix short date --- medtracker/views.py | 198 ++++++++++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 89 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index dc7c369..d9df49d 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1153,57 +1153,67 @@ def pt_to_pd(): today_pct = list(df["daily_pct"])[-1] week_count = list(df["total_completed_surveys"])[-1] week_pct = sum(list(df["daily_total_surveys"]))/sum(list(df["total_registered_students"]))*100 + patient_count = list(df["total_registered_students"])[-1] + + special_figs = [] + last7_figs = [] + from scipy import signal + def pos_plot(df,width=None,height=400): + layout = go.Layout( + autosize=True, + width=width, + height=height, + title={'text': 'Percent Positivity Rate', + 'y':0.9, + 'x':0.5, + 'xanchor': 'center', + 'yanchor': 'top'} + ) + fig = go.Figure(layout=layout) + fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) + try: + fig.add_trace(go.Scatter(x=df["index"], y=signal.savgol_filter(df["positivity_rate"],7,1),line_shape='spline', + name="Average (7 days)")) + except ValueError as err: + pass + fig.update_layout( xaxis_title='Date', + yaxis_title='Positivity Rate %') + fig.update_layout(legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + )) + return fig + special_figs.append(pos_plot(df)) + fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, + title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], + ylabel="# Students",xlabel="Date") + fig.update_layout(legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + )) + special_figs.append(fig) else: dash_figs = [] + special_figs = [] + last7_figs = [] today_count = 0 today_pct = 0 week_count = 0 week_pct = 0 + patient_count = 0 + today_positive = 0 + today_negative = 0 + today_pct_pos = 0 - patient_count = list(df["total_registered_students"])[-1] #device_count = len(devices) - - special_figs = [] - last7_figs = [] - from scipy import signal - def pos_plot(df,width=None,height=400): - layout = go.Layout( - autosize=True, - width=width, - height=height, - title={'text': 'Percent Positivity Rate', - 'y':0.9, - 'x':0.5, - 'xanchor': 'center', - 'yanchor': 'top'} - ) - fig = go.Figure(layout=layout) - fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) - fig.add_trace(go.Scatter(x=df["index"], y=signal.savgol_filter(df["positivity_rate"],7,1),line_shape='spline', - name="Average (7 days)")) - fig.update_layout( xaxis_title='Date', - yaxis_title='Positivity Rate %') - fig.update_layout(legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - )) - return fig - special_figs.append(pos_plot(df)) - fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, - title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], - ylabel="# Students",xlabel="Date") - fig.update_layout(legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - )) - special_figs.append(fig) + qres = pd.DataFrame(responses_last7) if len(qres)>0: @@ -1354,59 +1364,69 @@ def pt_to_pd(): week_count = list(df["total_completed_surveys"])[-1] week_pct = sum(list(df["daily_total_surveys"]))/sum(list(df["total_registered_students"]))*100 + patient_count = list(df["total_registered_students"])[-1] + #device_count = len(devices) + + special_figs = [] + last7_figs = [] + from scipy import signal + def pos_plot(df,width=None,height=400): + layout = go.Layout( + autosize=True, + width=width, + height=height, + title={'text': 'Percent Positive Screenings', + 'y':0.9, + 'x':0.5, + 'xanchor': 'center', + 'yanchor': 'top'} + ) + fig = go.Figure(layout=layout) + fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) + try: + fig.add_trace(go.Scatter(x=df["index"], y=signal.savgol_filter(df["positivity_rate"],7,1),line_shape='spline', + name="Average (7 days)")) + except ValueError as err: + pass + fig.update_layout( xaxis_title='Date', + yaxis_title='Positivity Rate %') + fig.update_layout(legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + )) + return fig + special_figs.append(pos_plot(df)) + fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, + title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], + ylabel="# Students",xlabel="Date") + fig.update_layout(legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + )) + special_figs.append(fig) + + for ix,fig in enumerate(special_figs): + special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) + + else: dash_figs = [] + special_figs = [] + last7_figs = [] today_count = 0 today_pct = 0 week_count = 0 week_pct = 0 - - patient_count = list(df["total_registered_students"])[-1] - #device_count = len(devices) - - special_figs = [] - last7_figs = [] - from scipy import signal - def pos_plot(df,width=None,height=400): - layout = go.Layout( - autosize=True, - width=width, - height=height, - title={'text': 'Percent Positive Screenings', - 'y':0.9, - 'x':0.5, - 'xanchor': 'center', - 'yanchor': 'top'} - ) - fig = go.Figure(layout=layout) - fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) - fig.add_trace(go.Scatter(x=df["index"], y=signal.savgol_filter(df["positivity_rate"],7,1),line_shape='spline', - name="Average (7 days)")) - fig.update_layout( xaxis_title='Date', - yaxis_title='Positivity Rate %') - fig.update_layout(legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - )) - return fig - special_figs.append(pos_plot(df)) - fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, - title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], - ylabel="# Students",xlabel="Date") - fig.update_layout(legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - )) - special_figs.append(fig) - - for ix,fig in enumerate(special_figs): - special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) + patient_count = 0 + today_positive = 0 + today_negative = 0 + today_pct_pos = 0 start_time = datetime.datetime.strftime(start_time,"%Y-%m-%d") end_time = datetime.datetime.strftime(end_time,"%Y-%m-%d") From 68587b8ee70f3df3695759543c6829ef47d7ce68 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 19 Oct 2020 11:03:05 -0400 Subject: [PATCH 03/44] fix pt count --- medtracker/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index d9df49d..69e97ea 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1153,7 +1153,7 @@ def pt_to_pd(): today_pct = list(df["daily_pct"])[-1] week_count = list(df["total_completed_surveys"])[-1] week_pct = sum(list(df["daily_total_surveys"]))/sum(list(df["total_registered_students"]))*100 - patient_count = list(df["total_registered_students"])[-1] + patient_count = sum(list(df["daily_registered_students"])) special_figs = [] last7_figs = [] @@ -1364,7 +1364,7 @@ def pt_to_pd(): week_count = list(df["total_completed_surveys"])[-1] week_pct = sum(list(df["daily_total_surveys"]))/sum(list(df["total_registered_students"]))*100 - patient_count = list(df["total_registered_students"])[-1] + patient_count = sum(list(df["daily_registered_students"])) #device_count = len(devices) special_figs = [] From ae183dc9fa036d1501ba52ccb0ffa8a025423491 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 19 Oct 2020 11:34:17 -0400 Subject: [PATCH 04/44] timezone fix again --- medtracker/views.py | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index 69e97ea..166a67e 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -17,6 +17,7 @@ import ast from collections import defaultdict from datetime import timezone +import time from sqlalchemy import or_ from pytz import timezone as pytztimezone @@ -959,11 +960,16 @@ def survey_response_dashboard(survey_id): start_time = (datetime.datetime.now()-datetime.timedelta(days=30)).date() end_time = (datetime.datetime.now(tz)).date() + time_end = end_time.timetuple() + time_start = start_time.timetuple() + time_end = datetime.datetime.fromtimestamp(time.mktime(time_end)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) + time_start = datetime.datetime.fromtimestamp(time.mktime(time_start)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) + survey = models.Survey.query.get_or_404(survey_id) pres = db.session.query(models.SurveyResponse).join(models.Patient)\ - .filter(models.SurveyResponse.start_time > start_time)\ - .filter(models.SurveyResponse.start_time <= (end_time+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ .all() def pt_to_pd(): @@ -982,12 +988,12 @@ def pt_to_pd(): sres.append(row) responses = [] - sr = survey.responses.filter(models.SurveyResponse.start_time <= (end_time+datetime.timedelta(days=1))).filter(models.SurveyResponse.start_time >= end_time) + sr = survey.responses.filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1))).filter(models.SurveyResponse.start_time >= time_end) for sre in sr.all(): responses.extend([r.to_dict() for r in sre.responses]) sig_r = [] for sre in sr.filter(models.SurveyResponse.exited==True).all(): sig_r.extend([sre.patient]) responses_last7 = responses - sr = survey.responses.filter(models.SurveyResponse.start_time > start_time).filter(models.SurveyResponse.start_time <= end_time) + sr = survey.responses.filter(models.SurveyResponse.start_time > time_start).filter(models.SurveyResponse.start_time <= time_end) for sre in sr.all(): responses_last7.extend([r.to_dict() for r in sre.responses]) if len(sres)>0: @@ -995,7 +1001,7 @@ def pt_to_pd(): sres.end_time = sres.end_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') sres.start_time = sres.start_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') sres["date"] = sres.start_time.dt.floor('d') - sres = sres.groupby(["date","patient_id"]).last() + sres = sres.groupby(["date","patient_id","completed","exited"]).last() sres = sres.reset_index() @@ -1283,11 +1289,16 @@ def survey_response_student_dashboard(): start_time = (datetime.datetime.now()-datetime.timedelta(days=30)).date() end_time = (datetime.datetime.now(tz)).date() + time_end = end_time.timetuple() + time_start = start_time.timetuple() + time_end = datetime.datetime.fromtimestamp(time.mktime(time_end)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) + time_start = datetime.datetime.fromtimestamp(time.mktime(time_start)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) + survey = models.Survey.query.get_or_404(survey_id) pres = db.session.query(models.SurveyResponse).join(models.Patient)\ - .filter(models.SurveyResponse.start_time > start_time)\ - .filter(models.SurveyResponse.start_time <= (end_time+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ .all() def pt_to_pd(): @@ -1310,7 +1321,7 @@ def pt_to_pd(): sres.end_time = sres.end_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') sres.start_time = sres.start_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') sres["date"] = sres.start_time.dt.floor('d') - sres = sres.groupby(["date","patient_id"]).last() + sres = sres.groupby(["date","patient_id","completed","exited"]).last() sres = sres.reset_index() From 0aae04631998f737077931e9354307755c25ff90 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 19 Oct 2020 11:52:26 -0400 Subject: [PATCH 05/44] change rolling average from savgol filter --- medtracker/views.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index 166a67e..0616f64 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1178,8 +1178,8 @@ def pos_plot(df,width=None,height=400): fig = go.Figure(layout=layout) fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) try: - fig.add_trace(go.Scatter(x=df["index"], y=signal.savgol_filter(df["positivity_rate"],7,1),line_shape='spline', - name="Average (7 days)")) + fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"].rolling(window=7,min_periods=1).mean(),line_shape='spline', + name="Average (7 days)")) except ValueError as err: pass fig.update_layout( xaxis_title='Date', @@ -1274,7 +1274,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -@cache.cached(timeout=None,key_prefix=make_cache_key) +#@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") @@ -1395,8 +1395,8 @@ def pos_plot(df,width=None,height=400): fig = go.Figure(layout=layout) fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) try: - fig.add_trace(go.Scatter(x=df["index"], y=signal.savgol_filter(df["positivity_rate"],7,1),line_shape='spline', - name="Average (7 days)")) + fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"].rolling(window=7,min_periods=1).mean(),line_shape='spline', + name="Average (7 days)")) except ValueError as err: pass fig.update_layout( xaxis_title='Date', From eee9f82472634bfc174152938030d510bbab88f6 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 19 Oct 2020 20:33:26 -0400 Subject: [PATCH 06/44] reenable cache --- medtracker/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/medtracker/views.py b/medtracker/views.py index 0616f64..5b4574f 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1274,7 +1274,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -#@cache.cached(timeout=None,key_prefix=make_cache_key) +@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") From 74cb32df61fed7f7249d3c857074b9d2fec98daa Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 23 Oct 2020 12:10:32 -0400 Subject: [PATCH 07/44] added RedCap style --- medtracker/templates/survey_complete.html | 62 +++++++++++++++++++++-- 1 file changed, 59 insertions(+), 3 deletions(-) diff --git a/medtracker/templates/survey_complete.html b/medtracker/templates/survey_complete.html index 8000fa1..1a3f08c 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -1,21 +1,77 @@ {% extends "layout.html" %} {% block title %}Completed: {{survey.title}} | ISMMS Health Check{% endblock %} {% block body %} + +
+
- {%if record.message %}

{{record.message}}

{% endif%} + {%if record.message %}

{{record.message}}

{% endif%}

Your answers have been recorded successfully. Please screenshot or print out this page for your records. You can also bookmark this page on this device to refer to it later.

Device ID: {{ fmt_id(patient.mrn) }}

Completion Date: {{ momentjs(record.end_time).format('MMMM Do YYYY, h:mm a') }}

{% if qrcode_out %}

QR Code:

{% endif %} -

See your records

+

See your records + +

-{% endblock %} +
+
+ + {% endblock %} From 1699e449ae376bfd7594cb2e526d4464a871ad79 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 23 Oct 2020 12:18:18 -0400 Subject: [PATCH 08/44] redcap tweak --- medtracker/templates/survey_complete.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/medtracker/templates/survey_complete.html b/medtracker/templates/survey_complete.html index 1a3f08c..b2df6f3 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -3,7 +3,7 @@ {% block body %} - +
From 3cb8b15035cc243072a4a1bc4719f6824dab296a Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 23 Oct 2020 12:19:25 -0400 Subject: [PATCH 09/44] redcap tweak --- medtracker/templates/survey_complete.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/medtracker/templates/survey_complete.html b/medtracker/templates/survey_complete.html index b2df6f3..42a0928 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -48,15 +48,13 @@

Thank you for taking the {{survey.title}}.

{%if record.message %}

{{record.message}}

{% endif%} -

Your answers have been recorded successfully. Please screenshot or print out this page for your records. You can also bookmark this page on this device to refer to it later.

+

Device ID: {{ fmt_id(patient.mrn) }}

Completion Date: {{ momentjs(record.end_time).format('MMMM Do YYYY, h:mm a') }}

{% if qrcode_out %}

QR Code:

{% endif %} -

-

From d2ed1294fd945efa09f0d6d6b84c83d1e5fd2d17 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 23 Oct 2020 12:20:30 -0400 Subject: [PATCH 10/44] redcap tweak --- medtracker/templates/survey_complete.html | 1 + 1 file changed, 1 insertion(+) diff --git a/medtracker/templates/survey_complete.html b/medtracker/templates/survey_complete.html index 42a0928..77ffa34 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -55,6 +55,7 @@

Thank you for taking the {{survey.title}}.

{% if qrcode_out %}

QR Code:

{% endif %} +

See your records

From d5c67a668e45e0e6c2b78ae8fee10eec50b9672f Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 23 Oct 2020 12:29:27 -0400 Subject: [PATCH 11/44] redcap tweak --- medtracker/templates/survey_complete.html | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/medtracker/templates/survey_complete.html b/medtracker/templates/survey_complete.html index 77ffa34..2ac63c7 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -5,7 +5,7 @@ -
+
@@ -16,6 +16,7 @@

Thank you for completing the Mount Sinai Health System student self-monitori margin-top:1px; margin-bottom:0px; font-family: "Open Sans", Helvetica, Arial, sans-serif; + font-size:24px; }

**********************************

COVID-19 ATTESTATION

@@ -23,7 +24,7 @@

Cleared for:
{{ momentjs(record.end_time).format('MM/DD/YYYY h:mma') }}

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

Mount Sinai Student Health ID:
{{ fmt_id(patient.mrn) }} From 83629fc43b786380e44b499456e5482b458cd1d2 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 23 Oct 2020 12:45:33 -0400 Subject: [PATCH 12/44] redcap tweak --- assets/fonts/OpenSans-Bold.woff | Bin 0 -> 20312 bytes assets/fonts/OpenSans.woff | Bin 0 -> 19476 bytes medtracker/templates/survey_complete.html | 39 +++++++++++----------- 3 files changed, 19 insertions(+), 20 deletions(-) create mode 100644 assets/fonts/OpenSans-Bold.woff create mode 100644 assets/fonts/OpenSans.woff diff --git a/assets/fonts/OpenSans-Bold.woff b/assets/fonts/OpenSans-Bold.woff new file mode 100644 index 0000000000000000000000000000000000000000..9cad9cd16ef7ee4a5542274f8735ff9aa70fd199 GIT binary patch literal 20312 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
  • 2&27;H>z-geqSFu;) zJgHA9q7<8WBUwd>hRc?5*=!d|wCd$)!NcZ=r3#lrs5*<~%+9c>Kv0pVReR8c~mx!jvjk$_< z?Ga=D9>M7`_nWg^r*`aQ(h4`W>?>|~lGS|qc$ zIn*f7?JcQDknmZMhUYXPJQPi(vTrQ>LT@SmODcYqC#wrRoG3sm2t^_g%OB_bXB5lh zM1gUcX+07s@}qSot*>4vvSGxFdgzk*&m4)uq@WDhQfZzOc%5bVBdl?DAd-=}y^_&V z;JEria)PZt9irP9X2$NNNVJ>-xj>CJ$g>-Ln!K#}oDfWsifi*X@8g%Bcbvhgu{YzN zAUvFuAUam;@2Z!4p@2J!*{A_ucgw}@k;@#V(|5JMtrj+jYcTJ7k7nT%}`>QAvkVqDqTyxVtp?gD!>gjJ+%09Hj z)81Y%b2Ue@4=Gq2W(W;5@K-JPZXNb?-1N7Ebbv#Qf!Fw_jtp)~adYLFeYx4XB~RA` zZS3XwF)h%Me6oTLiPxL#t}y<69g-F zNU^KHQG{Lci6rg)zPc5ql1}OO$Jy82FFP&)@Sn38%3nm28-p!y5-IhTx@;qUAR0Va zB3KkmjISP`Tf{>?Bt)o1(hrmI@0M?k@1D07WD25N8w3z_Ul2hjCg$IpMyY+XGqWQM z33&l|adA0$FaQ}I{O_Nb{v?9`%L)KjAOMgj&=s&Ha4>K^@HPk*h$x65h&xCoNHfR_ zC=sY3Xg?Szm^@fD*ep0dcsBSV1SA9ngaU*;#5kk^WIW^*6b+OqR5sKkG!C>Zv>|jP zbSLyW3^STd91)x(Tr^xa+%Y^dJS)6Byd!)vd?oxO0tJFMLOUWn zq84H%5(APoQYO+mG6pgOawu{?3Mh&ON;S$pDl@7hYCY;Y8X1}jS}a;8+7~)Hx)yr! zPage+`pfuNBL*Y}9Y#6EE~XY{Ip#4IC>AVMGFBzlA=V8x6}CHeI`%paG!6}p4o)ym zFU|`tJT3_?CvGZkIc_KJH100$9qt#NG@crsIi4q8G+r)VJ>DSRGTtdZKfVIKA-*$y zD1JJACH^{rD1jP*Ie{lZG(j%GKA|L`2BA5jJ7G9sCSf%ZCy^+T3Xw69Gf^l}I#DH2 zH_-}3Pf7XdnYKpy znlynIlS~MifFwZzl2!;t!juu*Fm}7JbkR37)HhK)0i6?Af3TM*B5#z)A38KNv~;w7 zyri|Ynx>-K!f*13>$)q=(7*m0HUaAXs^chehw^zd1d=lhv1AW{FkIM>Z}LyJt&%VW z7u!Zm!{^#pX0uAmnl|7%p|NrAhW$5nXDXj#hb4{+2%+$7aL0;WlV=Z*5tx-y6&a2R z^WREh&OBZ569QB;G@~@yQU4yl~)cT*-DZ)wv+a!ae@< z>yxYxd6Qe4B~P%*n4h4%Zo=O4zG z6yI4m%IfR*xukQQz63`{lwy0swXMp~JandFTPbn{)>m{xM}34N&<0nG@~+ps2VQk% z#D%)e8BfZ;p(Oq}FqaCBEIolT3fv}9g8-nMwT#2VR_Tmv7vjVHDf4cbW>xyr_rTKG z{#dh9T*>iC$wtyAC=MnoejMy;Uo?}6izd`#$qMFo18>dRPTn3}GXH*(41#qYzeY!0 z0?hX*D_a8k9)jH#u1x**nM~xJw2wCch*($jq9wD3g$#f1((^8X{i;Ie@Bt{<bYx?7YH1{9axhrpV;ddnf$is6hxv=38FMr}VJMd@*uC25>EbrTwDw=oK>&M6M?#9P|ds-i?qhYU9>D&Svz~xltspFJY3?#Mc zyKJrOY_~gq4p#+Nc<8vj%6aIxo6S7h*m&7}4!bZvI%9^ieaJH(UCDeQ`{0@VjRJb$ zudQ10iwBi6R>`Pb#uI9%9?P4izqybU@~)U$S)WawDXw-*PfJU;l2A?wQs7WZ4BFQ# zTbmX{l#+s=~PuKK?8?<(C^-Hn}t>>P#LUI`f^Cr@dneinR*-^Qh?PQFB zg_K$U7=WoIWv00gF>+oQietW-Q@-A=tptqKZ;Q;ZN z@~WK)-E(QS?(gUb8Fnr!@8!tR@vJW;?7(a?UIr5ccBV1WFIG*2Fs`=#m%DoNg7RL%bC&z9ojr z=sHL<=pXGa-%p=>^+OV_cFB4M$_emAwFN@ApejtLze;e@15+dqqJ>u4V6UuVE0P7M z_t{xyE*_MW!j1?HmHwiQ;~^`5inc;0ziNo=S*BeFm1$M~_6w3nPnMdMdrY6ja_)Ku5fBtsDnZ|{ER@tX6 z43!4*&OksObhuG&=CiM1*=<(czrIO=!!~RmV}<#IypMq~2Mcs`XZiO_fh(3~uig_? z_~@6gvrtL@lXg4$TZhc)a)DS2-H?a^J}U#6s?lpb9$DxCC22ARm&3ZGyG(U(&64CD z>K}3c;q9)mrsT&tsKf^KSXq95YFVpmG=+%8Rcc}F@L?mC6lggt#tfP-Etq7Vj3b-Y zZfI;Gcfm7=kj+$Oy5&WkF2QKBO&KQV&unG2w zkFzIMkCJ_wwCsYuVs{`Z?Oh=DgIZG&72kg^pb5pQqa^#S+k|Bor~R36K}NfPD{0gR zL@($zAo5G==?(e7lYcpz)@U8tj(UN zUc0$WEeu@XRp2~qeaKnc3#H(St8kpF>VC{Gm}@%2Pxq!LUHp>q{9WT}KLfFyd<#j13aC9&%sPZ(ojz13DP;{B$7*9wl=eK70;Yb>17A?-niln}xvjF2vMMMCvBxdOB;psu1RW z?GQY-##Pvx$bHfaU7S^fWpd&c46CJ6Yt)7gf6!hi^c&XDSRDjVA4IRTN^n!fOZkYL zvY7|P*ew9XIkzs<&DA>dKm|Juh(}2dHVF-NryNU^f~vsV7K4v4w?f>t^FKsp-O0p9W1A)qx_S7C=1~#}ndR zzS~Tv{IH&0`LJ!T9OOLN?vEZX%v(B}tdXqwOok$VsZ47P<#4rM%z+ere*b=-O}Zbs zqPg&}y@RpYEc4Qzt0xz$yvE#=#&^?y*fr^J_O!l9O@sC!?|mb=v?ZcHN0ydiC^2K^ zXWgxtiG|&4!h{_(jsSTI7rsOxnhKgxNL`9-!yuXd<$I=CiMO@|c}S>NC!!wAr>Y@_ zJ?S=ZQ0zjF|NiOI)b1~&xbud!^=Z4+M4oDOlzg(n-D#&cgr%`5Z=s2a(0=Z7ljr04 z!lGhjsA{)C+>z~5cA2(+3o*<(#Jg#_)&tDV=I2guXp2?(DG07uAQ86jHi$f|fzAZV z|Gb)t*0rkPzE5L=TCod*Qsh&Zz}dCtdd|&oDwx-PHD6YB^xly zfeL86UNqzz>nZvhj<)edy!u2<(D0O-F2sGzZu+ms`lYM;H@J+Ua z3oC39*IDkl`IER)lmEY|<3R(4mVC)v8Hf7vB8fe_#nl@S>2gTb1AVQg=r*x>f%#12 zE1i-c0s|<-161&YO<(i;rEp}p%oGb*7FbguBDI+Oph5Re)MLj<= z=3NpT?6Gz!QM$7>J?EZuB;4(n-KiL_t{l9DBYp$J4Be(~`}K$Lx`a-S2r=9ZMNYHD z1CU~PhlYtc+h50A7uI#Qfi)00U6A?l%yJo?LJ)JwzVg9Sv%v_?VHPXV7lpqq_L7p~ zr4|~}fn_0kKk5V|3QFiK0_e+8cSYGV7p)T>GaGLT?FQ-g^!>DN3W;o4HCi1wV1h58 z)HJGX4R@dHK#Wb5X;-4E+>VaJt~@+i(RsO@Xy2c6YU+gEZB;Wr=TynP^)FTnr97#F*;bPX- zBG(KWFosxE;~Lh~X~X`~-&U#71O2!m`@#@U1bRu4hSfkjCWR`<98HS^SHPOT0|#OM z8`>a#n`TLnF(?&Qwxys0cwfTE74g*!!H32#-;rvsCK0ie&e}K_U5%ixxBOcDcV!67 z%c3`ZV^$e}BbEim&k4_N4M>y}De7~iPAOz;*8+zl$LbZ5$2^F%Ga@XQ^M$RhkL~l% zG$L%h{=yR#KHVzI1nqU6wmjfQT%IV+2+~%2op3<60TYYYY(jnh+q0=(2tTr9v*EMP zI{kWY5%S%SzwToBMNKC|*zq>KWBN=PVa$iFgZ9f>MqcXdnSO*vbqR< z9@&sP9~-m)&Cg`vPy#zh0_Te0i-{D#Ed}F47kJn>fN-f<`E7N;gN!J7>c7 zl)QN7{Y3Vj>@_xxT1h=9b@NhVdlWam0Ckt-G9pXu9%0wnownkatpPI&!>)H@cEengF*A_HF>Gl?Srs6v@0^f zZar%(o+-S@?yQEPTIJ3FGz#27JcuJpM?NJO6bPk1B^Yuj_hQvvmrpbubddTk<7?eE z&N##*A#X3skkH4JXQC8phriCC?2k1A-ae$Xa;kabGpv!>xX(~D&_zU~U&)dPgzweB zPIb&p^fieAdgVZ|J(9k-yj_?CMIP!0qfQA^O0Xl&H4WVD^834iH4}v=EV7Dco?!%B9C^jDZGE;? zlS>c;Zz*-I9HNCae8)1N1!bHOVx1brx%pVhAq!CG?21epAgjo^q2Zx`it~^iZ{DJ6 zzon%=3Sv(o6NGrWGI(Nt-uBJxBaL`L3DJuwu=I}_lQ=mw$-z|ZH#!E^w76xX>k|Ck zt0avW$|-@teK$xH$+1@d4DpR3^nS7murc?Thve|MS+_RykDz=;hx00YJ)o(K}Uq4E|7c^xU z0BY|Dz1@ee6l>>Vx5ljtZ#_Ug_F)m1ib~X3XPxcFVUxGteYUm4c+2LSS z#M!_GH(N#{unD`z{rY9?AoD81C&UkVFK~%Vr7CpR?_d0x9;omrjG*a1-!XYO0^p?x zcU3d-nJ^Uq2xNDJEL*<-4SH%J2@d*qDxI#EFT{6$^{>qo)}2}-ZFM+0;_m=Kq)mtn z2gtKtaylsYM+}s#bsBRPtUogBA&ONHXtXeAkAVuighd)eQj=a&h1@P_#+h4#1JIp+ z=7FsDl`%y1e1;}w>a8yOTOK=}J8wBK@wTV&-*$oD^(|eIF}vN5#+7GAImw5xKpC2X zg!>Gs*)Qx=vhRy8;>!krTF-Y&+tc{-`i~`mrW3H>tOz4-#^&PvD-=BABD2O)i7)YY z8iI`ic=1$QS{BBau6xGmz8-CNce97E*J&%eZ^=7v4o$Y4i(1mzs-8o`JLg?oI`6Ds z*S8n&&v^FzGQ$m^uhe~m1AK#t%#j)iV0*=s&2Q8IxUHo;W|8J-3}7p3GRezzIMsni zHsVT^UGie0L@YFBtX zSFP+EA+S7jO?CH|!8z&*-R`-_;^v9z;k@i^Cp$>q3eaL`(h9@k6wM2N#u*|zUO{ZX z)(xcT_MX9q_^jf7mEond$XU$kjK#7%c-bvXJU zSE}p|;ji(;d4L-N8AHZ9zE>%H$NjDJ{#vwl9?A~EwOUi{Si5|)S?5FsRx2G@Znn1A z73Jw#JZk*6Rt4vc;k|q-6QWzxfOTx`?J@N3n8%EOR#_q143wQ4gyPBo@HYgoj{$6_ z;Cz|kSb}|a^?+YiGC_Nc?0LHTIB^D0ZVw>TA?gXZLzrU=_SnQ*Y;92UHZ0e~B;l|) zvTul#1ETtfjxtP3Zq&|E69MlMXvT^ol0H}-R7nmLRc4{8 zLqmn(9k6I;fEeT}&5!SyCkDpWAlOtF^@_@C!@AdQsAY6UtyWuard>~4l5vR>4Be#R zus~^!RFuuL<$>M}u?EBC2Q7#a)PQ&=s{gWak!|XDZ_rN7Kml&uNKBwXJIPJV!M<=w zv)6CFEu_=>dAyxr*@Q=mAwk|M7R4X@SU0(v7idpXw!ASOuwZp;LwlM(*aXhVLtfYs ze2{lJ=}JYFlx`74kwXbHNEWHWO0iCMK?-h8uvdRO`@WnO2iF~L)G9tPVy#lPpIL`x z8YB}LgbFv1YSz$j?*t@W4rzZ+YzZi%J!>e<4vC$#*1C`#YYU# zz`M53J!)Glt7@foBM-mifd@Cl!F7Ko@>VWP3l>(Z(cRZAW@!1YX@6t4>=bbzBRtswlVEB~tLRTFuI* zK704BQuTp%OF*CFNiVQxi5Ad>otu_x#yC1hs2hNz_XmPO)T4#;#)N`t`e;o?i!)0U zVNgQqh>SJWhkbdPkYo|B0o7kO_$QN zgdh1w(WY}5ugtgfydFe3dF3`!ba(mF#_d~eujTKM#RXgTi={ac#hvmVf?98lS$nrhpO8wBrsRu5qb z`fc-9s~TWx2!!PX*PZ}M{u^V`pJU0Y4*RdZn%I&#LEO4<)ezx%D_>ZmR*YEa6(alR z8bz^ODu3R*U<6F$?{Hqh+9KMb_ur?m^F1 z=Ia|ow#|%I&&^y()O{nstC9lG39@+5EK@@gC;Z&gTgq+-CdHS{3-bSsGl+Ad$*}^JNJA zaE@n-rB!>zm)CW06+LyQ6!7;NK_Nj#;#}RCqlzGawQMU7C~4Mr4)t|ER+(8rg8ER) zEg^yeXL}yjT~50)Pi#*y6Fa<2JPi!1 z1>b`+vIwVrlPnW-*3pAje5yQCNs%%4(r0Z4f#urY%jzuD2wiPnkEctw z%Xj=E0%_S>G0Qzx9RY#?C(^Kwu8Gc>tOv*Rav(WR0W#d zhWgP$Xvp9M%{xGkqVOz~-bn|4Qwv%~n|gy*pEi@9e4hoaYb24K>^?M@jFpMZ;Jq_{ zu$sDwemj5SdEXD%^2hIF5l|SsTJ+KqtJyXrst&IjE=Q0O@-XU4{N@dnNtdLAz)Hv- z0x`F^*f46g-03zl=BfrYK?T3tXriX@?Y^$oXKk~=4Hv5Nhp$knP%YECg;$gqMN+~> z62}j>W$9icSWX2jqH<|0*TVC(Khj!Z#jR;YSbgH}V_l`LGTMnd+6EO%@GGK>hluy) z&Y3n*`5h={t^xB_jTI{V`D0NcIutS*92bF#L=_o}CdC`yC`Ks?8A06uz8_bj=HmHC zlfh)NLWeeX#tc1UST!IOc0++py^1xBDy=v?45DWlu9pT--fbpGi=P8%66FiCn+7C4 z^>fUkAW*@(Um7=nf%XAhHb^)ngG>*H8Sl_+#h*v)Jon<0ld+ z&O8>Z7$2!*qQnb&eTbQ{vMih3W~BUYA5{4%8w%rD@dzi&?ZL_3xz8V@CZt6?$LE#A zGoHGP65r9W_mqt<-jIx_hCvs59BTMy*{)@68mLyxkS3@mOBid;OP z{vJ(SxR$JUe^7MWgb=*Tg`SQMWF%Gh$5;t}*lio?V20=ajl#mh1XRaJ2MGry-W#re zA=MgpIihLN?G1flz12G$%;wzlvQJ&p`JhjGTc5L9bi(k8l3GOb1k`aqw^Oat?kdvv z_UUV)eAm8JN|Aa#Z=YW(SyxJ9h9?^lDe7T2YN~*(3`J1~bs3TFUnp9Xbc+|+L>vqo z50(Pp8#a{!*HrCTErVX>`C9S$w6$=7`&l{lN8mMk!uLSb$O&tV=cR(5&pcX$qfga zno`9fa%-rVOGX#IBL+Ux?C{&U8$X}IKJoOtc$e$REQ;2YuUL}HaNCW49`;zF*J`at z_1B{v%(pr>_x8Z|`Q83vB$8Z;aBL=+CXk_1wQL%?md|n?H%xaBK&hd-Pzgy(P3dA2zvam&9-Y`#ySAv& z6l8BF8?|Kk5A!#uj3|;Csi+#7Pj}jVx$P+3(QoZl6pDWD4pgI6oWc2WBD%kxWF8h5 z%lmt+Odb}dNjjMoYzN+k>TWSU^=^A>1!UsP>tLKJqe;G=Ct%vEROgOZ6~kAvXOCn_ zQxq4?LfDVFMX9|z(oJ^EMBs6u?sLYQ*h=p~>*wrE-@6YkGq$9Af(*hrvAbEs7I+x8 zA4D8AFH{PdVtSTk+rj}XoIEwdqHEgEVg?8;IPGuuv?_eV8*4WpY!h%)R!i>?tG1`k z!z)`Ltq5iwCy?QTfBYfdq(CPO6v;_Qxp2XGgda1hwtiJk34Ibu!0zw*IF;`>7yOOme)Ym6NLtiNTAb+hO$`{4DkKXT zBk=a4clvFyKhj$Q={QAZnOx%%a}17wA87B{4E(xbT*iGP&wU1dut~Wis+B*twRbOs zP}R=K1;*A*#7=kwmfC3K4EtNZM%#bb`}|o+^%=khUmLmKLvyCBGV3L0Mow@yE*J9Z zLnyZ;;pNrfmUlIXjv7^VX25WKZE&7VxE-p@BNYGG;Ug`BZ}Gx~iP+8G4TVo2R6GlG zf6RgtwHTNc*s8Lhw}pmZ16GVBf(#;U85qy@wK-gH<|ObmWRDkeKrhFHX*)6mdQicmYY1DkCN*n}GB|n(6MDS2r5>KSD0!;|Wc9r#;-A&_C~m zy#_&yM>ZjJusRgwp+_1pvL9I{w@T#EUzju+ox>7ed-L}<D`RF|CTqWVbX7hSb`}-K*-s)ye7QxRlAG{I%m+tg6^f0 zB$;L6bg8CvN>Ao-t##l*e}zdy1vP%b39sg@y#0KJBkf%JkZ8_sd&Z6?BW-^>SU!j1 zYNrwBkT*=Fl(zhA`RaaztT`rd1ODOcdjpv&MM%xV<~`N#Kz>_4)TMHGNaJaFJ^f2Z zr+fe1b8iblq3)H{`n1#KmlIdYE4}vXixz~Nj!3-jU=G-J3h2T?rFj?c_FpoMkR&=i!RPiCK#_Xed{;7juhu$5Pz+$OD%%$yG++_8=AzN;=#rx~ z8SY#`6y<7@!{_bc`(Q?e?(%-|x3kgJNkK~EdL#FHiG_pR2*?TP*W@tp$ze0dKTisl zQaNEWa6{9yIQ~ewnSkVJYm(m?O(@V>yjcX&O*`xXhu)rdMh$^;B zW$KU%mQ0uacH~AVZn~DG<8bS@iGIX1zZAjd36Bg5N}UCL7J|-fj|3a|bwzR2wYb!| z*owIlvmOK5MCsUS+ngNM(0b( zRI@(9miarT!es3_eP zkJsC!jF<)0IP2H_K@ac3b5*#=#0D>?)lhzQMUEFoiTl*W*84&v(9(8t!rP2S`l05o z)G6Ynn)P@o`wcX^ zj?hq8k+|_C8RxQ#rZjXA`DBu%+ws6spRS`^3M8A-W_oGe;-ub4$?-&S>39t*$1_O{ zd4uR|u4}at89L<}T<%)Si$V7l>aT|l#N>Q}TS>&CQMg<>yPhzg0~=8}L!5Zn+BpMu zlHN7I%5$9VI8fp-zx`FH0HzKaP3a3TnSG@P$_iHYPuH^WdL>v9s}-I~yFg4u53kZH zAu0QQ%9m1^3Pa8)_&PfQ{|vd@_aC8Td_*mYRQzNdiSS54oj@Jb5Z$V|2c^ueOiwY^ zq+x6^m*B1yOkN;5Z#W#EtsA}9i3rLtvMmy+m4)krK|guP{a>;a102tmI3pA(OiP#q zECQDF{-{SKqrw1vv>-<;>37v6N=Tb_+k*)caZuqgubt=Jk(pH=d?T~lpUvGNfdotU zLcBeK8Wd7i9)Q`#Rs1@!4?CnwIx8Y{b!Y>1l7n3R0c=MA{n!0h@!TU=#) zp9FI8*>U!<{xZ~~!+14C0R;a1}?Dj)2zgOp->lVXQhj-?Z9rq1WAs+^(-rv{8QVd5a5p9uAhZY3}dtNq#5xpdv80fGLrrJCb zaU=F{)UMrIW65KAzd=%9y&CpK>bmcE7N5aoH~Tugdz|S4HE5T}<1U6k17C>bjwvNo zCvLpa2v);yh~HMJY$8(&TPhECDLGZXGL0LJB@{|$WwC=ToRFBH`RF*e_+6F%{zcru zCauf#KF_AigpaAbU3GEj%oCmrr(K`)Ui^V%@-%It(wwp7by6Po+3s?leT2El>2u8E z!DN1O)Mm0+=fO1ZhX&xC3#izTmGr7v>fR-}8kv6YSna@>QMA`U3~>xe?ezohbJZ#M zlNP&VuDG_J>Az(vHH$)dk{b=4!04eArCctPSJ>EVsyzs3J_%YUy-k z%5?O;wRuyPBYmOuy4Ke770_r5*Z;LMUbbWZ4u72L$m{hNEq%j&x-sQ7{!CWgN z2ofzf-Ax3%R!`mVPP^ zAjQ=bWmkd<^S6xC)t78n47~M#C^^$N6x;k?kP{+bv11{;WH12A`93KjVsRVh{R@H39FKY?pYOXerW|oHSoqLF>%{_lRTLv8}aS~EQL2O znt7Y#`5H&?gsH+LL^0C5WQD1g@#M?CuwmhtsEJWw2{o~V_{p=gvQNP?vW8}6!zbnl zl3+O8`Azk>PQKUEG}KiNv|vXvuq-=r92BRx?k>^E^F#NivAgs*T-hw%uQYOuar}iM zZLU%o|I*(-FrJHfEi#~~A6n5dCFSK6qU(r4RoRaj6)@9w02t8UoajxT-DT#G2_t@u$__rB% zXb+ts)!PIAIsq0z;?IfHk9#S173d`M-$E3Ttzu0IT1;$Us~C?uUFQjL%@}tqV$h~r zic+e7Q+!2kQ*(JLmU#hq1NfX6Jdd%tGibSGMX8S33)_P$O_nTwK?P%*r;6q=>XJ;j z$_fv!d_*JI!{th6Xax+8wg6d%QJ11Gp;pw%)(BQ#s$CGey&>_Jc!V_kb3L_fj{1ic5K{lI z;1<$)fa2x)>5nVM1|u}W$iPT#mBJ+3$<@Y(-IvMiI_OOuzXLAiv?1MGa%r7kAIlxp z*qS{_hqk+m*{JECTO5`%j&mwFC}s^jS1iZJZ6Sg2IR_MbYIVdm1-0pD`2+;6!}}1~ zGj{&M=(~<4gKXCaUnN>syNghAvdmH+Zf?)@ukVlbU2LId*;QQHGMui?o#KTT#xi5u z)BU@q>pG{^#^Sx-B4g9CEhjpz&OZIZuyET>rEr^(FSS(lzEcm};>z}%L!L^W zvP+`|ok%$ksnfE147Un8=)1^XCMd6ARpjX?uDyL?H3%23#l`1w`}TRer1+v`RpM(_ z+mY~NCaGhYhy1<3#l~JU5Oq+ z>+>rzVt+1l9Eshev7djsbhJZ|kz7yV=u>s9+P*poWP|kKvcvLHvj#Uqt+D9vE+A$G zce}u(W;ed=()iy__sg!-)yU97ct@Ke)6WT~IeM+TWQ0ujDNhF_g4$RWT?P^r9y;;4 z$GL>fWw%$idAu{uA_tUhwu@_4n}17%KwWAIdFgxiEt2NdJ>y-Q?=j7DYv+>l+wI_y z$>yW)^P_0^0;OB9-noU1dVPy|Bq+*C_B+4P4ATn49oh=iOqUMiC=cFLcs3_YBXy(M zp{=z!$j*+^swwn(l`^lX#>s9pY2fGqw#j0T$sv<)>IAYBwtPQ!N>6ZZAI*)N&;IgW zN4yzTwCj!tM|1Tkkxe(Dc4I&KD(6*SP?}@RFG(;nucw}P!G!l)UuP{X?p}8s^3v^N zkgtDBsr7R}lNzEDd!TyCZFe#Mm+lUm-ER5TM)Xy_&7?*-={^&?$A|@05e5zM&1|jT zz?x4_>S#H4chlLG&B@P$0w2x8c=4b7bH;eh4yy@kk(m<43?&z^MrsvHE;^O&l=CRV z?j#CNK8kx%#_<%7nwe8-T^qZeixTg+8WZLYx_jj|UV(y}N-GrI>n}7^^Tf>&O(c&m zfM7ij8=L+Jb?!?Wqm}WdjPm*^_QqzCrT2F#Ysu=we``uc#w?jg_RNpm8-s8tJEP#q zGh7K9P*?8gTt$<5gzgv?2I-F+(r3v6Wv(mbBxxrug=?JB2g#d}&S{m}Y)XY0I|y$3 z`3ylCM-DeuLYxJ5#*4SRt-2fAw=GEesq)0t7I!&KuUl(V_tH`$S^+)NfG^pD%iTDV zT-V~RyLvN!^+tQSr0UY$9`Lfpc1c|MeGZOpY?ssB_Bt1oEEXkVmX&{5U zadWrVfc)?94O;yfPu_8cK#`OqS&Z1IU76zdP2J?c%e!_CBY$_c^G|FJzrt$PoZLK* z-qP^DcD>Wc#~-1f!Xq2Q<}U0?{U&yuC!9o}M)4yZOqt1|ZPnHb>NM&~R}9W7iIa(p zXSFGhl5iVdkvOhaF9}Q+b`DO*Zo1)L-TraC+;fWXjoqDO8+@OxmX&(_tquJ^eFZUQ z_;&FNHuXh0Z-ZMmu0Uw z74I2O!*f4e&p%8lbTHme*Ok*7oNr|b)zZZozQkZf(bJbgEhf3-B-dmv0UbHJ; z*8obn+~#Lp+PiFO6)-Z`;81n6z-&s3H@## zJG(O?OQ68OfU0;-@KsM;2gdjB*SrwRRxG*RgBzj}WHo!uH85up&bs~M^7w+85hTS{ z0SZhb!G}4%RZbBj0{9TN4QsiF@>VS$6)7vJn@pS`!hY;VtMwb>DoVO2Icj*q?YYU{ znk0c%#>vF2X{t2Vjn*$Z0XUkiN6Ivex&zhIXP$L$Snp6UJGAUOZJ%K!GuzPbO0AZs z(IMd0tq0pTcM;fZJxU$X7aT50(S^Jg@78b9nE}d(294<)PIoj<6RHPm(zSivN(!!L z=j6Zmz7HE_oUJyRrKo6+t7pCRt=tpBF2{$CC4NbKS?K?(Dj*{EbRwc8y@6S@H zVQx`e^RJDMmvsi&sVwf3iZRZQ?;XxGlNbH?7#t1ehofdpAO)i*|5L`9 zhC{(<0od|ISxYF%PSzNskzve`T?UPH>{P~9vfYfy2r+iXFxEu2>>ih1Wd++l;&slz-^XL3H?+?t04|-OBA;Kr^QU(2nH7bQb+USH3^~U!-HS%6m z>Igpr;(?l2`m>L&C~Uqf8QZ3N{0=wCvSOW>K0KzR4-0EpeYC#*#nl}2%y$JM?S0Zn zNB>EfLR5dTxDi-uaE@>4z=Ov{`h-&JWMX|qHL{@>#VdmYR3JoTk0OMND&_10`Vv+~ z!P;j>)976zWd=vSP3~i=9e>G47@F#r3g#zB;K2Rw0K%pOIbT zQYzi9H7E)@mscKVgRoZkR-duTclI>A^M0WM`hi3)iI_$+sqKjdJ1rWy($%GjfK7cLhjW>jZk-S+y>%z5&+r<(dVG+^mNcpg2l zkwRtHNom?oZ8u!8ZdB%EEW4Z1opZu(H2J03WnHF@4yUp9Yy7XKjKDht&UKuAr+L@L zugIlKx$`i!Q9ua+dQnhvO9EWRi}6=Gn(~$x_p@u%m@*)Oj{0fs$X>=T_kS{lmVFz2wQ4o2rQE0_S8a9(>09I$A+ja>NGJAU zNTvZ}VLO5)+B#JiG(Kn~{Z>a(2_Ie=aw60i?mis<0cQhYE20nI+Yj%ziXF`0^t-X( zLIHAoQ0dmuWYCi)`(eM6mtB4A1l8NaN`US{ zdc$GS3k5fkf9W%8*RyvgFx=q)N`fPBPsIJ%TCoblGOOK*Ds;t=vGpEAF3nj8ND|I>8% zGQ^dCo}nVWlhoee%p8Bv&c-FmbpAyINy@>Z`_7Ad>d&%?3lW)CpUMp4(VCV(IqDCN zeo)DyxMn*#X>(MmQ5U~6i;S5}wB}eV_iS1FYUP3Mu*)BM0$hI|mEwB01zgZ~tuTp;wdgkG}{&T{| zxq3FowK(iY?V}%>W}E_fCSyezsK(;Np6S2bgLsejrqbq_b*+(;e|s(lbUgZLX~MT= zo1tY0%tn)i>pJo*6%i4~t0k1I$_qjqhVAuVFq$#+P8DAm`}dah_2RRZKKY>KHl#no zumoyQV?SI_8!B|z5oMy*7{a7yYWk1 z^BXm>r)LEXSzd;VxfPH_yRBS`lE)@oN?z_Mue>4jBYX$VZcpzHn8-El4qCvDSpfrr zp{(*FfVNn`kVI~*{Fu@r955(K;H#xbmDpBI^SXP~4jMA9u{(L3-$uG-c)p)}fb%O~ zEWpzJ})(=+<oJC1iqVL z;c-qEGa*j5I`?wpMcm@Z%V^a5FmB5SksbY$_tWx;*7Ko8vi?-kWjp_``2<7ok4$$% zmBsoRHu73jw7NvshR;Rb_H*Y#4zeALC1lf+@u{gpH=&T*Hxmih;_}4uaJl$Qmw#D_ zid?_o7q8u#Q|jWpJnIxM8~JH_3PRCC8^-Awozg2v`kz@~&O9jeH0RG8H>@;_r>8XR zf4nDWzLhyzCwW0rPogOzJo|cC7A}zCjCMa5(>ACSmEy@AyC3zl}8-#8bV3Z)_ zEs2(J-pRZAWbE}KeOsmvSs|20NDO2+oF44YQJ%9i@TmIyAqz-cw;M3JQ86ZkbUJypBG4j66~H0 z-H|*JI;sY+9-5g3bFW(X>8wm{tBDeT#Qf^*0Ty75-$N+oy9*aVpqr z*%Ls$*e!w;*-D>3zdtcY8a9v>OF1Ifr;f6Ih3NB(#V6`?=PupbHeD$8*zQwol)4ES zWx59&V!*TDV;VbW<&R7NJ-cQk24q)@A1obzGiy5d7*k`mT5N$UYpL(uv|$F-v^vf- zmn~?G*QQp}Ggjh1YS-Vm&*+OVxe{1sf2ut#^;rHX%i}h_t0?Ta;WZUkG}7wi=H09{5co>aVN!Ya5iQ1^CD(sD zv$6ie31?6gtmD-JW$q6~Td2*#1BElHk*ZYz4|cm&@uMOoIO)*;q9b`h-z%Slu#qh1 zw1|@a|F&oDAG+tdsU9g@4c@C{Pa*4S_k})vHTIo50{1EX+E4dAe4kRZDKK8Sqghj; z84Bt5pYRzbeIr8Qf(d|C0l@47VE><8IrOP+RGDhBt!!Ry8pB)}Lnd7Q@PzK_d(*a* zp4}PU^?k?Rs>rnNYSN={_!kD1L`Ts!-Ecfhq0?@8m5SuoUk9F^lkdkh+6_5W4l6J< z%G;-v%^n}&-jCYuV$!6=e>(oYiMknR2CBdO7rcrE43ZGZkJ+r_`DFB$Crqm$N&n4ZWCDsUwg;x)n;DlcRzeF zb|ZH?_Bvt1h0j~#v3?
    From 76f850e6c23dbb5185f59df8dc22ed4499be44d6 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 2 Nov 2020 22:53:44 +0000 Subject: [PATCH 13/44] changes to Twilio/sendgrid config on prod --- disable.py | 28 ++++++++++++++++++++++++++++ medtracker/email_helper.py | 4 ++-- medtracker/views.py | 6 ++---- send_message.py | 29 +++++++++++++++++++++++++++++ 4 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 disable.py create mode 100644 send_message.py 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/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/views.py b/medtracker/views.py index 5b4574f..9c31246 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -216,9 +216,9 @@ def reset_password_with_token(token): def send_reset_email(user, app=app): '''send an email to reset the user's password''' # look for configuration variables in params.conf file... - msg = Message(sender="ryan.neff@icahn.mssm.edu") + msg = Message(sender=config.mail_server_sender) msg.subject = "ISMMS Student Health Check Password Reset Request" - msg.sender = "ryan.neff@icahn.mssm.edu" + msg.sender = config.mail_server_sender msg.recipients = [user.email] user.reset_token = randomword(24) with app.app_context(): @@ -229,8 +229,6 @@ def send_reset_email(user, app=app): db.session.commit() return None -#### index pages - @app.route("/", methods=['GET']) @app.route("/index.html", methods=['GET']) def index(): diff --git a/send_message.py b/send_message.py new file mode 100644 index 0000000..0098cf8 --- /dev/null +++ b/send_message.py @@ -0,0 +1,29 @@ +import os +os.chdir("/home/ubuntu/medtracker") +import datetime, pytz, time +from medtracker import * +already_sent = [] +app.config["SERVER_NAME"]="ismmshealthcheck.com" +message="""ISMMS Student Health Check- Having trouble showing you took the screening? Click 'Use RedCap style' after taking your screening to show your COVID-19 attestation page at Mount Sinai.""" +with app.app_context(): + pts = models.Patient.query.filter(models.Patient.phone.isnot("")).all() + #today = datetime.datetime.now().astimezone(pytz.timezone('US/Eastern')).replace(hour=0,minute=0,second=0,microsecond=0).astimezone(timezone.utc).replace(tzinfo=None) + #status = dict() + #pts = [{"phone":""}] + for p in pts: + #ptstat = 0 + #taken = p.surveys.filter(SurveyResponse.end_time.isnot(None),SurveyResponse.start_time>today).order_by(SurveyResponse.id.desc()).first() + #if taken!=None: + # if taken.completed: + # ptstat = 1 + # if taken.exited: + # ptstat = -1 + #if ptstat==0: + if p.phone not in already_sent: + print("sms_trigger(message,"+str(p.phone)+",None)") + try: + sms_trigger(message, p.phone,None) + except: + print("error in sending...") + already_sent.append(p.phone) + time.sleep(0.2) From d5abaf6dbd5ad42f22d3c9372c88bed3cf57127d Mon Sep 17 00:00:00 2001 From: ryananeff Date: Tue, 10 Nov 2020 14:54:11 -0500 Subject: [PATCH 14/44] resolve security vuln --- Pipfile | 27 +++++++++++++++------------ medtracker/__init__.py | 2 +- medtracker/views.py | 17 +++++++---------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/Pipfile b/Pipfile index b3453da..6f7604d 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,26 +41,29 @@ 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 = "*" -flask-cache = "*" +WTForms-SQLAlchemy = "*" +WTForms-Alchemy = "*" +pyOpenSSL = ">=19.1.0" +importlib-metadata = "==2.0.0" + [requires] python_version = "3.6" diff --git a/medtracker/__init__.py b/medtracker/__init__.py index 67da3b1..6af6458 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 diff --git a/medtracker/views.py b/medtracker/views.py index 9c31246..cb218a7 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -252,9 +252,6 @@ def serve_survey_index(): except: s.description_html = '

    ' + s.description + '

    ' surveys.append(s) - for s in surveys: - print(s) - print(s.description_html) return render_template("surveys.html", surveys = surveys) @@ -381,7 +378,7 @@ def serve_survey(survey_id): survey_response = SurveyResponse.query.get_or_404(survey_response_id) formobj = QuestionView().get(question) if (request.method == 'POST') & (len(request.form.getlist("response"))!=0): - print("saving...") + #print("saving...") next_question, next_survey, exit, complete, message = save_response(request.form, question_id, session_id = sess, survey_response_id = survey_response.id) if next_question != None: return redirect(url_for("serve_survey",survey_id=survey.id,u=uniq_id,s=sess,sr=survey_response_id,question=next_question)) @@ -401,11 +398,11 @@ def serve_survey(survey_id): return redirect(url_for("complete_survey", session_id=survey_response.session_id)) return redirect(url_for('serve_survey', survey_id=survey_id, question=next_question, u=uniq_id, s=sess, sr = survey_response.id)) else: - print(request.form.getlist("response")) - print(len(request.form.getlist("response"))) - print(request.method) + #print(request.form.getlist("response")) + #print(len(request.form.getlist("response"))) + #print(request.method) if (request.method == 'POST') & (len(request.form.getlist("response"))==0): - print("failed!!") + #print("failed!!") flash("Please select a response") try: question.description_html = delta_html.render(json.loads(question.description)["ops"]) @@ -489,7 +486,7 @@ def complete_survey(session_id): return abort(401,"Your device appears to be unregistered. Only registered devices can view completion records.") def save_response(formdata, question_id, session_id=None, current_user = None, survey_response_id = None): - print(formdata.getlist("response")) + #print(formdata.getlist("response")) question = Question.query.get_or_404(question_id) survey_response = SurveyResponse.query.get_or_404(survey_response_id) _response = QuestionResponse( @@ -943,8 +940,8 @@ def make_cache_key(*args, **kwargs): return (path +args+responses).encode('utf-8') @app.route("/surveys//responses/dashboard/loaded",methods=["GET"]) -@cache.cached(timeout=None,key_prefix=make_cache_key) @flask_login.login_required +@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_dashboard(survey_id): start_request = request.values.get("start_date","2020-06-29") end_request = request.values.get("end_date",None) From 98412604fa87c4646f51766651d9913e38822584 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Thu, 12 Nov 2020 16:21:19 -0500 Subject: [PATCH 15/44] policy change to identify students; match EHS screening; emails to users --- Pipfile | 3 +- run.py => app.py | 0 medtracker/__init__.py | 9 +- medtracker/database.py | 9 +- medtracker/forms.py | 40 +- medtracker/models.py | 32 +- medtracker/templates/_forms_edit.html | 2 +- medtracker/templates/_render_field.html | 2 +- medtracker/templates/about.html | 6 +- .../templates/email_survey_complete.html | 354 ++++++++++++++++++ .../templates/email_survey_complete.txt | 18 + medtracker/templates/email_survey_exit.html | 323 ++++++++++++++++ medtracker/templates/email_survey_exit.txt | 12 + .../templates/form_signup_register.html | 18 +- medtracker/templates/index.html | 2 +- medtracker/templates/layout.html | 2 +- medtracker/templates/survey_complete.html | 74 ++-- medtracker/templates/view_patient_self.html | 35 +- medtracker/views.py | 44 ++- migrations/README | 1 + migrations/alembic.ini | 45 +++ migrations/env.py | 96 +++++ migrations/script.py.mako | 24 ++ .../9c76e093b8c6_initial_migration.py | 50 +++ 24 files changed, 1090 insertions(+), 111 deletions(-) rename run.py => app.py (100%) create mode 100644 medtracker/templates/email_survey_complete.html create mode 100644 medtracker/templates/email_survey_complete.txt create mode 100644 medtracker/templates/email_survey_exit.html create mode 100644 medtracker/templates/email_survey_exit.txt create mode 100644 migrations/README create mode 100644 migrations/alembic.ini create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/9c76e093b8c6_initial_migration.py diff --git a/Pipfile b/Pipfile index 6f7604d..d3f355e 100755 --- a/Pipfile +++ b/Pipfile @@ -63,7 +63,8 @@ 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/medtracker/__init__.py b/medtracker/__init__.py index 6af6458..156ac7a 100755 --- a/medtracker/__init__.py +++ b/medtracker/__init__.py @@ -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 @@ -34,6 +36,12 @@ app.config['WTF_CSRF_ENABLED']=True 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 MAIL_SERVER=mail_server_address, @@ -54,7 +62,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/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/forms.py b/medtracker/forms.py index 6329765..234f63f 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,35 @@ 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)') - age = StringField('Age (optional)') - email = StringField('Email address (optional)') - phone = StringField('Phone number (optional)') \ No newline at end of file + + phone = StringField('Phone number (optional)') + age = StringField('Age (optional)') \ No newline at end of file diff --git a/medtracker/models.py b/medtracker/models.py index 969ec52..69288b5 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() @@ -233,7 +232,7 @@ class SurveyResponse(db.Model): 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 +246,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 +289,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 +315,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 +325,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) @@ -371,13 +369,19 @@ class Patient(db.Model): id = db.Column(db.Integer, primary_key=True) mrn = db.Column(EncryptedType(db.String, flask_secret_key)) 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") @@ -396,4 +400,4 @@ class Device(db.Model): 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/_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

    • Make, edit, and store secure interactive surveys online via our lightweight web interface for any healthcare decision tree, workflow, or follow-up protocol, and share them securely with patients at home. @@ -23,7 +24,8 @@

      Features

    • Runs autonomously, even while you sleep, in the cloud and accessible via a lightweight web and mobile interface
    • -Backed by Mount Sinai managed HIPAA-compliant cloud services +Backed by Mount Sinai managed HIPAA-compliant servers and databases +
  • 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..3f06c50 --- /dev/null +++ b/medtracker/templates/email_survey_exit.html @@ -0,0 +1,323 @@ + + + + + + + + + + + + + + + + + + + + + + +
    + +
    + + + + + + +
    + +
    + + + + + + + + + + + + + + + + +
    + + + + + + +
    {{record.survey.title}} Taken
    +
    +

    + +
    +
    ISMMS Student Health Check
    +
    + +
    + + + + + + +
    + +
    Please stay home.
    + +
     
    + +
    Taken: {{ 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.
    + +
    + + + + +
    + + + + +
    + 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..aa031dd 100755 --- a/medtracker/templates/index.html +++ b/medtracker/templates/index.html @@ -15,7 +15,7 @@

    Welcome to the ISMMS Student Health Check

    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.

    diff --git a/medtracker/templates/layout.html b/medtracker/templates/layout.html index b9e8887..61a6c29 100755 --- a/medtracker/templates/layout.html +++ b/medtracker/templates/layout.html @@ -113,7 +113,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. Built by Ryan Neff at ISMMS.

    +

    © 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.

    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/survey_complete.html b/medtracker/templates/survey_complete.html index 8b4e9ee..b988bb1 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -1,44 +1,6 @@ {% extends "layout.html" %} {% block title %}Completed: {{survey.title}} | ISMMS Health Check{% endblock %} {% block body %} -
    +
    + +
    + *********************
    + COVID-19 ATTESTATION
    + Cleared for:
    + {{ momentjs(record.end_time).format('MM/DD/YYYY h:mma') }} +

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

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

    + *********************
    +
    +
    +
    +
    +

    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/survey_exit.html b/medtracker/templates/survey_exit.html index 5c9f1b7..ffca213 100755 --- a/medtracker/templates/survey_exit.html +++ b/medtracker/templates/survey_exit.html @@ -18,4 +18,21 @@

    Thank you for taking the {{survey.title}}.

    See your records

    +
    +
    +
    +

    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 %} From 6c733324b6b39523c3736f7927b37fe1c5e3d792 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Thu, 19 Nov 2020 23:22:17 -0500 Subject: [PATCH 28/44] SUPER SPEED STUDENT DASH --- medtracker/views.py | 134 ++++++++++++++------------------------------ 1 file changed, 43 insertions(+), 91 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index f2110ae..f570033 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1362,101 +1362,44 @@ def survey_response_student_dashboard(): end_request = request.values.get("end_date",None) dash_figs = [] question_figs = [] - - try: - start_time = (datetime.datetime.strptime(start_request,"%Y-%m-%d")).date() if start_request != None else (datetime.datetime.now()-datetime.timedelta(days=30)).date() - end_time = (datetime.datetime.strptime(end_request,"%Y-%m-%d")).date() if end_request != None else (datetime.datetime.now(tz)).date() - except: - start_time = (datetime.datetime.now()-datetime.timedelta(days=30)).date() - end_time = (datetime.datetime.now(tz)).date() - - time_end = end_time.timetuple() - time_start = start_time.timetuple() - time_end = datetime.datetime.fromtimestamp(time.mktime(time_end)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) - time_start = datetime.datetime.fromtimestamp(time.mktime(time_start)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) - - survey = models.Survey.query.get_or_404(survey_id) - - pres = db.session.query(models.SurveyResponse, models.Patient).join(models.Patient)\ - .filter(models.SurveyResponse.start_time > time_start)\ - .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ - .all() - - def pt_to_pd(): - res = [dict(r) for r in db.session.execute(models.Patient.query.statement)] - return pd.DataFrame(res) - - sres = [] - for r,p in pres: - row = r.to_dict() - del row["uniq_id"] - del row["user_id"] - row["patient_id"] = p.id - row["location"] = p.location.value - row["year"] = p.year - row["program"] = p.program.value - sres.append(row) - - if len(sres)>0: - sres = pd.DataFrame(sres) - sres.end_time = sres.end_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - sres.start_time = sres.start_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - sres["date"] = sres.start_time.dt.floor('d') - sres = sres.groupby(["date","patient_id","completed","exited"]).last() - - sres = sres.reset_index() - - #devs = model_to_pd(models.Device) - #devs.creation_time = devs.creation_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - #devs_per_day = pd.DataFrame(devs.groupby([devs.creation_time.dt.floor("d")])["creation_time"].count()) - #devs_per_day.columns = ["daily_new_devices"] - pts = pt_to_pd() - - pts.creation_time = pts.creation_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - pts_per_day = pd.DataFrame(pts.groupby([pts.creation_time.dt.floor("d")])["creation_time"].count()) - pts_per_day.columns = ["daily_registered_students"] - res_per_day = pd.DataFrame(sres.groupby([sres.end_time.dt.floor('d')])["end_time"].count()) - res_per_day.columns = ["daily_total_surveys"] - comp_per_day = pd.DataFrame(sres[sres.completed==True].groupby([sres.end_time.dt.floor('d')])["end_time"].count()) - comp_per_day.columns = ["daily_completed_surveys"] - exit_per_day = pd.DataFrame(sres[sres.exited==True].groupby([sres.end_time.dt.floor('d')])["end_time"].count()) - exit_per_day.columns = ["daily_exited_surveys"] - df = pd.merge(pts_per_day,res_per_day,left_index=True,right_index=True,how="outer") - df = pd.merge(df,comp_per_day,left_index=True,right_index=True,how="outer") - df = pd.merge(df,exit_per_day,left_index=True,right_index=True,how="outer") - df["positivity_rate"] = df["daily_exited_surveys"]/df["daily_total_surveys"].astype(float)*100. - #df = pd.merge(df,devs_per_day,left_index=True,right_index=True,how="outer") - begin_time = pts.creation_time[0].date() - df = df.tz_localize(None).reindex(pd.date_range(begin_time, end_time)).fillna(0) - df["total_registered_students"] = df["daily_registered_students"].cumsum() - df["total_completed_surveys"] = df["daily_completed_surveys"].cumsum() - #df["total_devices"] = df["daily_new_devices"].cumsum() - df["daily_uncompleted_surveys"] = df["total_registered_students"] - df["daily_total_surveys"] - df["daily_pct"] = df["daily_total_surveys"]/df["total_registered_students"]*100 - pts_df = pts.set_index("creation_time") - df = df.tz_localize(None).reindex(pd.date_range(start_time, end_time)).fillna(0) - - df.reset_index(inplace=True) - df = df.sort_values(by="index",ascending=True) - df["index"] = [datetime.datetime.strftime(a,"%D") for a in df["index"]] + if(True): + try: + start_time = (datetime.datetime.strptime(start_request,"%Y-%m-%d")).date() if start_request != None else (datetime.datetime.now()-datetime.timedelta(days=30)).date() + end_time = (datetime.datetime.strptime(end_request,"%Y-%m-%d")).date() if end_request != None else (datetime.datetime.now(tz)).date() + except: + start_time = (datetime.datetime.now()-datetime.timedelta(days=30)).date() + end_time = (datetime.datetime.now(tz)).date() + + time_end = end_time.timetuple() + time_start = start_time.timetuple() + time_end = datetime.datetime.fromtimestamp(time.mktime(time_end)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) + time_start = datetime.datetime.fromtimestamp(time.mktime(time_start)).replace(tzinfo=pytztimezone('EST')).astimezone(pytztimezone("UTC")) + + survey = models.Survey.query.get_or_404(survey_id) + + from sqlalchemy import func, cast, Date + cols = [func.substr(models.SurveyResponse.end_time,0,11), + func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + models.SurveyResponse.completed, + models.SurveyResponse.exited] + q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) + df_q = pd.read_sql(q.statement,con=db.engine) + df_q_2 = df_q.pivot_table(index="substr_1",columns=["completed","exited"]).fillna(0) + df_q_2.columns = ["daily_exited_surveys","daily_completed_surveys"] + df_q_2["daily_total_surveys"] = df_q_2["daily_exited_surveys"]+df_q_2["daily_completed_surveys"] + df_q_2["positivity_rate"] = df_q_2["daily_exited_surveys"]/df_q_2["daily_total_surveys"]*100 + df_q_2["fmt_date"] = df_q_2.index + df_q_2.index = [datetime.datetime.strptime(a,"%Y-%m-%d").strftime("%D") for a in df_q_2.index] + df = df_q_2.reset_index() df2 = df.loc[:,["index","daily_completed_surveys","daily_exited_surveys"]] df2.columns = ["index","Cleared","Sent Home"] df3 = df2.melt(id_vars="index") - pts["location"] = [str(i) for i in pts["location"]] - pts["program"] = [str(i) for i in pts["program"]] - - dash_figs = [] - - today_count = list(df["daily_total_surveys"])[-1] - today_positive = list(df["daily_exited_surveys"])[-1] - today_negative = list(df["daily_completed_surveys"])[-1] - today_pct_pos = list(df["positivity_rate"])[-1] - today_pct = list(df["daily_pct"])[-1] - week_count = list(df["total_completed_surveys"])[-1] - week_pct = sum(list(df["daily_total_surveys"]))/sum(list(df["total_registered_students"]))*100 - - patient_count = sum(list(df["daily_registered_students"])) #device_count = len(devices) special_figs = [] @@ -1506,6 +1449,15 @@ def pos_plot(df,width=None,height=400): for ix,fig in enumerate(special_figs): special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) + patient_count = models.Patient.query.count() + df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] + today_count = int(list(df["daily_total_surveys"])[-1]) + today_pct = list(df["positivity_rate"])[-1] + week_count = int(sum(list(df["daily_total_surveys"])[-7:])) + week_pct = sum(list(df["positivity_rate"])[-7:])/7 + today_positive = int(list(df["daily_exited_surveys"])[-1]) + today_negative = int(list(df["daily_completed_surveys"])[-1]) + today_pct_pos = today_pct else: dash_figs = [] From 921461c8bb73c0c2380c455bcb2c56803794d2db Mon Sep 17 00:00:00 2001 From: ryananeff Date: Thu, 19 Nov 2020 23:29:15 -0500 Subject: [PATCH 29/44] super speedy bugfix --- medtracker/views.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index f570033..001efcf 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1359,7 +1359,7 @@ def pos_plot(df,width=None,height=400): def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") - end_request = request.values.get("end_date",None) + end_request = request.values.get("end_date",(datetime.datetime.now(tz)).date().strftime("%Y-%m-%d")) dash_figs = [] question_figs = [] if(True): @@ -1450,14 +1450,24 @@ def pos_plot(df,width=None,height=400): special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) patient_count = models.Patient.query.count() - df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] - today_count = int(list(df["daily_total_surveys"])[-1]) - today_pct = list(df["positivity_rate"])[-1] - week_count = int(sum(list(df["daily_total_surveys"])[-7:])) - week_pct = sum(list(df["positivity_rate"])[-7:])/7 - today_positive = int(list(df["daily_exited_surveys"])[-1]) - today_negative = int(list(df["daily_completed_surveys"])[-1]) - today_pct_pos = today_pct + if end_request in df["fmt_date"]: + df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] + today_count = int(list(df["daily_total_surveys"])[-1]) + today_pct = list(df["positivity_rate"])[-1] + week_count = int(sum(list(df["daily_total_surveys"])[-7:])) + week_pct = sum(list(df["positivity_rate"])[-7:])/7 + today_positive = int(list(df["daily_exited_surveys"])[-1]) + today_negative = int(list(df["daily_completed_surveys"])[-1]) + today_pct_pos = today_pct + else: + today_count = 0 + today_pct = 0 + week_count = 0 + week_pct = 0 + patient_count = 0 + today_positive = 0 + today_negative = 0 + today_pct_pos = 0 else: dash_figs = [] From 390fb0518e1bfee337be16e647709b68ad718d4e Mon Sep 17 00:00:00 2001 From: ryananeff Date: Thu, 19 Nov 2020 23:31:11 -0500 Subject: [PATCH 30/44] super speedy bugfix --- medtracker/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/medtracker/views.py b/medtracker/views.py index 001efcf..158971d 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1450,7 +1450,7 @@ def pos_plot(df,width=None,height=400): special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) patient_count = models.Patient.query.count() - if end_request in df["fmt_date"]: + if end_request in list(df["fmt_date"]): df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] today_count = int(list(df["daily_total_surveys"])[-1]) today_pct = list(df["positivity_rate"])[-1] From 31756eb040b9ceeab423dec9a0c1033ff294edfc Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 20 Nov 2020 01:07:34 -0500 Subject: [PATCH 31/44] SUPER SPEED ADMINING --- medtracker/views.py | 549 +++++++++++++++++++++----------------------- 1 file changed, 257 insertions(+), 292 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index 158971d..8ab42ea 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -18,7 +18,8 @@ from collections import defaultdict from datetime import timezone import time -from sqlalchemy import or_ +from sqlalchemy import or_ +from sqlalchemy import func, cast, Date from pytz import timezone as pytztimezone tz = pytztimezone('EST') @@ -996,10 +997,10 @@ def make_cache_key(*args, **kwargs): @app.route("/surveys//responses/dashboard/loaded",methods=["GET"]) @flask_login.login_required -@cache.cached(timeout=None,key_prefix=make_cache_key) +#@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_dashboard(survey_id): start_request = request.values.get("start_date","2020-06-29") - end_request = request.values.get("end_date",None) + end_request = request.values.get("end_date",(datetime.datetime.now(tz)).date().strftime("%Y-%m-%d")) dash_figs = [] question_figs = [] @@ -1017,194 +1018,148 @@ def survey_response_dashboard(survey_id): survey = models.Survey.query.get_or_404(survey_id) - pres = db.session.query(models.SurveyResponse, models.Patient).join(models.Patient)\ - .filter(models.SurveyResponse.start_time > time_start)\ - .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ - .all() - def pt_to_pd(): res = [dict(r) for r in db.session.execute(models.Patient.query.statement)] return pd.DataFrame(res) - sres = [] - for r,p in pres: - row = r.to_dict() - del row["uniq_id"] - del row["user_id"] - row["patient_id"] = p.id - row["location"] = p.location.value - row["year"] = p.year - row["program"] = p.program.value - sres.append(row) - - responses = [] - sr = db.session.query(models.QuestionResponse,models.Question)\ - .join(models.Question,models.SurveyResponse)\ - .filter(models.SurveyResponse.survey_id==survey.id)\ - .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ - .filter(models.SurveyResponse.start_time >= time_end) - - for r,q in sr.all(): - row = r.to_dict() - row["question_title"] = q.body - row["question_choices"] = q.choices - row["question_type"] = q.kind.code - row["survey_title"] = survey.title - row["survey_id"] = survey.id - responses.append(row) - sig_r = db.session.query(models.Patient)\ - .join(models.SurveyResponse)\ - .filter(models.SurveyResponse.survey_id==survey.id)\ - .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ - .filter(models.SurveyResponse.start_time >= time_end)\ - .filter(models.SurveyResponse.exited==True).all() - - responses_last7 = responses - - sr = db.session.query(models.QuestionResponse,models.Question)\ - .join(models.Question,models.SurveyResponse)\ - .filter(models.SurveyResponse.survey_id==survey.id)\ - .filter(models.SurveyResponse.start_time > time_start)\ - .filter(models.SurveyResponse.start_time <= time_end) - - for r,q in sr.all(): - row = r.to_dict() - row["question_title"] = q.body - row["question_choices"] = q.choices - row["question_type"] = q.kind.code - row["survey_title"] = survey.title - row["survey_id"] = survey.id - responses_last7.append(row) - - if len(sres)>0: - sres = pd.DataFrame(sres) - sres.end_time = sres.end_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - sres.start_time = sres.start_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - sres["date"] = sres.start_time.dt.floor('d') - sres = sres.groupby(["date","patient_id","completed","exited"]).last() - - sres = sres.reset_index() - - #devs = model_to_pd(models.Device) - #devs.creation_time = devs.creation_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - #devs_per_day = pd.DataFrame(devs.groupby([devs.creation_time.dt.floor("d")])["creation_time"].count()) - #devs_per_day.columns = ["daily_new_devices"] - pts = pt_to_pd() - - pts.creation_time = pts.creation_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') - pts_per_day = pd.DataFrame(pts.groupby([pts.creation_time.dt.floor("d")])["creation_time"].count()) - pts_per_day.columns = ["daily_registered_students"] - res_per_day = pd.DataFrame(sres.groupby([sres.end_time.dt.floor('d')])["end_time"].count()) - res_per_day.columns = ["daily_total_surveys"] - comp_per_day = pd.DataFrame(sres[sres.completed==True].groupby([sres.end_time.dt.floor('d')])["end_time"].count()) - comp_per_day.columns = ["daily_completed_surveys"] - exit_per_day = pd.DataFrame(sres[sres.exited==True].groupby([sres.end_time.dt.floor('d')])["end_time"].count()) - exit_per_day.columns = ["daily_exited_surveys"] - df = pd.merge(pts_per_day,res_per_day,left_index=True,right_index=True,how="outer") - df = pd.merge(df,comp_per_day,left_index=True,right_index=True,how="outer") - df = pd.merge(df,exit_per_day,left_index=True,right_index=True,how="outer") - df["positivity_rate"] = df["daily_exited_surveys"]/df["daily_total_surveys"].astype(float)*100. - #df = pd.merge(df,devs_per_day,left_index=True,right_index=True,how="outer") - begin_time = pts.creation_time[0].date() - df = df.tz_localize(None).reindex(pd.date_range(begin_time, end_time)).fillna(0) - df["total_registered_students"] = df["daily_registered_students"].cumsum() - df["total_completed_surveys"] = df["daily_completed_surveys"].cumsum() - #df["total_devices"] = df["daily_new_devices"].cumsum() - df["daily_uncompleted_surveys"] = df["total_registered_students"] - df["daily_total_surveys"] - df["daily_pct"] = df["daily_total_surveys"]/df["total_registered_students"]*100 - pts_df = pts.set_index("creation_time") - - years = list(range(2021,2025)) - programs = set(sres.program) - locations = set(sres.location) - outdf_yr = [] - outdf_program = [] - outdf_location = [] - total_reg = defaultdict(int) - sres = sres.groupby('patient_id').last() - for i in pd.date_range(begin_time, end_time).tz_localize('US/Eastern'): - sliced = sres[(sres.end_time<(i+datetime.timedelta(days=1)))&(sres.end_time>i)] - for y in years: - slyr = sliced[sliced.year==y] - #print(len(slyr)) - pts_yr = pts_df[pts_df.year==y] #students in that year - daily_reg = len(pts_yr[(pts_yr.index<(i+datetime.timedelta(days=1)))&(pts_yr.index>i)]) #students registered in last day - total_reg[y] += daily_reg - responded = len(slyr) - completed = sum(slyr.completed) - exited = sum(slyr.exited) - #not_completed = len(set(pts_yr.id).difference(slyr.index)) - outdf_yr.append([i,y,total_reg[y],responded, completed, exited]) - for p in programs: - slyr = sliced[sliced.program==p] - #print(len(slyr)) - pts_yr = pts_df[pts_df.program==p] #students in that year - daily_reg = len(pts_yr[(pts_yr.index<(i+datetime.timedelta(days=1)))&(pts_yr.index>i)]) #students registered in last day - total_reg[y] += daily_reg - responded = len(slyr) - completed = sum(slyr.completed) - exited = sum(slyr.exited) - #not_completed = len(set(pts_yr.id).difference(slyr.index)) - outdf_program.append([i,p,total_reg[y],responded, completed, exited]) - for l in locations: - slyr = sliced[sliced.location==l] - #print(len(slyr)) - pts_yr = pts_df[pts_df.location==l] #students in that year - daily_reg = len(pts_yr[(pts_yr.index<(i+datetime.timedelta(days=1)))&(pts_yr.index>i)]) #students registered in last day - total_reg[y] += daily_reg - responded = len(slyr) - completed = sum(slyr.completed) - exited = sum(slyr.exited) - #not_completed = len(set(pts_yr.id).difference(slyr.index)) - outdf_location.append([i,l,total_reg[y],responded, completed, exited]) - - outdf_yr = pd.DataFrame(outdf_yr,columns=["date","year","total_registered","total_responded","Well","Sick"]) - outdf_p = pd.DataFrame(outdf_program,columns=["date","program","total_registered","total_responded","Well","Sick"]) - outdf_l = pd.DataFrame(outdf_location,columns=["date","location","total_registered","total_responded","Well","Sick"]) - - outdf = outdf_yr - outdf.date = [i.date() for i in outdf.date] - begin_time = start_time - outdf = outdf[[i in pd.date_range(start_time, end_time) for i in outdf["date"]]] - df = df.tz_localize(None).reindex(pd.date_range(start_time, end_time)).fillna(0) - todaydf = outdf[outdf["date"]==end_time].loc[:,["year","Well","Sick"]].melt(id_vars="year") - - fig1 = plotlyBarplot(data=todaydf,x="year",y="value",hue="variable",stacked=True,ylabel="# Students",xlabel="Expected Graduation", - title="Screenings by Year",colors=["red","green"],height=425,width=None,show_legend=True) - fig1.update_layout(legend=dict( + .join(models.SurveyResponse)\ + .filter(models.SurveyResponse.survey_id==survey.id)\ + .filter(func.substr(models.SurveyResponse.end_time,0,11)==end_request)\ + .filter(models.SurveyResponse.exited==True).all() + + cols = [func.substr(models.SurveyResponse.end_time,0,11), + func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + models.SurveyResponse.completed, + models.SurveyResponse.exited] + q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) + df_q = pd.read_sql(q.statement,con=db.engine) + df_q_2 = df_q.pivot_table(index="substr_1",columns=["completed","exited"]).fillna(0) + df_q_2.columns = ["daily_exited_surveys","daily_completed_surveys"] + df_q_2["daily_total_surveys"] = df_q_2["daily_exited_surveys"]+df_q_2["daily_completed_surveys"] + df_q_2["positivity_rate"] = df_q_2["daily_exited_surveys"]/df_q_2["daily_total_surveys"]*100 + df_q_2["fmt_date"] = df_q_2.index + df_q_2.index = [datetime.datetime.strptime(a,"%Y-%m-%d").strftime("%D") for a in df_q_2.index] + df = df_q_2.reset_index() + df2 = df.loc[:,["index","daily_completed_surveys","daily_exited_surveys"]] + df2.columns = ["index","Cleared","Sent Home"] + df3 = df2.melt(id_vars="index") + + if len(df) > 0: + pts = pt_to_pd() + + pts.creation_time = pts.creation_time.dt.tz_localize('UTC').dt.tz_convert('US/Eastern') + pts_df = pts.set_index("creation_time") + + cols = [func.substr(models.SurveyResponse.end_time,0,11), + func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + models.Patient.year, + models.SurveyResponse.completed, + models.SurveyResponse.exited] + q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.year, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) + df_q = pd.read_sql(q.statement,con=db.engine) + df_q_2 = df_q.pivot_table(index=["substr_1","year"],columns=["completed","exited"]).fillna(0) + df_q_2.columns = ["exited","completed"] + df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] + outdf_year = df_q_2.reset_index().loc[:,["substr_1","year","responded","completed","exited"]] + outdf_year = outdf_year[[i in list(range(2021,2025)) for i in outdf_year["year"]]] + + cols = [func.substr(models.SurveyResponse.end_time,0,11), + func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + models.Patient.year, + models.SurveyResponse.completed, + models.SurveyResponse.exited] + q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.year, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) + df_q = pd.read_sql(q.statement,con=db.engine) + df_q_2 = df_q.pivot_table(index=["substr_1","year"],columns=["completed","exited"]).fillna(0) + df_q_2.columns = ["exited","completed"] + df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] + outdf_yr = df_q_2.reset_index().loc[:,["substr_1","year","responded","completed","exited"]] + outdf_yr.columns = ["date","year","total_responded","Well","Sick"] + + cols = [func.substr(models.SurveyResponse.end_time,0,11), + func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + models.Patient.program, + models.SurveyResponse.completed, + models.SurveyResponse.exited] + q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.program, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) + df_q = pd.read_sql(q.statement,con=db.engine) + df_q["program"] = list([str(a) for a in df_q["program"]]) + df_q_2 = df_q.pivot_table(index=["substr_1","program"],columns=["completed","exited"]).fillna(0) + df_q_2.columns = ["exited","completed"] + df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] + outdf_p = df_q_2.reset_index().loc[:,["substr_1","program","responded","completed","exited"]] + outdf_p.columns = ["date","program","total_responded","Well","Sick"] + + cols = [func.substr(models.SurveyResponse.end_time,0,11), + func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + models.Patient.location, + models.SurveyResponse.completed, + models.SurveyResponse.exited] + q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.program, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) + df_q = pd.read_sql(q.statement,con=db.engine) + df_q["location"] = list([str(a) for a in df_q["location"]]) + df_q_2 = df_q.pivot_table(index=["substr_1","location"],columns=["completed","exited"]).fillna(0) + df_q_2.columns = ["exited","completed"] + df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] + outdf_l = df_q_2.reset_index().loc[:,["substr_1","location","responded","completed","exited"]] + outdf_l.columns = ["date","location","total_responded","Well","Sick"] + + outdf = outdf_yr + todaydf = outdf[outdf["date"]==str(end_time)].loc[:,["year","Well","Sick"]].melt(id_vars="year") + + fig1 = plotlyBarplot(data=todaydf,x="year",y="value",hue="variable",stacked=True,ylabel="# Students",xlabel="Expected Graduation", + title="Screenings by Year",colors=["red","green"],height=425,width=None,show_legend=True) + fig1.update_layout(legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 )) - outdf = outdf_p - outdf.date = [i.date() for i in outdf.date] - begin_time = start_time - outdf = outdf[[i in pd.date_range(start_time, end_time) for i in outdf["date"]]] - df = df.tz_localize(None).reindex(pd.date_range(start_time, end_time)).fillna(0) - todaydf = outdf[outdf["date"]==end_time].loc[:,["program","Well","Sick"]].melt(id_vars="program") - - fig2 = plotlyBarplot(data=todaydf,x="program",y="value",hue="variable",stacked=True,ylabel="# Students",xlabel="Expected Graduation", - title="Screenings by Program",colors=["red","green"],height=500,width=None,show_legend=True) - fig2.update_layout(legend=dict( + outdf = outdf_p + todaydf = outdf[outdf["date"]==str(end_time)].loc[:,["program","Well","Sick"]].melt(id_vars="program") + + fig2 = plotlyBarplot(data=todaydf,x="program",y="value",hue="variable",stacked=True,ylabel="# Students",xlabel="Expected Graduation", + title="Screenings by Program",colors=["red","green"],height=500,width=None,show_legend=True) + fig2.update_layout(legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1 )) - outdf = outdf_l - outdf.date = [i.date() for i in outdf.date] - begin_time = start_time - outdf = outdf[[i in pd.date_range(start_time, end_time) for i in outdf["date"]]] - df = df.tz_localize(None).reindex(pd.date_range(start_time, end_time)).fillna(0) - todaydf = outdf[outdf["date"]==end_time].loc[:,["location","Well","Sick"]].melt(id_vars="location") - - fig3 = plotlyBarplot(data=todaydf,x="location",y="value",hue="variable",stacked=True,ylabel="# Students",xlabel="Expected Graduation", - title="Screenings by Location",colors=["red","green"],height=500,width=None,show_legend=True) - fig3.update_layout(legend=dict( + outdf = outdf_l + todaydf = outdf[outdf["date"]==str(end_time)].loc[:,["location","Well","Sick"]].melt(id_vars="location") + + fig3 = plotlyBarplot(data=todaydf,x="location",y="value",hue="variable",stacked=True,ylabel="# Students",xlabel="Expected Graduation", + title="Screenings by Location",colors=["red","green"],height=500,width=None,show_legend=True) + fig3.update_layout(legend=dict( orientation="h", yanchor="bottom", y=1.02, @@ -1212,130 +1167,141 @@ def pt_to_pd(): x=1 )) #outdf["date"] = [datetime.datetime.strftime(a,"%D") for a in outdf["date"]] - #fig1 = plotlyBarplot(data=outdf,x="date",y="total_registered",hue="year",stacked=True,width=None,height=400, - # title="Students Registered",ylabel="# Students",show_legend=True,xlabel="Date") - - df.reset_index(inplace=True) - df = df.sort_values(by="index",ascending=True) - df["index"] = [datetime.datetime.strftime(a,"%D") for a in df["index"]] - df2 = df.loc[:,["index","daily_completed_surveys","daily_exited_surveys"]] - df2.columns = ["index","Cleared","Sent Home"] - df3 = df2.melt(id_vars="index") - - pts["location"] = [str(i) for i in pts["location"]] - pts["program"] = [str(i) for i in pts["program"]] - reg_per_year = plotlyBarplot(data=pd.DataFrame(pts.groupby(["year","program"]).count()["id"]).reset_index(),y="id",x="year",hue="program", - width=None, height=400, title="Registered by Year",stacked=True,xlabel="Expected Graduation",ylabel="# Students",show_legend=True,xtype="linear") - reg_per_program = plotlyBarplot(data=pd.DataFrame(pts.groupby(["program","year"]).count()["id"]).reset_index(),y="id",x="program",hue="year", - width=None, height=500, title="Registered by Program",show_legend=True,stacked=True,xlabel="Program",ylabel="# Students") - reg_per_location = plotlyBarplot(data=pd.DataFrame(pts.groupby(["location"]).count()["id"]).reset_index(),y="id",x="location", - stacked=True, width=None, height=500, title="Registered by Location",show_legend=False,xlabel="Location",ylabel="# Students") - - dash_figs = [fig1,fig2,fig3] - - today_count = list(df["daily_total_surveys"])[-1] - today_positive = list(df["daily_exited_surveys"])[-1] - today_negative = list(df["daily_completed_surveys"])[-1] - today_pct_pos = list(df["positivity_rate"])[-1] - today_pct = list(df["daily_pct"])[-1] - week_count = list(df["total_completed_surveys"])[-1] - week_pct = sum(list(df["daily_total_surveys"]))/sum(list(df["total_registered_students"]))*100 - patient_count = sum(list(df["daily_registered_students"])) - - special_figs = [] - last7_figs = [] - from scipy import signal - def pos_plot(df,width=None,height=400): - layout = go.Layout( - autosize=True, - width=width, - height=height, - title={'text': 'Percent Positivity Rate', - 'y':0.9, - 'x':0.5, - 'xanchor': 'center', - 'yanchor': 'top'} - ) - fig = go.Figure(layout=layout) - fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) - try: - fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"].rolling(window=7,min_periods=1).mean(),line_shape='spline', - name="Average (7 days)")) - except ValueError as err: - pass - fig.update_layout( xaxis_title='Date', - yaxis_title='Positivity Rate %') - fig.update_layout(legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - )) - return fig - special_figs.append(pos_plot(df)) - fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, - title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], - ylabel="# Students",xlabel="Date") - fig.update_layout(legend=dict( - orientation="h", - yanchor="bottom", - y=1.02, - xanchor="right", - x=1 - )) - special_figs.append(fig) + #fig1 = plotlyBarplot(data=outdf,x="date",y="total_registered",hue="year",stacked=True,width=None,height=400, + # title="Students Registered",ylabel="# Students",show_legend=True,xlabel="Date") + + dash_figs = [fig1,fig2,fig3] + + patient_count = models.Patient.query.count() + if end_request in list(df["fmt_date"]): + df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] + today_count = int(list(df["daily_total_surveys"])[-1]) + today_pct = list(df["positivity_rate"])[-1] + week_count = int(sum(list(df["daily_total_surveys"])[-7:])) + week_pct = sum(list(df["positivity_rate"])[-7:])/7 + today_positive = int(list(df["daily_exited_surveys"])[-1]) + today_negative = int(list(df["daily_completed_surveys"])[-1]) + today_pct_pos = today_pct + else: + today_count = 0 + today_pct = 0 + week_count = 0 + week_pct = 0 + patient_count = 0 + today_positive = 0 + today_negative = 0 + today_pct_pos = 0 + + special_figs = [] + last7_figs = [] + from scipy import signal + def pos_plot(df,width=None,height=400): + layout = go.Layout( + autosize=True, + width=width, + height=height, + title={'text': 'Percent Positivity Rate', + 'y':0.9, + 'x':0.5, + 'xanchor': 'center', + 'yanchor': 'top'} + ) + fig = go.Figure(layout=layout) + fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"],line_shape='hv',name="Values")) + try: + fig.add_trace(go.Scatter(x=df["index"], y=df["positivity_rate"].rolling(window=7,min_periods=1).mean(),line_shape='spline', + name="Average (7 days)")) + except ValueError as err: + pass + fig.update_layout( xaxis_title='Date', + yaxis_title='Positivity Rate %') + fig.update_layout(legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + )) + return fig + special_figs.append(pos_plot(df)) + fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, + title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], + ylabel="# Students",xlabel="Date") + fig.update_layout(legend=dict( + orientation="h", + yanchor="bottom", + y=1.02, + xanchor="right", + x=1 + )) + special_figs.append(fig) else: - dash_figs = [] - special_figs = [] - last7_figs = [] - today_count = 0 - today_pct = 0 - week_count = 0 - week_pct = 0 - patient_count = 0 - today_positive = 0 - today_negative = 0 - today_pct_pos = 0 + dash_figs = [] + special_figs = [] + last7_figs = [] + today_count = 0 + today_pct = 0 + week_count = 0 + week_pct = 0 + patient_count = 0 + today_positive = 0 + today_negative = 0 + today_pct_pos = 0 #device_count = len(devices) - qres = pd.DataFrame(responses_last7) + cols = [func.substr(models.QuestionResponse.time,0,11), + models.QuestionResponse.id, + models.QuestionResponse.uniq_id, + models.QuestionResponse._response, + models.QuestionResponse.question_id, + models.Question.body, + models.Question.choices, + models.Question.kind] + sr = db.session.query(*cols)\ + .join(models.Question,models.SurveyResponse)\ + .filter(models.SurveyResponse.survey_id==survey.id)\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= time_end) + + df_q = pd.read_sql(sr.statement,con=db.engine) + df_q.columns = ["date","response_id","uniq_id","response","question_id","question_title","question_choices","question_type"] + qres = df_q if len(qres)>0: - qres.time = qres.time - qres["date"] = [i.date() for i in qres.time] - qres = qres.groupby(["date","question_id","uniq_id"]).first() - qres = qres.reset_index() - e = [str(a.date()) for a in list(pd.date_range(start_time,end_time))] - - for n,g in qres.groupby("question_id"): - title = list(g.question_title)[0] - choices = list(g.question_choices)[0] - choices = ast.literal_eval(choices) if choices != "" else {} - kind = list(g.question_type)[0] - xtype = "category" - pltdf = g.loc[:,["date","response"]] - pltdf["count"] = 1 - pltdf.sort_values("date",inplace=True) - pltdf["date"] = pltdf["date"].astype(str) - refmt = [] - for ix,row in pltdf.iterrows(): - row = list(row) - for rr in row[1].split(";"): - refmt.append([row[0],rr,row[2]]) - pltdf = pd.DataFrame(refmt,columns=pltdf.columns) - pltdf.set_index("date",inplace=True) - for ix in e: - if ix not in pltdf.index: - pltdf.loc[ix] = [None,0] - pltdf.sort_index(inplace=True) - pltdf.reset_index(inplace=True) - pltdf=pltdf.fillna(method="bfill") - fig = plotlyBarplot(data=pltdf,x="date",y="count",hue="response", - xtype=xtype,grouped=True,ordered=False,stacked=True,order2=True,width=None,height=None,show_legend=True,ylabel="# Responses",xlabel="Date",title=title) - last7_figs.append(fig) + qres = qres.groupby(["date","question_id","uniq_id"]).first() + qres = qres.reset_index() + e = [str(a.date()) for a in list(pd.date_range(start_time,end_time))] + + for n,g in qres.groupby("question_id"): + title = list(g.question_title)[0] + choices = list(g.question_choices)[0] + choices = ast.literal_eval(choices) if choices != "" else {} + kind = list(g.question_type)[0] + xtype = "category" + pltdf = g.loc[:,["date","response"]] + pltdf["count"] = 1 + pltdf.sort_values("date",inplace=True) + pltdf["date"] = pltdf["date"].astype(str) + refmt = [] + for ix,row in pltdf.iterrows(): + row = list(row) + for rr in row[1].split(";"): + refmt.append([row[0],rr,row[2]]) + pltdf = pd.DataFrame(refmt,columns=pltdf.columns) + pltdf.set_index("date",inplace=True) + for ix in e: + if ix not in pltdf.index: + pltdf.loc[ix] = [None,0] + pltdf.sort_index(inplace=True) + pltdf.reset_index(inplace=True) + pltdf=pltdf.fillna(method="bfill") + fig = plotlyBarplot(data=pltdf,x="date",y="count",hue="response", + xtype=xtype,grouped=True,ordered=False,stacked=True,order2=True,width=None,height=None,show_legend=True,ylabel="# Responses",xlabel="Date",title=title) + last7_figs.append(fig) + + for ix,fig in enumerate(dash_figs): dash_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) @@ -1377,7 +1343,6 @@ def survey_response_student_dashboard(): survey = models.Survey.query.get_or_404(survey_id) - from sqlalchemy import func, cast, Date cols = [func.substr(models.SurveyResponse.end_time,0,11), func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), models.SurveyResponse.completed, From 263cf953ca6f777e16aeec16d63b66efa292d164 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 20 Nov 2020 01:10:26 -0500 Subject: [PATCH 32/44] SUPER SPEED ADMINING cache enable --- medtracker/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/medtracker/views.py b/medtracker/views.py index 8ab42ea..b0cb89d 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -997,7 +997,7 @@ def make_cache_key(*args, **kwargs): @app.route("/surveys//responses/dashboard/loaded",methods=["GET"]) @flask_login.login_required -#@cache.cached(timeout=None,key_prefix=make_cache_key) +@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_dashboard(survey_id): start_request = request.values.get("start_date","2020-06-29") end_request = request.values.get("end_date",(datetime.datetime.now(tz)).date().strftime("%Y-%m-%d")) From 5b88aa846339a41af9da2aa345f968d1b063dfb9 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 20 Nov 2020 01:44:10 -0500 Subject: [PATCH 33/44] misc bugfixes (decimal counts\?) --- medtracker/views.py | 70 +++++++++++++++++++++++++++------------------ 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index b0cb89d..fc8ad7f 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -997,7 +997,7 @@ def make_cache_key(*args, **kwargs): @app.route("/surveys//responses/dashboard/loaded",methods=["GET"]) @flask_login.login_required -@cache.cached(timeout=None,key_prefix=make_cache_key) +#@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_dashboard(survey_id): start_request = request.values.get("start_date","2020-06-29") end_request = request.values.get("end_date",(datetime.datetime.now(tz)).date().strftime("%Y-%m-%d")) @@ -1033,19 +1033,23 @@ def pt_to_pd(): models.SurveyResponse.completed, models.SurveyResponse.exited] q = db.session.query(*cols).join(models.Patient)\ + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ .filter(models.SurveyResponse.end_time > time_start)\ .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ .filter(models.SurveyResponse.end_time != None)\ .group_by(func.substr(models.SurveyResponse.end_time,0,11), models.SurveyResponse.completed,models.SurveyResponse.exited,) df_q = pd.read_sql(q.statement,con=db.engine) - df_q_2 = df_q.pivot_table(index="substr_1",columns=["completed","exited"]).fillna(0) + df_q_2 = df_q.pivot_table(index="substr_1",columns=["completed","exited"],values="per_date",aggfunc='sum').fillna(0) df_q_2.columns = ["daily_exited_surveys","daily_completed_surveys"] df_q_2["daily_total_surveys"] = df_q_2["daily_exited_surveys"]+df_q_2["daily_completed_surveys"] df_q_2["positivity_rate"] = df_q_2["daily_exited_surveys"]/df_q_2["daily_total_surveys"]*100 df_q_2["fmt_date"] = df_q_2.index df_q_2.index = [datetime.datetime.strptime(a,"%Y-%m-%d").strftime("%D") for a in df_q_2.index] df = df_q_2.reset_index() + if end_request in list(df["fmt_date"]): + df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] df2 = df.loc[:,["index","daily_completed_surveys","daily_exited_surveys"]] df2.columns = ["index","Cleared","Sent Home"] df3 = df2.melt(id_vars="index") @@ -1062,13 +1066,15 @@ def pt_to_pd(): models.SurveyResponse.completed, models.SurveyResponse.exited] q = db.session.query(*cols).join(models.Patient)\ - .filter(models.SurveyResponse.end_time > time_start)\ - .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ - .filter(models.SurveyResponse.end_time != None)\ - .group_by(models.Patient.year, func.substr(models.SurveyResponse.end_time,0,11), - models.SurveyResponse.completed,models.SurveyResponse.exited,) + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.year, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) df_q = pd.read_sql(q.statement,con=db.engine) - df_q_2 = df_q.pivot_table(index=["substr_1","year"],columns=["completed","exited"]).fillna(0) + df_q_2 = df_q.pivot_table(index=["substr_1","year"],columns=["completed","exited"],values="per_date",aggfunc='sum').fillna(0) df_q_2.columns = ["exited","completed"] df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] outdf_year = df_q_2.reset_index().loc[:,["substr_1","year","responded","completed","exited"]] @@ -1080,11 +1086,13 @@ def pt_to_pd(): models.SurveyResponse.completed, models.SurveyResponse.exited] q = db.session.query(*cols).join(models.Patient)\ - .filter(models.SurveyResponse.end_time > time_start)\ - .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ - .filter(models.SurveyResponse.end_time != None)\ - .group_by(models.Patient.year, func.substr(models.SurveyResponse.end_time,0,11), - models.SurveyResponse.completed,models.SurveyResponse.exited,) + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.year, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) df_q = pd.read_sql(q.statement,con=db.engine) df_q_2 = df_q.pivot_table(index=["substr_1","year"],columns=["completed","exited"]).fillna(0) df_q_2.columns = ["exited","completed"] @@ -1093,38 +1101,42 @@ def pt_to_pd(): outdf_yr.columns = ["date","year","total_responded","Well","Sick"] cols = [func.substr(models.SurveyResponse.end_time,0,11), - func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + func.count(models.SurveyResponse.uniq_id).label("per_date"), models.Patient.program, models.SurveyResponse.completed, models.SurveyResponse.exited] q = db.session.query(*cols).join(models.Patient)\ - .filter(models.SurveyResponse.end_time > time_start)\ - .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ - .filter(models.SurveyResponse.end_time != None)\ - .group_by(models.Patient.program, func.substr(models.SurveyResponse.end_time,0,11), - models.SurveyResponse.completed,models.SurveyResponse.exited,) + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.program, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) df_q = pd.read_sql(q.statement,con=db.engine) df_q["program"] = list([str(a) for a in df_q["program"]]) - df_q_2 = df_q.pivot_table(index=["substr_1","program"],columns=["completed","exited"]).fillna(0) + df_q_2 = df_q.pivot_table(index=["substr_1","program"],columns=["completed","exited"],values="per_date",aggfunc='sum').fillna(0) df_q_2.columns = ["exited","completed"] df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] outdf_p = df_q_2.reset_index().loc[:,["substr_1","program","responded","completed","exited"]] outdf_p.columns = ["date","program","total_responded","Well","Sick"] cols = [func.substr(models.SurveyResponse.end_time,0,11), - func.count(func.distinct(models.SurveyResponse.uniq_id)).label("per_date"), + func.count(models.SurveyResponse.uniq_id).label("per_date"), models.Patient.location, models.SurveyResponse.completed, models.SurveyResponse.exited] q = db.session.query(*cols).join(models.Patient)\ - .filter(models.SurveyResponse.end_time > time_start)\ - .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ - .filter(models.SurveyResponse.end_time != None)\ - .group_by(models.Patient.program, func.substr(models.SurveyResponse.end_time,0,11), - models.SurveyResponse.completed,models.SurveyResponse.exited,) + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time > time_start)\ + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1)))\ + .filter(models.SurveyResponse.end_time != None)\ + .group_by(models.Patient.program, func.substr(models.SurveyResponse.end_time,0,11), + models.SurveyResponse.completed,models.SurveyResponse.exited,) df_q = pd.read_sql(q.statement,con=db.engine) df_q["location"] = list([str(a) for a in df_q["location"]]) - df_q_2 = df_q.pivot_table(index=["substr_1","location"],columns=["completed","exited"]).fillna(0) + df_q_2 = df_q.pivot_table(index=["substr_1","location"],columns=["completed","exited"],values="per_date",aggfunc='sum').fillna(0) df_q_2.columns = ["exited","completed"] df_q_2["responded"] = df_q_2["exited"] + df_q_2["completed"] outdf_l = df_q_2.reset_index().loc[:,["substr_1","location","responded","completed","exited"]] @@ -1262,6 +1274,8 @@ def pos_plot(df,width=None,height=400): sr = db.session.query(*cols)\ .join(models.Question,models.SurveyResponse)\ .filter(models.SurveyResponse.survey_id==survey.id)\ + .filter(models.SurveyResponse.start_time > time_start)\ + .filter(models.SurveyResponse.start_time <= time_end)\ .filter(models.SurveyResponse.end_time > time_start)\ .filter(models.SurveyResponse.end_time <= time_end) @@ -1354,7 +1368,7 @@ def survey_response_student_dashboard(): .group_by(func.substr(models.SurveyResponse.end_time,0,11), models.SurveyResponse.completed,models.SurveyResponse.exited,) df_q = pd.read_sql(q.statement,con=db.engine) - df_q_2 = df_q.pivot_table(index="substr_1",columns=["completed","exited"]).fillna(0) + df_q_2 = df_q.pivot_table(index="substr_1",columns=["completed","exited"],values="per_date",aggfunc='sum').fillna(0) df_q_2.columns = ["daily_exited_surveys","daily_completed_surveys"] df_q_2["daily_total_surveys"] = df_q_2["daily_exited_surveys"]+df_q_2["daily_completed_surveys"] df_q_2["positivity_rate"] = df_q_2["daily_exited_surveys"]/df_q_2["daily_total_surveys"]*100 From f96b81c19c915e8c849fbc5d7f82cb00d4cab32a Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 23 Nov 2020 11:27:14 -0500 Subject: [PATCH 34/44] made figs bigger on student dash --- medtracker/views.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index fc8ad7f..4709249 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1335,7 +1335,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -@cache.cached(timeout=None,key_prefix=make_cache_key) +#@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") @@ -1409,13 +1409,14 @@ def pos_plot(df,width=None,height=400): yanchor="bottom", y=1.02, xanchor="right", - x=1 + x=1, )) + fig.update_layout(margin=dict(r=0,l=0)) return fig special_figs.append(pos_plot(df)) fig = plotlyBarplot(data=df3,x="index",y="value",hue="variable",width=None, height=400, title="Compliance History",stacked=True,show_legend=True,colors=["green","red"], - ylabel="# Students",xlabel="Date") + ylabel="# Students",xlabel="Date",margins=dict(r=0,l=0)) fig.update_layout(legend=dict( orientation="h", yanchor="bottom", @@ -1569,7 +1570,8 @@ def plotlyBarplot(x=None,y=None,hue=None,data=None,ylabel="",xlabel="",title="", fig.update_layout(xaxis={"categoryorder":"array","categoryarray":catorder}) fig.update_layout(legend=dict(x=1.01, y=0)) fig.update_layout(showlegend=show_legend) - + fig.update_layout(bargap = 0) + fig.update_traces(marker_line_width=0) return fig @app.route("/users/") From d6523f0cead559a71309df733ab4772b34ca1a0c Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 23 Nov 2020 11:31:04 -0500 Subject: [PATCH 35/44] mobile student dash layout fix --- medtracker/templates/dashboard_student.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/medtracker/templates/dashboard_student.html b/medtracker/templates/dashboard_student.html index 1cc9e3b..6dad015 100755 --- a/medtracker/templates/dashboard_student.html +++ b/medtracker/templates/dashboard_student.html @@ -43,7 +43,7 @@
    Percent Positive Screenings
    {% for fig in special_figs %} -
    {{fig|safe}}
    +
    {{fig|safe}}
    {% endfor %}
    From f7b229c165b4518b9f8ac70a01b0fa2745734b80 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Mon, 23 Nov 2020 16:25:00 -0500 Subject: [PATCH 36/44] reenable cache --- medtracker/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/medtracker/views.py b/medtracker/views.py index 4709249..ea3990c 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1335,7 +1335,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -#@cache.cached(timeout=None,key_prefix=make_cache_key) +@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") From c8d16613c4a41a76ee75afe6a46d5413256e4ec5 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Tue, 24 Nov 2020 01:28:28 -0500 Subject: [PATCH 37/44] added regional metrics --- medtracker/data/nyc_plots.py | 167 ++++++++++++++++++++ medtracker/templates/dashboard_student.html | 6 + medtracker/views.py | 12 +- 3 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 medtracker/data/nyc_plots.py diff --git a/medtracker/data/nyc_plots.py b/medtracker/data/nyc_plots.py new file mode 100644 index 0000000..742505f --- /dev/null +++ b/medtracker/data/nyc_plots.py @@ -0,0 +1,167 @@ +import pandas as pd +import plotly.graph_objects as go +import numpy as np + +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 +url = "https://usafactsstatic.blob.core.windows.net/public/data/covid-19/covid_confirmed_usafacts.csv" +confirmed_df=pd.read_csv(url) + +nyc_counties_df = confirmed_df.loc[(confirmed_df['State'] == 'NY') & (confirmed_df['countyFIPS'].isin([36005, 36061, 36081, 36085, 36047]))] +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) +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://usafactsstatic.blob.core.windows.net/public/data/covid-19/covid_deaths_usafacts.csv" +deaths_df = pd.read_csv(url) + +nyc_counties_deaths_df = deaths_df.loc[(deaths_df['State'] == 'NY') & (deaths_df['countyFIPS'].isin([36005, 36061, 36081, 36085, 36047]))] + +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("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="NYC Cases 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("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("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("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="Most Recent R0:\n"+ro_latest, + font=dict(family="Helvetica, Arial",size=28), + xref="paper", yref="paper", + x=1, y=1, showarrow=False) +fig2.write_json("r0_estimate.json") \ No newline at end of file diff --git a/medtracker/templates/dashboard_student.html b/medtracker/templates/dashboard_student.html index 6dad015..f6cb783 100755 --- a/medtracker/templates/dashboard_student.html +++ b/medtracker/templates/dashboard_student.html @@ -46,5 +46,11 @@
    Percent Positive Screenings
    {{fig|safe}}
    {% endfor %} +

    NYC Region Metrics

    +
    + {% for fig in special_figs_2 %} +
    {{fig|safe}}
    + {% endfor %} +
    {% endblock %} diff --git a/medtracker/views.py b/medtracker/views.py index ea3990c..7c04cd2 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -13,6 +13,7 @@ import pandas as pd import numpy as np import datetime +import plotly import plotly.graph_objects as go import ast from collections import defaultdict @@ -1335,7 +1336,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -@cache.cached(timeout=None,key_prefix=make_cache_key) +#@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") @@ -1426,8 +1427,15 @@ def pos_plot(df,width=None,height=400): )) special_figs.append(fig) + ##special COVID tracking stats - NYC area + special_figs_2 = [] + special_figs_2.append(plotly.io.read_json(open("medtracker/data/daily_cases.json","r"))) + special_figs_2.append(plotly.io.read_json(open("medtracker/data/r0_estimate.json","r"))) + for ix,fig in enumerate(special_figs): special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) + for ix,fig in enumerate(special_figs_2): + special_figs_2[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) patient_count = models.Patient.query.count() if end_request in list(df["fmt_date"]): @@ -1469,7 +1477,7 @@ def pos_plot(df,width=None,height=400): today_count=today_count, today_pct=today_pct, week_count=week_count, week_pct=week_pct, survey=survey, start_date = start_time, end_date=end_time, today_positive=today_positive,today_negative = today_negative, - today_pct_pos=today_pct_pos, special_figs=special_figs) + today_pct_pos=today_pct_pos, special_figs=special_figs,special_figs_2=special_figs_2) def plotlyBarplot(x=None,y=None,hue=None,data=None,ylabel="",xlabel="",title="", From 593f628bbaab5625d1b079dacd7c107c71363890 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Tue, 24 Nov 2020 06:36:51 +0000 Subject: [PATCH 38/44] prod changes --- medtracker/views.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index 7c04cd2..a71fda8 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1336,7 +1336,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -#@cache.cached(timeout=None,key_prefix=make_cache_key) +@cache.cached(timeout=None,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29") @@ -1429,8 +1429,8 @@ def pos_plot(df,width=None,height=400): ##special COVID tracking stats - NYC area special_figs_2 = [] - special_figs_2.append(plotly.io.read_json(open("medtracker/data/daily_cases.json","r"))) - special_figs_2.append(plotly.io.read_json(open("medtracker/data/r0_estimate.json","r"))) + special_figs_2.append(plotly.io.read_json(open("/home/ubuntu/medtracker/medtracker/data/daily_cases.json","r"))) + special_figs_2.append(plotly.io.read_json(open("/home/ubuntu/medtracker/medtracker/data/r0_estimate.json","r"))) for ix,fig in enumerate(special_figs): special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) From d33577f95156499cfff2f4a167ac2a0e75e98f31 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Tue, 24 Nov 2020 01:48:49 -0500 Subject: [PATCH 39/44] send email on exit --- medtracker/templates/email_survey_exit.html | 8 ++++++-- medtracker/views.py | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/medtracker/templates/email_survey_exit.html b/medtracker/templates/email_survey_exit.html index 3f06c50..1eebef8 100644 --- a/medtracker/templates/email_survey_exit.html +++ b/medtracker/templates/email_survey_exit.html @@ -188,7 +188,6 @@ align="center" class="" style="" > -
    Taken: {{ 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.
    +
    Name: {{patient.fullname}}
    +
    Life Number: {{patient.lifenumber}}
    +
    Taken: {{ record.end_time.strftime('%B %-d, %Y') }}
    +
    - diff --git a/medtracker/views.py b/medtracker/views.py index a71fda8..7272e5c 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -263,6 +263,7 @@ def send_survey_response_email(patient, record, app=app): msg.html = render_template('email_survey_complete.html', patient=patient,record=record) msg.body = render_template('email_survey_complete.txt', patient=patient,record=record) elif record.exited: + msg.cc = ["StudentCovidResponse@mssm.edu"] msg.html = render_template('email_survey_exit.html', patient=patient,record=record) #change me! msg.body = render_template('email_survey_exit.txt', patient=patient,record=record) #change me! mail.send(msg) From ee161d8d2caa7ce2985173394fadf13392a144ed Mon Sep 17 00:00:00 2001 From: ryananeff Date: Wed, 3 Feb 2021 04:26:00 +0000 Subject: [PATCH 40/44] fix 500 --- medtracker/data/nyc_plots.py | 16 ++++++++-------- medtracker/templates/500.html | 2 +- medtracker/templates/survey_complete.html | 12 +++++++++--- 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/medtracker/data/nyc_plots.py b/medtracker/data/nyc_plots.py index 742505f..0b3b87b 100644 --- a/medtracker/data/nyc_plots.py +++ b/medtracker/data/nyc_plots.py @@ -100,7 +100,7 @@ def dailydata(dfcounty): 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("cfr.json") +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/cfr.json") #daily new cases df2 = DailyCases_df @@ -114,11 +114,11 @@ def dailydata(dfcounty): 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="NYC Cases on "+latest.name.strftime("%m/%d")+":\n"+str(int(latest.Total)), +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("daily_cases.json") +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/daily_cases.json") #daily new deaths df2 = DailyDeaths_df @@ -132,14 +132,14 @@ def dailydata(dfcounty): 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("daily_deaths.json") +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("hospitalizations.json") +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") @@ -160,8 +160,8 @@ def dailydata(dfcounty): 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="Most Recent R0:\n"+ro_latest, - font=dict(family="Helvetica, Arial",size=28), +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("r0_estimate.json") \ No newline at end of file +fig2.write_json("/home/ubuntu/medtracker/medtracker/data/r0_estimate.json") diff --git a/medtracker/templates/500.html b/medtracker/templates/500.html index e5e2082..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/survey_complete.html b/medtracker/templates/survey_complete.html index eb13046..0ea43b2 100755 --- a/medtracker/templates/survey_complete.html +++ b/medtracker/templates/survey_complete.html @@ -10,13 +10,19 @@

    Thank you for taking the {{survey.title}}.

    {%if record.message %}

    {{record.message}}

    {% endif%} -

    Your answers have been recorded successfully. Your attestation is shown below.

    -

    Device ID: {{ fmt_id(patient.mrn) }}

    +

    Your answers have been recorded successfully. Your attestation is shown at the bottom of this page.

    +

    Device ID: {{ fmt_id(patient.mrn) }}

    Completion Date: {{ momentjs(record.end_time).format('MMMM Do YYYY, h:mm a') }}

    {% if qrcode_out %}

    QR Code:

    {% endif %} -

    See your records

    +

    See your records +

    +

    You may also wish to make an appointment for asymptomatic testing at Mount Sinai, which is now available for all students. There are two locations + available: 1190 Fifth Avenue and 2 West 86th Street. Click on the button below to schedule an appointment: +

    + Asymptomatic testing +

    From 043ad80997db293dd36c2962972f8f01941965ff Mon Sep 17 00:00:00 2001 From: ryananeff Date: Thu, 25 Mar 2021 20:51:43 +0000 Subject: [PATCH 41/44] update views --- medtracker/data/nyc_plots.py | 8 +++++--- medtracker/templates/layout.html | 2 +- medtracker/views.py | 16 ++++++++-------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/medtracker/data/nyc_plots.py b/medtracker/data/nyc_plots.py index 0b3b87b..535df67 100644 --- a/medtracker/data/nyc_plots.py +++ b/medtracker/data/nyc_plots.py @@ -28,11 +28,13 @@ def prepFigure(figure, title): confirmed_df=pd.read_csv(url) 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.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'] @@ -41,9 +43,9 @@ def prepFigure(figure, title): deaths_df = pd.read_csv(url) 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.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') diff --git a/medtracker/templates/layout.html b/medtracker/templates/layout.html index 61a6c29..4e95a39 100755 --- a/medtracker/templates/layout.html +++ b/medtracker/templates/layout.html @@ -113,7 +113,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/views.py b/medtracker/views.py index 7272e5c..cdb9248 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -1048,7 +1048,7 @@ def pt_to_pd(): df_q_2["daily_total_surveys"] = df_q_2["daily_exited_surveys"]+df_q_2["daily_completed_surveys"] df_q_2["positivity_rate"] = df_q_2["daily_exited_surveys"]/df_q_2["daily_total_surveys"]*100 df_q_2["fmt_date"] = df_q_2.index - df_q_2.index = [datetime.datetime.strptime(a,"%Y-%m-%d").strftime("%D") for a in df_q_2.index] + df_q_2.index = [a for a in df_q_2.index] df = df_q_2.reset_index() if end_request in list(df["fmt_date"]): df = df.loc[df.index[0]:df[df["fmt_date"]==end_request].index[0]] @@ -1277,9 +1277,9 @@ def pos_plot(df,width=None,height=400): .join(models.Question,models.SurveyResponse)\ .filter(models.SurveyResponse.survey_id==survey.id)\ .filter(models.SurveyResponse.start_time > time_start)\ - .filter(models.SurveyResponse.start_time <= time_end)\ + .filter(models.SurveyResponse.start_time <= (time_end+datetime.timedelta(days=1)))\ .filter(models.SurveyResponse.end_time > time_start)\ - .filter(models.SurveyResponse.end_time <= time_end) + .filter(models.SurveyResponse.end_time <= (time_end+datetime.timedelta(days=1))) df_q = pd.read_sql(sr.statement,con=db.engine) df_q.columns = ["date","response_id","uniq_id","response","question_id","question_title","question_choices","question_type"] @@ -1288,7 +1288,7 @@ def pos_plot(df,width=None,height=400): if len(qres)>0: qres = qres.groupby(["date","question_id","uniq_id"]).first() qres = qres.reset_index() - e = [str(a.date()) for a in list(pd.date_range(start_time,end_time))] + e = [str(a.date()) for a in list(pd.date_range(start_time,(datetime.datetime.strptime(end_request,"%Y-%m-%d")+datetime.timedelta(days=1)).date()))] for n,g in qres.groupby("question_id"): title = list(g.question_title)[0] @@ -1299,7 +1299,7 @@ def pos_plot(df,width=None,height=400): pltdf = g.loc[:,["date","response"]] pltdf["count"] = 1 pltdf.sort_values("date",inplace=True) - pltdf["date"] = pltdf["date"].astype(str) + #pltdf["date"] = pltdf["date"].astype(str) refmt = [] for ix,row in pltdf.iterrows(): row = list(row) @@ -1375,7 +1375,7 @@ def survey_response_student_dashboard(): df_q_2["daily_total_surveys"] = df_q_2["daily_exited_surveys"]+df_q_2["daily_completed_surveys"] df_q_2["positivity_rate"] = df_q_2["daily_exited_surveys"]/df_q_2["daily_total_surveys"]*100 df_q_2["fmt_date"] = df_q_2.index - df_q_2.index = [datetime.datetime.strptime(a,"%Y-%m-%d").strftime("%D") for a in df_q_2.index] + df_q_2.index = [a for a in df_q_2.index] df = df_q_2.reset_index() df2 = df.loc[:,["index","daily_completed_surveys","daily_exited_surveys"]] df2.columns = ["index","Cleared","Sent Home"] @@ -1431,7 +1431,7 @@ def pos_plot(df,width=None,height=400): ##special COVID tracking stats - NYC area special_figs_2 = [] special_figs_2.append(plotly.io.read_json(open("/home/ubuntu/medtracker/medtracker/data/daily_cases.json","r"))) - special_figs_2.append(plotly.io.read_json(open("/home/ubuntu/medtracker/medtracker/data/r0_estimate.json","r"))) + special_figs_2.append(plotly.io.read_json(open("/home/ubuntu/medtracker/medtracker/data/hospitalizations.json","r"))) for ix,fig in enumerate(special_figs): special_figs[ix] = offline.plot(fig,show_link=False, output_type="div", include_plotlyjs=False) @@ -1537,7 +1537,7 @@ def plotlyBarplot(x=None,y=None,hue=None,data=None,ylabel="",xlabel="",title="", counts = round(counts/totalcounts.loc[counts.index,]*100,1) x = [str(i) for i in counts.index] y = counts.values - name = str(hue)[0:8] + ".." if len(str(hue))>10 else str(hue) + name = str(hue)[0:18] + ".." if len(str(hue))>20 else str(hue) trace = go.Bar(x=x,y=y, text=y, textposition='auto', From c34c25391135a356364466b4d3beb55631e7083e Mon Sep 17 00:00:00 2001 From: ryananeff Date: Wed, 2 Jun 2021 16:05:03 +0000 Subject: [PATCH 42/44] refresh pt id --- medtracker/views.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/medtracker/views.py b/medtracker/views.py index cdb9248..fd076e7 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -100,6 +100,12 @@ def remember_pt_id(response): else: g.patient_ident = g.device.device_id g.patient = Patient.query.filter_by(mrn=g.patient_ident).first() + + @after_this_request + def refresh_pt_id(response): + response.set_cookie('patient_ident', g.patient_ident, max_age=datetime.timedelta(weeks=52)) + return response + if current_user.is_authenticated: current_user.surveys = Survey.query current_user.patients = Patient.query From 586fc80cc6946acaa40cd905119e1b92024687d0 Mon Sep 17 00:00:00 2001 From: ryananeff Date: Wed, 2 Jun 2021 16:07:45 +0000 Subject: [PATCH 43/44] refresh pt id --- medtracker/views.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/medtracker/views.py b/medtracker/views.py index fd076e7..99d0959 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -100,11 +100,10 @@ def remember_pt_id(response): else: g.patient_ident = g.device.device_id g.patient = Patient.query.filter_by(mrn=g.patient_ident).first() - - @after_this_request - def refresh_pt_id(response): - response.set_cookie('patient_ident', g.patient_ident, max_age=datetime.timedelta(weeks=52)) - return response + @after_this_request + def refresh_pt_id(response): + response.set_cookie('patient_ident', g.patient_ident, max_age=datetime.timedelta(weeks=52)) + return response if current_user.is_authenticated: current_user.surveys = Survey.query From cfb913f312bd1e002ce5aaba1a68aba899761a7d Mon Sep 17 00:00:00 2001 From: ryananeff Date: Fri, 27 Aug 2021 20:51:28 +0000 Subject: [PATCH 44/44] expired tracker; fixed dashboard 8/26 update --- assets/main.css | 3 ++- medtracker/__init__.py | 1 + medtracker/data/nyc_plots.py | 19 +++++++++++++---- medtracker/templates/layout.html | 5 +++-- medtracker/views.py | 36 +++++++++++++++++++------------- 5 files changed, 42 insertions(+), 22 deletions(-) diff --git a/assets/main.css b/assets/main.css index 5b8b0e2..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; @@ -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/medtracker/__init__.py b/medtracker/__init__.py index 0eb6646..2544936 100755 --- a/medtracker/__init__.py +++ b/medtracker/__init__.py @@ -37,6 +37,7 @@ app.config['SESSION_COOKIE_SECURE'] = False app.config['SECRET_KEY'] = flask_secret_key app.config['WTF_CSRF_ENABLED']=True +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 diff --git a/medtracker/data/nyc_plots.py b/medtracker/data/nyc_plots.py index 535df67..ab6df11 100644 --- a/medtracker/data/nyc_plots.py +++ b/medtracker/data/nyc_plots.py @@ -1,6 +1,8 @@ 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', @@ -24,8 +26,15 @@ def prepFigure(figure, title): start_date = "2020-03-01" #confirmed cases -url = "https://usafactsstatic.blob.core.windows.net/public/data/covid-19/covid_confirmed_usafacts.csv" -confirmed_df=pd.read_csv(url) +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"]] @@ -39,8 +48,10 @@ def prepFigure(figure, title): #confirmed deaths -url = "https://usafactsstatic.blob.core.windows.net/public/data/covid-19/covid_deaths_usafacts.csv" -deaths_df = pd.read_csv(url) +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"]] diff --git a/medtracker/templates/layout.html b/medtracker/templates/layout.html index 4e95a39..8e6ac0c 100755 --- a/medtracker/templates/layout.html +++ b/medtracker/templates/layout.html @@ -1,8 +1,8 @@ - - + + @@ -24,6 +24,7 @@ + diff --git a/medtracker/views.py b/medtracker/views.py index 99d0959..42caa28 100755 --- a/medtracker/views.py +++ b/medtracker/views.py @@ -87,22 +87,26 @@ def detect_user_session(): if (patient_ident_req is None)|(g.device is None): g.patient_ident = randomword(16) + flash("The COVID19 tracker will be retired on July 1st, 2021. No new devices may be registered. Your device is currently unregistered.") # when the response exists, set a cookie with the language - @after_this_request - def remember_pt_id(response): - response.set_cookie('patient_ident', g.patient_ident, max_age=datetime.timedelta(weeks=52)) - device = Device(device_id = g.patient_ident) - db.session.add(device) - db.session.commit() - g.device = device - return response + #@after_this_request + #def remember_pt_id(response): + #response.set_cookie('patient_ident', g.patient_ident, max_age=datetime.timedelta(weeks=52)) + #device = Device(device_id = g.patient_ident) + #db.session.add(device) + #db.session.commit() + #g.device = device + #return response else: g.patient_ident = g.device.device_id g.patient = Patient.query.filter_by(mrn=g.patient_ident).first() @after_this_request def refresh_pt_id(response): - response.set_cookie('patient_ident', g.patient_ident, max_age=datetime.timedelta(weeks=52)) + if hasattr(g,"flashed") == False: + response.set_cookie('patient_ident', g.patient_ident, max_age=(datetime.datetime(2021,7,1)-datetime.datetime.now())) + flash("The COVID19 tracker will be retired on July 1st, 2021. Your device will be unregistered automatically at that time.") + g.flashed=True return response if current_user.is_authenticated: @@ -281,7 +285,8 @@ def send_survey_response_email(patient, record, app=app): def index(): g.patient = Patient.query.filter_by(mrn=g.patient_ident).first() if g.patient == None: - flash("Your device appears to be unregistered. Please register your device.") + pass + #flash("Your device appears to be unregistered. Please register your device.") elif check_patient_identified(g.patient)==False: flash("Due to changes at Student Health, please update your records before continuing.") return render_template("index.html") @@ -409,8 +414,8 @@ def serve_survey(survey_id): today = datetime.datetime.now().date() previous_responses = SurveyResponse.query.filter(SurveyResponse.uniq_id==g.patient.id).\ filter(SurveyResponse.end_time.isnot(None),SurveyResponse.start_time>today).first() - if (current_user.is_authenticated==False) & (previous_responses!=None): - return render_template("survey_quit.html",survey=survey, patient=g.patient,message="You can only take the survey once per day.") + #if (current_user.is_authenticated==False) & (previous_responses!=None): + #return render_template("survey_quit.html",survey=survey, patient=g.patient,message="You can only take the survey once per day.") survey_response_id = request.values.get("sr", None) question_id = request.values.get("question", None) @@ -666,7 +671,8 @@ def remove_question(_id): ### controller for patient functions @app.route('/patients/signup/', methods=['GET', 'POST']) def patient_signup(survey_id): - '''GUI: add a patient to the DB via user sign up''' + + #GUI: add a patient to the DB via user sign up patient = Patient.query.filter_by(mrn=g.patient_ident).first() if patient == None: new_patient = True @@ -688,7 +694,7 @@ def patient_signup(survey_id): Please keep this ID for your records: %s"""% fmt_id(g.patient_ident)) return redirect(url_for("start_survey",survey_id=survey_id)) if new_patient: - return render_template('form_signup_register.html', action="Register", data_type="device", form=formobj) + return abort(401,"The student health tracker is being retired and no new devices may be registered at this time. Thank you for using the student health check app! :)") else: if (patient.fullname != None) & (patient.fullname != ""): flash("Welcome back, %s! (ID:%s)" % (patient.fullname,fmt_id(patient.mrn))) @@ -1342,7 +1348,7 @@ def pos_plot(df,width=None,height=400): today_pct_pos=today_pct_pos, patients = sig_r,special_figs=special_figs) @app.route("/covid/dashboard",methods=["GET"]) -@cache.cached(timeout=None,key_prefix=make_cache_key) +#@cache.cached(timeout=1800,key_prefix=make_cache_key) def survey_response_student_dashboard(): survey_id = 1 start_request = request.values.get("start_date","2020-06-29")
    See my records