diff --git a/portal/migrations/versions/4f5daa2b48db_.py b/portal/migrations/versions/4f5daa2b48db_.py new file mode 100644 index 000000000..cef95eb30 --- /dev/null +++ b/portal/migrations/versions/4f5daa2b48db_.py @@ -0,0 +1,24 @@ +"""remove non english rows from report data (IRONN-264) + +Revision ID: 4f5daa2b48db +Revises: 5a300be640fb +Create Date: 2024-11-25 13:48:01.321510 + +""" +from alembic import op + + +# revision identifiers, used by Alembic. +revision = '4f5daa2b48db' +down_revision = '5a300be640fb' + + +def upgrade(): + connection = op.get_bind() + connection.execute("DELETE FROM research_data WHERE NOT (data->>'timepoint' ~ '^(Baseline|Month)');") + # next run of `cache_research_data()` will pick up those just deleted. + + +def downgrade(): + # no point in bringing those back + pass diff --git a/portal/models/qb_timeline.py b/portal/models/qb_timeline.py index a35d04635..81c129222 100644 --- a/portal/models/qb_timeline.py +++ b/portal/models/qb_timeline.py @@ -1316,8 +1316,8 @@ def qb_status_visit_name(user_id, research_study_id, as_of_date): :returns: dictionary with key/values for: status: string like 'expired' - visit_name: for the period, i.e. '3 months' - action_state: 'not applicable', or status of follow up action + visit_name: for the period, i.e. '3 months'. ALWAYS in english, clients must translate + action_state: 'not applicable', or status of follow-up action """ from .research_study import EMPRO_RS_ID diff --git a/portal/models/questionnaire_bank.py b/portal/models/questionnaire_bank.py index c964f3c82..6a0e2822b 100644 --- a/portal/models/questionnaire_bank.py +++ b/portal/models/questionnaire_bank.py @@ -592,6 +592,10 @@ def qbs_by_rp(rp_id, classification): def visit_name(qbd): + """returns string repr of visit, i.e. 'Month 3' or 'Baseline' + + NB - only returns english version. See `translate_visit_name()` + """ from .research_study import ( EMPRO_RS_ID, research_study_id_from_questionnaire, @@ -617,12 +621,22 @@ def visit_name(qbd): clm += (clrd.years * 12) if clrd.years else 0 total = clm * qbd.iteration + sm if rs_id == EMPRO_RS_ID: - return _('Month %(month_total)d', month_total=total+1) - return _('Month %(month_total)d', month_total=total) + return f'Month {total+1}' + return f'Month {total}' if rs_id == EMPRO_RS_ID: - return _('Month %(month_total)d', month_total=1) - return _(qbd.questionnaire_bank.classification.title()) + return 'Month 1' + return qbd.questionnaire_bank.classification.title() + + +def translate_visit_name(visit_name): + """parse the english version of visit name for front end translation needs""" + if not visit_name: + return visit_name + if visit_name.startswith('Month '): + number = int(visit_name[6:]) + return _('Month %(month_total)d', month_total=number) + return _(visit_name) def add_static_questionnaire_bank(): diff --git a/portal/models/questionnaire_response.py b/portal/models/questionnaire_response.py index bf52df00b..e24ad500d 100644 --- a/portal/models/questionnaire_response.py +++ b/portal/models/questionnaire_response.py @@ -28,6 +28,7 @@ QuestionnaireBank, QuestionnaireBankQuestionnaire, trigger_date, + translate_visit_name, visit_name, ) from .research_data import ResearchData @@ -401,7 +402,7 @@ def extensions(self): relative_start=None, iteration=self.qb_iteration, recur_id=recur_id, qb_id=self.questionnaire_bank_id) results.append({ - 'visit_name': visit_name(qbd), + 'visit_name': translate_visit_name(visit_name(qbd)), 'url': TRUENTH_VISIT_NAME_EXTENSION}) expires_at = expires(self.subject_id, qbd) @@ -909,11 +910,16 @@ def qnr_document_id( QuestionnaireResponse.subject_id == subject_id).filter( QuestionnaireResponse.document[ ('questionnaire', 'reference') - ].astext.endswith(questionnaire_name)).filter( - QuestionnaireResponse.questionnaire_bank_id == - questionnaire_bank_id).with_entities( + ].astext.endswith(questionnaire_name)).with_entities( QuestionnaireResponse.document[( 'identifier', 'value')]) + if questionnaire_name != 'irondemog_v3': + # Another special indefinite workaround. irondemog_v3 happens to live + # in multiple questionnaire banks, thus the lookup will fail when + # restricted by QB.id, should the org have transitioned since the user + # left work incomplete from the previous protocol (TN-2747) + qnr = qnr.filter(QuestionnaireResponse.questionnaire_bank_id == questionnaire_bank_id) + if iteration is not None: qnr = qnr.filter(QuestionnaireResponse.qb_iteration == iteration) else: diff --git a/portal/models/reporting.py b/portal/models/reporting.py index 9db76b46b..d42f7319c 100644 --- a/portal/models/reporting.py +++ b/portal/models/reporting.py @@ -6,7 +6,6 @@ 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 @@ -30,12 +29,25 @@ qnr_csv_column_headers, generate_qnr_csv, ) -from .research_study import BASE_RS_ID, EMPRO_RS_ID +from .research_study import BASE_RS_ID, EMPRO_RS_ID, ResearchStudy from .role import ROLE, Role from .user import User, UserRoles, patients_query from .user_consent import consent_withdrawal_dates +def update_patient_adherence_data(patient_id): + """Cache invalidation and force rebuild for given patient's adherence data + + NB - any timeline or questionnaire response data changes are invalidated and + updated as part of `invalidate_users_QBT()`. This function is for edge cases + such as changing a user's study-id. + """ + patient = User.query.get(patient_id) + AdherenceData.query.filter(AdherenceData.patient_id==patient_id).delete() + for rs_id in ResearchStudy.assigned_to(patient): + single_patient_adherence_data(patient_id=patient_id, research_study_id=rs_id) + + def single_patient_adherence_data(patient_id, research_study_id): """Update any missing (from cache) adherence data for patient diff --git a/portal/static/js/src/admin.js b/portal/static/js/src/admin.js index 718b1d8a4..f2704e6f5 100644 --- a/portal/static/js/src/admin.js +++ b/portal/static/js/src/admin.js @@ -608,6 +608,7 @@ let requestTimerId = 0; this.filterOptionsList.forEach((o) => { for (const [key, values] of Object.entries(o)) { values.forEach((value) => { + if (!value[1]) return true; if ( $( `#adminTable .bootstrap-table-filter-control-${key} option[value='${value[0]}']` @@ -1412,7 +1413,7 @@ let requestTimerId = 0; max_attempts: 1, }, function (result) { - if (!result?.error) self.currentTablePreference = data; + if (result && !result.error) self.currentTablePreference = data; if (callback) callback(); } ); diff --git a/portal/templates/admin/admin_base.html b/portal/templates/admin/admin_base.html index 94d851c85..2c8375ccd 100644 --- a/portal/templates/admin/admin_base.html +++ b/portal/templates/admin/admin_base.html @@ -107,5 +107,6 @@ // placeholder variables var qStatusFilterOptions = {}; var clinicianActionStateFilterOptions = {}; + var visitOptions = {}; {%- endmacro -%} diff --git a/portal/templates/admin/patients_by_org.html b/portal/templates/admin/patients_by_org.html index 2465f98f5..a86c496bb 100644 --- a/portal/templates/admin/patients_by_org.html +++ b/portal/templates/admin/patients_by_org.html @@ -56,7 +56,7 @@