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

TN-3292 fix performance of patient list #4408

Merged
merged 83 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
83 commits
Select commit Hold shift + click to select a range
8fa52d1
fix patient list table options to retrieve data via ajax
Sep 26, 2024
4e16ae8
fix call, remove fields
Oct 1, 2024
2ef425d
New table and migration for paginated view of /patients
pbugni Oct 1, 2024
227e60d
Keep patient_list in sync, updating whenever adherence_cache gets the…
pbugni Oct 1, 2024
b0876e8
initial (very basic! missing lots) implementation of /patients/page …
pbugni Oct 1, 2024
33a42a4
populate data.rows from SQLA row object (not as simple as i had hoped).
pbugni Oct 1, 2024
9270389
fix template
Oct 1, 2024
cc4ebe0
populate table with ajax data, bug fix
Oct 1, 2024
b5d6a8a
add a job to pick up all deleted users when building patient list
pbugni Oct 2, 2024
c390571
remove obsolete code. patch check for `include_test_role`
pbugni Oct 2, 2024
25d2532
allow null and non-unique email in patient_list, as required for `del…
pbugni Oct 2, 2024
6ca5b98
fix deleted account rows
Oct 2, 2024
043689a
format date
Oct 2, 2024
7d6d8d8
populate remaining columns for global study in patient list
pbugni Oct 2, 2024
9571dbb
populate remaining columns for global study in patient list
pbugni Oct 2, 2024
a11cb42
filter patient list by organization filter.
pbugni Oct 2, 2024
fe5a478
must cast query string param to int!
pbugni Oct 2, 2024
09e91a1
fix org column, pass research study id, code clean up
Oct 2, 2024
3b93bfd
put userid column back
Oct 2, 2024
d761015
allow userid field to be searchable, remove search fields for dob and
Oct 3, 2024
cb5a389
implement search in patient list
pbugni Oct 3, 2024
96baffd
implement order by column and direction
pbugni Oct 3, 2024
9c802fb
fix column selection & filter displays
Oct 3, 2024
dc8ac82
look to db table_preferences if sort & filter args aren't found in re…
pbugni Oct 3, 2024
d873338
fix empro field names, remove un-needed fields
Oct 3, 2024
86327b8
add EMPRO data; all fields renamed in patient_list to match front en…
pbugni Oct 4, 2024
7a6e499
fix sort by id issue
Oct 4, 2024
f8d0068
fix patient list table options to retrieve data via ajax
Sep 26, 2024
8a3291d
fix call, remove fields
Oct 1, 2024
6ae8d43
New table and migration for paginated view of /patients
pbugni Oct 1, 2024
cbfe0c6
Keep patient_list in sync, updating whenever adherence_cache gets the…
pbugni Oct 1, 2024
6bd5dd0
initial (very basic! missing lots) implementation of /patients/page …
pbugni Oct 1, 2024
189adb7
populate data.rows from SQLA row object (not as simple as i had hoped).
pbugni Oct 1, 2024
93e0cf6
fix template
Oct 1, 2024
f22d7d5
populate table with ajax data, bug fix
Oct 1, 2024
b607ea7
add a job to pick up all deleted users when building patient list
pbugni Oct 2, 2024
2a10482
remove obsolete code. patch check for `include_test_role`
pbugni Oct 2, 2024
31d0675
allow null and non-unique email in patient_list, as required for `del…
pbugni Oct 2, 2024
5d0c5ac
fix deleted account rows
Oct 2, 2024
f4324ca
format date
Oct 2, 2024
fecc3f1
populate remaining columns for global study in patient list
pbugni Oct 2, 2024
eb8e0d3
populate remaining columns for global study in patient list
pbugni Oct 2, 2024
5404a21
filter patient list by organization filter.
pbugni Oct 2, 2024
620b319
must cast query string param to int!
pbugni Oct 2, 2024
61e541f
fix org column, pass research study id, code clean up
Oct 2, 2024
cc8b43f
put userid column back
Oct 2, 2024
cbc5454
allow userid field to be searchable, remove search fields for dob and
Oct 3, 2024
fe3ba58
implement search in patient list
pbugni Oct 3, 2024
b9b4896
implement order by column and direction
pbugni Oct 3, 2024
1fba217
fix column selection & filter displays
Oct 3, 2024
2dab7dd
look to db table_preferences if sort & filter args aren't found in re…
pbugni Oct 3, 2024
e696c55
fix empro field names, remove un-needed fields
Oct 3, 2024
389a995
add EMPRO data; all fields renamed in patient_list to match front en…
pbugni Oct 4, 2024
1eeaf6a
fix sort by id issue
Oct 4, 2024
6fc58ce
select sort fields need exact match
pbugni Oct 4, 2024
e311bb6
overlooked `empro_visit`
pbugni Oct 4, 2024
506ee46
fix filter placeholder text
Oct 4, 2024
2f1cc8b
fix consent date field label
Oct 4, 2024
14bd71f
included `_untranslated` versions of all translated columns in patien…
pbugni Oct 4, 2024
e5a3a4c
update patient_list cache on demographics change
pbugni Oct 5, 2024
febce33
don't allow non-patients to sneak into patient_list
pbugni Oct 5, 2024
2dcbf9b
fix filter select lists
Oct 7, 2024
157801f
Merge branch 'feature/fast-patient-list' of https://github.com/uwcirg…
Oct 7, 2024
209ae40
Update admin_base.html - remove loader code
achen2401 Oct 7, 2024
e61035f
Update admin.js - remove buggy code
achen2401 Oct 7, 2024
aab4709
filter bug fixes
Oct 7, 2024
2cbcf47
fix retry attempts
Oct 7, 2024
d05fb72
include all "options" with both translated and english versions for a…
pbugni Oct 7, 2024
c9fb633
use filter option list from backend
Oct 7, 2024
ea45f61
update patient_list for freshly deleted patients.
pbugni Oct 7, 2024
aac7eff
bug fix, remove debug statement
Oct 8, 2024
4f4d77d
add debouncing
Oct 8, 2024
8124b16
add missing `ondelete='cascade'` clauses to foreign keys preventing p…
pbugni Oct 8, 2024
e4462df
add missing `ondelete='cascade'` clauses to foreign keys preventing p…
pbugni Oct 8, 2024
176dfc7
Merge branch 'develop' of https://github.com/uwcirg/true_nth_usa_port…
Oct 8, 2024
35fa2cc
as user demographics changes frequently come in one after another, mo…
pbugni Oct 9, 2024
bb0e7b8
code cleanup
Oct 9, 2024
fdc2cc3
fix loader style
Oct 9, 2024
f190c17
remove un-used code
Oct 9, 2024
2d7088e
need to commit changes on early exit from patient_list update!
pbugni Oct 9, 2024
907ade4
remove test failing, as the intervention column is no longer a part o…
pbugni Oct 9, 2024
2e187c3
pep8
pbugni Oct 9, 2024
1328a09
missed a reuse of an existing foreign key constraint during development.
pbugni Oct 9, 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
8 changes: 8 additions & 0 deletions portal/config/eproms/ScheduledJob.json
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,14 @@
"schedule": "0 0 0 0 0",
"task": "raise_background_exception_task"
},
{
"active": false,
"args": null,
"name": "Populate Patient List",
"resourceType": "ScheduledJob",
"schedule": "0 0 0 0 0",
"task": "cache_patient_list"
},
{
"active": true,
"args": null,
Expand Down
76 changes: 76 additions & 0 deletions portal/migrations/versions/038a1a5f4218_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""add patient_list table for paginated /patients view

Revision ID: 038a1a5f4218
Revises: daee63f50d35
Create Date: 2024-09-30 16:10:26.216512

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = '038a1a5f4218'
down_revision = 'daee63f50d35'


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('patient_list',
sa.Column('userid', sa.Integer(), nullable=False),
sa.Column('study_id', sa.Text(), nullable=True),
sa.Column('firstname', sa.String(length=64), nullable=True),
sa.Column('lastname', sa.String(length=64), nullable=True),
sa.Column('birthdate', sa.Date(), nullable=True),
sa.Column('email', sa.String(length=120), nullable=True),
sa.Column('questionnaire_status', sa.Text(), nullable=True),
sa.Column('empro_status', sa.Text(), nullable=True),
sa.Column('clinician', sa.Text(), nullable=True),
sa.Column('action_state', sa.Text(), nullable=True),
sa.Column('visit', sa.Text(), nullable=True),
sa.Column('empro_visit', sa.Text(), nullable=True),
sa.Column('consentdate', sa.DateTime(), nullable=True),
sa.Column('empro_consentdate', sa.DateTime(), nullable=True),
sa.Column('org_name', sa.Text(), nullable=True),
sa.Column('deleted', sa.Boolean(), nullable=True),
sa.Column('test_role', sa.Boolean(), nullable=True),
sa.Column('org_id', sa.Integer(), nullable=True),
sa.Column('last_updated', sa.DateTime(), nullable=False),
sa.ForeignKeyConstraint(['org_id'], ['organizations.id'], ),
sa.PrimaryKeyConstraint('userid')
)
op.create_index(op.f('ix_patient_list_action_state'), 'patient_list', ['action_state'], unique=False)
op.create_index(op.f('ix_patient_list_birthdate'), 'patient_list', ['birthdate'], unique=False)
op.create_index(op.f('ix_patient_list_clinician'), 'patient_list', ['clinician'], unique=False)
op.create_index(op.f('ix_patient_list_consentdate'), 'patient_list', ['consentdate'], unique=False)
op.create_index(op.f('ix_patient_list_email'), 'patient_list', ['email'], unique=False)
op.create_index(op.f('ix_patient_list_empro_consentdate'), 'patient_list', ['empro_consentdate'], unique=False)
op.create_index(op.f('ix_patient_list_empro_status'), 'patient_list', ['empro_status'], unique=False)
op.create_index(op.f('ix_patient_list_empro_visit'), 'patient_list', ['empro_visit'], unique=False)
op.create_index(op.f('ix_patient_list_firstname'), 'patient_list', ['firstname'], unique=False)
op.create_index(op.f('ix_patient_list_lastname'), 'patient_list', ['lastname'], unique=False)
op.create_index(op.f('ix_patient_list_org_name'), 'patient_list', ['org_name'], unique=False)
op.create_index(op.f('ix_patient_list_questionnaire_status'), 'patient_list', ['questionnaire_status'], unique=False)
op.create_index(op.f('ix_patient_list_study_id'), 'patient_list', ['study_id'], unique=False)
op.create_index(op.f('ix_patient_list_visit'), 'patient_list', ['visit'], unique=False)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_patient_list_visit'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_study_id'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_questionnaire_status'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_org_name'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_lastname'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_firstname'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_empro_visit'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_empro_status'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_empro_consentdate'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_email'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_consentdate'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_clinician'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_birthdate'), table_name='patient_list')
op.drop_index(op.f('ix_patient_list_action_state'), table_name='patient_list')
op.drop_table('patient_list')
# ### end Alembic commands ###
52 changes: 52 additions & 0 deletions portal/migrations/versions/5a300be640fb_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""empty message

Revision ID: 5a300be640fb
Revises: 038a1a5f4218
Create Date: 2024-10-08 14:34:28.085963

"""
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql

# revision identifiers, used by Alembic.
revision = '5a300be640fb'
down_revision = '038a1a5f4218'


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('adherence_data_patient_id_fkey', 'adherence_data')
op.create_foreign_key(
'adherence_data_patient_id_fkey',
'adherence_data',
'users', ['patient_id'], ['id'], ondelete='cascade')
op.alter_column('patient_list', 'last_updated',
existing_type=postgresql.TIMESTAMP(),
nullable=True)
op.create_foreign_key(
'patient_list_userid_fkey',
'patient_list',
'users', ['userid'], ['id'], ondelete='cascade')
op.drop_constraint('research_data_subject_id_fkey', 'research_data', type_='foreignkey')
op.create_foreign_key(
'research_data_subject_id_fkey',
'research_data',
'users', ['subject_id'], ['id'], ondelete='cascade')
op.drop_constraint('research_data_questionnaire_response_id_fkey', 'research_data', type_='foreignkey')
op.create_foreign_key(
'research_data_questionnaire_response_id_fkey',
'research_data',
'questionnaire_responses', ['questionnaire_response_id'], ['id'], ondelete='cascade')
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('research_data_subject_id_fkey', 'research_data', type_='foreignkey')
op.create_foreign_key('research_data_subject_id_fkey', 'research_data', 'users', ['subject_id'], ['id'])
op.drop_constraint('patient_list_userid_fkey', 'patient_list', type_='foreignkey')
op.alter_column('patient_list', 'last_updated',
existing_type=postgresql.TIMESTAMP(),
nullable=False)
# ### end Alembic commands ###
3 changes: 2 additions & 1 deletion portal/models/adherence_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ class AdherenceData(db.Model):
"""
__tablename__ = 'adherence_data'
id = db.Column(db.Integer, primary_key=True)
patient_id = db.Column(db.ForeignKey('users.id'), index=True, nullable=False)
patient_id = db.Column(
db.ForeignKey('users.id', ondelete='cascade'), index=True, nullable=False)
rs_id_visit = db.Column(
db.Text, index=True, nullable=False,
doc="rs_id:visit_name")
Expand Down
101 changes: 101 additions & 0 deletions portal/models/patient_list.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""Module for PatientList, used specifically to populate and page patients"""
from datetime import datetime, timedelta
from ..database import db
from .research_study import BASE_RS_ID, EMPRO_RS_ID


class PatientList(db.Model):
"""Maintain columns for all list fields, all indexed for quick sort

Table used to generate pages of results for patient lists. Acts
as a cache, values should be updated on any change (questionnaire,
demographics, deletion, etc.)

All columns in both patients and sub-study lists are defined.
"""
__tablename__ = 'patient_list'
userid = db.Column(
db.ForeignKey('users.id', ondelete='cascade'), primary_key=True, nullable=False)
study_id = db.Column(db.Text, index=True)
firstname = db.Column(db.String(64), index=True)
lastname = db.Column(db.String(64), index=True)
birthdate = db.Column(db.Date, index=True)
email = db.Column(db.String(120), index=True)
questionnaire_status = db.Column(db.Text, index=True)
empro_status = db.Column(db.Text, index=True)
clinician = db.Column(db.Text, index=True)
action_state = db.Column(db.Text, index=True)
visit = db.Column(db.Text, index=True)
empro_visit = db.Column(db.Text, index=True)
consentdate = db.Column(db.DateTime, index=True)
empro_consentdate = db.Column(db.DateTime, index=True)
org_name = db.Column(db.Text, index=True)
deleted = db.Column(db.Boolean, default=False)
test_role = db.Column(db.Boolean)
org_id = db.Column(db.ForeignKey('organizations.id')) # used for access control
last_updated = db.Column(db.DateTime)


def patient_list_update_patient(patient_id, research_study_id=None):
"""Update given patient

:param research_study_id: define to optimize time for updating
only values from the given research_study_id. by default, all columns
are (re)set to current info.
"""
from .qb_timeline import qb_status_visit_name
from .role import ROLE
from .user import User
from .user_consent import consent_withdrawal_dates
from ..views.clinician import clinician_name_map

user = User.query.get(patient_id)
if not user.has_role(ROLE.PATIENT.value):
return

patient = PatientList.query.get(patient_id)
new_record = False
if not patient:
new_record = True
patient = PatientList(userid=patient_id)
db.session.add(patient)

if research_study_id is None or new_record:
patient.study_id = user.external_study_id
patient.firstname = user.first_name
patient.lastname = user.last_name
patient.email = user.email
patient.birthdate = user.birthdate
patient.deleted = user.deleted_id is not None
patient.test_role = True if user.has_role(ROLE.TEST.value) else False
patient.org_id = user.organizations[0].id if user.organizations else None
patient.org_name = user.organizations[0].name if user.organizations else None

# necessary to avoid recursive loop via some update paths
now = datetime.utcnow()
if patient.last_updated and patient.last_updated + timedelta(seconds=10) > now:
db.session.commit()
return

patient.last_updated = now
if research_study_id == BASE_RS_ID or research_study_id is None:
rs_id = BASE_RS_ID
qb_status = qb_status_visit_name(
patient.userid, research_study_id=rs_id, as_of_date=now)
patient.questionnaire_status = str(qb_status['status'])
patient.visit = qb_status['visit_name']
patient.consentdate, _ = consent_withdrawal_dates(user=user, research_study_id=rs_id)

if (research_study_id == EMPRO_RS_ID or research_study_id is None) and user.clinicians:
rs_id = EMPRO_RS_ID
patient.clinician = '; '.join(
(clinician_name_map().get(c.id, "not in map") for c in user.clinicians)) or ""
qb_status = qb_status_visit_name(
patient.userid, research_study_id=rs_id, as_of_date=now)
patient.empro_status = str(qb_status['status'])
patient.empro_visit = qb_status['visit_name']
patient.action_state = qb_status['action_state'].title() \
if qb_status['action_state'] else ""
patient.empro_consentdate, _ = consent_withdrawal_dates(
user=user, research_study_id=rs_id)
db.session.commit()
5 changes: 5 additions & 0 deletions portal/models/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from flask import current_app
from flask_babel import force_locale
from flask_login import login_manager
from werkzeug.exceptions import Unauthorized

from ..audit import auditable_event
Expand Down Expand Up @@ -53,6 +54,10 @@ def single_patient_adherence_data(patient_id, research_study_id):
if not patient.has_role(ROLE.PATIENT.value):
return

# keep patient list data in sync
from .patient_list import patient_list_update_patient
patient_list_update_patient(patient_id=patient_id, research_study_id=research_study_id)

as_of_date = datetime.utcnow()
cache_moderation = CacheModeration(key=ADHERENCE_DATA_KEY.format(
patient_id=patient_id,
Expand Down
6 changes: 4 additions & 2 deletions portal/models/research_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ class ResearchData(db.Model):
"""
__tablename__ = 'research_data'
id = db.Column(db.Integer, primary_key=True)
subject_id = db.Column(db.ForeignKey('users.id'), index=True, nullable=False)
subject_id = db.Column(
db.ForeignKey('users.id', ondelete='cascade'), index=True, nullable=False)
questionnaire_response_id = db.Column(
db.ForeignKey('questionnaire_responses.id'), index=True, unique=True, nullable=False,
db.ForeignKey('questionnaire_responses.id', ondelete='cascade'),
index=True, unique=True, nullable=False,
doc="source questionnaire response")
instrument = db.Column(db.Text, index=True, nullable=False)
research_study_id = db.Column(db.Integer, index=True, nullable=False)
Expand Down
Loading