diff --git a/portal/config/eproms/ScheduledJob.json b/portal/config/eproms/ScheduledJob.json index d8b630fea..2881a9b8b 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, diff --git a/portal/migrations/versions/038a1a5f4218_.py b/portal/migrations/versions/038a1a5f4218_.py new file mode 100644 index 000000000..2762fe6ee --- /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 000000000..8dce15c9e --- /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/models/adherence_data.py b/portal/models/adherence_data.py index 7927982a6..121dea032 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") diff --git a/portal/models/patient_list.py b/portal/models/patient_list.py new file mode 100644 index 000000000..9d693bf97 --- /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/reporting.py b/portal/models/reporting.py index 1adbc7e2a..9db76b46b 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, diff --git a/portal/models/research_data.py b/portal/models/research_data.py index b9aab529a..771cb4613 100644 --- a/portal/models/research_data.py +++ b/portal/models/research_data.py @@ -23,9 +23,11 @@ class ResearchData(db.Model): """ __tablename__ = 'research_data' id = db.Column(db.Integer, primary_key=True) - subject_id = db.Column(db.ForeignKey('users.id'), index=True, nullable=False) + subject_id = db.Column( + db.ForeignKey('users.id', ondelete='cascade'), index=True, nullable=False) questionnaire_response_id = db.Column( - db.ForeignKey('questionnaire_responses.id'), index=True, unique=True, nullable=False, + db.ForeignKey('questionnaire_responses.id', ondelete='cascade'), + index=True, unique=True, nullable=False, doc="source questionnaire response") instrument = db.Column(db.Text, index=True, nullable=False) research_study_id = db.Column(db.Integer, index=True, nullable=False) diff --git a/portal/static/js/src/admin.js b/portal/static/js/src/admin.js index f849e3ad7..8d757c186 100644 --- a/portal/static/js/src/admin.js +++ b/portal/static/js/src/admin.js @@ -2,1260 +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 > 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.") - }); - //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 + $("#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( + `` + ); + }); + } + }); + }, + 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}
", - "
", - " ", - "", - "
" - ] - .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}
", + "
", + " ", + "", + "
", + ] + .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(''.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( + '' + .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/modules/TnthAjax.js b/portal/static/js/src/modules/TnthAjax.js index a3dec8ef4..f6cbad11d 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; @@ -1240,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; diff --git a/portal/static/less/eproms.less b/portal/static/less/eproms.less index fb6726a4f..250e9d80a 100644 --- a/portal/static/less/eproms.less +++ b/portal/static/less/eproms.less @@ -2351,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; @@ -2380,9 +2379,6 @@ div.footer-wrapper.right-panel { } &.active { opacity: 1; - .fixed-table-loading { - max-height: 0; - } .fht-cell, .filterControl { opacity: 1; diff --git a/portal/tasks.py b/portal/tasks.py index 98570a152..f1202ea2f 100644 --- a/portal/tasks.py +++ b/portal/tasks.py @@ -202,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 9eb5b3018..4d4db9f8a 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) -%} -
- -
+ {%- 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 01a518658..6c0514d51 100644 --- a/portal/templates/admin/patients_by_org.html +++ b/portal/templates/admin/patients_by_org.html @@ -18,16 +18,6 @@

{{_("Patient List")}}

{{orgsSelector()}} - {% if 'status' in config.PATIENT_LIST_ADDL_FIELDS %} -
-
- - - - -
-
- {% endif %} {% if account_deactivation_enabled %} {{deletedUsersFilter()}} {% endif %} @@ -47,123 +37,38 @@

{{_("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-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 %} -
- {% for doc in patient.documents.distinct('intervention_id').filter_by(document_type='PatientReport') | selectattr('intervention_id') | sort(attribute='intervention_id') %}{% if doc.intervention %}{% if doc.intervention.description == 'Symptom Tracker' %}{{_('ST')}}{% else %}{{ _('DS')}}{% endif %}{% endif %}{% endfor %} -
- {% 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 %} - - {% 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 dd43f3244..aa54240fc 100644 --- a/portal/templates/admin/patients_substudy.html +++ b/portal/templates/admin/patients_substudy.html @@ -24,14 +24,6 @@

{{list_title}}

{{orgsSelector()}} -
-
- - - - -
-
{# variable for checking if user is a researcher #} {% set isResearcher = user.has_role(ROLE.RESEARCHER.value) and not(user.has_role(ROLE.ADMIN.value)) %} @@ -48,65 +40,38 @@

{{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-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/clinician.py b/portal/views/clinician.py index 7138b75ea..6a60ed0f3 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 174317688..962d52062 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/patients.py b/portal/views/patients.py index b43042670..f4fe7705d 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 87c33460f..06175bd3c 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_portal.py b/tests/test_portal.py index 1e3f2be2d..618be53d1 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()