Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EMPRO opt out UI #4335

Merged
merged 40 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
96d8ccf
EMPRO opt out UI
Aug 14, 2023
22f2335
add error message display
Sep 5, 2023
96dc098
refactor, bug fix
Sep 12, 2023
1d68f28
TN-3236, sequential hard trigger counts kept in trigger_states.trigge…
pbugni Sep 6, 2023
e6f4c32
expand migration downgrade step, to remove all sequential hard domain…
pbugni Sep 20, 2023
c419436
add debugging into to exception text
pbugni Nov 17, 2023
2e67aef
add exception to catch details for bogus records
pbugni Nov 29, 2023
8b456b3
add `sanity_check()` to /timewarp - confirm invariants before and aft…
pbugni Nov 29, 2023
98fb482
rework alembic migration order to reapply hard trigger migration again
pbugni Jan 25, 2024
ae92158
refactor to reuse same migration again, with patched code in place
pbugni Jan 25, 2024
b57f478
refactor email generation to common empro_messages module.
pbugni Mar 13, 2024
f9afc90
progress towards regenerating a user's trigger_states table on EMPRO …
pbugni Mar 13, 2024
0facf6e
correct order of migrations with work done outside branch.
pbugni Mar 13, 2024
85e635a
allow for minor EMPRO consent changes and withdrawal calls w/o raisin…
pbugni Mar 13, 2024
20490b7
minor refactor typo; initialize variable only set on a match before t…
pbugni Mar 14, 2024
1aaf5a2
alert on production, purge on dev/test, problem trigger_states rows f…
pbugni Mar 15, 2024
31ae28d
Merge branch 'develop' of https://github.com/uwcirg/true_nth_usa_port…
Mar 15, 2024
6633f56
Merge branch 'feature/sequential-hard-triggers-fix' of https://github…
Mar 15, 2024
792e2c6
add error check
Mar 15, 2024
8ec3f85
add call to API, bug fixes and styling fixes
Mar 18, 2024
a305711
Delete portal/eproms/static/css/eproms.css
achen2401 Mar 18, 2024
93dd9ac
sequence check fix
Mar 18, 2024
bf86dc3
Merge branch 'feature/opt-out-UI' of https://github.com/uwcirg/true_n…
Mar 18, 2024
ae34c63
fix error check, styling fixes
Mar 18, 2024
dfd9c3a
fix text
Mar 18, 2024
5ce76ea
update test data with real example
Mar 19, 2024
100b0b8
bug fix, refactor
Mar 19, 2024
05c6b72
fix text per feedback, runtime error fix
Mar 19, 2024
793fcc4
fix based on feedback
Mar 19, 2024
cc23b5a
fix study check
Mar 19, 2024
b51e912
minor refactor, styling fix
Mar 19, 2024
6014adf
Update empro.js
achen2401 Mar 20, 2024
3912144
Merge branch 'feature/sequential-hard-triggers-fix' of https://github…
Mar 20, 2024
9807d67
add successful save feedback from API call, styling fix
Mar 20, 2024
b5fe8d5
remove commented out code
Mar 20, 2024
b937fb6
remove un-needed import, fix after save code
Mar 20, 2024
43dd921
Merge branch 'feature/sequential-hard-triggers-fix' of https://github…
Mar 20, 2024
bbb111c
Merge branch 'feature/sequential-hard-triggers-fix' of https://github…
Mar 21, 2024
71a0967
use PUT
Mar 21, 2024
aa356aa
fix opt out submit data
Mar 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions portal/eproms/templates/eproms/assessment_engine.html
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@
<div class="error error-message"></div>
</div>
{% endif %}
{%- from "eproms/assessment_engine/ae_macros.html" import empro_thankyou_modal, empro_optout_modal -%}
{{empro_optout_modal(user)}}
{{empro_thankyou_modal(user)}}

Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
<section class="portal-main portal-flex-container">
{%- block body -%}{%- endblock -%}
</section>
<div class="loading-container hide"><div class="content"><i class="fa fa-spinner fa-spin fa-2x"></i> Loading ...</div></div>
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{%- from "eproms/assessment_engine/ae_macros.html" import render_header, render_greeting, render_card_content, render_call_to_button, due_card, completed_card, empro_due, empro_completed, empro_expired, completed_cards -%}
{%- from "eproms/assessment_engine/ae_macros.html" import empro_script, render_header, render_greeting, render_card_content, render_call_to_button, due_card, completed_card, empro_due, empro_completed, empro_expired, completed_cards -%}
{% extends "eproms/assessment_engine/ae_base.html" %}
<!-- baseline due -->
{% block head %}
Expand Down
88 changes: 86 additions & 2 deletions portal/eproms/templates/eproms/assessment_engine/ae_macros.html
Original file line number Diff line number Diff line change
Expand Up @@ -227,13 +227,19 @@ <h4>{{_("Your Support Team")}}</h4>

