diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml
index c06caf6346..6026553b4b 100644
--- a/docker/docker-compose.yaml
+++ b/docker/docker-compose.yaml
@@ -88,7 +88,7 @@ services:
image: redis
db:
- image: postgres:${POSTGRES_VERSION:-13}
+ image: postgres:${POSTGRES_VERSION:-16}
environment:
# use generic postgres env vars to configure env vars specific to dockerized postgres
POSTGRES_DB: ${PGDATABASE:-portaldb}
diff --git a/manage.py b/manage.py
index 13cadc19d7..8e42299efb 100644
--- a/manage.py
+++ b/manage.py
@@ -756,8 +756,8 @@ def find_overlaps(correct_overlaps, reprocess_qnrs):
acting_user_id=admin.id,
)
- update_users_QBT(
- patient.id, research_study_id=0, invalidate_existing=True)
+ invalidate_users_QBT(user_id=patient.id, research_study_id=0)
+ update_users_QBT(user_id=patient.id, research_study_id=0)
present_before_after_state(
patient.id, patient.external_study_id, b4)
@@ -838,8 +838,8 @@ def preview_site_update(org_id, retired):
research_study_id=0,
acting_user_id=admin.id,
)
- update_users_QBT(
- patient.id, research_study_id=0, invalidate_existing=True)
+ invalidate_users_QBT(user_id=patient.id, research_study_id=0)
+ update_users_QBT(user_id=patient.id, research_study_id=0)
after_qnrs, after_timeline, qnrs_lost_reference, _ = present_before_after_state(
patient.id, patient.external_study_id, patient_state[patient.id])
total_qnrs += len(patient_state[patient.id]['qnrs'])
diff --git a/portal/config/eproms/ScheduledJob.json b/portal/config/eproms/ScheduledJob.json
index 744fab81f9..2881a9b8bd 100644
--- a/portal/config/eproms/ScheduledJob.json
+++ b/portal/config/eproms/ScheduledJob.json
@@ -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,
@@ -300,6 +308,15 @@
"resourceType": "ScheduledJob",
"schedule": "30 14 * * *",
"task": "cache_adherence_data_task"
+ },
+ {
+ "active": true,
+ "args": null,
+ "kwargs": null,
+ "name": "Cache Research Report Data",
+ "resourceType": "ScheduledJob",
+ "schedule": "30 23 * * *",
+ "task": "cache_research_data_task"
}
],
"id": "SitePersistence v0.2",
diff --git a/portal/eproms/templates/eproms/assessment_engine/ae_base.html b/portal/eproms/templates/eproms/assessment_engine/ae_base.html
index 5d2bd9b62a..167113f1ee 100644
--- a/portal/eproms/templates/eproms/assessment_engine/ae_base.html
+++ b/portal/eproms/templates/eproms/assessment_engine/ae_base.html
@@ -5,3 +5,18 @@
{%- block body -%}{%- endblock -%}
+
+
+
+
+
+
{{_("Loading")}} ...
+
+
diff --git a/portal/migrations/versions/038a1a5f4218_.py b/portal/migrations/versions/038a1a5f4218_.py
new file mode 100644
index 0000000000..2762fe6ee8
--- /dev/null
+++ b/portal/migrations/versions/038a1a5f4218_.py
@@ -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 ###
diff --git a/portal/migrations/versions/5a300be640fb_.py b/portal/migrations/versions/5a300be640fb_.py
new file mode 100644
index 0000000000..8dce15c9ec
--- /dev/null
+++ b/portal/migrations/versions/5a300be640fb_.py
@@ -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 ###
diff --git a/portal/migrations/versions/daee63f50d35_.py b/portal/migrations/versions/daee63f50d35_.py
new file mode 100644
index 0000000000..0ecb472ccc
--- /dev/null
+++ b/portal/migrations/versions/daee63f50d35_.py
@@ -0,0 +1,57 @@
+"""Add research_data table, to hold questionnaire response research data in a cache
+
+Revision ID: daee63f50d35
+Revises: cf586ed4f043
+Create Date: 2024-05-21 17:00:58.204998
+
+"""
+from alembic import op
+import sqlalchemy as sa
+from sqlalchemy.dialects import postgresql
+
+# revision identifiers, used by Alembic.
+revision = 'daee63f50d35'
+down_revision = '6120fcfc474a'
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table(
+ 'research_data',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('subject_id', sa.Integer(), nullable=False),
+ sa.Column('questionnaire_response_id', sa.Integer(), nullable=False),
+ sa.Column('instrument', sa.Text(), nullable=False),
+ sa.Column('research_study_id', sa.Integer(), nullable=False),
+ sa.Column('authored', sa.DateTime(), nullable=False),
+ sa.Column('data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
+ sa.ForeignKeyConstraint(['subject_id'], ['users.id'], ),
+ sa.ForeignKeyConstraint(
+ ['questionnaire_response_id'], ['questionnaire_responses.id'], ),
+ sa.PrimaryKeyConstraint('id'),
+ )
+
+ op.create_index(
+ op.f('ix_research_data_authored'), 'research_data', ['authored'], unique=False)
+ op.create_index(
+ op.f('ix_research_data_instrument'), 'research_data', ['instrument'], unique=False)
+ op.create_index(
+ op.f('ix_research_data_subject_id'), 'research_data', ['subject_id'], unique=False)
+ op.create_index(
+ op.f('ix_research_data_questionnaire_response_id'),
+ 'research_data', ['questionnaire_response_id'], unique=True)
+ op.create_index(
+ op.f('ix_research_data_research_study_id'),
+ 'research_data', ['research_study_id'], unique=False)
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.drop_index(op.f('ix_research_data_research_study_id'), table_name='research_data')
+ op.drop_index(op.f('ix_research_data_questionnaire_response_id'), table_name='research_data')
+ op.drop_index(op.f('ix_research_data_subject_id'), table_name='research_data')
+ op.drop_index(op.f('ix_research_data_instrument'), table_name='research_data')
+ op.drop_index(op.f('ix_research_data_authored'), table_name='research_data')
+ op.drop_table('research_data')
+ # ### end Alembic commands ###
diff --git a/portal/models/adherence_data.py b/portal/models/adherence_data.py
index f704736561..121dea032c 100644
--- a/portal/models/adherence_data.py
+++ b/portal/models/adherence_data.py
@@ -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")
@@ -77,12 +78,20 @@ def persist(patient_id, rs_id_visit, valid_for_days, data):
except TypeError:
raise ValueError(f"couldn't encode {k}:{v}, {type(v)}")
- record = AdherenceData(
- patient_id=patient_id,
- rs_id_visit=rs_id_visit,
- valid_till=valid_till,
- data=data)
- db.session.add(record)
+ # only a single row for a given patient, rs_id_visit allowed. replace or add
+ record = AdherenceData.query.filter(
+ AdherenceData.patient_id == patient_id).filter(
+ AdherenceData.rs_id_visit == rs_id_visit).first()
+ if record:
+ record.valid_till = valid_till
+ record.data = data
+ else:
+ record = AdherenceData(
+ patient_id=patient_id,
+ rs_id_visit=rs_id_visit,
+ valid_till=valid_till,
+ data=data)
+ db.session.add(record)
db.session.commit()
return db.session.merge(record)
diff --git a/portal/models/patient_list.py b/portal/models/patient_list.py
new file mode 100644
index 0000000000..9d693bf972
--- /dev/null
+++ b/portal/models/patient_list.py
@@ -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()
diff --git a/portal/models/qb_timeline.py b/portal/models/qb_timeline.py
index 3287f77829..a35d046352 100644
--- a/portal/models/qb_timeline.py
+++ b/portal/models/qb_timeline.py
@@ -26,6 +26,7 @@
visit_name,
)
from .questionnaire_response import QNR_results, QuestionnaireResponse
+from .research_data import ResearchData
from .research_protocol import ResearchProtocol
from .role import ROLE
from .user import User
@@ -741,17 +742,23 @@ def ordered_qbs(user, research_study_id, classification=None):
def invalidate_users_QBT(user_id, research_study_id):
- """Mark the given user's QBT rows and adherence_data invalid (by deletion)
+ """invalidate the given user's QBT rows and related cached data, by deletion
+
+ This also clears a users cached adherence and research data rows from their
+ respective caches.
:param user_id: user for whom to purge all QBT rows
:param research_study_id: set to limit invalidation to research study or
use string 'all' to invalidate all QBT rows for a user
"""
+ if research_study_id is None:
+ raise ValueError('research_study_id must be defined or use "all"')
if research_study_id == 'all':
QBT.query.filter(QBT.user_id == user_id).delete()
AdherenceData.query.filter(
AdherenceData.patient_id == user_id).delete()
+ ResearchData.query.filter(ResearchData.subject_id == user_id).delete()
else:
QBT.query.filter(QBT.user_id == user_id).filter(
QBT.research_study_id == research_study_id).delete()
@@ -761,6 +768,8 @@ def invalidate_users_QBT(user_id, research_study_id):
# SQL alchemy can't combine `like` expression with delete op.
for ad in adh_data:
db.session.delete(ad)
+ ResearchData.query.filter(ResearchData.subject_id == user_id).filter(
+ ResearchData.research_study_id == research_study_id).delete()
if not current_app.config.get("TESTING", False):
# clear the timeout lock as well, since we need a refresh
@@ -773,6 +782,7 @@ def invalidate_users_QBT(user_id, research_study_id):
cache_moderation.reset()
+ # clear cached qb_status_visit_name() using current as_of value
# args have to match order and values - no wild carding avail
as_of = QB_StatusCacheKey().current()
if research_study_id != 'all':
@@ -863,18 +873,17 @@ def int_or_none(value):
return True
-def update_users_QBT(user_id, research_study_id, invalidate_existing=False):
+def update_users_QBT(user_id, research_study_id):
"""Populate the QBT rows for given user, research_study
:param user: the user to add QBT rows for
:param research_study_id: the research study being processed
- :param invalidate_existing: set true to wipe any current rows first
A user may be eligible for any number of research studies. QBT treats
each (user, research_study) independently, as should clients.
"""
- def attempt_update(user_id, research_study_id, invalidate_existing):
+ def attempt_update(user_id, research_study_id):
"""Updates user's QBT or raises if lock is unattainable"""
from .qb_status import patient_research_study_status
from ..tasks import LOW_PRIORITY, cache_single_patient_adherence_data
@@ -886,18 +895,6 @@ def attempt_update(user_id, research_study_id, invalidate_existing):
user_id, research_study_id)
with TimeoutLock(key=key, timeout=timeout):
- if invalidate_existing:
- QBT.query.filter(QBT.user_id == user_id).filter(
- QBT.research_study_id == research_study_id).delete()
- adh_data = AdherenceData.query.filter(
- AdherenceData.patient_id == user_id).filter(
- AdherenceData.rs_id_visit.like(f"{research_study_id}:%")
- )
- # SQL alchemy can't combine `like` expression with delete op.
- for ad in adh_data:
- db.session.delete(ad)
- db.session.commit()
-
# if any rows are found, assume this user/study is current
if QBT.query.filter(QBT.user_id == user_id).filter(
QBT.research_study_id == research_study_id).count():
@@ -1206,10 +1203,7 @@ def attempt_update(user_id, research_study_id, invalidate_existing):
success = False
for attempt in range(1, 6):
try:
- attempt_update(
- user_id=user_id,
- research_study_id=research_study_id,
- invalidate_existing=invalidate_existing)
+ attempt_update(user_id=user_id, research_study_id=research_study_id)
success = True
break
except ConnectionError as ce:
diff --git a/portal/models/questionnaire_response.py b/portal/models/questionnaire_response.py
index 55e729b9b6..bf52df00b8 100644
--- a/portal/models/questionnaire_response.py
+++ b/portal/models/questionnaire_response.py
@@ -8,7 +8,6 @@
from flask import current_app, has_request_context, url_for
from flask_swagger import swagger
import jsonschema
-from sqlalchemy import or_
from sqlalchemy.dialects.postgresql import ENUM, JSONB
from sqlalchemy.orm.exc import MultipleResultsFound
@@ -31,8 +30,8 @@
trigger_date,
visit_name,
)
+from .research_data import ResearchData
from .research_study import EMPRO_RS_ID, research_study_id_from_questionnaire
-from .reference import Reference
from .user import User, current_user, patients_query
from .user_consent import consent_withdrawal_dates
@@ -356,8 +355,7 @@ def quote_double_quote(value):
questionnaire_map = questionnaire.questionnaire_code_map()
for question in document.get('group', {}).get('question', ()):
-
- combined_answers = consolidate_answer_pairs(question['answer'])
+ combined_answers = consolidate_answer_pairs(question.get('answer', ()))
# Separate out text and coded answer, then override text
text_and_coded_answers = []
@@ -372,7 +370,8 @@ def quote_double_quote(value):
answer['valueCoding'].get('text')
)
- text_and_coded_answers.append({'valueString': text_answer})
+ if text_answer is not None:
+ text_and_coded_answers.append({'valueString': text_answer})
elif 'valueString' in answer and '"' in answer['valueString']:
answer['valueString'] = quote_double_quote(answer['valueString'])
@@ -844,7 +843,7 @@ def required_qs(self, qb_id):
def aggregate_responses(
instrument_ids, current_user, research_study_id, patch_dstu2=False,
- ignore_qb_requirement=False, celery_task=None, patient_ids=None):
+ celery_task=None, patient_ids=None):
"""Build a bundle of QuestionnaireResponses
:param instrument_ids: list of instrument_ids to restrict results to
@@ -852,7 +851,6 @@ def aggregate_responses(
to list of patients the current_user has permission to see
:param research_study_id: study being processed
:param patch_dstu2: set to make bundle DSTU2 compliant
- :param ignore_qb_requirement: set to include all questionnaire responses
:param celery_task: if defined, send occasional progress updates
:param patient_ids: if defined, limit result set to given patient list
@@ -861,6 +859,11 @@ def aggregate_responses(
"""
from .qb_timeline import qb_status_visit_name # avoid cycle
+ if celery_task:
+ celery_task.update_state(
+ state='PROGRESS',
+ meta={'current': 1, 'total': 100})
+
# Gather up the patient IDs for whom current user has 'view' permission
user_ids = patients_query(
current_user,
@@ -868,82 +871,26 @@ def aggregate_responses(
filter_by_ids=patient_ids,
).with_entities(User.id)
- annotated_questionnaire_responses = []
- questionnaire_responses = QuestionnaireResponse.query.filter(
- QuestionnaireResponse.subject_id.in_(user_ids)).order_by(
- QuestionnaireResponse.document['authored'].desc())
- # TN-3250, don't include QNRs without assigned visits, i.e. qb_id > 0
- if not ignore_qb_requirement:
- questionnaire_responses = questionnaire_responses.filter(
- QuestionnaireResponse.questionnaire_bank_id > 0)
-
- if instrument_ids:
- instrument_filters = (
- QuestionnaireResponse.document[
- ("questionnaire", "reference")
- ].astext.endswith(instrument_id)
- for instrument_id in instrument_ids
- )
- questionnaire_responses = questionnaire_responses.filter(
- or_(*instrument_filters))
-
- patient_fields = ("careProvider", "identifier")
- system_filter = current_app.config.get('REPORTING_IDENTIFIER_SYSTEMS')
if celery_task:
- current, total = 0, questionnaire_responses.count()
-
- for questionnaire_response in questionnaire_responses:
- document = questionnaire_response.document_answered.copy()
- subject = questionnaire_response.subject
- encounter = questionnaire_response.encounter
- encounter_fhir = encounter.as_fhir()
- document["encounter"] = encounter_fhir
+ celery_task.update_state(
+ state='PROGRESS',
+ meta={'current': 10, 'total': 100})
- document["subject"] = {
- k: v for k, v in subject.as_fhir().items() if k in patient_fields
- }
+ query = ResearchData.query.filter(
+ ResearchData.subject_id.in_(user_ids)).order_by(
+ ResearchData.authored.desc(), ResearchData.subject_id).with_entities(
+ ResearchData.data)
- if subject.organizations:
- providers = []
- for org in subject.organizations:
- org_ref = Reference.organization(org.id).as_fhir()
- identifiers = [i.as_fhir() for i in org.identifiers if
- i.system in system_filter]
- if identifiers:
- org_ref['identifier'] = identifiers
- providers.append(org_ref)
- document["subject"]["careProvider"] = providers
-
- qb_status = qb_status_visit_name(
- subject.id,
- research_study_id,
- FHIR_datetime.parse(questionnaire_response.document['authored']))
- document["timepoint"] = qb_status['visit_name']
-
- # Hack: add missing "resource" wrapper for DTSU2 compliance
- # Remove when all interventions compliant
- if patch_dstu2:
- document = {
- 'resource': document,
- # Todo: return URL to individual QuestionnaireResponse resource
- 'fullUrl': url_for(
- 'assessment_engine_api.assessment',
- patient_id=subject.id,
- _external=True,
- ),
- }
-
- annotated_questionnaire_responses.append(document)
-
- if celery_task:
- current += 1
- if current % 25 == 0:
- celery_task.update_state(
- state='PROGRESS',
- meta={'current': current, 'total': total})
-
- return bundle_results(elements=annotated_questionnaire_responses)
+ if instrument_ids:
+ query = query.filter(ResearchData.instrument.in_(tuple(instrument_ids)))
+ if celery_task:
+ # the delay is now a single big query, and then bundling - mark near done
+ celery_task.update_state(
+ state='PROGRESS',
+ meta={'current': 70, 'total': 100})
+ elements = [i.data for i in query.all()]
+ return bundle_results(elements=elements)
def qnr_document_id(
diff --git a/portal/models/reporting.py b/portal/models/reporting.py
index eb916b27a3..9db76b46ba 100644
--- a/portal/models/reporting.py
+++ b/portal/models/reporting.py
@@ -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
@@ -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,
@@ -465,8 +470,7 @@ def patient_generator():
def research_report(
instrument_ids, research_study_id, acting_user_id, patch_dstu2,
- request_url, response_format, lock_key, ignore_qb_requirement,
- celery_task):
+ request_url, response_format, lock_key, celery_task):
"""Generates the research report
Designed to be executed in a background task - all inputs and outputs are
@@ -479,7 +483,6 @@ def research_report(
:param request_url: original request url, for inclusion in FHIR bundle
:param response_format: 'json' or 'csv'
:param lock_key: name of TimeoutLock key used to throttle requests
- :param ignore_qb_requirement: Set to include all questionnaire responses
:param celery_task: used to update status when run as a celery task
:return: dictionary of results, easily stored as a task output, including
any details needed to assist the view method
@@ -494,7 +497,6 @@ def research_report(
research_study_id=research_study_id,
current_user=acting_user,
patch_dstu2=patch_dstu2,
- ignore_qb_requirement=ignore_qb_requirement,
celery_task=celery_task
)
bundle.update({
diff --git a/portal/models/research_data.py b/portal/models/research_data.py
new file mode 100644
index 0000000000..771cb46138
--- /dev/null
+++ b/portal/models/research_data.py
@@ -0,0 +1,142 @@
+""" model data for questionnaire response 'research data' reports """
+from datetime import datetime, timedelta
+from flask import current_app
+from sqlalchemy.dialects.postgresql import JSONB
+from sqlalchemy import UniqueConstraint
+import re
+
+from ..database import db
+from ..date_tools import FHIR_datetime
+from .reference import Reference
+from .research_study import research_study_id_from_questionnaire
+from .user import User
+
+
+class ResearchData(db.Model):
+ """ Cached adherence report data
+
+ Full history research data is expensive to generate and rarely changes,
+ except on receipt of new questionnaire response inserts and updates.
+
+ Cache reportable data in simple JSON structure, maintaining indexed columns
+ for lookup and invalidation.
+ """
+ __tablename__ = 'research_data'
+ id = db.Column(db.Integer, primary_key=True)
+ subject_id = db.Column(
+ db.ForeignKey('users.id', ondelete='cascade'), index=True, nullable=False)
+ questionnaire_response_id = db.Column(
+ 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)
+ authored = db.Column(
+ db.DateTime, nullable=False, index=True,
+ doc="document.authored used for sorting")
+ data = db.Column(JSONB)
+
+
+def cache_research_data(job_id=None, manual_run=None):
+ """add all missing questionnaire response rows to research_data table
+
+ The ResearchData table holds a row per questionnaire response, used to
+ generate research data reports. The only exceptions are questionnaire
+ responses from deleted users, and questionnaire responses without visit
+ (questionnaire bank) associations.
+
+ This routine is called as a scheduled task, to pick up any interrupted
+ or overlooked rows. As questionnaire responses are posted to the system,
+ they are added to the cache immediately.
+ """
+ from .questionnaire_response import QuestionnaireResponse
+ deleted_subjects = db.session.query(User.id).filter(User.deleted_id.isnot(None)).subquery()
+ already_cached = db.session.query(ResearchData.questionnaire_response_id).subquery()
+ qnrs = QuestionnaireResponse.query.filter(
+ QuestionnaireResponse.questionnaire_bank_id > 0).filter(
+ QuestionnaireResponse.subject_id.notin_(deleted_subjects)).filter(
+ QuestionnaireResponse.id.notin_(already_cached))
+
+ current_app.logger.info(
+ f"found {qnrs.count()} questionnaire responses missing from research_data cache")
+ for qnr in qnrs:
+ # research_study_id of None triggers a lookup
+ add_questionnaire_response(qnr, research_study_id=None)
+
+
+def invalidate_qnr_research_data(questionnaire_response):
+ """invalidate row for given questionnaire response"""
+ ResearchData.query.filter(
+ ResearchData.questionnaire_response_id == questionnaire_response.id).delete()
+ db.session.commit()
+
+
+def invalidate_patient_research_data(subject_id, research_study_id):
+ """invalidate applicable rows via removal"""
+ ResearchData.query.filter(ResearchData.subject_id == subject_id).filter(
+ ResearchData.research_study_id == research_study_id).delete()
+ db.session.commit()
+
+
+def update_single_patient_research_data(subject_id):
+ """back door to build research data for single patient"""
+ from .questionnaire_response import QuestionnaireResponse
+ qnrs = QuestionnaireResponse.query.filter(
+ QuestionnaireResponse.questionnaire_bank_id > 0).filter(
+ QuestionnaireResponse.subject_id == subject_id)
+ for qnr in qnrs:
+ # research_study_id of None triggers a lookup
+ add_questionnaire_response(qnr, research_study_id=None)
+
+
+def add_questionnaire_response(questionnaire_response, research_study_id):
+ """Insert single questionnaire response details into ResearchData table
+
+ :param questionnaire_response: the questionnaire response to add to the cache
+ :param research_study_id: the research_study_id, if known. pass None to force lookup
+
+ """
+ from .qb_timeline import qb_status_visit_name
+
+ # TN-3250, don't include QNRs without assigned visits, i.e. qb_id > 0
+ if not questionnaire_response.questionnaire_bank_id:
+ return
+
+ instrument = questionnaire_response.document['questionnaire']['reference'].split('/')[-1]
+ if research_study_id is None:
+ research_study_id = research_study_id_from_questionnaire(instrument)
+
+ patient_fields = ("careProvider", "identifier")
+ document = questionnaire_response.document_answered.copy()
+ subject = questionnaire_response.subject
+ document['encounter'] = questionnaire_response.encounter.as_fhir()
+ document["subject"] = {
+ k: v for k, v in subject.as_fhir().items() if k in patient_fields
+ }
+
+ if subject.organizations:
+ providers = []
+ for org in subject.organizations:
+ org_ref = Reference.organization(org.id).as_fhir()
+ identifiers = [i.as_fhir() for i in org.identifiers if i.system == "http://pcctc.org/"]
+ if identifiers:
+ org_ref['identifier'] = identifiers
+ providers.append(org_ref)
+ document["subject"]["careProvider"] = providers
+
+ qb_status = qb_status_visit_name(
+ subject.id,
+ research_study_id,
+ FHIR_datetime.parse(questionnaire_response.document['authored']))
+ document["timepoint"] = qb_status['visit_name']
+
+ research_data = ResearchData(
+ subject_id=subject.id,
+ questionnaire_response_id=questionnaire_response.id,
+ instrument=instrument,
+ research_study_id=research_study_id,
+ authored=FHIR_datetime.parse(document['authored']),
+ data=document
+ )
+ db.session.add(research_data)
+ db.session.commit()
diff --git a/portal/static/js/src/admin.js b/portal/static/js/src/admin.js
index db81fdc48c..8d757c186a 100644
--- a/portal/static/js/src/admin.js
+++ b/portal/static/js/src/admin.js
@@ -2,1258 +2,1661 @@ import tnthAjax from "./modules/TnthAjax.js";
import tnthDates from "./modules/TnthDate.js";
import Utility from "./modules/Utility.js";
import CurrentUser from "./mixins/CurrentUser.js";
-import {EPROMS_MAIN_STUDY_ID, EPROMS_SUBSTUDY_ID} from "./data/common/consts.js";
+import {
+ EPROMS_MAIN_STUDY_ID,
+ EPROMS_SUBSTUDY_ID,
+} from "./data/common/consts.js";
-(function () { /*global Vue DELAY_LOADING i18next $ */
- var DELAY_LOADING = true; //a workaround for hiding of loading indicator upon completion of loading of portal wrapper - loading indicator needs to continue displaying until patients list has finished loading
- $.ajaxSetup({
- contentType: "application/json; charset=utf-8"
- });
- var AdminObj = window.AdminObj = new Vue({
- el: "#adminTableContainer",
- errorCaptured: function (Error, Component, info) {
- console.error("Error: ", Error, " Component: ", Component, " Message: ", info); /* console global */
- this.setContainerVis();
- return false;
+let requestTimerId = 0;
+(function () {
+ /*global Vue DELAY_LOADING i18next $ */
+ var DELAY_LOADING = true; //a workaround for hiding of loading indicator upon completion of loading of portal wrapper - loading indicator needs to continue displaying until patients list has finished loading
+ $.ajaxSetup({
+ contentType: "application/json; charset=utf-8",
+ });
+ window.AdminObj = new Vue({
+ el: "#adminTableContainer",
+ errorCaptured: function (Error, Component, info) {
+ console.error(
+ "Error: ",
+ Error,
+ " Component: ",
+ Component,
+ " Message: ",
+ info
+ ); /* console global */
+ this.setContainerVis();
+ return false;
+ },
+ errorHandler: function (err, vm) {
+ this.dataError = true;
+ console.warn("Admin Vue instance threw an error: ", vm, this);
+ console.error("Error thrown: ", err);
+ this.setError("Error occurred initializing Admin Vue instance.");
+ this.setContainerVis();
+ },
+ created: function () {
+ this.injectDependencies();
+ },
+ mounted: function () {
+ var self = this;
+ Utility.VueErrorHandling(); /* global VueErrorHandling */
+ this.preConfig(function () {
+ if ($("#adminTable").length > 0) {
+ self.setLoaderContent();
+ self.rowLinkEvent();
+ self.initToggleListEvent();
+ self.initExportReportDataSelector();
+ self.initTableEvents();
+ self.handleDeletedUsersVis();
+ self.setRowItemEvent();
+ self.handleAffiliatedUIVis();
+ if (self.userId) {
+ self.handleCurrentUser();
+ }
+ if (!self.isPatientsList()) {
+ setTimeout(function () {
+ self.setContainerVis();
+ }, 350);
+ }
+ } else {
+ self.handleCurrentUser();
+ }
+ });
+ },
+ mixins: [CurrentUser],
+ data: {
+ dataError: false,
+ configured: false,
+ initIntervalId: 0,
+ accessed: false,
+ sortFilterEnabled: false,
+ showDeletedUsers: false,
+ orgsSelector: {
+ selectAll: false,
+ clearAll: false,
+ close: false,
+ },
+ ROW_ID_PREFIX: "data_row_",
+ ROW_ID: "userid",
+ tableIdentifier: "adminList",
+ popoverEventInitiated: false,
+ dependencies: {},
+ tableConfig: {
+ formatShowingRows: function (pageFrom, pageTo, totalRows) {
+ var rowInfo;
+ setTimeout(function () {
+ rowInfo = i18next
+ .t("Showing {pageFrom} to {pageTo} of {totalRows} users")
+ .replace("{pageFrom}", pageFrom)
+ .replace("{pageTo}", pageTo)
+ .replace("{totalRows}", totalRows);
+ $(".pagination-detail .pagination-info").html(rowInfo);
+ }, 10);
+ return rowInfo;
},
- errorHandler: function (err, vm) {
- this.dataError = true;
- var errorElement = document.getElementById("admin-table-error-message");
- if (errorElement) {
- errorElement.innerHTML = "Error occurred initializing Admin Vue instance.";
- }
- console.warn("Admin Vue instance threw an error: ", vm, this);
- console.error("Error thrown: ", err);
- this.setContainerVis();
+ formatRecordsPerPage: function (pageNumber) {
+ return i18next
+ .t("{pageNumber} records per page")
+ .replace("{pageNumber}", pageNumber);
},
- created: function () {
- this.injectDependencies();
+ formatToggle: function () {
+ return i18next.t("Toggle");
},
- mounted: function () {
- var self = this;
- Utility.VueErrorHandling(); /* global VueErrorHandling */
- this.preConfig(function () {
- if ($("#adminTable").length > 0) {
- self.setLoaderContent();
- self.rowLinkEvent();
- self.initToggleListEvent();
- self.initExportReportDataSelector();
- self.initTableEvents();
- self.handleDeletedUsersVis();
- self.setRowItemEvent();
- self.handleAffiliatedUIVis();
- self.addFilterPlaceHolders();
- if (self.userId) {
- self.handleCurrentUser();
- self.setColumnSelections();
- self.setTableFilters(self.userId); //set user's preference for filter(s)
- }
- setTimeout(function() {
- self.setContainerVis();
- }, 350);
- } else {
- self.handleCurrentUser();
- }
- });
+ formatColumns: function () {
+ return i18next.t("Columns");
},
- mixins: [CurrentUser],
- data: {
- dataError: false,
- configured: false,
- initIntervalId: 0,
- sortFilterEnabled: false,
- showDeletedUsers: false,
- orgsSelector: {
- selectAll: false,
- clearAll: false,
- close: false
- },
- ROW_ID_PREFIX: "data_row_",
- tableIdentifier: "adminList",
- popoverEventInitiated: false,
- dependencies: {},
- tableConfig: {
- formatShowingRows: function (pageFrom, pageTo, totalRows) {
- var rowInfo;
- setTimeout(function () {
- rowInfo = i18next.t("Showing {pageFrom} to {pageTo} of {totalRows} users").replace("{pageFrom}", pageFrom).replace("{pageTo}", pageTo).replace("{totalRows}", totalRows);
- $(".pagination-detail .pagination-info").html(rowInfo);
- }, 10);
- return rowInfo;
- },
- formatRecordsPerPage: function (pageNumber) {
- return i18next.t("{pageNumber} records per page").replace("{pageNumber}", pageNumber);
- },
- formatToggle: function () {
- return i18next.t("Toggle");
- },
- formatColumns: function () {
- return i18next.t("Columns");
- },
- formatAllRows: function () {
- return i18next.t("All rows");
- },
- formatSearch: function () {
- return i18next.t("Search");
- },
- formatNoMatches: function () {
- return i18next.t("No matching records found");
- },
- formatExport: function () {
- return i18next.t("Export data");
- }
- },
- currentTablePreference: null,
- errorCollection: {
- orgs: "",
- demo: ""
- },
- patientReports: {
- data: [],
- message: "",
- loading: false
- },
- exportReportTimeoutID: 0,
- exportReportProgressTime: 0,
- arrExportReportTimeoutID: [],
- exportDataType: ""
+ formatAllRows: function () {
+ return i18next.t("All rows");
},
- methods: {
- injectDependencies: function () {
- var self = this;
- window.portalModules = window.portalModules || {}; /*eslint security/detect-object-injection: off */
- window.portalModules["tnthAjax"] = tnthAjax;
- window.portalModules["tnthDates"] = tnthDates;
- for (var key in window.portalModules) {
- if ({}.hasOwnProperty.call(window.portalModules, key)) {
- self.dependencies[key] = window.portalModules[key];
- }
- }
- },
- getDependency: function (key) {
- if (key && this.dependencies.hasOwnProperty(key)) {
- return this.dependencies[key];
- } else {
- throw Error("Dependency " + key + " not found."); //throw error ? should be visible in console
- }
- },
- setLoaderContent: function() {
- $("#adminTableContainer .fixed-table-loading").html("");
- },
- setContainerVis: function() {
- $("#adminTableContainer").addClass("active");
- this.fadeLoader();
- },
- showMain: function () {
- $("#mainHolder").css({
- "visibility": "visible",
- "-ms-filter": "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)",
- "filter": "alpha(opacity=100)",
- "-moz-opacity": 1,
- "-khtml-opacity": 1,
- "opacity": 1
- });
- },
- handleCurrentUser: function() {
- var self = this;
- this.initCurrentUser(function() {
- self.onCurrentUserInit();
- }, true);
- },
- isSubStudyPatientView: function() {
- return $("#patientList").hasClass("substudy");
- },
- allowSubStudyView: function() {
- return this.userResearchStudyIds.indexOf(EPROMS_SUBSTUDY_ID) !== -1;
- },
- setSubStudyUIElements: function() {
- if (this.allowSubStudyView()) {
- $("#patientList .eproms-substudy").removeClass("tnth-hide").show();
- return;
- }
- $("#patientList .eproms-substudy").hide();
- },
- getExportReportUrl: function() {
- let dataType = this.exportDataType||"json";
- let researchStudyID = this.isSubStudyPatientView() ? EPROMS_SUBSTUDY_ID: EPROMS_MAIN_STUDY_ID;
- return `/api/report/questionnaire_status?research_study_id=${researchStudyID}&format=${dataType}`;
- },
- clearExportReportTimeoutID: function() {
- if (!this.arrExportReportTimeoutID.length) {
- return false;
- }
- let self = this;
- for (var index=0; index < self.arrExportReportTimeoutID.length; index++) {
- clearTimeout(self.arrExportReportTimeoutID[index]);
- }
- },
- onBeforeExportReportData: function() {
- $("#exportReportContainer").removeClass("open").popover("show");
- $("#btnExportReport").attr("disabled", true);
- $(".exportReport__status").addClass("active");
- this.clearExportReportTimeoutID();
- this.exportReportProgressTime = new Date();
- let pastInfo = this.getCacheReportInfo();
- if (pastInfo) {
- $(".exportReport__history").html(`${i18next.t("View last result exported on {date}").replace("{date}", tnthDates.formatDateString(pastInfo.date, "iso"))} `);
- }
- },
- onAfterExportReportData: function(options) {
- options = options || {};
- $("#btnExportReport").attr("disabled", false);
- $(".exportReport__status").removeClass("active");
- if (options.error) {
- this.updateProgressDisplay("", "");
- $(".exportReport__error .message").html(`Request to export report data failed.${options.message?" "+options.message: ""}`);
- $(".exportReport__retry").removeClass("tnth-hide");
- this.clearExportReportTimeoutID();
- return;
- }
+ formatSearch: function () {
+ return i18next.t("Search");
+ },
+ formatNoMatches: function () {
+ return i18next.t("No matching records found");
+ },
+ formatExport: function () {
+ return i18next.t("Export data");
+ },
+ },
+ currentTablePreference: null,
+ errorCollection: {
+ orgs: "",
+ demo: "",
+ },
+ patientReports: {
+ data: [],
+ message: "",
+ loading: false,
+ },
+ exportReportTimeoutID: 0,
+ exportReportProgressTime: 0,
+ arrExportReportTimeoutID: [],
+ exportDataType: "",
+ filterOptionsList: [],
+ },
+ methods: {
+ setError: function (errorMessage) {
+ if (!errorMessage) return;
+ var errorElement = document.getElementById("admin-table-error-message");
+ if (errorElement) {
+ errorElement.innerHTML = errorMessage;
+ }
+ },
+ injectDependencies: function () {
+ var self = this;
+ window.portalModules =
+ window.portalModules ||
+ {}; /*eslint security/detect-object-injection: off */
+ window.portalModules["tnthAjax"] = tnthAjax;
+ window.portalModules["tnthDates"] = tnthDates;
+ for (var key in window.portalModules) {
+ if ({}.hasOwnProperty.call(window.portalModules, key)) {
+ self.dependencies[key] = window.portalModules[key];
+ }
+ }
+ },
+ getDependency: function (key) {
+ if (key && this.dependencies.hasOwnProperty(key)) {
+ return this.dependencies[key];
+ } else {
+ throw Error("Dependency " + key + " not found."); //throw error ? should be visible in console
+ }
+ },
+ setLoaderContent: function () {
+ $("#adminTableContainer .fixed-table-loading").html(
+ ` `
+ );
+ },
+ setContainerVis: function () {
+ $("#adminTableContainer").addClass("active");
+ this.fadeLoader();
+ },
+ showMain: function () {
+ $("#mainHolder").css({
+ visibility: "visible",
+ "-ms-filter": "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)",
+ filter: "alpha(opacity=100)",
+ "-moz-opacity": 1,
+ "-khtml-opacity": 1,
+ opacity: 1,
+ });
+ },
+ getRemotePatientListData: function (params) {
+ if (this.accessed) {
+ var self = this;
+ this.setTablePreference(
+ this.userId,
+ this.tableIdentifier,
+ null,
+ null,
+ null,
+ function () {
+ clearTimeout(requestTimerId);
+ requestTimerId = setTimeout(() => {
+ self.patientDataAjaxRequest(params);
+ }, 200);
+ }
+ );
+ return;
+ }
+ this.patientDataAjaxRequest(params);
+ },
+ patientDataAjaxRequest: function (params) {
+ var includeTestUsers = $("#include_test_role").is(":checked");
+ if (includeTestUsers) {
+ params.data["include_test_role"] = true;
+ }
+ params.data["research_study_id"] =
+ this.tableIdentifier === "patientList" ? 0 : 1;
+ console.log("param data ? ", params.data);
+ var url = "/patients/page";
+ var self = this;
+ $.get(url + "?" + $.param(params.data)).then(function (results) {
+ console.log("row results ", results);
+ if (!self.accessed && results && results.options) {
+ self.filterOptionsList = results.options;
+ }
+ self.accessed = true;
+ params.success(results);
+ });
+ },
+ handleCurrentUser: function () {
+ var self = this;
+ this.initCurrentUser(function () {
+ self.onCurrentUserInit();
+ }, true);
+ },
+ isSubStudyPatientView: function () {
+ return $("#patientList").hasClass("substudy");
+ },
+ allowSubStudyView: function () {
+ return this.userResearchStudyIds.indexOf(EPROMS_SUBSTUDY_ID) !== -1;
+ },
+ setSubStudyUIElements: function () {
+ if (this.allowSubStudyView()) {
+ $("#patientList .eproms-substudy").removeClass("tnth-hide").show();
+ return;
+ }
+ $("#patientList .eproms-substudy").hide();
+ },
+ getExportReportUrl: function () {
+ let dataType = this.exportDataType || "json";
+ let researchStudyID = this.isSubStudyPatientView()
+ ? EPROMS_SUBSTUDY_ID
+ : EPROMS_MAIN_STUDY_ID;
+ return `/api/report/questionnaire_status?research_study_id=${researchStudyID}&format=${dataType}`;
+ },
+ clearExportReportTimeoutID: function () {
+ if (!this.arrExportReportTimeoutID.length) {
+ return false;
+ }
+ let self = this;
+ for (
+ var index = 0;
+ index < self.arrExportReportTimeoutID.length;
+ index++
+ ) {
+ clearTimeout(self.arrExportReportTimeoutID[index]);
+ }
+ },
+ onBeforeExportReportData: function () {
+ $("#exportReportContainer").removeClass("open").popover("show");
+ $("#btnExportReport").attr("disabled", true);
+ $(".exportReport__status").addClass("active");
+ this.clearExportReportTimeoutID();
+ this.exportReportProgressTime = new Date();
+ let pastInfo = this.getCacheReportInfo();
+ if (pastInfo) {
+ $(".exportReport__history").html(
+ `${i18next
+ .t("View last result exported on {date}")
+ .replace(
+ "{date}",
+ tnthDates.formatDateString(pastInfo.date, "iso")
+ )} `
+ );
+ }
+ },
+ onAfterExportReportData: function (options) {
+ options = options || {};
+ $("#btnExportReport").attr("disabled", false);
+ $(".exportReport__status").removeClass("active");
+ if (options.error) {
+ this.updateProgressDisplay("", "");
+ $(".exportReport__error .message").html(
+ `Request to export report data failed.${
+ options.message ? " " + options.message : ""
+ }`
+ );
+ $(".exportReport__retry").removeClass("tnth-hide");
+ this.clearExportReportTimeoutID();
+ return;
+ }
+ $("#exportReportContainer").popover("hide");
+ $(".exportReport__error .message").html("");
+ $(".exportReport__retry").addClass("tnth-hide");
+ },
+ initToggleListEvent: function () {
+ if (!$("#patientListToggle").length) return;
+ $("#patientListToggle a").on("click", (e) => {
+ e.preventDefault();
+ });
+ $("#patientListToggle .radio, #patientListToggle .label").on(
+ "click",
+ function (e) {
+ e.stopImmediatePropagation();
+ $("#patientListToggle").addClass("loading");
+ setTimeout(
+ function () {
+ window.location = $(this).closest("a").attr("href");
+ }.bind(this),
+ 50
+ );
+ }
+ );
+ },
+ initExportReportDataSelector: function () {
+ let self = this;
+ tnthAjax.getConfiguration(this.userId, false, function (data) {
+ if (
+ !data ||
+ !data.PATIENT_LIST_ADDL_FIELDS ||
+ data.PATIENT_LIST_ADDL_FIELDS.indexOf("status") === -1
+ ) {
+ $("#exportReportContainer").hide();
+ return false;
+ }
+ let html = $("#exportReportPopoverWrapper").html();
+ $("#adminTableContainer .fixed-table-toolbar .columns-right").append(
+ html
+ );
+ $("#exportReportContainer").attr(
+ "data-content",
+ $("#exportReportPopoverContent").html()
+ );
+ $("#exportReportContainer .data-types li").each(function () {
+ $(this).attr("title", self.getExportReportUrl());
+ });
+ $("#exportReportContainer").on("shown.bs.popover", function () {
+ $(".exportReport__retry")
+ .off("click")
+ .on("click", function (e) {
+ e.stopImmediatePropagation();
$("#exportReportContainer").popover("hide");
- $(".exportReport__error .message").html("");
- $(".exportReport__retry").addClass("tnth-hide");
- },
- initToggleListEvent: function() {
- if (!$("#patientListToggle").length) return;
- $("#patientListToggle a").on("click", e => {
- e.preventDefault();
- });
- $("#patientListToggle .radio, #patientListToggle .label").on("click", function(e) {
- e.stopImmediatePropagation();
- $("#patientListToggle").addClass("loading");
- setTimeout(function() {
- window.location = $(this).closest("a").attr("href");
- }.bind(this), 50);
- });
- },
- initExportReportDataSelector: function() {
- let self = this;
- tnthAjax.getConfiguration(this.userId, false, function(data) {
- if (!data || !data.PATIENT_LIST_ADDL_FIELDS || data.PATIENT_LIST_ADDL_FIELDS.indexOf("status") === -1) {
- $("#exportReportContainer").hide();
- return false;
- }
- let html = $("#exportReportPopoverWrapper").html();
- $("#adminTableContainer .fixed-table-toolbar .columns-right").append(html);
- $("#exportReportContainer").attr("data-content", $("#exportReportPopoverContent").html());
- $("#exportReportContainer .data-types li").each(function() {
- $(this).attr("title", self.getExportReportUrl());
- });
- $("#exportReportContainer").on("shown.bs.popover", function () {
- $(".exportReport__retry").off("click").on("click", function(e) {
- e.stopImmediatePropagation();
- $("#exportReportContainer").popover("hide");
- setTimeout(function() {
- $("#btnExportReport").trigger("click");
- }, 50);
- });
- });
- $("#exportReportContainer").on("hide.bs.popover", function () {
- self.clearExportReportTimeoutID();
- });
- $("#exportReportContainer .data-types li").on("click", function(e) {
- e.stopPropagation();
- self.exportDataType = $(this).attr("data-type");
- let reportUrl = self.getExportReportUrl();
- self.updateProgressDisplay("", "");
- $.ajax({
- type: "GET",
- url: reportUrl,
- beforeSend: function() {
- self.onBeforeExportReportData();
- },
- success: function(data, status, request) {
- let statusUrl= request.getResponseHeader("Location");
- self.updateExportProgress(statusUrl, function(data) {
- self.onAfterExportReportData(data);
- });
- },
- error: function(xhr) {
- self.onAfterExportReportData({error: true, message: xhr.responseText});
- }
- });
- });
- $("#adminTableContainer .columns-right .export button").attr("title", i18next.t("Export patient list"));
- });
- },
- updateProgressDisplay: function(status, percentage, showLoader) {
- $(".exportReport__percentage").text(percentage);
- $(".exportReport__status").text(status);
- if (showLoader) {
- $(".exportReport__loader").removeClass("tnth-hide");
- } else {
- $(".exportReport__loader").addClass("tnth-hide")
- }
- },
- setCacheReportInfo: function(resultUrl) {
- if (!resultUrl) return false;
- localStorage.setItem("exportReportInfo_"+this.userId+"_"+this.exportDataType, JSON.stringify({
- date: new Date(),
- url: resultUrl
- }));
- },
- getCacheReportInfo: function() {
- let cachedItem = localStorage.getItem("exportReportInfo_"+this.userId+"_"+this.exportDataType);
- if (!cachedItem) return false;
- return JSON.parse(cachedItem);
- },
- updateExportProgress: function(statusUrl, callback) {
- callback = callback || function() {};
- if (!statusUrl) {
- callback({error: true});
- return;
- }
- let self = this;
- // send GET request to status URL
- let rqId = $.getJSON(statusUrl, function(data) {
- if (!data) {
- callback({error: true});
- return;
- }
- let percent = "0%", exportStatus = data["state"].toUpperCase();
- if (data["current"] && data["total"] && parseInt(data["total"]) > 0) {
- percent = parseInt(data['current'] * 100 / data['total']) + "%";
- }
- //update status and percentage displays
- self.updateProgressDisplay(exportStatus, percent, true);
- let arrIncompleteStatus = ["PENDING", "PROGRESS", "STARTED"];
- if (arrIncompleteStatus.indexOf(exportStatus) === -1) {
- if (exportStatus === "SUCCESS") {
- setTimeout(function() {
- let resultUrl = statusUrl.replace("/status", "");
- self.setCacheReportInfo(resultUrl);
- window.location.assign(resultUrl);
- }.bind(self), 50); //wait a bit before retrieving results
- }
- self.updateProgressDisplay(data["state"], "");
- setTimeout(function() {
- callback(exportStatus === "SUCCESS" ? data : {error: true});
- }, 300);
- }
- else {
- //check how long the status stays in pending
- if (exportStatus === "PENDING") {
- let passedTime = ((new Date()).getTime() - self.exportReportProgressTime.getTime()) / 1000;
- if (passedTime > 60) {
- //more than a minute passed and the task is still in PENDING status
- //never advanced to PROGRESS to start the export process
- //abort
- self.onAfterExportReportData({
- "error": true,
- "message": i18next.t("More than a minute spent in pending status.")
- });
- return;
- }
- }
- // rerun in 2 seconds
- self.exportReportTimeoutID = setTimeout(function() {
- self.updateExportProgress(statusUrl, callback);
- }.bind(self), 2000); //each update invocation should be assigned a unique timeoutid
- (self.arrExportReportTimeoutID).push(self.exportReportTimeoutID);
- }
- }).fail(function(xhr) {
- callback({error: true, message: xhr.responseText});
- });
- },
- onCurrentUserInit: function() {
- if (this.userOrgs.length === 0) {
- $("#createUserLink").attr("disabled", true);
- }
- this.handleDisableFields();
- if (this.hasOrgsSelector()) {
- this.initOrgsFilter();
- this.initOrgsEvent();
- }
- this.setSubStudyUIElements();
- this.initRoleBasedEvent();
- this.fadeLoader();
- setTimeout(function() {
- this.setOrgsFilterWarning();
- }.bind(this), 650);
- },
- setOrgsMenuHeight: function (padding) {
- padding = padding || 85;
- var h = parseInt($("#fillOrgs").height());
- if (h > 0) {
- var adminTable = $("div.admin-table"),
- orgMenu = $("#org-menu");
- var calculatedHeight = h + padding;
- $("#org-menu").height(calculatedHeight);
- if (adminTable.height() < orgMenu.height()) {
- setTimeout(function () {
- adminTable.height(orgMenu.height() + calculatedHeight);
- }, 0);
- }
- }
- },
- clearFilterButtons: function () {
- this.setOrgsSelector({
- selectAll: false,
- clearAll: false,
- close: false
- });
- },
- fadeLoader: function () {
- var self = this;
- self.showMain();
setTimeout(function () {
- $("body").removeClass("vis-on-callback");
- $("#loadingIndicator").fadeOut().css("visibility", "hidden");
- }, 150);
- },
- showLoader: function () {
- $("#loadingIndicator").show().css("visibility", "visible");
- },
- preConfig: function (callback) {
- var self = this,
- tnthAjax = this.getDependency("tnthAjax");
- callback = callback || function () {};
- tnthAjax.getCurrentUser(function (data) {
- if (data) {
- self.userId = data.id;
- self.setIdentifier();
- self.setSortFilterProp();
- self.configTable();
- self.configured = true;
- setTimeout(function () {
- callback();
- }, 50);
- } else {
- alert(i18next.t("User Id is required")); /* global i18next */
- self.configured = true;
- return false;
- }
- }, {
- sync: true
+ $("#btnExportReport").trigger("click");
+ }, 50);
+ });
+ });
+ $("#exportReportContainer").on("hide.bs.popover", function () {
+ self.clearExportReportTimeoutID();
+ });
+ $("#exportReportContainer .data-types li").on("click", function (e) {
+ e.stopPropagation();
+ self.exportDataType = $(this).attr("data-type");
+ let reportUrl = self.getExportReportUrl();
+ self.updateProgressDisplay("", "");
+ $.ajax({
+ type: "GET",
+ url: reportUrl,
+ beforeSend: function () {
+ self.onBeforeExportReportData();
+ },
+ success: function (data, status, request) {
+ let statusUrl = request.getResponseHeader("Location");
+ self.updateExportProgress(statusUrl, function (data) {
+ self.onAfterExportReportData(data);
});
- },
- setIdentifier: function () {
- var adminTableContainer = $("#adminTableContainer");
- if (adminTableContainer.hasClass("patient-view")) {
- this.tableIdentifier = "patientList";
- }
- if (adminTableContainer.hasClass("staff-view")) {
- this.tableIdentifier = "staffList";
- }
- if (adminTableContainer.hasClass("substudy")) {
- this.tableIdentifier = "substudyPatientList";
- }
- },
- setOrgsSelector: function (obj) {
- if (!obj) {
- return false;
- }
- var self = this;
- for (var prop in obj) {
- if (self.orgsSelector.hasOwnProperty(prop)) {
- self.orgsSelector[prop] = obj[prop];
- }
- }
- },
- setSortFilterProp: function () {
- this.sortFilterEnabled = (this.tableIdentifier === "patientList" || this.tableIdentifier === "substudyPatientList");
- },
- configTable: function () {
- var options = {};
- var sortObj = this.getTablePreference(this.userId, this.tableIdentifier);
- sortObj = sortObj || this.getDefaultTablePreference();
- options.sortName = sortObj.sort_field;
- options.sortOrder = sortObj.sort_order;
- options.filterBy = sortObj;
- options.exportOptions = { /* global Utility getExportFileName*/
- fileName: Utility.getExportFileName($("#adminTableContainer").attr("data-export-prefix"))
- };
- $("#adminTable").bootstrapTable(this.getTableConfigOptions(options));
- },
- getTableConfigOptions: function (options) {
- if (!options) {
- return this.tableConfig;
- }
- return $.extend({}, this.tableConfig, options);
- },
- initRoleBasedEvent: function() {
- let self = this;
- if (this.isAdminUser()) { /* turn on test account toggle checkbox if admin user */
- $("#frmTestUsersContainer").removeClass("tnth-hide");
- $("#include_test_role").on("click", function() {
- self.showLoader();
- $("#frmTestUsers").submit();
- });
- }
- },
- initTableEvents: function () {
- var self = this;
- $("#adminTable").on("post-body.bs.table", function() {
- self.setContainerVis();
- });
- $("#adminTable").on("reset-view.bs.table", function () {
- self.addFilterPlaceHolders();
- self.resetRowVisByActivationStatus();
- self.setRowItemEvent();
- });
- $("#adminTable").on("search.bs.table", function () {
- self.resetRowVisByActivationStatus();
- self.setRowItemEvent();
- });
- $(window).bind("scroll mousedown mousewheel keyup", function () {
- if ($("html, body").is(":animated")) {
- $("html, body").stop(true, true);
- }
+ },
+ error: function (xhr) {
+ self.onAfterExportReportData({
+ error: true,
+ message: xhr.responseText,
});
- $("#chkDeletedUsersFilter").on("click", function () {
- self.handleDeletedUsersVis();
+ },
+ });
+ });
+ $("#adminTableContainer .columns-right .export button").attr(
+ "title",
+ i18next.t("Export patient list")
+ );
+ });
+ },
+ updateProgressDisplay: function (status, percentage, showLoader) {
+ $(".exportReport__percentage").text(percentage);
+ $(".exportReport__status").text(status);
+ if (showLoader) {
+ $(".exportReport__loader").removeClass("tnth-hide");
+ } else {
+ $(".exportReport__loader").addClass("tnth-hide");
+ }
+ },
+ setCacheReportInfo: function (resultUrl) {
+ if (!resultUrl) return false;
+ localStorage.setItem(
+ "exportReportInfo_" + this.userId + "_" + this.exportDataType,
+ JSON.stringify({
+ date: new Date(),
+ url: resultUrl,
+ })
+ );
+ },
+ getCacheReportInfo: function () {
+ let cachedItem = localStorage.getItem(
+ "exportReportInfo_" + this.userId + "_" + this.exportDataType
+ );
+ if (!cachedItem) return false;
+ return JSON.parse(cachedItem);
+ },
+ updateExportProgress: function (statusUrl, callback) {
+ callback = callback || function () {};
+ if (!statusUrl) {
+ callback({ error: true });
+ return;
+ }
+ let self = this;
+ // send GET request to status URL
+ let rqId = $.getJSON(statusUrl, function (data) {
+ if (!data) {
+ callback({ error: true });
+ return;
+ }
+ let percent = "0%",
+ exportStatus = data["state"].toUpperCase();
+ if (data["current"] && data["total"] && parseInt(data["total"]) > 0) {
+ percent = parseInt((data["current"] * 100) / data["total"]) + "%";
+ }
+ //update status and percentage displays
+ self.updateProgressDisplay(exportStatus, percent, true);
+ let arrIncompleteStatus = ["PENDING", "PROGRESS", "STARTED"];
+ if (arrIncompleteStatus.indexOf(exportStatus) === -1) {
+ if (exportStatus === "SUCCESS") {
+ setTimeout(
+ function () {
+ let resultUrl = statusUrl.replace("/status", "");
+ self.setCacheReportInfo(resultUrl);
+ window.location.assign(resultUrl);
+ }.bind(self),
+ 50
+ ); //wait a bit before retrieving results
+ }
+ self.updateProgressDisplay(data["state"], "");
+ setTimeout(function () {
+ callback(exportStatus === "SUCCESS" ? data : { error: true });
+ }, 300);
+ } else {
+ //check how long the status stays in pending
+ if (exportStatus === "PENDING") {
+ let passedTime =
+ (new Date().getTime() -
+ self.exportReportProgressTime.getTime()) /
+ 1000;
+ if (passedTime > 300) {
+ //more than 5 minutes passed and the task is still in PENDING status
+ //never advanced to PROGRESS to start the export process
+ //abort
+ self.onAfterExportReportData({
+ error: true,
+ message: i18next.t(
+ "More than 5 minutes spent in pending status."
+ ),
});
- if (this.sortFilterEnabled) {
- $("#adminTable").on("sort.bs.table", function (e, name, order) {
- self.setTablePreference(self.userId, self.tableIdentifier, name, order);
- }).on("column-search.bs.table", function () {
- self.setTablePreference(self.userId);
- }).on("column-switch.bs.table", function () {
- self.setTablePreference(self.userId);
- });
+ //log error
+ tnthAjax.reportError(
+ self.userId,
+ window.location.pathname,
+ "Request to export report data failed. More than 5 minutes spent in pending status."
+ );
+ return;
+ }
+ }
+ // rerun in 2 seconds
+ self.exportReportTimeoutID = setTimeout(
+ function () {
+ self.updateExportProgress(statusUrl, callback);
+ }.bind(self),
+ 2000
+ ); //each update invocation should be assigned a unique timeoutid
+ self.arrExportReportTimeoutID.push(self.exportReportTimeoutID);
+ }
+ }).fail(function (xhr) {
+ callback({ error: true, message: xhr.responseText });
+ });
+ },
+ onCurrentUserInit: function () {
+ if (this.userOrgs.length === 0) {
+ $("#createUserLink").attr("disabled", true);
+ }
+ this.handleDisableFields();
+ if (this.hasOrgsSelector()) {
+ this.initOrgsFilter();
+ this.initOrgsEvent();
+ }
+ this.setSubStudyUIElements();
+ this.initRoleBasedEvent();
+ this.fadeLoader();
+ setTimeout(
+ function () {
+ this.setOrgsFilterWarning();
+ }.bind(this),
+ 650
+ );
+ },
+ setOrgsMenuHeight: function (padding) {
+ padding = padding || 85;
+ var h = parseInt($("#fillOrgs").height());
+ if (h > 0) {
+ var adminTable = $("div.admin-table"),
+ orgMenu = $("#org-menu");
+ var calculatedHeight = h + padding;
+ $("#org-menu").height(calculatedHeight);
+ if (adminTable.height() < orgMenu.height()) {
+ setTimeout(function () {
+ adminTable.height(orgMenu.height() + calculatedHeight);
+ }, 0);
+ }
+ }
+ },
+ clearFilterButtons: function () {
+ this.setOrgsSelector({
+ selectAll: false,
+ clearAll: false,
+ close: false,
+ });
+ },
+ fadeLoader: function () {
+ var self = this;
+ self.showMain();
+ setTimeout(function () {
+ $("body").removeClass("vis-on-callback");
+ $("#loadingIndicator").fadeOut().css("visibility", "hidden");
+ }, 150);
+ },
+ showLoader: function () {
+ $("#loadingIndicator").show().css("visibility", "visible");
+ },
+ preConfig: function (callback) {
+ var self = this,
+ tnthAjax = this.getDependency("tnthAjax");
+ callback = callback || function () {};
+ tnthAjax.getCurrentUser(
+ function (data) {
+ if (data) {
+ self.userId = data.id;
+ self.setIdentifier();
+ self.setSortFilterProp();
+ self.configTable();
+ self.configured = true;
+ setTimeout(function () {
+ callback();
+ }, 50);
+ } else {
+ alert(i18next.t("User Id is required")); /* global i18next */
+ self.configured = true;
+ return false;
+ }
+ },
+ {
+ sync: true,
+ }
+ );
+ },
+ setIdentifier: function () {
+ var adminTableContainer = $("#adminTableContainer");
+ if (adminTableContainer.hasClass("patient-view")) {
+ this.tableIdentifier = "patientList";
+ }
+ if (adminTableContainer.hasClass("staff-view")) {
+ this.tableIdentifier = "staffList";
+ }
+ if (adminTableContainer.hasClass("substudy")) {
+ this.tableIdentifier = "substudyPatientList";
+ }
+ },
+ setOrgsSelector: function (obj) {
+ if (!obj) {
+ return false;
+ }
+ var self = this;
+ for (var prop in obj) {
+ if (self.orgsSelector.hasOwnProperty(prop)) {
+ self.orgsSelector[prop] = obj[prop];
+ }
+ }
+ },
+ setSortFilterProp: function () {
+ this.sortFilterEnabled =
+ this.tableIdentifier === "patientList" ||
+ this.tableIdentifier === "substudyPatientList";
+ },
+ setFilterOptionsList: function () {
+ this.filterOptionsList.forEach((o) => {
+ for (const [key, values] of Object.entries(o)) {
+ values.forEach((value) => {
+ if (
+ $(
+ `#adminTable .bootstrap-table-filter-control-${key} option[value='${value[0]}']`
+ ).length > 0
+ ) {
+ // Option exists
+ return true;
+ }
+ $(`#adminTable .bootstrap-table-filter-control-${key}`).append(
+ `${value[1]} `
+ );
+ });
+ }
+ });
+ },
+ configTable: function () {
+ var options = {};
+ var sortObj = this.getTablePreference(
+ this.userId,
+ this.tableIdentifier
+ );
+ sortObj = sortObj || this.getDefaultTablePreference();
+ options.sortName = sortObj.sort_field;
+ options.sortOrder = sortObj.sort_order;
+ options.filterBy = sortObj;
+ options.exportOptions = {
+ /* global Utility getExportFileName*/
+ fileName: Utility.getExportFileName(
+ $("#adminTableContainer").attr("data-export-prefix")
+ ),
+ };
+ $("#adminTable").bootstrapTable(this.getTableConfigOptions(options));
+ },
+ getTableConfigOptions: function (options) {
+ if (!options) {
+ return this.tableConfig;
+ }
+ return $.extend({}, this.tableConfig, options);
+ },
+ initRoleBasedEvent: function () {
+ let self = this;
+ if (this.isAdminUser()) {
+ /* turn on test account toggle checkbox if admin user */
+ $("#frmTestUsersContainer").removeClass("tnth-hide");
+ $("#include_test_role").on("click", function () {
+ $("#adminTable").bootstrapTable("refresh");
+ });
+ }
+ },
+ handleDeletedAccountRows: function (tableData) {
+ const rows = tableData && tableData.rows ? tableData.rows : [];
+ const self = this;
+ $("#adminTable tbody tr").each(function () {
+ const rowId = $(this).attr("data-uniqueid");
+ const isDeleted = rows.find(
+ (o) => parseInt(o[self.ROW_ID]) === parseInt(rowId) && o.deleted
+ );
+ if (!!isDeleted) {
+ $(this).addClass("deleted-user-row");
+ }
+ });
+ },
+ handleDateFields: function (tableData) {
+ const rows = tableData && tableData.rows ? tableData.rows : [];
+ const self = this;
+ $("#adminTable tbody tr").each(function () {
+ const rowId = $(this).attr("data-uniqueid");
+ const matchedRow = rows.find(
+ (o) => parseInt(o[self.ROW_ID]) === parseInt(rowId)
+ );
+ if (matchedRow) {
+ $(this)
+ .find(".birthdate-field")
+ .text(
+ tnthDates.getDateWithTimeZone(matchedRow.birthdate, "d M y")
+ );
+ $(this)
+ .find(".consentdate-field")
+ .text(
+ tnthDates.getDateWithTimeZone(matchedRow.consentdate, "d M y")
+ );
+ }
+ });
+ },
+ initTableEvents: function () {
+ var self = this;
+ $("#adminTable").on("post-body.bs.table", function () {
+ if (!self.isPatientsList()) {
+ self.setContainerVis();
+ }
+ self.setFilterOptionsList();
+ });
+ $("#adminTable").on("load-error.bs.table", function (status, jqXHR) {
+ self.setError(
+ `Error occurred: status ${status}. See console for detail.`
+ );
+ self.setContainerVis();
+ console.error(jqXHR.responseText);
+ });
+ $("#adminTable").on("load-success.bs.table", function (e, data) {
+ self.setColumnSelections();
+ self.addFilterPlaceHolders();
+ self.setTableFilters(self.userId); //set user's preference for filter(s)
+ self.handleDeletedAccountRows(data);
+ self.handleDateFields(data);
+ self.setContainerVis();
+ });
+ $("#adminTable").on("reset-view.bs.table", function () {
+ self.addFilterPlaceHolders();
+ self.resetRowVisByActivationStatus();
+ self.setRowItemEvent();
+ });
+ $("#adminTable").on("search.bs.table", function () {
+ self.resetRowVisByActivationStatus();
+ self.setRowItemEvent();
+ });
+ $("#adminTable").on(
+ "click-row.bs.table",
+ function (e, row, $element, field) {
+ e.stopPropagation();
+ if (row.deleted) return;
+ window.location =
+ "/patients/patient_profile/" + $element.attr("data-uniqueid");
+ }
+ );
+ $(window).bind("scroll mousedown mousewheel keyup", function () {
+ if ($("html, body").is(":animated")) {
+ $("html, body").stop(true, true);
+ }
+ });
+ $("#chkDeletedUsersFilter").on("click", function () {
+ self.handleDeletedUsersVis();
+ });
+ if (this.sortFilterEnabled) {
+ $("#adminTable").on("column-switch.bs.table", function () {
+ self.setTablePreference(self.userId);
+ });
+ }
+ $("#adminTableToolbar .orgs-filter-warning").popover();
+ $("#adminTable .filterControl select").on("change", function () {
+ if ($(this).find("option:selected").val()) {
+ $(this).addClass("active");
+ return;
+ }
+ $(this).removeClass("active");
+ });
+ $("#adminTable .filterControl input").on("change", function () {
+ if ($(this).val()) {
+ $(this).addClass("active");
+ return;
+ }
+ $(this).removeClass("active");
+ });
+ },
+ allowDeletedUserFilter: function () {
+ return $("#chkDeletedUsersFilter").length;
+ },
+ setShowDeletedUsersFlag: function () {
+ if (!this.allowDeletedUserFilter()) {
+ return;
+ }
+ this.showDeletedUsers = $("#chkDeletedUsersFilter").is(":checked");
+ },
+ handleDeletedUsersVis: function () {
+ if (!this.allowDeletedUserFilter()) {
+ return;
+ }
+ this.setShowDeletedUsersFlag();
+ if (this.showDeletedUsers) {
+ $("#adminTable").bootstrapTable("filterBy", {
+ activationstatus: "deactivated",
+ });
+ } else {
+ $("#adminTable").bootstrapTable("filterBy", {
+ activationstatus: "activated",
+ });
+ }
+ },
+ handleAffiliatedUIVis: function () {
+ $(
+ "#adminTableContainer input[data-field='id']:checkbox, #adminTableContainer input[data-field='deactivate']:checkbox, #adminTableContainer input[data-field='activationstatus']:checkbox"
+ )
+ .closest("label")
+ .hide(); //hide checkbox for hidden id field and deactivate account field from side menu
+ $("#patientReportModal").modal({
+ show: false,
+ });
+ },
+ setRowItemEvent: function () {
+ var self = this;
+ $("#adminTableContainer .btn-report")
+ .off("click")
+ .on("click", function (e) {
+ e.stopPropagation();
+ if ($(this).closest(".deleted-user-row").length) {
+ //prevent viewing of report for deleted users
+ return false;
+ }
+ self.getReportModal($(this).attr("data-patient-id"), {
+ documentDataType: $(this).attr("data-document-type"),
+ });
+ });
+ $("#adminTableContainer [name='chkRole']").each(function () {
+ $(this)
+ .off("click")
+ .on("click", function (e) {
+ e.stopPropagation();
+ var userId = $(this).attr("data-user-id");
+ if (!userId) {
+ return false;
+ }
+ var role = $(this).attr("data-role"),
+ checked = $(this).is(":checked"),
+ tnthAjax = self.getDependency("tnthAjax");
+ $("#loadingIndicator_" + userId).show();
+ $("#" + self.ROW_ID_PREFIX + userId).addClass("loading");
+ tnthAjax.getRoles(userId, function (data) {
+ if (!data || data.error) {
+ $("#loadingIndicator_" + userId).hide();
+ $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
+ alert(i18next.t("Error occurred retrieving roles for user"));
+ return false;
}
- $("#adminTableToolbar .orgs-filter-warning").popover();
- $("#adminTable .filterControl select").on("change", function() {
- if ($(this).find("option:selected").val()) {
- $(this).addClass("active");
- return;
- }
- $(this).removeClass("active");
+ var arrRoles = data.roles;
+ arrRoles = $.grep(arrRoles, function (item) {
+ return (
+ String(item.name).toLowerCase() !==
+ String(role).toLowerCase()
+ );
});
- $("#adminTable .filterControl input").on("change", function() {
- if ($(this).val()) {
- $(this).addClass("active");
- return;
- }
- $(this).removeClass("active");
- });
- },
- allowDeletedUserFilter: function() {
- return $("#chkDeletedUsersFilter").length;
- },
- setShowDeletedUsersFlag: function () {
- if (!this.allowDeletedUserFilter()) {
- return;
+ if (checked) {
+ arrRoles = arrRoles.concat([{ name: role }]);
}
- this.showDeletedUsers = $("#chkDeletedUsersFilter").is(":checked");
- },
- handleDeletedUsersVis: function () {
- if (!this.allowDeletedUserFilter()) {
- return;
- }
- this.setShowDeletedUsersFlag();
- if (this.showDeletedUsers) {
- $("#adminTable").bootstrapTable("filterBy", {
- activationstatus: "deactivated"
- });
- } else {
- $("#adminTable").bootstrapTable("filterBy", {
- activationstatus: "activated"
- });
- }
- },
- handleAffiliatedUIVis: function () {
- $("#adminTableContainer input[data-field='id']:checkbox, #adminTableContainer input[data-field='deactivate']:checkbox, #adminTableContainer input[data-field='activationstatus']:checkbox").closest("label").hide(); //hide checkbox for hidden id field and deactivate account field from side menu
- $("#patientReportModal").modal({
- "show": false
- });
- },
- setRowItemEvent: function () {
- var self = this;
- $("#adminTableContainer .btn-report").off("click").on("click", function (e) {
- e.stopPropagation();
- if ($(this).closest(".deleted-user-row").length) { //prevent viewing of report for deleted users
- return false;
+ tnthAjax.putRoles(
+ userId,
+ { roles: arrRoles },
+ "",
+ function (data) {
+ $("#loadingIndicator_" + userId).hide();
+ $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
+ if (data.error) {
+ alert(i18next.t("Error occurred updating user roles"));
+ return false;
}
- self.getReportModal($(this).attr("data-patient-id"), {
- documentDataType: $(this).attr("data-document-type")
- });
- });
- $("#adminTableContainer [name='chkRole']").each(function() {
- $(this).off("click").on("click", function(e) {
- e.stopPropagation();
- var userId = $(this).attr("data-user-id");
- if (!userId) {
- return false;
- }
- var role = $(this).attr("data-role"), checked = $(this).is(":checked"), tnthAjax = self.getDependency("tnthAjax");
- $("#loadingIndicator_"+userId).show();
- $("#" + self.ROW_ID_PREFIX + userId).addClass("loading");
- tnthAjax.getRoles(userId, function(data) {
- if (!data || data.error) {
- $("#loadingIndicator_"+userId).hide();
- $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
- alert(i18next.t("Error occurred retrieving roles for user"));
- return false;
- }
- var arrRoles = data.roles;
- arrRoles = $.grep(arrRoles, function(item) {
- return String(item.name).toLowerCase() !== String(role).toLowerCase();
- });
- if (checked) {
- arrRoles = arrRoles.concat([{name: role}]);
- }
- tnthAjax.putRoles(userId, {roles:arrRoles}, "", function(data) {
- $("#loadingIndicator_"+userId).hide();
- $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
- if (data.error) {
- alert(i18next.t("Error occurred updating user roles"));
- return false;
- }
- });
- });
- });
- });
- $("#adminTableContainer .btn-delete-user").each(function () {
- $(this).popover({
- container: "#adminTable",
- html: true,
- content: ["{title}
",
- "",
- "{yes} ",
- "{no} ",
- "
"
- ]
- .join("")
- .replace("{title}", i18next.t("Are you sure you want to deactivate this account?"))
- .replace(/\{userid\}/g, $(this).attr("data-user-id"))
- .replace("{yes}", i18next.t("Yes"))
- .replace("{no}", i18next.t("No")),
- placement: "top"
- });
- $(this).off("click").on("click", function (e) {
- e.stopPropagation();
- $(this).popover("show");
- var userId = $(this).attr("data-user-id");
- if (!($("#data-delete-loader-" + userId).length)) {
- $(this).parent().append(" ".replace("{userid}", userId));
- }
- });
- });
- $(document).undelegate(".popover-btn-deactivate", "click").on("click", ".popover-btn-deactivate", function (e) {
- e.stopPropagation();
- var userId = $(this).attr("data-user-id");
- var loader = $("#data-delete-loader-" + userId);
- loader.show();
- $("#btnDeleted" + userId).hide();
- $(this).closest(".popover").popover("hide");
- setTimeout(function () {
- self.deactivateUser(userId, !self.showDeletedUsers, function () {
- loader.hide();
- $("#btnDeleted" + userId).show();
- });
- }, 150);
- });
- $("#adminTable .reactivate-icon").off("click").on("click", function (e) {
- e.stopPropagation();
- self.reactivateUser($(this).attr("data-user-id"));
- });
+ }
+ );
+ });
+ });
+ });
+ $("#adminTableContainer .btn-delete-user").each(function () {
+ $(this).popover({
+ container: "#adminTable",
+ html: true,
+ content: [
+ "{title}
",
+ "",
+ "{yes} ",
+ "{no} ",
+ "
",
+ ]
+ .join("")
+ .replace(
+ "{title}",
+ i18next.t("Are you sure you want to deactivate this account?")
+ )
+ .replace(/\{userid\}/g, $(this).attr("data-user-id"))
+ .replace("{yes}", i18next.t("Yes"))
+ .replace("{no}", i18next.t("No")),
+ placement: "top",
+ });
+ $(this)
+ .off("click")
+ .on("click", function (e) {
+ e.stopPropagation();
+ $(this).popover("show");
+ var userId = $(this).attr("data-user-id");
+ if (!$("#data-delete-loader-" + userId).length) {
+ $(this)
+ .parent()
+ .append(
+ ' '.replace(
+ "{userid}",
+ userId
+ )
+ );
+ }
+ });
+ });
+ $(document)
+ .undelegate(".popover-btn-deactivate", "click")
+ .on("click", ".popover-btn-deactivate", function (e) {
+ e.stopPropagation();
+ var userId = $(this).attr("data-user-id");
+ var loader = $("#data-delete-loader-" + userId);
+ loader.show();
+ $("#btnDeleted" + userId).hide();
+ $(this).closest(".popover").popover("hide");
+ setTimeout(function () {
+ self.deactivateUser(userId, !self.showDeletedUsers, function () {
+ loader.hide();
+ $("#btnDeleted" + userId).show();
+ });
+ }, 150);
+ });
+ $("#adminTable .reactivate-icon")
+ .off("click")
+ .on("click", function (e) {
+ e.stopPropagation();
+ self.reactivateUser($(this).attr("data-user-id"));
+ });
- $(document).undelegate(".popover-btn-cancel", "click").on("click", ".popover-btn-cancel", function (e) {
- e.stopPropagation();
- $(this).closest(".popover").popover("hide");
- });
- $(document).on("click", function () {
- $("#adminTable .popover").popover("hide");
- });
- },
- addFilterPlaceHolders: function () {
- $("#adminTable .filterControl input").attr("placeholder", i18next.t("Enter Text"));
- $("#adminTable .filterControl select option[value='']").text(i18next.t("Select"));
- },
- isPatientsList: function() {
- return $("#adminTableContainer").hasClass("patient-view");//check if this is a patients list
- },
- /*
- * a function dedicated to hide account creation button based on org name from setting
- * @params
- * setting_name String, generally a configuration/setting variable name whose values corresponds to an org name of interest e.g. PROTECTED_ORG
- * params Object, passed to ajax call to get configuration settings
- */
- setCreateAccountVisByTopOrgSetting: function(setting_name, params) {
- if (!setting_name) {
- return false;
- }
- var self = this, tnthAjax = this.getDependency("tnthAjax");
- params = params || {};
- tnthAjax.sendRequest("/api/settings", "GET", this.userId, params, function (data) {
- if (!data || data.error || !data[setting_name]) {
- return false;
- }
- var nonMatch = $.grep(self.topLevelOrgs, function (org) {
- return data[setting_name] !== org;
- });
- //has top org affiliation other than matched org setting
- if (nonMatch.length) {
- return false;
- }
- //has top org affiliation with matched org setting
- var match = $.grep(self.topLevelOrgs, function (org) {
- return data[setting_name] === org;
- });
- if (match.length === 0) {
- return false;
- }
- self.setCreateAccountVis(true);
- });
- },
- /*
- * a function specifically created to handle MedidataRave-related UI events/changes
- */
- handleMedidataRave: function (params) {
- if (!this.isPatientsList()) { //check if this is a patients list
- return false;
- }
- //hide account creation button based on PROTECTED_ORG setting
- this.setCreateAccountVisByTopOrgSetting("PROTECTED_ORG", params);
- },
- /*
- * a function dedicated to handle MUSIC-related UI events/changes
- */
- handleMusic: function(params) {
- if (!this.isPatientsList()) { //check if this is a patients list
- return false;
- }
- //hide account creation button based on ACCEPT TERMS ON NEXT ORG setting (MUSIC)
- this.setCreateAccountVisByTopOrgSetting("ACCEPT_TERMS_ON_NEXT_ORG", params);
- },
- setCreateAccountVis: function (hide) {
- var createAccountElements = $("#patientListOptions .or, #createUserLink");
- if (hide) {
- createAccountElements.css("display", "none");
- return;
- }
- createAccountElements.css("display", "block");
- },
- handleDisableFields: function () {
- if (this.isAdminUser()) {
- return false;
- }
- this.handleMedidataRave();
- this.handleMusic();
- //can do other things related to disabling fields here if need be
- },
- hasOrgsSelector: function() {
- return $("#orglistSelector").length;
- },
- siteFilterApplied: function () {
- return this.currentTablePreference &&
- this.currentTablePreference.filters &&
- this.currentTablePreference.filters.orgs_filter_control &&
- (typeof this.currentTablePreference.filters.orgs_filter_control ===
- "object") &&
- this.currentTablePreference.filters.orgs_filter_control.length;
- },
- initOrgsFilter: function () {
- var orgFields = $("#userOrgs input[name='organization']");
- var fi = this.currentTablePreference ? this.currentTablePreference.filters : {};
- var fa = this.siteFilterApplied() ? fi.orgs_filter_control : [];
- let ot = this.getOrgTool();
- let isSubStudyPatientView = this.isSubStudyPatientView();
- orgFields.each(function () {
- $(this).prop("checked", false);
- var oself = $(this),
- val = oself.val();
- fa = fa.map(function (item) {
- return String(item);
- });
- oself.prop("checked", fa.indexOf(String(val)) !== -1);
- });
- if (this.getHereBelowOrgs().length === 1) {
- orgFields.prop("checked", true);
- }
- },
- initSubStudyOrgsVis: function () {
- var orgFields = $("#userOrgs input[name='organization']");
- let ot = this.getOrgTool();
- let isSubStudyPatientView = this.isSubStudyPatientView();
- orgFields.each(function () {
- var val = $(this).val();
- if (val && isSubStudyPatientView && !ot.isSubStudyOrg(val, {async: true})) {
- $(this).attr("disabled", true);
- $(this).parent("label").addClass("disabled")
- }
- });
- },
- setOrgsFilterWarning: function() {
- if (!this.siteFilterApplied()) {
- return;
- }
- /*
- * display organization filtered popover warning text
- */
- $("#adminTableToolbar .orgs-filter-warning").popover("show");
- setTimeout(function() {
- $("#adminTableToolbar .orgs-filter-warning").popover("hide");
- }, 10000);
- },
- initOrgsEvent: function () {
- var ofields = $("#userOrgs input[name='organization']");
- if (ofields.length === 0) {
- return false;
- }
- var self = this;
+ $(document)
+ .undelegate(".popover-btn-cancel", "click")
+ .on("click", ".popover-btn-cancel", function (e) {
+ e.stopPropagation();
+ $(this).closest(".popover").popover("hide");
+ });
+ $(document).on("click", function () {
+ $("#adminTable .popover").popover("hide");
+ });
+ },
+ addFilterPlaceHolders: function () {
+ $("#adminTable .filterControl input").attr(
+ "placeholder",
+ i18next.t("Enter Text")
+ );
+ $("#adminTable .filterControl select option[value='']").text(
+ i18next.t("Select")
+ );
+ },
+ isPatientsList: function () {
+ return $("#adminTableContainer").hasClass("patient-view"); //check if this is a patients list
+ },
+ /*
+ * a function dedicated to hide account creation button based on org name from setting
+ * @params
+ * setting_name String, generally a configuration/setting variable name whose values corresponds to an org name of interest e.g. PROTECTED_ORG
+ * params Object, passed to ajax call to get configuration settings
+ */
+ setCreateAccountVisByTopOrgSetting: function (setting_name, params) {
+ if (!setting_name) {
+ return false;
+ }
+ var self = this,
+ tnthAjax = this.getDependency("tnthAjax");
+ params = params || {};
+ tnthAjax.sendRequest(
+ "/api/settings",
+ "GET",
+ this.userId,
+ params,
+ function (data) {
+ if (!data || data.error || !data[setting_name]) {
+ return false;
+ }
+ var nonMatch = $.grep(self.topLevelOrgs, function (org) {
+ return data[setting_name] !== org;
+ });
+ //has top org affiliation other than matched org setting
+ if (nonMatch.length) {
+ return false;
+ }
+ //has top org affiliation with matched org setting
+ var match = $.grep(self.topLevelOrgs, function (org) {
+ return data[setting_name] === org;
+ });
+ if (match.length === 0) {
+ return false;
+ }
+ self.setCreateAccountVis(true);
+ }
+ );
+ },
+ /*
+ * a function specifically created to handle MedidataRave-related UI events/changes
+ */
+ handleMedidataRave: function (params) {
+ if (!this.isPatientsList()) {
+ //check if this is a patients list
+ return false;
+ }
+ //hide account creation button based on PROTECTED_ORG setting
+ this.setCreateAccountVisByTopOrgSetting("PROTECTED_ORG", params);
+ },
+ /*
+ * a function dedicated to handle MUSIC-related UI events/changes
+ */
+ handleMusic: function (params) {
+ if (!this.isPatientsList()) {
+ //check if this is a patients list
+ return false;
+ }
+ //hide account creation button based on ACCEPT TERMS ON NEXT ORG setting (MUSIC)
+ this.setCreateAccountVisByTopOrgSetting(
+ "ACCEPT_TERMS_ON_NEXT_ORG",
+ params
+ );
+ },
+ setCreateAccountVis: function (hide) {
+ var createAccountElements = $(
+ "#patientListOptions .or, #createUserLink"
+ );
+ if (hide) {
+ createAccountElements.css("display", "none");
+ return;
+ }
+ createAccountElements.css("display", "block");
+ },
+ handleDisableFields: function () {
+ if (this.isAdminUser()) {
+ return false;
+ }
+ this.handleMedidataRave();
+ this.handleMusic();
+ //can do other things related to disabling fields here if need be
+ },
+ hasOrgsSelector: function () {
+ return $("#orglistSelector").length;
+ },
+ siteFilterApplied: function () {
+ return (
+ this.currentTablePreference &&
+ this.currentTablePreference.filters &&
+ this.currentTablePreference.filters.orgs_filter_control &&
+ typeof this.currentTablePreference.filters.orgs_filter_control ===
+ "object" &&
+ this.currentTablePreference.filters.orgs_filter_control.length
+ );
+ },
+ initOrgsFilter: function () {
+ var orgFields = $("#userOrgs input[name='organization']");
+ var fi = this.currentTablePreference
+ ? this.currentTablePreference.filters
+ : {};
+ var fa = this.siteFilterApplied() ? fi.orgs_filter_control : [];
+ orgFields.each(function () {
+ $(this).prop("checked", false);
+ var oself = $(this),
+ val = oself.val();
+ fa = fa.map(function (item) {
+ return String(item);
+ });
+ oself.prop("checked", fa.indexOf(String(val)) !== -1);
+ });
+ if (this.getHereBelowOrgs().length === 1) {
+ orgFields.prop("checked", true);
+ }
+ },
+ initSubStudyOrgsVis: function () {
+ var orgFields = $("#userOrgs input[name='organization']");
+ let ot = this.getOrgTool();
+ let isSubStudyPatientView = this.isSubStudyPatientView();
+ orgFields.each(function () {
+ var val = $(this).val();
+ if (
+ val &&
+ isSubStudyPatientView &&
+ !ot.isSubStudyOrg(val, { async: true })
+ ) {
+ $(this).attr("disabled", true);
+ $(this).parent("label").addClass("disabled");
+ }
+ });
+ },
+ setOrgsFilterWarning: function () {
+ if (!this.siteFilterApplied()) {
+ return;
+ }
+ /*
+ * display organization filtered popover warning text
+ */
+ $("#adminTableToolbar .orgs-filter-warning").popover("show");
+ setTimeout(function () {
+ $("#adminTableToolbar .orgs-filter-warning").popover("hide");
+ }, 10000);
+ },
+ initOrgsEvent: function () {
+ var ofields = $("#userOrgs input[name='organization']");
+ if (ofields.length === 0) {
+ return false;
+ }
+ var self = this;
- $("#orglistSelector .orgs-filter-warning").popover();
+ $("#orglistSelector .orgs-filter-warning").popover();
- $("body").on("click", function (e) {
- if ($(e.target).closest("#orglistSelector").length === 0) {
- $("#orglistSelector").removeClass("open");
- }
- });
+ $("body").on("click", function (e) {
+ if ($(e.target).closest("#orglistSelector").length === 0) {
+ $("#orglistSelector").removeClass("open");
+ }
+ });
- $("#orglist-dropdown").on("click touchstart", function () {
- $(this).find(".glyphicon-menu-up, .glyphicon-menu-down").toggleClass("tnth-hide"); //toggle menu up/down button
- self.initSubStudyOrgsVis();
- setTimeout(function () {
- self.setOrgsMenuHeight(95);
- self.clearFilterButtons();
- }, 100);
- });
- /* attach orgs related events to UI components */
- ofields.each(function () {
- $(this).on("click touchstart", function (e) {
- e.stopPropagation();
- var isChecked = $(this).is(":checked");
- var childOrgs = self.getSelectedOrgHereBelowOrgs($(this).val());
- if (childOrgs && childOrgs.length) {
- childOrgs.forEach(function (org) {
- $("#userOrgs input[name='organization'][value='" + org + "']").prop("checked", isChecked);
- });
- }
- if (!isChecked) {
- var ot = self.getOrgTool();
- var currentOrgId = $(this).val();
- var parentOrgId = ot.getParentOrgId($(this).val());
- if (parentOrgId) {
- /*
- * if all child organizations(s) are unchecked under a parent org, uncheck that parent org as well
- */
- var cn = ot.getHereBelowOrgs([parentOrgId]);
- var hasCheckedChilds = cn.filter(function(item) {
- return parseInt(item) !== parseInt(currentOrgId) &&
- (parseInt(item) !== parseInt(parentOrgId)) &&
- (ot.getElementByOrgId(item).prop("checked"));
- });
- if (!hasCheckedChilds.length) {
- ot.getElementByOrgId(parentOrgId).prop("checked", false);
- }
- }
- }
- self.setOrgsSelector({
- selectAll: false,
- clearAll: false,
- close: false
- });
- self.onOrgListSelectFilter();
- });
- });
- $("#orglist-selectall-ckbox").on("click touchstart", function (e) {
- e.stopPropagation();
- var orgsList = [];
- self.setOrgsSelector({
- selectAll: true,
- clearAll: false,
- close: false
- });
- $("#userOrgs input[name='organization']").filter(":visible").each(function () {
- if ($(this).css("display") !== "none") {
- $(this).prop("checked", true);
- orgsList.push($(this).val());
- }
- });
- if (orgsList.length === 0) return;
- self.onOrgListSelectFilter();
-
- });
- $("#orglist-clearall-ckbox").on("click touchstart", function (e) {
- e.stopPropagation();
- self.clearOrgsSelection();
- self.setOrgsSelector({
- selectAll: false,
- clearAll: true,
- close: false
- });
- self.onOrgListSelectFilter();
- });
- $("#orglist-close-ckbox").on("click touchstart", function (e) {
- e.stopPropagation();
- self.setOrgsSelector({
- selectAll: false,
- clearAll: false,
- close: true
- });
- $("#orglistSelector").trigger("click");
- return false;
+ $("#orglist-dropdown").on("click touchstart", function () {
+ $(this)
+ .find(".glyphicon-menu-up, .glyphicon-menu-down")
+ .toggleClass("tnth-hide"); //toggle menu up/down button
+ self.initSubStudyOrgsVis();
+ setTimeout(function () {
+ self.setOrgsMenuHeight(95);
+ self.clearFilterButtons();
+ }, 100);
+ });
+ /* attach orgs related events to UI components */
+ ofields.each(function () {
+ $(this).on("click touchstart", function (e) {
+ e.stopPropagation();
+ var isChecked = $(this).is(":checked");
+ var childOrgs = self.getSelectedOrgHereBelowOrgs($(this).val());
+ if (childOrgs && childOrgs.length) {
+ childOrgs.forEach(function (org) {
+ $(
+ "#userOrgs input[name='organization'][value='" + org + "']"
+ ).prop("checked", isChecked);
+ });
+ }
+ if (!isChecked) {
+ var ot = self.getOrgTool();
+ var currentOrgId = $(this).val();
+ var parentOrgId = ot.getParentOrgId($(this).val());
+ if (parentOrgId) {
+ /*
+ * if all child organizations(s) are unchecked under a parent org, uncheck that parent org as well
+ */
+ var cn = ot.getHereBelowOrgs([parentOrgId]);
+ var hasCheckedChilds = cn.filter(function (item) {
+ return (
+ parseInt(item) !== parseInt(currentOrgId) &&
+ parseInt(item) !== parseInt(parentOrgId) &&
+ ot.getElementByOrgId(item).prop("checked")
+ );
});
- },
- clearOrgsSelection: function () {
- $("#userOrgs input[name='organization']").prop("checked", false);
- this.clearFilterButtons();
- },
- onOrgListSelectFilter: function() {
- this.setTablePreference(this.userId, this.tableIdentifier, null, null, null, function () {
- // callback from setting the filter preference
- // this ensures that the table filter preference is saved before reloading the page
- // so the backend can present patient list based on that saved preference
- setTimeout(function () {
- this.showLoader();
- location.reload();
- }.bind(this), 350);
- }.bind(this));
- },
- getDefaultTablePreference: function () {
- return {
- sort_field: "id",
- sort_order: "desc"
- };
- },
- getTablePreference: function (userId, tableName, setFilter, setColumnSelections) {
- if (this.currentTablePreference) {
- return this.currentTablePreference;
+ if (!hasCheckedChilds.length) {
+ ot.getElementByOrgId(parentOrgId).prop("checked", false);
}
- var prefData = null,
- self = this,
- uid = userId || self.userId;
- var tableIdentifier = tableName || self.tableIdentifier;
- var tnthAjax = self.getDependency("tnthAjax");
-
- tnthAjax.getTablePreference(uid, tableIdentifier, {
- "sync": true
- }, function (data) {
- if (!data || data.error) {
- return false;
- }
- prefData = data || self.getDefaultTablePreference();
- self.currentTablePreference = prefData;
+ }
+ }
+ self.setOrgsSelector({
+ selectAll: false,
+ clearAll: false,
+ close: false,
+ });
+ self.onOrgListSelectFilter();
+ });
+ });
+ $("#orglist-selectall-ckbox").on("click touchstart", function (e) {
+ e.stopPropagation();
+ var orgsList = [];
+ self.setOrgsSelector({
+ selectAll: true,
+ clearAll: false,
+ close: false,
+ });
+ $("#userOrgs input[name='organization']")
+ .filter(":visible")
+ .each(function () {
+ if ($(this).css("display") !== "none") {
+ $(this).prop("checked", true);
+ orgsList.push($(this).val());
+ }
+ });
+ if (orgsList.length === 0) return;
+ self.onOrgListSelectFilter();
+ });
+ $("#orglist-clearall-ckbox").on("click touchstart", function (e) {
+ e.stopPropagation();
+ self.clearOrgsSelection();
+ self.setOrgsSelector({
+ selectAll: false,
+ clearAll: true,
+ close: false,
+ });
+ self.onOrgListSelectFilter();
+ });
+ $("#orglist-close-ckbox").on("click touchstart", function (e) {
+ e.stopPropagation();
+ self.setOrgsSelector({
+ selectAll: false,
+ clearAll: false,
+ close: true,
+ });
+ $("#orglistSelector").trigger("click");
+ return false;
+ });
+ },
+ clearOrgsSelection: function () {
+ $("#userOrgs input[name='organization']").prop("checked", false);
+ this.clearFilterButtons();
+ },
+ onOrgListSelectFilter: function () {
+ this.setTablePreference(
+ this.userId,
+ this.tableIdentifier,
+ null,
+ null,
+ null,
+ function () {
+ // callback from setting the filter preference
+ // this ensures that the table filter preference is saved before reloading the page
+ // so the backend can present patient list based on that saved preference
+ setTimeout(
+ function () {
+ $("#adminTable").bootstrapTable("refresh");
+ }.bind(this),
+ 350
+ );
+ }.bind(this)
+ );
+ },
+ getDefaultTablePreference: function () {
+ return {
+ sort_field: this.ROW_ID,
+ sort_order: "desc",
+ };
+ },
+ getTablePreference: function (
+ userId,
+ tableName,
+ setFilter,
+ setColumnSelections
+ ) {
+ if (this.currentTablePreference) {
+ return this.currentTablePreference;
+ }
+ var prefData = null,
+ self = this,
+ uid = userId || self.userId;
+ var tableIdentifier = tableName || self.tableIdentifier;
+ var tnthAjax = self.getDependency("tnthAjax");
- if (setFilter) { //set filter values
- self.setTableFilters(uid);
- }
- if (setColumnSelections) { //set column selection(s)
- self.setColumnSelections();
- }
- });
- return prefData;
- },
- setColumnSelections: function () {
- if (!this.sortFilterEnabled) {
- return false;
- }
- var prefData = this.getTablePreference(this.userId, this.tableIdentifier);
- var hasColumnSelections = prefData && prefData.filters && prefData.filters.column_selections;
- if (!hasColumnSelections) {
- return false;
- }
- var visibleColumns = $("#adminTable").bootstrapTable("getVisibleColumns");
- visibleColumns.forEach(function (c) { //hide visible columns
- if (String(c.class).toLowerCase() === "always-visible") {
- return true;
- }
- $("#adminTable").bootstrapTable("hideColumn", c.field);
- });
- prefData.filters.column_selections.forEach(function (column) { //show column(s) based on preference
- $(".fixed-table-toolbar input[type='checkbox'][data-field='" + column + "']").prop("checked", true);
- $("#adminTable").bootstrapTable("showColumn", column);
- });
- },
- setTableFilters: function (userId) {
- var prefData = this.currentTablePreference,
- tnthAjax = this.getDependency("tnthAjax");
- if (!prefData) {
- tnthAjax.getTablePreference(userId || this.userId, this.tableIdentifier, {
- "sync": true
- }, function (data) {
- if (!data || data.error) {
- return false;
- }
- prefData = data;
- });
- }
- if (prefData && prefData.filters) { //set filter values
- var fname = "";
- for (var item in prefData.filters) {
- fname = "#adminTable .bootstrap-table-filter-control-" + item;
- if ($(fname).length === 0) {
- continue;
- }
- //note this is based on the trigger event for filtering specify in the plugin
- $(fname).val(prefData.filters[item]);
- if (prefData.filters[item]) {
- $(fname).addClass("active");
- }
- if ($(fname).get(0))
- $(fname).trigger($(fname).get(0).tagName === "INPUT" ? "keyup" : "change");
- }
- }
- },
- setTablePreference: function (userId, tableName, sortField, sortOrder, filters, callback) {
- var tnthAjax = this.getDependency("tnthAjax");
- tableName = tableName || this.tableIdentifier;
- if (!tableName) {
- return false;
- }
- userId = userId || this.userId;
- var data = this.getDefaultTablePreference();
- if (sortField && sortOrder) {
- data["sort_field"] = sortField;
- data["sort_order"] = sortOrder;
- } else {
- //get selected sorted field information on UI
- var sortedField = $("#adminTable th[data-field]").has(".sortable.desc, .sortable.asc");
- if (sortedField.length > 0) {
- data["sort_field"] = sortedField.attr("data-field");
- var sortedOrder = "desc";
- sortedField.find(".sortable").each(function () {
- if ($(this).hasClass("desc")) {
- sortedOrder = "desc";
- } else if ($(this).hasClass("asc")) {
- sortedOrder = "asc";
- }
- });
- data["sort_order"] = sortedOrder;
- }
- }
- var __filters = filters || {};
+ tnthAjax.getTablePreference(
+ uid,
+ tableIdentifier,
+ {
+ sync: true,
+ },
+ function (data) {
+ if (!data || data.error) {
+ return false;
+ }
+ prefData = data || self.getDefaultTablePreference();
+ self.currentTablePreference = prefData;
- //get fields
- if (Object.keys(__filters).length === 0) {
- $("#adminTable .filterControl select, #adminTable .filterControl input").each(function () {
- if ($(this).val()) {
- var field = $(this).closest("th").attr("data-field");
- if ($(this).get(0)) {
- __filters[field] = $(this).get(0).nodeName.toLowerCase() === "select" ? $(this).find("option:selected").text() : $(this).val();
- }
- }
- });
- }
- //get selected orgs from the filter list by site control
- var selectedOrgs = [];
- $("#userOrgs input[name='organization']").each(function () {
- if ($(this).is(":checked") && ($(this).css("display") !== "none")) {
- selectedOrgs.push(parseInt($(this).val()));
- }
- });
- __filters["orgs_filter_control"] = selectedOrgs;
- //get column selections
- __filters["column_selections"] = [];
- $(".fixed-table-toolbar input[type='checkbox'][data-field]:checked").each(function () {
- __filters["column_selections"].push($(this).attr("data-field"));
- });
- data["filters"] = __filters;
+ if (setFilter) {
+ //set filter values
+ self.setTableFilters(uid);
+ }
+ if (setColumnSelections) {
+ //set column selection(s)
+ self.setColumnSelections();
+ }
+ }
+ );
+ return prefData;
+ },
+ setColumnSelections: function () {
+ if (!this.sortFilterEnabled) {
+ return false;
+ }
+ var prefData = this.getTablePreference(
+ this.userId,
+ this.tableIdentifier
+ );
+ var hasColumnSelections =
+ prefData && prefData.filters && prefData.filters.column_selections;
+ if (!hasColumnSelections) {
+ return false;
+ }
+ var visibleColumns =
+ $("#adminTable").bootstrapTable("getVisibleColumns");
+ visibleColumns.forEach(function (c) {
+ //hide visible columns
+ if (String(c.class).toLowerCase() === "always-visible") {
+ return true;
+ }
+ $("#adminTable").bootstrapTable("hideColumn", c.field);
+ });
+ prefData.filters.column_selections.forEach(function (column) {
+ //show column(s) based on preference
+ $(
+ ".fixed-table-toolbar input[type='checkbox'][data-field='" +
+ column +
+ "']"
+ ).prop("checked", true);
+ $("#adminTable").bootstrapTable("showColumn", column);
+ });
+ },
+ setTableFilters: function (userId) {
+ var prefData = this.currentTablePreference,
+ tnthAjax = this.getDependency("tnthAjax");
+ if (!prefData) {
+ tnthAjax.getTablePreference(
+ userId || this.userId,
+ this.tableIdentifier,
+ {
+ sync: true,
+ },
+ function (data) {
+ if (!data || data.error) {
+ return false;
+ }
+ prefData = data;
+ }
+ );
+ }
+ if (prefData && prefData.filters) {
+ //set filter values
+ var fname = "";
+ for (var item in prefData.filters) {
+ fname = "#adminTable .bootstrap-table-filter-control-" + item;
+ if ($(fname).length === 0) {
+ continue;
+ }
+ //note this is based on the trigger event for filtering specify in the plugin
+ $(fname).val(prefData.filters[item]);
+ if (prefData.filters[item]) {
+ $(fname).addClass("active");
+ }
+ }
+ }
+ },
+ setTablePreference: function (
+ userId,
+ tableName,
+ sortField,
+ sortOrder,
+ filters,
+ callback
+ ) {
+ var tnthAjax = this.getDependency("tnthAjax");
+ tableName = tableName || this.tableIdentifier;
+ if (!tableName) {
+ return false;
+ }
+ userId = userId || this.userId;
+ var data = this.getDefaultTablePreference();
+ if (sortField && sortOrder) {
+ data["sort_field"] = sortField;
+ data["sort_order"] = sortOrder;
+ } else {
+ //get selected sorted field information on UI
+ var sortedField = $("#adminTable th[data-field]").has(
+ ".sortable.desc, .sortable.asc"
+ );
+ if (sortedField.length > 0) {
+ data["sort_field"] = sortedField.attr("data-field");
+ var sortedOrder = "desc";
+ sortedField.find(".sortable").each(function () {
+ if ($(this).hasClass("desc")) {
+ sortedOrder = "desc";
+ } else if ($(this).hasClass("asc")) {
+ sortedOrder = "asc";
+ }
+ });
+ data["sort_order"] = sortedOrder;
+ }
+ }
+ var __filters = filters || {};
- if (Object.keys(data).length > 0) {
- // make this a synchronous call
- tnthAjax.setTablePreference(userId, this.tableIdentifier, {
- "data": JSON.stringify(data),
- "sync": true
- }, callback);
- this.currentTablePreference = data;
- }
- },
- getReportModal: function (patientId, options) {
- $("#patientReportModal").modal("show");
- this.patientReports.loading = true;
- var self = this,
- tnthDates = self.getDependency("tnthDates"),
- tnthAjax = self.getDependency("tnthAjax");
- options = options || {};
- tnthAjax.patientReport(patientId, options, function (data) {
- self.patientReports.data = [];
- if (!data || data.error) {
- self.patientReports.message = i18next.t("Error occurred retrieving patient report");
- return false;
- }
- if (data["user_documents"] && data["user_documents"].length > 0) {
- var existingItems = {},
- count = 0;
- var documents = data["user_documents"].sort(function (a, b) { //sort to get the latest first
- return new Date(b.uploaded_at) - new Date(a.uploaded_at);
- });
- documents.forEach(function (item) {
- var c = item["contributor"];
- if (c && !existingItems[c]) { //only draw the most recent, same report won't be displayed
- if (options.documentDataType && String(options.documentDataType).toLowerCase() !== String(c).toLowerCase()) {
- return false;
- }
- self.patientReports.data.push({
- contributor: item.contributor,
- fileName: item.filename,
- date: tnthDates.formatDateString(item.uploaded_at, "iso"),
- download: " "
- });
- existingItems[c] = true;
- count++;
- }
- });
- if (count > 1) {
- $("#patientReportModal .modal-title").text(i18next.t("Patient Reports"));
- } else {
- $("#patientReportModal .modal-title").text(i18next.t("Patient Report"));
- }
- self.patientReports.message = "";
- $("#patientReportContent .btn-all").attr("href", "patient_profile/" + patientId + "#profilePatientReportTable");
+ //get fields
+ if (Object.keys(__filters).length === 0) {
+ $(
+ "#adminTable .filterControl select, #adminTable .filterControl input"
+ ).each(function () {
+ if ($(this).val()) {
+ var field = $(this).closest("th").attr("data-field");
+ if ($(this).get(0)) {
+ __filters[field] =
+ $(this).get(0).nodeName.toLowerCase() === "select"
+ ? $(this).find("option:selected").val()
+ : $(this).val();
+ }
+ }
+ });
+ }
+ //get selected orgs from the filter list by site control
+ var selectedOrgs = [];
+ $("#userOrgs input[name='organization']").each(function () {
+ if ($(this).is(":checked") && $(this).css("display") !== "none") {
+ selectedOrgs.push(parseInt($(this).val()));
+ }
+ });
+ __filters["orgs_filter_control"] = selectedOrgs;
+ //get column selections
+ __filters["column_selections"] = [];
+ $(
+ ".fixed-table-toolbar input[type='checkbox'][data-field]:checked"
+ ).each(function () {
+ __filters["column_selections"].push($(this).attr("data-field"));
+ });
+ data["filters"] = __filters;
- } else {
- self.patientReports.message = i18next.t("No report data found.");
- }
- setTimeout(function () {
- self.patientReports.loading = false;
- }, 550);
- });
- },
- rowLinkEvent: function () {
- $("#admin-table-body.data-link").delegate("tr", "click", function (e) {
- if (e.target && (e.target.tagName.toLowerCase() !== "td")) {
- if (e.target.tagName.toLowerCase() === "a" && e.target.click) {
- return;
- }
- }
- e.preventDefault();
- e.stopPropagation();
- var row = $(this).closest("tr");
- if (row.hasClass("deleted-user-row") || row.hasClass("loading")) {
- return false;
- }
- if (!row.hasClass("no-records-found")) {
- $("#adminTable .popover").popover("hide");
- document.location = $(this).closest("tr").attr("data-link");
- }
- });
- },
- deactivationEnabled: function () {
- return $("#chkDeletedUsersFilter").length > 0;
- },
- reactivateUser: function (userId) {
- var tnthAjax = this.getDependency("tnthAjax"),
- self = this;
- if (!this.isDeactivatedRow(userId)) {
- return false;
- }
- $("#" + self.ROW_ID_PREFIX + userId).addClass("loading");
- tnthAjax.reactivateUser(userId, {
- "async": true
- }, function (data) {
- $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
- if (data.error) {
- alert(data.error);
- return;
- }
- self.handleReactivatedRow(userId);
- setTimeout(function() {
- self.handleDeletedUsersVis(); //reset rows displayed
- }, 150);
- });
- },
- deactivateUser: function (userId, hideRow, callback) {
- callback = callback || function () {};
- if (!userId) {
- callback({
- error: i18next.t("User id is required.")
- });
- return false;
- }
- if (this.isDeactivatedRow(userId)) {
- callback();
- return false;
- }
- var tnthAjax = this.getDependency("tnthAjax"),
- self = this;
- $("#" + self.ROW_ID_PREFIX + userId).addClass("loading");
- tnthAjax.deactivateUser(userId, {
- "async": true
- }, function (data) {
- $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
- if (data.error) {
- callback({
- error: data.error
- });
- alert(data.error);
- return;
- }
- callback();
- if (hideRow) {
- $("#" + self.ROW_ID_PREFIX + userId).fadeOut();
- }
- self.handleDeactivatedRow(userId);
- setTimeout(function() {
- self.handleDeletedUsersVis(); //reset rows displayed
- }, 150);
- });
- },
- getRowData: function (userId) {
- if (!userId) {
- return false;
- }
- return $("#adminTable").bootstrapTable("getRowByUniqueId", userId);
- },
- isDeactivatedRow: function (userId) {
- var rowData = this.getRowData(userId);
- return rowData && String(rowData.activationstatus).toLowerCase() === "deactivated";
- },
- resetRowVisByActivationStatus: function () {
- var self = this;
- $("#adminTable [data-index]").each(function () {
- var userId = $(this).attr("data-uniqueid");
- if (self.isDeactivatedRow(userId)) {
- self.handleDeactivatedRowVis(userId);
- } else {
- self.handleReactivatedRowVis(userId);
- }
- });
- },
- updateFieldData: function (userId, data) {
- if (!userId || !data) {
- return false;
- }
- $("#adminTable").bootstrapTable("updateCell", data);
- },
- getRowIndex: function (userId) {
- if (!userId) {
- return false;
- }
- return $("#" + this.ROW_ID_PREFIX + userId).attr("data-index");
- },
- handleDeactivatedRow: function (userId) {
- this.updateFieldData(userId, {
- index: this.getRowIndex(userId),
- field: "activationstatus",
- value: "deactivated",
- reinit: true
- });
- this.handleDeactivatedRowVis(userId);
- },
- handleDeactivatedRowVis: function (userId) {
- if (!userId) {
- return false;
- }
- var allowReactivate = $("#adminTable").attr("data-allow-reactivate");
- $("#" + this.ROW_ID_PREFIX + userId).addClass("deleted-user-row").addClass("rowlink-skip").find(".deleted-button-cell").html('{inactivetext} '.replace("{class}", allowReactivate?"":"tnth-hide").replace("{userid}", userId).replace("{inactivetext}", i18next.t("Inactive"))).find("a.profile-link").remove();
- if (!this.showDeletedUsers) {
- $("#" + this.ROW_ID_PREFIX + userId).hide();
- }
- },
- handleReactivatedRow: function (userId) {
- if (!userId) {
- return false;
+ if (Object.keys(data).length > 0) {
+ var self = this;
+ tnthAjax.setTablePreference(
+ userId,
+ this.tableIdentifier,
+ {
+ data: JSON.stringify(data),
+ //sync: true,
+ max_attempts: 1,
+ },
+ function (result) {
+ if (!result?.error) self.currentTablePreference = data;
+ if (callback) callback();
+ }
+ );
+ }
+ },
+ getReportModal: function (patientId, options) {
+ $("#patientReportModal").modal("show");
+ this.patientReports.loading = true;
+ var self = this,
+ tnthDates = self.getDependency("tnthDates"),
+ tnthAjax = self.getDependency("tnthAjax");
+ options = options || {};
+ tnthAjax.patientReport(patientId, options, function (data) {
+ self.patientReports.data = [];
+ if (!data || data.error) {
+ self.patientReports.message = i18next.t(
+ "Error occurred retrieving patient report"
+ );
+ return false;
+ }
+ if (data["user_documents"] && data["user_documents"].length > 0) {
+ var existingItems = {},
+ count = 0;
+ var documents = data["user_documents"].sort(function (a, b) {
+ //sort to get the latest first
+ return new Date(b.uploaded_at) - new Date(a.uploaded_at);
+ });
+ documents.forEach(function (item) {
+ var c = item["contributor"];
+ if (c && !existingItems[c]) {
+ //only draw the most recent, same report won't be displayed
+ if (
+ options.documentDataType &&
+ String(options.documentDataType).toLowerCase() !==
+ String(c).toLowerCase()
+ ) {
+ return false;
}
- this.updateFieldData(userId, {
- index: this.getRowIndex(userId),
- field: "activationstatus",
- value: "activated",
- reinit: true
+ self.patientReports.data.push({
+ contributor: item.contributor,
+ fileName: item.filename,
+ date: tnthDates.formatDateString(item.uploaded_at, "iso"),
+ download:
+ " ",
});
- this.handleReactivatedRowVis(userId);
- },
- handleReactivatedRowVis: function (userId) {
- if (!userId) {
- return false;
- }
- $("#data_row_" + userId).removeClass("deleted-user-row").removeClass("rowlink-skip").find(".deleted-button-cell").html('{buttontext} '.replace(/\{userid\}/g, userId).replace("{buttontext}", i18next.t("Deactivate"))).append(" ");
- if (this.showDeletedUsers) {
- $("#" + this.ROW_ID_PREFIX + userId).hide();
- }
+ existingItems[c] = true;
+ count++;
+ }
+ });
+ if (count > 1) {
+ $("#patientReportModal .modal-title").text(
+ i18next.t("Patient Reports")
+ );
+ } else {
+ $("#patientReportModal .modal-title").text(
+ i18next.t("Patient Report")
+ );
+ }
+ self.patientReports.message = "";
+ $("#patientReportContent .btn-all").attr(
+ "href",
+ "patient_profile/" + patientId + "#profilePatientReportTable"
+ );
+ } else {
+ self.patientReports.message = i18next.t("No report data found.");
+ }
+ setTimeout(function () {
+ self.patientReports.loading = false;
+ }, 550);
+ });
+ },
+ rowLinkEvent: function () {
+ $("#admin-table-body.data-link").delegate("tr", "click", function (e) {
+ if (e.target && e.target.tagName.toLowerCase() !== "td") {
+ if (e.target.tagName.toLowerCase() === "a" && e.target.click) {
+ return;
+ }
+ }
+ e.preventDefault();
+ e.stopPropagation();
+ var row = $(this).closest("tr");
+ if (row.hasClass("deleted-user-row") || row.hasClass("loading")) {
+ return false;
+ }
+ if (!row.hasClass("no-records-found")) {
+ $("#adminTable .popover").popover("hide");
+ document.location = $(this).closest("tr").attr("data-link");
+ }
+ });
+ },
+ deactivationEnabled: function () {
+ return $("#chkDeletedUsersFilter").length > 0;
+ },
+ reactivateUser: function (userId) {
+ var tnthAjax = this.getDependency("tnthAjax"),
+ self = this;
+ if (!this.isDeactivatedRow(userId)) {
+ return false;
+ }
+ $("#" + self.ROW_ID_PREFIX + userId).addClass("loading");
+ tnthAjax.reactivateUser(
+ userId,
+ {
+ async: true,
+ },
+ function (data) {
+ $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
+ if (data.error) {
+ alert(data.error);
+ return;
+ }
+ self.handleReactivatedRow(userId);
+ setTimeout(function () {
+ self.handleDeletedUsersVis(); //reset rows displayed
+ }, 150);
+ }
+ );
+ },
+ deactivateUser: function (userId, hideRow, callback) {
+ callback = callback || function () {};
+ if (!userId) {
+ callback({
+ error: i18next.t("User id is required."),
+ });
+ return false;
+ }
+ if (this.isDeactivatedRow(userId)) {
+ callback();
+ return false;
+ }
+ var tnthAjax = this.getDependency("tnthAjax"),
+ self = this;
+ $("#" + self.ROW_ID_PREFIX + userId).addClass("loading");
+ tnthAjax.deactivateUser(
+ userId,
+ {
+ async: true,
+ },
+ function (data) {
+ $("#" + self.ROW_ID_PREFIX + userId).removeClass("loading");
+ if (data.error) {
+ callback({
+ error: data.error,
+ });
+ alert(data.error);
+ return;
+ }
+ callback();
+ if (hideRow) {
+ $("#" + self.ROW_ID_PREFIX + userId).fadeOut();
}
+ self.handleDeactivatedRow(userId);
+ setTimeout(function () {
+ self.handleDeletedUsersVis(); //reset rows displayed
+ }, 150);
+ }
+ );
+ },
+ getRowData: function (userId) {
+ if (!userId) {
+ return false;
+ }
+ return $("#adminTable").bootstrapTable("getRowByUniqueId", userId);
+ },
+ isDeactivatedRow: function (userId) {
+ var rowData = this.getRowData(userId);
+ return (
+ rowData &&
+ String(rowData.activationstatus).toLowerCase() === "deactivated"
+ );
+ },
+ resetRowVisByActivationStatus: function () {
+ var self = this;
+ $("#adminTable [data-index]").each(function () {
+ var userId = $(this).attr("data-uniqueid");
+ if (self.isDeactivatedRow(userId)) {
+ self.handleDeactivatedRowVis(userId);
+ } else {
+ self.handleReactivatedRowVis(userId);
+ }
+ });
+ },
+ updateFieldData: function (userId, data) {
+ if (!userId || !data) {
+ return false;
+ }
+ $("#adminTable").bootstrapTable("updateCell", data);
+ },
+ getRowIndex: function (userId) {
+ if (!userId) {
+ return false;
+ }
+ return $("#" + this.ROW_ID_PREFIX + userId).attr("data-index");
+ },
+ handleDeactivatedRow: function (userId) {
+ this.updateFieldData(userId, {
+ index: this.getRowIndex(userId),
+ field: "activationstatus",
+ value: "deactivated",
+ reinit: true,
+ });
+ this.handleDeactivatedRowVis(userId);
+ },
+ handleDeactivatedRowVis: function (userId) {
+ if (!userId) {
+ return false;
+ }
+ var allowReactivate = $("#adminTable").attr("data-allow-reactivate");
+ $("#" + this.ROW_ID_PREFIX + userId)
+ .addClass("deleted-user-row")
+ .addClass("rowlink-skip")
+ .find(".deleted-button-cell")
+ .html(
+ '{inactivetext} '
+ .replace("{class}", allowReactivate ? "" : "tnth-hide")
+ .replace("{userid}", userId)
+ .replace("{inactivetext}", i18next.t("Inactive"))
+ )
+ .find("a.profile-link")
+ .remove();
+ if (!this.showDeletedUsers) {
+ $("#" + this.ROW_ID_PREFIX + userId).hide();
+ }
+ },
+ handleReactivatedRow: function (userId) {
+ if (!userId) {
+ return false;
+ }
+ this.updateFieldData(userId, {
+ index: this.getRowIndex(userId),
+ field: "activationstatus",
+ value: "activated",
+ reinit: true,
+ });
+ this.handleReactivatedRowVis(userId);
+ },
+ handleReactivatedRowVis: function (userId) {
+ if (!userId) {
+ return false;
+ }
+ $("#data_row_" + userId)
+ .removeClass("deleted-user-row")
+ .removeClass("rowlink-skip")
+ .find(".deleted-button-cell")
+ .html(
+ '{buttontext} '
+ .replace(/\{userid\}/g, userId)
+ .replace("{buttontext}", i18next.t("Deactivate"))
+ )
+ .append(" ");
+ if (this.showDeletedUsers) {
+ $("#" + this.ROW_ID_PREFIX + userId).hide();
}
- });
+ },
+ },
+ });
})();
diff --git a/portal/static/js/src/empro.js b/portal/static/js/src/empro.js
index b9e4a324ed..8c895d4022 100644
--- a/portal/static/js/src/empro.js
+++ b/portal/static/js/src/empro.js
@@ -205,20 +205,21 @@ emproObj.prototype.onAfterSubmitOptoutData = function (data) {
}, 1000);
return true;
};
-emproObj.prototype.handleNoOptOutSelection = function() {
- EmproObj.initOptOutModal(false);
- EmproObj.initThankyouModal(true);
+emproObj.prototype.handleNoOptOutSelection = function () {
+ EmproObj.initOptOutModal(false);
+ EmproObj.initThankyouModal(true);
};
-emproObj.prototype.isFullOptout = function() {
- return this.submittedOptOutDomains.length > 0 && (
- this.submittedOptOutDomains.length === this.hardTriggerDomains.length
+emproObj.prototype.isFullOptout = function () {
+ return (
+ this.submittedOptOutDomains.length > 0 &&
+ this.submittedOptOutDomains.length === this.hardTriggerDomains.length
);
-}
-emproObj.prototype.handleFullOptout = function() {
+};
+emproObj.prototype.handleFullOptout = function () {
if (this.isFullOptout()) {
$(".full-optout-hide").addClass("hide");
}
-}
+};
emproObj.prototype.handleSubmitOptoutData = function () {
// if (!EmproObj.hasErrorText() && !EmproObj.hasSelectedOptOutDomains()) {
// EmproObj.setOptoutError(
@@ -251,9 +252,10 @@ emproObj.prototype.initOptOutElementEvents = function () {
return;
}
// x, close button in OPT OUT modal, need to make sure thank you modal is initiated after closing out opt out modal
- $("#emproOptOutModal .close").on("click", function(e) {
+ $("#emproOptOutModal .close").on("click", function (e) {
EmproObj.initOptOutModal(false);
EmproObj.initThankyouModal(true);
+ EmproObj.postToAudit("Opt out modal dismissed");
});
// submit buttons
@@ -326,7 +328,7 @@ emproObj.prototype.initOptOutModal = function (autoShow) {
}
$("#emproOptOutModal").modal({
backdrop: "static",
- keyboard: false
+ keyboard: false,
});
$("#emproOptOutModal").modal(autoShow ? "show" : "hide");
};
@@ -335,6 +337,7 @@ emproObj.prototype.onDetectOptOutDomains = function () {
this.initOptOutElementEvents();
this.initOptOutModal(true);
this.initThankyouModal(false);
+ this.postToAudit("Opt out modal presented");
};
emproObj.prototype.initReportLink = function () {
if (!this.hasThankyouModal()) return;
@@ -387,6 +390,13 @@ emproObj.prototype.checkUserOrgAllowOptOut = function (
);
});
};
+emproObj.prototype.postToAudit = function (message) {
+ if (!message) return;
+ tnthAjax.postAuditLog(this.userId, {
+ message: message,
+ context: "assessment"
+ });
+};
emproObj.prototype.init = function () {
const self = this;
this.setLoadingVis(true);
@@ -500,7 +510,10 @@ emproObj.prototype.init = function () {
this.initTriggerDomains(
{
- maxTryAttempts: !autoShowModal ? 0 : isDebugging ? 0 : 5, //no need to retry if thank you modal isn't supposed to show
+ // retry up to at least a minute, the call times out a 5 seconds, retry after 1.5 ~ 2.5 second each time (including browser connection time)
+ // so 5 * 14 * 2.5 seconds (175 seconds, approximately 2 minute and 35 seconds)
+ // hopefully gives the server plenty of time to process triggers
+ maxTryAttempts: !autoShowModal ? 0 : isDebugging ? 0 : 14, //no need to retry if thank you modal isn't supposed to show
clearCache: autoShowModal,
},
(result) => {
@@ -511,7 +524,6 @@ emproObj.prototype.init = function () {
console.log("Error retrieving trigger data: ", result.reason);
}
}
-
/*
* set thank you modal accessed flag here
*/
@@ -533,7 +545,7 @@ emproObj.prototype.init = function () {
});
};
emproObj.prototype.setLoadingVis = function (loading) {
- var LOADING_INDICATOR_ID = ".portal-body .loading-container";
+ var LOADING_INDICATOR_ID = ".portal-body .wait-indicator-wrapper";
if (!loading) {
$(LOADING_INDICATOR_ID).addClass("hide");
return;
diff --git a/portal/static/js/src/modules/TnthAjax.js b/portal/static/js/src/modules/TnthAjax.js
index 34b04dbf2f..f6cbad11d9 100644
--- a/portal/static/js/src/modules/TnthAjax.js
+++ b/portal/static/js/src/modules/TnthAjax.js
@@ -23,7 +23,7 @@ export default { /*global $ */
"sendRequest": function(url, method, userId, params, callback) {
if (!url) { return false; }
var REQUEST_TIMEOUT_INTERVAL = 5000; // default timed out at 5 seconds
- var defaultParams = {type: method ? method : "GET", url: url, attempts: 0, max_attempts: MAX_ATTEMPTS, contentType: "application/json; charset=utf-8", dataType: "json", sync: false, timeout: REQUEST_TIMEOUT_INTERVAL, data: null, useWorker: false, async: true};
+ var defaultParams = {type: method ? method : "GET", url: url, attempts: 0, max_attempts: params && params.max_attempts ? params.max_attempts : MAX_ATTEMPTS, contentType: "application/json; charset=utf-8", dataType: "json", sync: false, timeout: REQUEST_TIMEOUT_INTERVAL, data: null, useWorker: false, async: true};
params = params || defaultParams;
params = $.extend({}, defaultParams, params);
params.timeout = params.timeout || REQUEST_TIMEOUT_INTERVAL;
@@ -365,18 +365,27 @@ export default { /*global $ */
const dataState = String(data.state).toLowerCase();
params = params || {};
+ const isUnprocessed = EMPRO_TRIGGER_UNPROCCESSED_STATES.indexOf(dataState) !== -1;
//if the trigger data has not been processed, try again until maximum number of attempts has been reached
if (params.retryAttempt < params.maxTryAttempts &&
- EMPRO_TRIGGER_UNPROCCESSED_STATES.indexOf(dataState) !== -1) {
+ isUnprocessed) {
params.retryAttempt++;
setTimeout(function() {
this.getSubStudyTriggers(userId, params, callback);
}.bind(this), 1500*params.retryAttempt);
+ if (params.retryAttempt === params.maxTryAttempts) {
+ this.postAuditLog(userId, {
+ context: "assessment",
+ message: `maximum retry attempts reached for retrieving triggers, state: ${dataState ? dataState : "unknown"}`
+ });
+ }
return false;
}
params.retryAttempt = 0;
- sessionStorage.setItem(triggerDataKey, JSON.stringify(data));
+ if (!isUnprocessed) {
+ sessionStorage.setItem(triggerDataKey, JSON.stringify(data));
+ }
callback(data);
return true;
});
@@ -1231,7 +1240,7 @@ export default { /*global $ */
callback({error: "User Id and table name is required for setting preference."});
return false;
}
- this.sendRequest("/api/user/" + userId + "/table_preferences/" + tableName, "PUT", userId, {"data": params.data,"sync": params.sync}, function(data) {
+ this.sendRequest("/api/user/" + userId + "/table_preferences/" + tableName, "PUT", userId, {...params, "data": params.data,"sync": params.sync}, function(data) {
if (!data || data.error) {
callback({"error": i18next.t("Error occurred setting table preference.")});
return false;
@@ -1272,6 +1281,21 @@ export default { /*global $ */
}
});
},
+ "postAuditLog": function(userId, payload, callback) {
+ callback = callback || function() {};
+ //url, method, userId, params, callback
+ this.sendRequest("/api/auditlog", "POST", userId, {"data": payload, "contentType": "application/x-www-form-urlencoded; charset=UTF-8"}, function(data) {
+ if (data) {
+ if (!data.error) {
+ callback(data);
+ } else {
+ callback({"error": i18next.t("Error occurred posting to audit log.")});
+ }
+ } else {
+ callback({"error": i18next.t("no data returned")});
+ }
+ });
+ },
"auditLog": function(userId, params, callback) {
callback = callback || function() {};
if (!userId) {
diff --git a/portal/static/less/eproms.less b/portal/static/less/eproms.less
index 6fe691b062..250e9d80a8 100644
--- a/portal/static/less/eproms.less
+++ b/portal/static/less/eproms.less
@@ -107,6 +107,9 @@
@toolbarBorderColor: #f5f4f4;
@emproBtnPrimaryColor: #60676E;
@alertColor: #cc0a0a;
+@hourGlassColor: #919EB3;
+@hourGlassFrameColor: #919EB3;
+@hourGlassSandColor: #EDD0AA;
@@ -2348,7 +2351,6 @@ div.footer-wrapper.right-panel {
}
.fixed-table-loading {
background-color: hsla(0, 0%, 100%, 0.7);
- display: block !important;
max-height: 2500px;
overflow: hidden;
-webkit-transition: max-height 550ms ease 150ms;
@@ -2377,9 +2379,6 @@ div.footer-wrapper.right-panel {
}
&.active {
opacity: 1;
- .fixed-table-loading {
- max-height: 0;
- }
.fht-cell,
.filterControl {
opacity: 1;
@@ -6868,6 +6867,234 @@ div.or {
margin-top: 16px;
}
}
+.wait-indicator-wrapper {
+ position: fixed;
+ background: hsla(0, 7%, 12%, 0.35);
+ top: 0;
+ bottom: 0;
+ right: 0;
+ left: 0;
+ z-index: 888;
+}
+// modified from https://codepen.io/aitchiss/pen/rNxqEMP
+.wait-indicator-container {
+ position: relative;
+ margin: auto;
+ display: flex;
+ margin-top: calc(~"25% - 24px");
+ margin-bottom: 8%;
+ width: 120px;
+ height: 280px;
+ .loading-text-container {
+ position: relative;
+ width: 120px;
+ //top: 16px;
+ margin: auto;
+ color: #FFF;
+ font-weight: 600;
+ font-size: 1.1em;
+ letter-spacing: .1rem;
+ }
+ .frame {
+ position: absolute;
+ width: 100px;
+ height: 100px;
+ border-top: 10px solid @hourGlassFrameColor;
+ border-bottom: 10px solid @hourGlassColor;
+ border-radius: 4px;
+ animation: rotateFrame 460s infinite;
+ }
+ .top {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 68px;
+ height: 40px;
+ clip-path: polygon(45% 100%, 55% 100%, 100% 0, 0 0);
+ }
+ /* Sand - top */
+ .top::before {
+ content: '';
+ position: absolute;
+ width: 68px;
+ height: 44px;
+ bottom: 0;
+ background: @hourGlassSandColor;
+ animation: 520s lowerTopSand infinite;
+ }
+
+ .top::after {
+ content: '';
+ position: absolute;
+ top: 0px;
+ left: -15px;
+ width: 190px;
+ height: 190px;
+ transform: rotate(-90deg);
+ background: conic-gradient(
+ from 0deg,
+ white 0deg,
+ transparent 90deg,
+ white 180deg
+ );
+ }
+ .bottom {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ top: 40px;
+ width: 68px;
+ height: 40px;
+ clip-path: polygon(45% 0, 55% 0, 100% 100%, 0 100%);
+ }
+
+ /* Bottom sand */
+ .bottom::before {
+ content: '';
+ position: absolute;
+ transform: translateX(-50%);
+ left: 50%;
+ width: 160px;
+ height: 80px;
+ bottom: 0;
+ background: @hourGlassSandColor;
+ animation: 520s raiseBottomSand infinite;
+ }
+
+ .blob {
+ position: absolute;
+ transform: translateX(-50%);
+ top: 10px;
+ left: 50%;
+ content: '';
+ width: 50px;
+ height: 50px;
+ border-radius: 50%;
+ background: @hourGlassSandColor;
+ animation: raiseMound 460s infinite;
+ }
+ /* Drip through to bottom */
+ .drip {
+ position: absolute;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 0;
+ height: 0;
+ border-left: 8px solid transparent;
+ border-right: 8px solid transparent;
+ border-top: 10px solid @hourGlassSandColor;
+ animation: fadeDrip 2s infinite;
+ }
+
+ .drip::before {
+ content: '';
+ position: absolute;
+ left: -1px;
+ width: 3px;
+ height: 200px;
+ background: repeating-linear-gradient(to bottom,
+ @hourGlassSandColor,
+ @hourGlassSandColor 5px,
+ transparent 5px,
+ transparent 10px
+ );
+ animation: drip 2s infinite;
+ }
+
+ .glass {
+ position: absolute;
+ top: -90px;
+ left: -15px;
+ width: 190px;
+ height: 190px;
+ transform: rotate(-270deg);
+ background: conic-gradient(
+ from 0deg,
+ white 0deg,
+ transparent 90deg,
+ white 180deg
+ );
+ }
+
+}
+
+@keyframes rotateFrame {
+ 0% {
+ transform: none;
+ }
+
+ 90% {
+ transform: none;
+ }
+
+ 100% {
+ transform: rotate(180deg);
+ }
+}
+
+@keyframes lowerTopSand {
+ 0% {
+ transform: none;
+ }
+
+ 100% {
+ transform: translateY(80px);
+ }
+}
+
+
+@keyframes raiseMound {
+ 0% {
+ transform: translate(-50%, 80px);
+ width: 180px;
+ }
+
+ 100% {
+ transform: translateX(-50%);
+ width: 50px;
+ }
+}
+
+@keyframes raiseBottomSand {
+ 0% {
+ transform: translate(-50%, 80px);
+ boder-radius: 0;
+ }
+
+ 100% {
+ transform: translateX(-50%);
+ border-radius: 50% 50% 0 0;
+ }
+}
+
+@keyframes fadeDrip {
+ 0% {
+ opacity: 1;
+ }
+
+ 70% {
+ opacity: 1;
+ }
+
+ 100% {
+ opacity: 0;
+ }
+}
+
+@keyframes drip {
+ 0% {
+ transform: translateY(-150px);
+ opacity: 1;
+ }
+
+ 99% {
+ opacity: 1;
+ }
+
+ 100% {
+ transform: translateY(30px);
+ }
+}
@media screen {
#printSection {
diff --git a/portal/tasks.py b/portal/tasks.py
index 6cfb1f19e2..f1202ea2f4 100644
--- a/portal/tasks.py
+++ b/portal/tasks.py
@@ -34,6 +34,7 @@
research_report,
single_patient_adherence_data,
)
+from .models.research_data import cache_research_data
from .models.research_study import ResearchStudy
from .models.role import ROLE, Role
from .models.scheduled_job import check_active, update_job_status
@@ -131,6 +132,13 @@ def adherence_report_task(self, **kwargs):
return adherence_report(**kwargs)
+@celery.task(queue=LOW_PRIORITY)
+@scheduled_task
+def cache_research_data_task(**kwargs):
+ """Queues up all patients needing a cache refresh"""
+ return cache_research_data(**kwargs)
+
+
@celery.task(bind=True, track_started=True, queue=LOW_PRIORITY)
def research_report_task(self, **kwargs):
current_app.logger.debug("launch research report task: %s", self.request.id)
@@ -194,6 +202,27 @@ def cache_assessment_status(**kwargs):
update_patient_loop(update_cache=True, queue_messages=False, as_task=True)
+@celery.task()
+@scheduled_task
+def cache_patient_list(**kwargs):
+ """Populate patient list cache
+
+ Patient list is a cached table, enabling quick pagination on the /patients
+ view functions. Kept up to date on changes with qb_status as part of
+ the adherence cache chain. This task is NOT scheduled but can be run on
+ new deploys to pick up all the deleted patients, that are otherwise missed
+ but do need to be in the patient list for proper function.
+ """
+ from portal.models.patient_list import patient_list_update_patient
+ patient_role_id = Role.query.filter(
+ Role.name == ROLE.PATIENT.value).with_entities(Role.id).first()[0]
+ all_patients = User.query.join(UserRoles).filter(and_(
+ User.id == UserRoles.user_id,
+ UserRoles.role_id == patient_role_id)).with_entities(User.id)
+ for patient_id in all_patients:
+ patient_list_update_patient(patient_id[0])
+
+
@celery.task(queue=LOW_PRIORITY)
@scheduled_task
def prepare_communications(**kwargs):
diff --git a/portal/templates/admin/admin_base.html b/portal/templates/admin/admin_base.html
index 9eb5b30181..4d4db9f8a0 100644
--- a/portal/templates/admin/admin_base.html
+++ b/portal/templates/admin/admin_base.html
@@ -27,7 +27,7 @@
-
+
{%-endmacro %}
{% macro deletedUsersFilter() -%}
@@ -42,12 +42,10 @@
{%- endmacro %}
{%- macro testUsersCheckbox(postUrl) -%}
-
+
+
+ {{_("include test accounts")}}
+
{%- endmacro -%}
@@ -89,3 +87,23 @@
{%- endmacro -%}
+{%- macro ajaxDataScript(research_study_id) -%}
+
+{%- endmacro -%}
+{%- macro filterOptionsVar() -%}
+
+{%- endmacro -%}
diff --git a/portal/templates/admin/patients_by_org.html b/portal/templates/admin/patients_by_org.html
index 376c83318a..6c0514d51a 100644
--- a/portal/templates/admin/patients_by_org.html
+++ b/portal/templates/admin/patients_by_org.html
@@ -18,20 +18,12 @@ {{_("Patient List")}}
+ {# variable for checking if user is a researcher #}
+ {% set isResearcher = user.has_role(ROLE.RESEARCHER.value) and not(user.has_role(ROLE.ADMIN.value)) %}
{{_("Patient List")}}
data-show-toggle="true"
data-show-columns="true"
data-smart-display="true"
- data-unique-id="id"
- data-id-field="id"
+ data-unique-id="userid"
+ data-id-field="userid"
data-filter-control="true"
- data-show-export="true"
+ data-side-pagination="server"
+ data-ajax="patientDataAjaxRequest"
+ data-cache="false"
+ {%- if not isResearcher -%} data-show-export="true" {%- endif -%}
data-export-data-type="all"
>
{{testUsersCheckbox(postUrl=url_for('patients.patients_root'))}}
-
-
- {{_("TrueNTH ID")}}
- {{ _("Username") }}
+ {{_("TrueNTH ID")}}
{{ _("First Name") }}
{{ _("Last Name") }}
- {{ _("Date of Birth") }}
+ {{ _("Date of Birth") }}
{{ _("Email") }}
- {% if 'reports' in config.PATIENT_LIST_ADDL_FIELDS %}{{ _("Reports") }} {% endif %}
{% if 'status' in config.PATIENT_LIST_ADDL_FIELDS %}
- {{ _("Questionnaire Status") }}
+ {{ _("Questionnaire Status") }}
{{ _("Visit") }}
{% endif %}
{% if 'study_id' in config.PATIENT_LIST_ADDL_FIELDS %}{{ _("Study ID") }} {% endif %}
- {{ app_text('consent date label') }} {{_("(GMT)")}}
- {{ _("Site(s)") }}
- {%- if user.has_role(ROLE.ADMIN.value, ROLE.INTERVENTION_STAFF.value) -%}
- {{ _("Interventions") }}
- {%- endif -%}
- {% if account_deactivation_enabled %}
- {{ _("Deactivate") }}
- {% endif %}
- {{_("activation status")}}
-
+ {{ _("Study Consent Date") }}
+ {{ _("Site") }}
+
-
- {% for patient in patients_list | sort(attribute='id')%}
-
- {{patient.id}}
- {{ patient.id }}
- {{ patient.username if patient.username}}
- {{ patient.first_name if patient.first_name }}
- {{ patient.last_name if patient.last_name }}
- {{ patient.birthdate.strftime('%-d %b %Y') if patient.birthdate }}
- {{ patient.email if patient.email }}
- {% if 'reports' in config.PATIENT_LIST_ADDL_FIELDS %}
- {%-if patient.staff_html() -%}{{ patient.staff_html() | safe }}
{%-endif-%}
- {% if not patient.deleted and patient.documents %}
-
- {% endif %}
-
- {% endif %}
- {% if 'status' in config.PATIENT_LIST_ADDL_FIELDS %}
- {{patient.assessment_status if patient.assessment_status}}
- {{patient.current_qb if patient.current_qb}}
- {% endif %}
- {% if 'study_id' in config.PATIENT_LIST_ADDL_FIELDS %}{%if patient.external_study_id%}{{ patient.external_study_id }}{%endif%}
- {% endif %}
- {%- if patient.valid_consents -%}
- {%-for consent in patient.valid_consents -%}
- {%- if consent.research_study_id == 0 -%}
- {{consent.acceptance_date.strftime('%-d %b %Y')}}
- {%- endif -%}
- {%-endfor-%}
- {%-endif-%}
-
- {% for org in patient.organizations | sort(attribute='id') %}{{org.name}} {% endfor %}
- {%- if user.has_role(ROLE.ADMIN.value, ROLE.INTERVENTION_STAFF.value) -%}
- {% for intervention in patient.interventions | sort(attribute='description') %}{{intervention.description}} {% endfor %}
- {%- endif -%}
- {% if account_deactivation_enabled %}{{deletedUserCell(patient, allowReactivate=True)}}{% endif %}
- {% if patient.deleted %}deactivated{%else%}activated{%endif%}
-
- {% endfor %}
-
- {% if 'reports' in config.PATIENT_LIST_ADDL_FIELDS %}
-
-
-
-
-
-
-
-
-
- {{_("Type")}} {{_("Report Name")}} {{_("Generated (GMT)")}} {{_("Downloaded")}}
-
-
- {% raw %}
- {{item.contributor}} {{item.fileName}} {{item.date}}
- {% endraw %}
-
-
-
-
{{_("View All")}}
-
- {% raw %}
-
{{patientReports.message}}
- {% endraw %}
-
-
-
-
-
- {% endif %}
{{ExportPopover()}}
+{{ajaxDataScript(research_study_id=0)}}
+{{filterOptionsVar()}}
{% endblock %}
{% block footer %}{{footer(user=user)}}{% endblock %}
diff --git a/portal/templates/admin/patients_substudy.html b/portal/templates/admin/patients_substudy.html
index c4cfe7e8c6..aa54240fcf 100644
--- a/portal/templates/admin/patients_substudy.html
+++ b/portal/templates/admin/patients_substudy.html
@@ -24,15 +24,9 @@ {{list_title}}
+ {# variable for checking if user is a researcher #}
+ {% set isResearcher = user.has_role(ROLE.RESEARCHER.value) and not(user.has_role(ROLE.ADMIN.value)) %}
{{list_title}}
data-show-toggle="true"
data-show-columns="true"
data-smart-display="true"
- data-unique-id="id"
- data-id-field="id"
+ data-unique-id="userid"
+ data-id-field="userid"
data-filter-control="true"
- data-show-export="true"
+ data-side-pagination="server"
+ data-ajax="patientDataAjaxRequest"
+ data-cache="false"
+ {%- if not isResearcher -%} data-show-export="true" {%- endif -%}
data-export-data-type="all"
>
{{testUsersCheckbox(postUrl=url_for('patients.patients_substudy'))}}
- {{_("TrueNTH ID")}}
+ {{_("TrueNTH ID")}}
{{ _("First Name") }}
{{ _("Last Name") }}
{{ _("Username (email)") }}
- {{ _("Date of Birth") }}
- {{ _("Treating Clinician") }}
- {{_("EMPRO Questionnaire Status")}}
- {{ _("Visit") }}
- {{ _("Clinician Action Status") }}
+ {{ _("Date of Birth") }}
+ {{ _("Treating Clinician") }}
+ {{_("EMPRO Questionnaire Status")}}
+ {{ _("Visit") }}
+ {{ _("Clinician Action Status") }}
{{ _("Study ID") }}
- {{ app_text('consent date label') }} {{_("(GMT)")}}
- {{ _("Site(s)") }}
+ {{ _("Study Consent Date") }}
+ {{ _("Site") }}
-
- {% for patient in patients_list | sort(attribute='id')%}
-
- {{ patient.id }}
- {{ patient.first_name if patient.first_name }}
- {{ patient.last_name if patient.last_name }}
- {{ patient.email if patient.email }}
- {{ patient.birthdate.strftime('%-d %b %Y') if patient.birthdate }}
- {{patient.clinician if patient.clinician else ""}}
- {{patient.assessment_status if patient.assessment_status}}
- {{patient.current_qb if patient.current_qb}}
-
- {%- if patient.action_state in ("Required", "Due", "Overdue") -%}
- {{patient.action_state}}
- {%- else -%}
- {{patient.action_state}}
- {%- endif -%}
-
- {%if patient.external_study_id%}{{ patient.external_study_id }}{%endif%}
- {%- if patient.valid_consents -%}
- {%-for consent in patient.valid_consents -%}
- {%- if consent.research_study_id == 1 -%}
- {{consent.acceptance_date.strftime('%-d %b %Y')}}
- {%- endif -%}
- {%-endfor-%}
- {%-endif-%}
-
- {% for org in patient.organizations | sort(attribute='id') %}{{org.name}} {% endfor %}
-
- {% endfor %}
-
{{ExportPopover(title=_("Export EMPRO adherence report"))}}
+{{ajaxDataScript(research_study_id=1)}}
+{{filterOptionsVar()}}
{% endblock %}
{% block footer %}{{footer(user=user)}}{% endblock %}
-
diff --git a/portal/views/assessment_engine.py b/portal/views/assessment_engine.py
index b1b2412177..bec7484d65 100644
--- a/portal/views/assessment_engine.py
+++ b/portal/views/assessment_engine.py
@@ -851,7 +851,6 @@ def get_assessments():
research_studies = set()
questionnaire_list = request.args.getlist('instrument_id')
- ignore_qb_requirement = request.args.get("ignore_qb_requirement", False)
for q in questionnaire_list:
research_studies.add(research_study_id_from_questionnaire(q))
if len(research_studies) != 1:
@@ -872,7 +871,6 @@ def get_assessments():
'patch_dstu2': request.args.get('patch_dstu2'),
'request_url': request.url,
'lock_key': "research_report_task_lock",
- 'ignore_qb_requirement': request.args.get("ignore_qb_requirement"),
'response_format': request.args.get('format', 'json').lower()
}
@@ -930,6 +928,10 @@ def assessment_update(patient_id):
- ServiceToken: []
"""
+ from ..models.research_data import (
+ add_questionnaire_response,
+ invalidate_qnr_research_data,
+ )
if not hasattr(request, 'json') or not request.json:
return jsonify(message='Invalid request - requires JSON'), 400
@@ -990,6 +992,9 @@ def assessment_update(patient_id):
response.update({'message': 'previous questionnaire response found'})
existing_qnr = existing_qnr.first()
+ # remove this QNR from the report data cache, so it can be subsequently updated
+ invalidate_qnr_research_data(existing_qnr)
+
# TN-3184, report any in-process QNRs attempting to change authored dates
date_change_snippet = ""
if FHIR_datetime.parse(existing_qnr.document["authored"]) != FHIR_datetime.parse(updated_qnr["authored"]):
@@ -1019,6 +1024,7 @@ def assessment_update(patient_id):
response.update({'message': 'questionnaire response updated successfully'})
if research_study_id is not None:
invalidate_users_QBT(patient.id, research_study_id=research_study_id)
+ add_questionnaire_response(existing_qnr, research_study_id=research_study_id)
return jsonify(response)
@@ -1635,6 +1641,8 @@ def assessment_add(patient_id):
- ServiceToken: []
"""
+ from ..models.research_data import add_questionnaire_response
+
if not hasattr(request, 'json') or not request.json:
return jsonify(message='Invalid request - requires JSON'), 400
@@ -1735,6 +1743,7 @@ def assessment_add(patient_id):
if research_study_id is not None:
invalidate_users_QBT(patient.id, research_study_id=research_study_id)
+ add_questionnaire_response(questionnaire_response, research_study_id)
return jsonify(response)
diff --git a/portal/views/clinician.py b/portal/views/clinician.py
index 7138b75eaf..6a60ed0f33 100644
--- a/portal/views/clinician.py
+++ b/portal/views/clinician.py
@@ -15,6 +15,20 @@
clinician_api = Blueprint('clinician_api', __name__)
+def clinician_name_map():
+ roles = [ROLE.CLINICIAN.value, ROLE.PRIMARY_INVESTIGATOR.value]
+ query = User.query.join(UserRoles).filter(
+ User.deleted_id.is_(None)).filter(
+ UserRoles.user_id == User.id).join(Role).filter(
+ UserRoles.role_id == Role.id).filter(
+ Role.name.in_(roles))
+
+ _clinician_name_map = {None: None}
+ for clinician in query:
+ _clinician_name_map[clinician.id] = f"{clinician.last_name}, {clinician.first_name}"
+ return _clinician_name_map
+
+
def clinician_query(acting_user, org_filter=None, include_staff=False):
"""Builds a live query for all clinicians the acting user can view"""
roles = [ROLE.CLINICIAN.value, ROLE.PRIMARY_INVESTIGATOR.value]
diff --git a/portal/views/demographics.py b/portal/views/demographics.py
index 174317688e..962d520627 100644
--- a/portal/views/demographics.py
+++ b/portal/views/demographics.py
@@ -146,6 +146,7 @@ def demographics_set(patient_id):
- ServiceToken: []
"""
+ from ..models.patient_list import patient_list_update_patient
patient = get_user(patient_id, 'edit')
if not request.json:
abort(
@@ -174,4 +175,8 @@ def demographics_set(patient_id):
auditable_event("updated demographics on user {0} from input {1}".format(
patient_id, json.dumps(request.json)), user_id=current_user().id,
subject_id=patient_id, context='user')
+
+ # update the patient_table cache with any change from above
+ patient_list_update_patient(patient_id)
+
return jsonify(patient.as_fhir(include_empties=False))
diff --git a/portal/views/patient.py b/portal/views/patient.py
index f4c90f2c54..5d43fdbbec 100644
--- a/portal/views/patient.py
+++ b/portal/views/patient.py
@@ -27,7 +27,7 @@
)
from ..models.message import EmailMessage
from ..models.overall_status import OverallStatus
-from ..models.qb_timeline import QBT, update_users_QBT
+from ..models.qb_timeline import QBT, invalidate_users_QBT, update_users_QBT
from ..models.questionnaire_bank import QuestionnaireBank, trigger_date
from ..models.questionnaire_response import QuestionnaireResponse
from ..models.reference import Reference
@@ -361,10 +361,9 @@ def patient_timeline(patient_id):
acting_user_id=current_user().id)
cache.delete_memoized(trigger_date)
- update_users_QBT(
- patient_id,
- research_study_id=research_study_id,
- invalidate_existing=purge)
+ if purge:
+ invalidate_users_QBT(user_id=patient_id, research_study_id=research_study_id)
+ update_users_QBT(user_id=patient_id, research_study_id=research_study_id)
except ValueError as ve:
abort(500, str(ve))
@@ -478,37 +477,44 @@ def get_recur_id(qnr):
cache_single_patient_adherence_data(**kwargs)
adherence_data = sorted_adherence_data(patient_id, research_study_id)
- qnr_responses = aggregate_responses(
- instrument_ids=None,
- current_user=current_user(),
- research_study_id=research_study_id,
- patch_dstu2=True,
- ignore_qb_requirement=True,
- patient_ids=[patient_id]
- )
+ agg_args = {
+ 'instrument_ids': None,
+ 'current_user': current_user(),
+ 'research_study_id': research_study_id,
+ 'patch_dstu2': True,
+ 'patient_ids': [patient_id],
+ }
+ qnr_responses = aggregate_responses(**agg_args)
+
+ if qnr_responses['total'] == 0:
+ from ..models.research_data import update_single_patient_research_data
+ update_single_patient_research_data(patient_id)
+ qnr_responses = aggregate_responses(**agg_args)
+
# filter qnr data to a manageable result data set
qnr_data = []
for row in qnr_responses['entry']:
i = {}
- d = row['resource']
- i['questionnaire'] = d['questionnaire']['reference'].split('/')[-1]
+ i['questionnaire'] = row['questionnaire']['reference'].split('/')[-1]
# qnr_responses return all. filter to requested research_study
study_id = research_study_id_from_questionnaire(i['questionnaire'])
if study_id != research_study_id:
continue
- i['auth_method'] = d['encounter']['auth_method']
- i['encounter_period'] = d['encounter']['period']
- i['document_authored'] = d['authored']
+ i['auth_method'] = row['encounter']['auth_method']
+ i['encounter_period'] = row['encounter']['period']
+ i['document_authored'] = row['authored']
try:
- i['ae_session'] = d['identifier']['value']
+ i['ae_session'] = row['identifier']['value']
except KeyError:
# happens with sub-study follow up, skip ae_session
pass
- i['status'] = d['status']
- i['org'] = d['subject']['careProvider'][0]['display']
- i['visit'] = d['timepoint']
+ i['status'] = row['status']
+ i['org'] = ': '.join((
+ row['subject']['careProvider'][0]['identifier'][0]['value'],
+ row['subject']['careProvider'][0]['display']))
+ i['visit'] = row['timepoint']
qnr_data.append(i)
consent_date, withdrawal_date = consent_withdrawal_dates(user, research_study_id)
@@ -651,10 +657,8 @@ def sanity_check():
research_study_id=research_study_id,
acting_user_id=current_user().id)
- update_users_QBT(
- patient_id,
- research_study_id=research_study_id,
- invalidate_existing=True)
+ invalidate_users_QBT(user_id=patient_id, research_study_id=research_study_id)
+ update_users_QBT(user_id=patient_id, research_study_id=research_study_id)
auditable_event(
message=f"TIME WARPED existing data back {days} days.",
diff --git a/portal/views/patients.py b/portal/views/patients.py
index b430426703..f4fe7705dc 100644
--- a/portal/views/patients.py
+++ b/portal/views/patients.py
@@ -1,6 +1,5 @@
"""Patient view functions (i.e. not part of the API or auth)"""
-from datetime import datetime
-
+import json
from flask import (
Blueprint,
abort,
@@ -11,85 +10,226 @@
)
from flask_babel import gettext as _
from flask_user import roles_required
+from sqlalchemy import asc, desc
-from .clinician import clinician_query
from ..extensions import oauth
from ..models.coding import Coding
from ..models.intervention import Intervention
-from ..models.organization import Organization
+from ..models.organization import Organization, OrgTree
+from ..models.patient_list import PatientList
from ..models.qb_status import patient_research_study_status
-from ..models.qb_timeline import QB_StatusCacheKey, qb_status_visit_name
from ..models.role import ROLE
from ..models.research_study import EMPRO_RS_ID, ResearchStudy
from ..models.table_preference import TablePreference
-from ..models.user import current_user, get_user, patients_query
+from ..models.user import current_user, get_user
patients = Blueprint('patients', __name__, url_prefix='/patients')
-def org_preference_filter(user, table_name):
+def users_table_pref_from_research_study_id(user, research_study_id):
+ """Returns user's table preferences for given research_study id"""
+ if research_study_id == 0:
+ table_name = 'patientList'
+ elif research_study_id == 1:
+ table_name = 'substudyPatientList'
+ else:
+ raise ValueError('Invalid research_study_id')
+
+ return TablePreference.query.filter_by(
+ table_name=table_name, user_id=user.id).first()
+
+
+def org_preference_filter(user, research_study_id):
"""Obtain user's preference for filtering organizations
:returns: list of org IDs to use as filter, or None
-
"""
- # check user table preference for organization filters
- pref = TablePreference.query.filter_by(
- table_name=table_name, user_id=user.id).first()
+ pref = users_table_pref_from_research_study_id(
+ user=user, research_study_id=research_study_id)
if pref and pref.filters:
return pref.filters.get('orgs_filter_control')
- return None
-def render_patients_list(
- request, research_study_id, table_name, template_name):
- include_test_role = request.args.get('include_test_role')
+def preference_filter(user, research_study_id, arg_filter):
+ """Obtain user's preference for filtering
- if request.form.get('reset_cache'):
- QB_StatusCacheKey().update(datetime.utcnow())
- if research_study_id == EMPRO_RS_ID:
- clinician_name_map = {None: None}
- for clinician in clinician_query(current_user()):
- clinician_name_map[clinician.id] = f"{clinician.last_name}, {clinician.first_name}"
+ Looks first in request args, defaults to table preferences if not found
+
+ :param user: current user
+ :param research_study_id: 0 or 1, i.e. EMPRO_STUDY_ID
+ :param arg_filter: value of request.args.get("filter")
+
+ returns: dictionary of key/value pairs for filtering
+ """
+ # if arg_filter is defined, use as return value
+ if arg_filter:
+ # Convert from query string to dict
+ filters = json.loads(arg_filter)
+ return filters
+
+ # otherwise, check db for filters from previous requests
+ pref = users_table_pref_from_research_study_id(
+ user=user, research_study_id=research_study_id)
+ if pref and pref.filters:
+ # return all but orgs and column selections
+ return {
+ k: v for k, v in pref.filters.items()
+ if k not in ['orgs_filter_control', 'column_selections']}
+
+
+def preference_sort(user, research_study_id, arg_sort, arg_order):
+ """Obtain user's preference for sorting
+
+ Looks first in request args, defaults to table preferences if not found
+
+ :param user: current user
+ :param research_study_id: 0 or 1, i.e. EMPRO_STUDY_ID
+ :param arg_sort: value of request.args.get("sort")
+ :param arg_sort: value of request.args.get("order")
+
+ returns: tuple: (sort_field, sort_order)
+ """
+ # if args are defined, use as return value
+ if arg_sort and arg_order:
+ return arg_sort, arg_order
+
+ # otherwise, check db for filters from previous requests
+ pref = users_table_pref_from_research_study_id(
+ user=user, research_study_id=research_study_id)
+ if not pref:
+ return "userid", "asc" # reasonable defaults
+ return pref.sort_field, pref.sort_order
+
+
+def filter_query(query, filter_field, filter_value):
+ """Extend patient list query with requested filter/search"""
+ if not hasattr(PatientList, filter_field):
+ # these should never get passed, but it has happened in test.
+ # ignore requests to filter by unknown column
+ return query
+
+ if filter_field == 'userid':
+ query = query.filter(PatientList.userid == int(filter_value))
+ return query
+
+ if filter_field in ('questionnaire_status', 'empro_status', 'action_state'):
+ query = query.filter(getattr(PatientList, filter_field) == filter_value)
+
+ pattern = f"%{filter_value.lower()}%"
+ query = query.filter(getattr(PatientList, filter_field).ilike(pattern))
+ return query
+
+
+def sort_query(query, sort_column, direction):
+ """Extend patient list query with requested sorting criteria"""
+ sort_method = asc if direction == 'asc' else desc
+
+ if not hasattr(PatientList, sort_column):
+ # these should never get passed, but it has happened in test.
+ # ignore requests to sort by unknown column
+ return query
+ query = query.order_by(sort_method(getattr(PatientList, sort_column)))
+ return query
+
+
+@patients.route("/page", methods=["GET"])
+@roles_required([
+ ROLE.INTERVENTION_STAFF.value,
+ ROLE.STAFF.value,
+ ROLE.STAFF_ADMIN.value])
+@oauth.require_oauth()
+def page_of_patients():
+ """called via ajax from the patient list, deliver next page worth of patients
+
+ Following query string parameters are expected:
+ :param search: search string,
+ :param sort: column to sort by,
+ :param order: direction to apply to sorted column,
+ :param offset: offset from first page of the given search params
+ :param limit: count in a page
+ :param research_study_id: default 0, set to 1 for EMPRO
+
+ """
+ def requested_orgs(user, research_study_id):
+ """Return set of requested orgs limited to those the user is allowed to view"""
+ # start with set of org ids the user has permission to view
+ viewable_orgs = set()
+ for org in user.organizations:
+ ids = OrgTree().here_and_below_id(org.id)
+ viewable_orgs.update(ids)
+
+ # Reduce viewable orgs by filter preferences
+ filtered_orgs = org_preference_filter(user=user, research_study_id=research_study_id)
+ if filtered_orgs:
+ viewable_orgs = viewable_orgs.intersection(filtered_orgs)
+ return viewable_orgs
user = current_user()
- query = patients_query(
- acting_user=user,
- include_test_role=include_test_role,
- include_deleted=True,
- research_study_id=research_study_id,
- requested_orgs=org_preference_filter(user, table_name=table_name))
-
- # get assessment status only if it is needed as specified by config
- qb_status_cache_age = 0
- if 'status' in current_app.config.get('PATIENT_LIST_ADDL_FIELDS'):
- status_cache_key = QB_StatusCacheKey()
- cached_as_of_key = status_cache_key.current()
- qb_status_cache_age = status_cache_key.minutes_old()
- patients_list = []
- for patient in query:
- if patient.deleted:
- patients_list.append(patient)
- continue
- qb_status = qb_status_visit_name(
- patient.id, research_study_id, cached_as_of_key)
- patient.assessment_status = _(qb_status['status'])
- patient.current_qb = qb_status['visit_name']
- if research_study_id == EMPRO_RS_ID:
- patient.clinician = '; '.join(
- (clinician_name_map.get(c.id, "not in map") for c in
- patient.clinicians)) or ""
- patient.action_state = qb_status['action_state'].title() \
- if qb_status['action_state'] else ""
- patients_list.append(patient)
+ research_study_id = int(request.args.get("research_study_id", 0))
+ # due to potentially translated content, need to capture all potential values to sort
+ # (not just the current page) for the front-end options list
+ options = []
+ if research_study_id == EMPRO_RS_ID:
+ distinct_status = PatientList.query.distinct(PatientList.empro_status).with_entities(
+ PatientList.empro_status)
+ options.append({"empro_status": [(status[0], _(status[0])) for status in distinct_status]})
+ distinct_action = PatientList.query.distinct(PatientList.action_state).with_entities(
+ PatientList.action_state)
+ options.append({"action_state": [(state[0], _(state[0])) for state in distinct_action]})
else:
- patients_list = query
+ distinct_status = PatientList.query.distinct(
+ PatientList.questionnaire_status).with_entities(PatientList.questionnaire_status)
+ options.append(
+ {"questionnaire_status": [(status[0], _(status[0])) for status in distinct_status]})
- return render_template(
- template_name, patients_list=patients_list, user=user,
- qb_status_cache_age=qb_status_cache_age, wide_container="true",
- include_test_role=include_test_role)
+ viewable_orgs = requested_orgs(user, research_study_id)
+ query = PatientList.query.filter(PatientList.org_id.in_(viewable_orgs))
+ if research_study_id == EMPRO_RS_ID:
+ # only include those in the study. use empro_consentdate as a quick check
+ query = query.filter(PatientList.empro_consentdate.isnot(None))
+ if not request.args.get('include_test_role', "false").lower() == "true":
+ query = query.filter(PatientList.test_role.is_(False))
+
+ filters = preference_filter(
+ user=user, research_study_id=research_study_id, arg_filter=request.args.get("filter"))
+ if filters:
+ for key, value in filters.items():
+ query = filter_query(query, key, value)
+
+ sort_column, sort_order = preference_sort(
+ user=user, research_study_id=research_study_id, arg_sort=request.args.get("sort"),
+ arg_order=request.args.get("order"))
+ query = sort_query(query, sort_column, sort_order)
+
+ total = query.count()
+ query = query.offset(request.args.get('offset', 0))
+ query = query.limit(request.args.get('limit', 10))
+
+ # Returns structured JSON with totals and rows
+ data = {"total": total, "totalNotFiltered": total, "rows": [], "options": options}
+ for row in query:
+ data['rows'].append({
+ "userid": row.userid,
+ "firstname": row.firstname,
+ "lastname": row.lastname,
+ "birthdate": row.birthdate,
+ "email": row.email,
+ "questionnaire_status": _(row.questionnaire_status),
+ "empro_status": _(row.empro_status),
+ "action_state": _(row.action_state),
+ "visit": row.visit,
+ "empro_visit": row.empro_visit,
+ "study_id": row.study_id,
+ "consentdate": row.consentdate,
+ "empro_consentdate": row.empro_consentdate,
+ "clinician": row.clinician,
+ "org_id": row.org_id,
+ "org_name": row.org_name,
+ "deleted": row.deleted,
+ "test_role": row.test_role,
+ })
+ return jsonify(data)
@patients.route('/', methods=('GET', 'POST'))
@@ -115,11 +255,11 @@ def patients_root():
expected and will raise a 400: Bad Request
"""
- return render_patients_list(
- request,
- research_study_id=0,
- table_name='patientList',
- template_name='admin/patients_by_org.html')
+ user = current_user()
+ return render_template(
+ 'admin/patients_by_org.html', user=user,
+ wide_container="true",
+ )
@patients.route('/substudy', methods=('GET', 'POST'))
@@ -140,11 +280,11 @@ def patients_substudy():
staff, staff_admin: all patients with common consented organizations
"""
- return render_patients_list(
- request,
- research_study_id=EMPRO_RS_ID,
- table_name='substudyPatientList',
- template_name='admin/patients_substudy.html')
+ user = current_user()
+ return render_template(
+ 'admin/patients_substudy.html', user=user,
+ wide_container="true",
+ )
@patients.route('/patient-profile-create')
diff --git a/portal/views/user.py b/portal/views/user.py
index 87c33460fe..06175bd3c7 100644
--- a/portal/views/user.py
+++ b/portal/views/user.py
@@ -33,6 +33,7 @@
from ..models.intervention import Intervention
from ..models.message import EmailMessage
from ..models.organization import Organization
+from ..models.patient_list import patient_list_update_patient
from ..models.questionnaire_bank import trigger_date
from ..models.qb_timeline import QB_StatusCacheKey, invalidate_users_QBT
from ..models.questionnaire_response import QuestionnaireResponse
@@ -357,6 +358,8 @@ def delete_user(user_id):
user = get_user(user_id, 'edit')
try:
user.delete_user(acting_user=current_user())
+ # update patient list given change in patient status
+ patient_list_update_patient(patient_id=user.id, research_study_id=None)
except ValueError as v:
return jsonify(message=str(v))
return jsonify(message="deleted")
diff --git a/tests/test_assessment_engine.py b/tests/test_assessment_engine.py
index c71ca8c8b4..0526d32bf2 100644
--- a/tests/test_assessment_engine.py
+++ b/tests/test_assessment_engine.py
@@ -1,5 +1,5 @@
"""Unit test module for Assessment Engine API"""
-from datetime import datetime
+from datetime import datetime, timedelta
import json
import os
@@ -369,12 +369,21 @@ def test_submit_nearfuture_assessment(self):
assert response.status_code == 200
def test_update_assessment(self):
+ # mock questionnaire banks, as only QB associated QNRs land in the bundle
+ from .test_assessment_status import mock_eproms_questionnairebanks
+ mock_eproms_questionnairebanks()
+
swagger_spec = swagger(self.app)
completed_qnr = swagger_spec['definitions']['QuestionnaireResponse'][
'example']
instrument_id = (completed_qnr['questionnaire']['reference'].split(
'/')[-1])
+ # patch the authored date, so QB assignment will line up
+ now = datetime.utcnow()
+ yesterday = now - timedelta(days=1)
+ completed_qnr['authored'] = FHIR_datetime.as_fhir(now)
+
questions = completed_qnr['group']['question']
incomplete_questions = []
@@ -391,7 +400,7 @@ def test_update_assessment(self):
})
self.login()
- self.bless_with_basics()
+ self.bless_with_basics(setdate=yesterday, local_metastatic='localized')
self.promote_user(role_name=ROLE.STAFF.value)
self.promote_user(role_name=ROLE.RESEARCHER.value)
self.add_system_user()
@@ -412,9 +421,7 @@ def test_update_assessment(self):
updated_qnr_response = self.results_from_async_call(
'/api/patient/assessment',
- query_string={
- 'instrument_id': instrument_id,
- 'ignore_qb_requirement': True})
+ query_string={'instrument_id': instrument_id})
assert updated_qnr_response.status_code == 200
assert (
updated_qnr_response.json['entry'][0]['group']
@@ -440,14 +447,23 @@ def test_no_update_assessment(self):
assert update_qnr_response.status_code == 404
def test_assessments_bundle(self):
+ # mock questionnaire banks, as only QB associated QNRs land in the bundle
+ from .test_assessment_status import mock_eproms_questionnairebanks
+ mock_eproms_questionnairebanks()
+
swagger_spec = swagger(self.app)
example_data = swagger_spec['definitions']['QuestionnaireResponse'][
'example']
instrument_id = example_data['questionnaire']['reference'].split('/')[
-1]
+ # patch the authored date, so QB assignment will line up
+ now = datetime.utcnow()
+ yesterday = now - timedelta(days=1)
+ example_data['authored'] = FHIR_datetime.as_fhir(now)
+
self.login()
- self.bless_with_basics()
+ self.bless_with_basics(setdate=yesterday, local_metastatic='localized')
self.promote_user(role_name=ROLE.STAFF.value)
self.promote_user(role_name=ROLE.RESEARCHER.value)
self.add_system_user()
@@ -459,9 +475,7 @@ def test_assessments_bundle(self):
response = self.results_from_async_call(
'/api/patient/assessment',
- query_string={
- 'instrument_id': instrument_id,
- 'ignore_qb_requirement': True})
+ query_string={'instrument_id': instrument_id})
assert response.status_code == 200
response = response.json
diff --git a/tests/test_assessment_status.py b/tests/test_assessment_status.py
index 39043130d8..7ec4235847 100644
--- a/tests/test_assessment_status.py
+++ b/tests/test_assessment_status.py
@@ -1,6 +1,5 @@
"""Module to test assessment_status"""
-import copy
from datetime import datetime
from pytz import timezone, utc
from random import choice
@@ -13,11 +12,8 @@
from portal.date_tools import FHIR_datetime, utcnow_sans_micro
from portal.extensions import db
-from portal.models.audit import Audit
-from portal.models.clinical_constants import CC
from portal.models.encounter import Encounter
from portal.models.identifier import Identifier
-from portal.models.intervention import INTERVENTION
from portal.models.organization import Organization
from portal.models.overall_status import OverallStatus
from portal.models.qb_status import QB_Status
@@ -34,6 +30,11 @@
qnr_document_id,
)
from portal.models.recur import Recur
+from portal.models.research_data import (
+ add_questionnaire_response,
+ invalidate_patient_research_data,
+ update_single_patient_research_data,
+)
from portal.models.research_protocol import ResearchProtocol
from portal.models.role import ROLE
from portal.models.user import User
@@ -95,6 +96,8 @@ def mock_qr(
db.session.add(qr)
db.session.commit()
invalidate_users_QBT(user_id=user_id, research_study_id='all')
+ qr = db.session.merge(qr)
+ add_questionnaire_response(questionnaire_response=qr, research_study_id=0)
localized_instruments = {'eproms_add', 'epic26', 'comorb'}
@@ -320,6 +323,11 @@ def test_aggregate_response_timepoints(self):
Organization.name == 'metastatic').one())
self.promote_user(staff, role_name=ROLE.STAFF.value)
staff = db.session.merge(staff)
+
+ # testing situation, each mock_qr() call invalidates cached and adds
+ # only the latest to the research data cache. flush and add all
+ invalidate_patient_research_data(TEST_USER_ID, research_study_id=0)
+ update_single_patient_research_data(TEST_USER_ID)
bundle = aggregate_responses(
instrument_ids=[instrument_id],
research_study_id=0,
diff --git a/tests/test_portal.py b/tests/test_portal.py
index 1e3f2be2d1..618be53d13 100644
--- a/tests/test_portal.py
+++ b/tests/test_portal.py
@@ -75,32 +75,6 @@ def test_user_card_html(self):
intervention.display_for_user(user).link_label
in response.get_data(as_text=True))
- def test_staff_html(self):
- """Interventions can customize the staff text """
- client = self.add_client()
- intervention = INTERVENTION.sexual_recovery
- client.intervention = intervention
- ui = UserIntervention(
- user_id=TEST_USER_ID,
- intervention_id=intervention.id)
- ui.staff_html = "Custom text for staff "
- with SessionScope(db):
- db.session.add(ui)
- db.session.commit()
-
- self.bless_with_basics()
- self.login()
- self.promote_user(role_name=ROLE.INTERVENTION_STAFF.value)
-
- # This test requires PATIENT_LIST_ADDL_FIELDS includes the
- # 'reports' field
- self.app.config['PATIENT_LIST_ADDL_FIELDS'] = ['reports']
- response = self.client.get('/patients/')
-
- ui = db.session.merge(ui)
- results = response.get_data(as_text=True)
- assert ui.staff_html in results
-
def test_public_access(self):
"""Interventions w/o public access should be hidden"""
client = self.add_client()
diff --git a/tests/test_trigger_states.py b/tests/test_trigger_states.py
index 48d92979b7..11c34ed89f 100644
--- a/tests/test_trigger_states.py
+++ b/tests/test_trigger_states.py
@@ -63,7 +63,7 @@ def test_qnr_holiday_delay(test_user, clinician_response_holiday_delay):
with patch('portal.models.questionnaire_response.QuestionnaireResponse') as mockQNR:
getbyid = mockQNR.query.get
getbyid.return_value = clinician_response_holiday_delay
- assert tsr.resolution_delayed_by_holiday(0) == True
+ assert tsr.resolution_delayed_by_holiday(0) is True
def test_initiate_trigger(test_user):