</div>
{%- endmacro -%}
{%- macro empro_no_contact_notice() -%}
<div class="no-contact-list-wrapper hide">
<b>Note:</b> You have chosen not to be contacted about <span id="noContactTriggerList"></span>. Your care team will not discuss these issues this month.
</div>
{%- endmacro -%}
{%- macro empro_modal_hardTrigger_supportTeam_block(organization="") -%}
<div class="item">
<h4>{{_("Your Support Team")}}</h4>
<p>{{_("You reported experiencing or feeling:")}}</p>
<ul class="hardTriggersDisplayList">
<!-- dynamically populated based on user trigger domain(s) -->
</ul>
{{empro_no_contact_notice()}}
<p><b>{{_("To help address any issues, we've informed your care team and they'll be in contact with you soon.")}}</b></p>
{% if organization %}
<p>{{_("In the meantime, if you have any questions or need assistance, please contact your team at %(organization)s directly. They're happy to help.", organization=organization)}}</p>
Expand Down Expand Up @@ -270,8 +276,8 @@ <h4>{{_("Your Health Tips")}}</h4>
<!--sub-study macro -->
{%- macro empro_thankyou_modal(user) -%}
<!-- sub-study modal for display domain topic(s), resources link and summary report link to the user -->
<div class="modal fade" tabindex="-1" role="dialog" id="emproModal" {% if user and user.current_encounter().auth_method == 'url_authenticated'%}data-url-authenticated="true"{% endif %}>
<div class="modal-dialog">
<div class="modal fade" role="dialog" id="emproModal" tabindex="-1" aria-hidden="true" {% if user and user.current_encounter().auth_method == 'url_authenticated'%}data-url-authenticated="true"{% endif %}>
<div class="modal-dialog modal-xl">
<div class="modal-content">
<button type="button" class="close" data-dismiss="modal" aria-label="{{_('Close')}}">
<span aria-hidden="true">&times;</span>
Expand Down Expand Up @@ -311,6 +317,84 @@ <h2 class="title">
</div>
{{empro_script()}}
{%- endmacro -%}
<!--sub-study optout modal macro -->
{%- macro empro_optout_modal(user) -%}
<!-- sub-study modal for display domain topic(s), resources link and summary report link to the user -->
<div class="modal fade" role="dialog" id="emproOptOutModal" data-backdrop="static" tabindex="-1" aria-hidden="true" {% if user and user.current_encounter().auth_method == 'url_authenticated'%}data-url-authenticated="true"{% endif %}>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<button type="button" class="close btn-dismiss" data-dismiss="modal" aria-label="{{_('Close')}}">
<span aria-hidden="true">&times;</span>
</button>
<div class="modal-body">
<div class="header-section">
<h2 class="title">{{_("We want to check with you ...")}}</h2>
<div class="subtitle" style="margin-bottom: 8px">
{{_("About your preference to be contacted by your care team for these ongoing issues.")}}
</div>
</div>
<!-- <div class="items-section">
<div class="item wrap">
<ul>
<li>anxious</li>
<li>discouraged</li>
<li>fatigue</li>
</ul>
<ul>
<li>general pain</li>
<li>insomnia</li>
</ul>
</div>
</div> -->
<p>{{_("We’ve noticed you’re continuing to experience challenges with the issues listed below.")}}</p>
<p class="text-warning">
<b>{{_("If you prefer not to be contacted by your care team for any (or all) of these issues, please check the box(es) below.")}}</b>
</p>
<p>
{{_("Your care team will continue to contact you as usual for other identified issues.")}}<br/>
{{_("If you have any questions or need assistance, please contact your team directly at %(organization)s. They'll be happy to help.", organization=user.organizations[0].name if user else "")}}
</p>
<div class="items-wrapper">
<p><b>{{_("Please do not contact me about:")}}</b></p>
<div class="items optout-domains-checkbox-list">
<!-- <div class="item">
<input type="checkbox">
<span>anxious</span>
</div>
<div class="item">
<input type="checkbox">
<span>discouraged</span>
</div>
<div class="item">
<input type="checkbox">
<span>fatigue</span>
</div>
<div class="item">
<input type="checkbox">
<span>general pain</span>
</div>
<div class="item">
<input type="checkbox">
<span>insomnia</span>
</div> -->
</div>
</div>
<div class="error-message"></div>
<div class="continue-container hide">
<button class="btn btn-default continue-button">Continue</button>
</div>
</div>
<div class="modal-footer">
<div>
<button class="btn btn-empro-primary btn-submit">Submit</button>
<button class="btn btn-default btn-dismiss">Dismiss</button>
</div>
<div class="saving-indicator-container hide"><i class="fa fa-spinner fa-spin"></i> {{_("Saving data...")}}</div>
</div>
</div>
</div>
</div>
{%- endmacro -%}
{%- macro empro_script() -%}
<script src="{{ url_for('static', filename='js/dist/empro.bundle.js') }}" defer></script>
{%- endmacro -%}
92 changes: 86 additions & 6 deletions portal/migrations/versions/80c3b1e96c45_.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
"""Add sequential hard trigger count to EMPRO trigger_states.triggers domains.

Revision ID: 80c3b1e96c45
Revises: 2e9b9e696bb8
Revises: 3c871e710277
Create Date: 2023-07-24 17:08:35.128975

"""
from collections import defaultdict
from copy import deepcopy
from alembic import op
from io import StringIO
from flask import current_app
import logging
from sqlalchemy.orm import sessionmaker
from portal.database import db
from portal.trigger_states.empro_domains import (
Expand All @@ -19,12 +21,48 @@

# revision identifiers, used by Alembic.
revision = '80c3b1e96c45'
down_revision = '2e9b9e696bb8'
down_revision = '3c871e710277'

Session = sessionmaker()

log = logging.getLogger("alembic.runtime.migration")
log.setLevel(logging.DEBUG)


def validate_users_trigger_states(session, patient_id):
"""Confirm user has sequential visits in trigger states table.

Due to allowance of moving EMPRO consents and no previous checks,
some users on test have invalid overlapping trigger states rows.
"""
ts_rows = session.query(TriggerState).filter(
TriggerState.user_id == patient_id).order_by(TriggerState.id)
month_counter = -1
for row in ts_rows:
if row.state == 'due':
# skipping months is okay, but every due should be sequentially greater than previous
if month_counter >= row.visit_month:
raise ValueError(f"{patient_id} expected month > {month_counter}, got {row.visit_month}")
month_counter = row.visit_month
else:
# states other than 'due' should be grouped together with same visit_month
if month_counter != row.visit_month:
raise ValueError(f"{patient_id} expected month {month_counter}, got {row.visit_month}")

def purge_trigger_states(session, patient_id):
"""Clean up test system problems from moving consent dates"""
log.info(f"Purging trigger states for {patient_id}")
session.query(TriggerState).filter(TriggerState.user_id == patient_id).delete()


def upgrade():
# Add sequential counts to appropriate trigger_states rows.

# this migration was applied once before, but the code wasn't correctly
# maintaining the sequential counts. start by removing all for a clean
# slate via the same `downgrade()` step
downgrade()

# for each active EMPRO patient with at least 1 hard triggered domain,
# walk through their monthly reports, adding the sequential count for
# the opt-out feature.
Expand All @@ -42,6 +80,15 @@ def upgrade():
# can't just send through current process, as it'll attempt to
# insert undesired rows in the trigger_states table. need to
# add the sequential count to existing rows.
try:
validate_users_trigger_states(session, pid)
except ValueError as e:
if current_app.config.get('SYSTEM_TYPE') in ('development', 'test'):
purge_trigger_states(session, pid)
continue
else:
raise e

output.write(f"\n\nPatient: {pid} storing all zeros for sequential hard triggers except:\n")
output.write(" (visit month : domain : # hard sequential)\n")
sequential_by_domain = defaultdict(list)
Expand All @@ -54,9 +101,10 @@ def upgrade():
for d in EMPRO_DOMAINS:
sequential_hard_for_this_domain = 0
if d not in improved_triggers["domain"]:
# only seen on test, fill in the missing domain
print(f"missing {d} in {pid}:{ts.visit_month}?")
improved_triggers["domain"][d] = {}
# shouldn't happen, SDC typically includes all domains
# but a few records are lacking
log.warning(f"{pid} missing domain {d} in {ts.visit_month} response")
continue

if any(v == "hard" for v in improved_triggers["domain"][d].values()):
sequential_by_domain[d].append(ts.visit_month)
Expand All @@ -81,4 +129,36 @@ def upgrade():


def downgrade():
pass # no value in removing
# for each active EMPRO patient with at least 1 hard triggered domain,
# remove any sequential counts found
bind = op.get_bind()
session = Session(bind=bind)

patient_ids = []
for patient_id in session.execute(
"SELECT DISTINCT(user_id) FROM trigger_states JOIN users"
" ON users.id = user_id WHERE deleted_id IS NULL"):
patient_ids.append(patient_id[0])

output = StringIO()
for pid in patient_ids:
output.write(f"\n\nPatient: {pid}\n")
trigger_states = db.session.query(TriggerState).filter(
TriggerState.user_id == pid).filter(
TriggerState.state == "resolved").order_by(
TriggerState.timestamp.asc())
for ts in trigger_states:
improved_triggers = deepcopy(ts.triggers)
for d in EMPRO_DOMAINS:
if d not in improved_triggers["domain"]:
log.warning(f"{d} missing from {ts.id}(month: {ts.visit_month}) for {pid}")
continue
if sequential_hard_trigger_count_key in improved_triggers["domain"][d]:
del improved_triggers["domain"][d][sequential_hard_trigger_count_key]
output.write(f" removed sequential from {ts.visit_month}:{d} {improved_triggers['domain'][d]}\n")

# retain triggers now containing sequential counts
ts.triggers = improved_triggers

db.session.commit()
print(output.getvalue())
2 changes: 1 addition & 1 deletion portal/migrations/versions/d1f3ed8d16ef_.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

# revision identifiers, used by Alembic.
revision = 'd1f3ed8d16ef'
down_revision = '80c3b1e96c45'
down_revision = '2e9b9e696bb8'


def upgrade():
Expand Down
13 changes: 9 additions & 4 deletions portal/models/qb_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,8 @@ def warn_on_duplicate_request(self, requested_set):
f" {requested_indef} already!")


def patient_research_study_status(patient, ignore_QB_status=False):
def patient_research_study_status(
patient, ignore_QB_status=False, as_of_date=None, skip_initiate=False):
"""Returns details regarding patient readiness for available studies

Wraps complexity of checking multiple QB_Status and ResearchStudy
Expand All @@ -532,6 +533,8 @@ def patient_research_study_status(patient, ignore_QB_status=False):
:param patient: subject to check
:param ignore_QB_status: set to prevent recursive call, if used during
process of evaluating QB_status. Will restrict results to eligible
:param as_of_date: set to check status at alternative time
:param skip_initiate: set only when rebuilding to avoid state change
:returns: dictionary of applicable studies keyed by research_study_id.
Each contains a dictionary with keys:
- eligible: set True if assigned to research study and pre-requisites
Expand All @@ -546,7 +549,8 @@ def patient_research_study_status(patient, ignore_QB_status=False):
"""
from datetime import datetime
from .research_study import EMPRO_RS_ID, ResearchStudy
as_of_date = datetime.utcnow()
if as_of_date is None:
as_of_date = datetime.utcnow()

results = {}
# check studies in required order - first found with pending work
Expand Down Expand Up @@ -601,7 +605,8 @@ def patient_research_study_status(patient, ignore_QB_status=False):
elif rs_status['ready']:
# As user may have just entered ready status on EMPRO
# move trigger_states.state to due
from ..trigger_states.empro_states import initiate_trigger
initiate_trigger(patient.id)
if not skip_initiate:
from ..trigger_states.empro_states import initiate_trigger
initiate_trigger(patient.id)

return results
3 changes: 2 additions & 1 deletion portal/models/qb_timeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,8 @@ def qbds_for_rp(rp, classification, trigger_date):
)
if curRPD.retired == nextRPD.retired:
raise ValueError(
"Invalid state: multiple RPs w/ same retire date")
"Invalid state: multiple RPs w/ same retire date: "
f"{next_rp} : {curRPD.retired}")
else:
nextRPD = None
yield curRPD, nextRPD
Expand Down
Loading