diff --git a/corehq/apps/geospatial/views.py b/corehq/apps/geospatial/views.py index e77df0f138b4..e2a84dcdbd66 100644 --- a/corehq/apps/geospatial/views.py +++ b/corehq/apps/geospatial/views.py @@ -365,7 +365,7 @@ def __init__(self, request, domain, **kwargs): # override super class corehq.apps.reports.generic.GenericReportView init method to # avoid failures for missing expected properties for a report and keep only necessary properties self.request = request - self.request_params = json_request(self.request.GET) + self._request_params = json_request(self.request.GET) self.domain = domain def _base_query(self): diff --git a/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_migration.py b/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_migration.py index 687ab6f1d0ac..be671aba8760 100644 --- a/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_migration.py +++ b/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_migration.py @@ -10,6 +10,7 @@ get_all_javascript_paths_for_app, get_split_paths, get_short_path, + get_bootstrap5_path, ) from corehq.apps.hqwebapp.utils.management_commands import ( get_confirmation, @@ -142,7 +143,7 @@ def sanitize_bootstrap3_from_filename(self, filename): f"You specified '{filename}', which appears to be a Bootstrap 3 path!\n" f"This file cannot be marked as complete with this tool.\n\n" )) - filename = filename.replace('/bootstrap3/', '/bootstrap5/') + filename = get_bootstrap5_path(filename) confirm = get_confirmation(f"Did you mean '{filename}'?") if not confirm: self.stdout.write("Ok, aborting operation.\n\n") diff --git a/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_report.py b/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_report.py new file mode 100644 index 000000000000..e711db4da5e2 --- /dev/null +++ b/corehq/apps/hqwebapp/management/commands/complete_bootstrap5_report.py @@ -0,0 +1,168 @@ +from django.core.management import BaseCommand + +from corehq.apps.hqwebapp.utils.bootstrap.paths import get_bootstrap5_path +from corehq.apps.hqwebapp.utils.bootstrap.git import apply_commit, get_commit_string +from corehq.apps.hqwebapp.utils.bootstrap.reports.progress import ( + get_migrated_reports, + get_migrated_filters, + mark_filter_as_complete, + mark_report_as_complete, + get_migrated_filter_templates, + mark_filter_template_as_complete, +) +from corehq.apps.hqwebapp.utils.bootstrap.reports.stats import ( + get_report_class, + get_bootstrap5_reports, +) +from corehq.apps.hqwebapp.utils.management_commands import get_confirmation + + +class Command(BaseCommand): + help = "This command helps mark reports and associated filters (and their templates) as migrated." + + def add_arguments(self, parser): + parser.add_argument('report_class_name') + + def handle(self, report_class_name, **options): + self.stdout.write("\n\n") + report_class = get_report_class(report_class_name) + if report_class is None: + self.stdout.write(self.style.ERROR( + f"Could not find report {report_class_name}. Are you sure it exists?" + )) + return + + migrated_reports = get_migrated_reports() + if not self.is_safe_to_migrate_report(report_class_name, migrated_reports): + self.stdout.write(self.style.ERROR( + f"\nAborting migration of {report_class_name}...\n\n" + )) + return + + if report_class_name in migrated_reports: + self.stdout.write(self.style.WARNING( + f"The report {report_class_name} has already been marked as migrated." + )) + confirm = get_confirmation("Re-run report migration checks?", default='y') + else: + confirm = get_confirmation(f"Proceed with marking {report_class_name} as migrated?", default='y') + + if not confirm: + return + + if report_class.debug_bootstrap5: + self.stdout.write(self.style.ERROR( + f"Could not complete migration of {report_class.__name__} because " + f"`debug_bootstrap5` is still set to `True`.\n" + f"Please remove this property to continue." + )) + return + + self.migrate_report_and_filters(report_class) + self.stdout.write("\n\n") + + def is_safe_to_migrate_report(self, report_class_name, migrated_reports): + """ + Sometimes a report will be migrated that's then inherited by downstream reports. + This check ensures that this is not overlooked when marking a report as migrated, + and it's why we keep the list of intentionally migrated reports separated from + a dynamically generated one from `get_bootstrap5_reports()`. + + Either those reports must be migrated first OR they should have `use_bootstrap5 = False`, + to override the setting of the inherited report. + """ + bootstrap5_reports = set(get_bootstrap5_reports()) + intentionally_migrated_reports = set([report_class_name] + migrated_reports) + overlooked_reports = bootstrap5_reports.difference(intentionally_migrated_reports) + if not overlooked_reports: + return True + self.stdout.write(self.style.ERROR( + f"It is not safe to migrate {report_class_name}!" + )) + self.stdout.write("There are other reports that inherit from this report.") + self.stdout.write("You can either migrate these reports first OR " + "set use_bootstrap = False on them to continue migrating this report.\n\n") + self.stdout.write("\t" + "\n\t".join(overlooked_reports)) + return False + + def migrate_report_and_filters(self, report_class): + from corehq.apps.reports.generic import get_filter_class + self.stdout.write(self.style.MIGRATE_HEADING(f"\nMigrating {report_class.__name__}...")) + migrated_filters = get_migrated_filters() + migrated_filter_templates = get_migrated_filter_templates() + for field in report_class.fields: + if field not in migrated_filters: + filter_class = get_filter_class(field) + if not self.is_filter_migrated_prompts(field, filter_class, migrated_filter_templates): + return + + confirm_columns = get_confirmation( + f"Did you pass the value for use_bootstrap5 from the {report_class.__name__} to all " + f"`DataTablesColumn` instances related to that report?" + ) + if not confirm_columns: + self.stdout.write(self.style.ERROR( + f"Cannot mark {report_class.__name__} as complete until `DataTablesColumn` " + f"instances are updated." + )) + return + + confirm_sorting = get_confirmation( + f"Did you check to see if {report_class.__name__} sorting works as expected?" + ) + if not confirm_sorting: + self.stdout.write(self.style.ERROR( + f"Cannot mark {report_class.__name__} as complete until sorting is verified." + )) + return + + self.stdout.write( + self.style.SUCCESS(f"All done! {report_class.__name__} has been migrated to Bootstrap5!") + ) + mark_report_as_complete(report_class.__name__) + self.suggest_commit_message(f"Completed report: {report_class.__name__}.") + + def is_filter_migrated_prompts(self, field, filter_class, migrated_filter_templates): + if filter_class is None: + self.stdout.write(self.style.ERROR( + f"The filter {field} could not be found. Check report for errors.\n\n" + f"Did this field not show up? Check feature flags for report.\n" + )) + return False + self.stdout.write(self.style.MIGRATE_LABEL(f"\nChecking report filter: {filter_class.__name__}")) + confirm = get_confirmation( + "Did you test the filter to make sure it loads on the page without error and modifies " + "the report as expected?", default='y' + ) + if not confirm: + self.stdout.write(self.style.ERROR( + f"The filter {field} is not fully migrated yet." + )) + return False + mark_filter_as_complete(field) + + template = get_bootstrap5_path(filter_class.template) + if template not in migrated_filter_templates: + confirm = get_confirmation( + f"Did you migrate its template ({template})?", default='y' + ) + if not confirm: + self.stdout.write(self.style.ERROR( + f"The filter {field} template {template} is not fully migrated yet." + )) + return False + migrated_filter_templates.append(template) + mark_filter_template_as_complete(template) + return True + + def suggest_commit_message(self, message, show_apply_commit=False): + self.stdout.write("\nNow would be a good time to review changes with git and commit.") + if show_apply_commit: + confirm = get_confirmation("\nAutomatically commit these changes?", default='y') + if confirm: + apply_commit(message) + return + commit_string = get_commit_string(message) + self.stdout.write("\n\nSuggested command:\n") + self.stdout.write(self.style.MIGRATE_HEADING(commit_string)) + self.stdout.write("\n") diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/datatables_config.js.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/datatables_config.js.diff.txt index 4c3deaa70407..183af099775d 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/datatables_config.js.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/datatables_config.js.diff.txt @@ -1,6 +1,6 @@ --- +++ -@@ -1,340 +1,339 @@ +@@ -1,340 +1,308 @@ -hqDefine("reports/js/bootstrap3/datatables_config", [ - 'jquery', - 'underscore', @@ -33,35 +33,29 @@ - var loadingTemplate = _.template($(self.loadingTemplateSelector).html() || self.loadingText); - self.loadingText = loadingTemplate({}); +import $ from 'jquery'; ++ ++import 'datatables.net/js/jquery.dataTables'; ++import 'datatables.net-fixedcolumns/js/dataTables.fixedColumns'; ++import 'datatables.net-fixedcolumns-bs5/js/fixedColumns.bootstrap5'; ++ +import _ from 'underscore'; +import googleAnalytics from 'analytix/js/google'; +import {Tooltip} from 'bootstrap5'; + -+import 'datatables.bootstrap'; -+import 'datatables.fixedColumns'; -+ + +var HQReportDataTables = function (options) { + var self = {}; -+ self.dataTableElem = options.dataTableElem || '.datatable'; -+ self.paginationType = options.paginationType || 'bs_normal'; ++ self.dataTableElem = options.dataTableElem || '.table-hq-report'; + self.forcePageSize = options.forcePageSize || false; + self.defaultRows = options.defaultRows || 10; -+ self.startAtRowNum = options.startAtRowNum || 0; + self.showAllRowsOption = options.showAllRowsOption || false; -+ self.aoColumns = options.aoColumns; ++ self.columns = options.aoColumns; // todo eventually rename aoColumns outside of file + self.autoWidth = (options.autoWidth !== undefined) ? options.autoWidth : true; + self.defaultSort = (options.defaultSort !== undefined) ? options.defaultSort : true; + self.customSort = options.customSort || null; + self.ajaxParams = options.ajaxParams || {}; + self.ajaxSource = options.ajaxSource; -+ self.ajaxMethod = options.ajaxMethod || 'GET'; -+ self.loadingText = options.loadingText || " " + gettext("Loading"); -+ self.loadingTemplateSelector = options.loadingTemplateSelector; -+ if (self.loadingTemplateSelector !== undefined) { -+ var loadingTemplate = _.template($(self.loadingTemplateSelector).html() || self.loadingText); -+ self.loadingText = loadingTemplate({}); -+ } ++ self.loadingText = options.loadingText || gettext("Loading"); + self.emptyText = options.emptyText || gettext("No data available to display. " + + "Please try changing your filters."); + self.errorText = options.errorText || "" + gettext("Sorry!") + " " + @@ -73,7 +67,7 @@ + self.fixColsNumLeft = options.fixColsNumLeft || 1; + self.fixColsWidth = options.fixColsWidth || 100; + self.show_pagination = (options.show_pagination === undefined) ? true : options.bPaginate; -+ self.aaSorting = options.aaSorting || null; ++ self.order = options.aaSorting || null; // todo eventually rename aaSorting outside of file + // a list of functions to call back to after ajax. + // see user configurable charts for an example usage + self.successCallbacks = options.successCallbacks; @@ -393,129 +387,125 @@ - return 1; + applyBootstrapMagic(); + -+ var dataTablesDom = "frt<'row dataTables_control'<'col-sm-5'il><'col-sm-7 text-right'p>>"; ++ var dataTablesDom = "frt<'d-flex mb-1'<'p-2 ps-3'i><'p-2 ps-0'l><'ms-auto p-2 pe-3'p>>"; + $(self.dataTableElem).each(function () { -+ var params = { -+ sDom: dataTablesDom, -+ bPaginate: self.show_pagination, -+ sPaginationType: self.paginationType, -+ iDisplayLength: self.defaultRows, -+ bAutoWidth: self.autoWidth, -+ sScrollX: "100%", -+ bSort: self.defaultSort, -+ bFilter: self.includeFilter, ++ const params = { ++ dom: dataTablesDom, ++ filter: false, ++ paging: self.show_pagination, ++ pageLength: self.defaultRows, ++ autoWidth: self.autoWidth, ++ scrollX: "100%", ++ ordering: self.defaultSort, ++ searching: self.includeFilter, + }; -+ if (self.aaSorting !== null || self.customSort !== null) { -+ params.aaSorting = self.aaSorting || self.customSort; ++ if (self.order !== null || self.customSort !== null) { ++ params.order = self.order || self.customSort; + } + + if (self.ajaxSource) { -+ params.bServerSide = true; -+ params.bProcessing = true; -+ params.sAjaxSource = { ++ params.serverSide = true; ++ params.processing = true; ++ params.ajax = { + url: self.ajaxSource, -+ method: self.ajaxMethod, ++ method: 'POST', ++ data: function (data) { ++ // modify the query sent to server to include HQ Filters ++ self.addHqFiltersToServerSideQuery(data); ++ }, ++ error: function (jqXHR, statusText, errorThrown) { ++ $(".dataTables_processing").hide(); ++ if (jqXHR.status === 400) { ++ let errorMessage = self.badRequestErrorText; ++ if (jqXHR.responseText) { ++ errorMessage = "
" + gettext("Sorry!") + " " + jqXHR.responseText + "
"; ++ } ++ $(".dataTables_empty").html(errorMessage); ++ } else { ++ $(".dataTables_empty").html(self.errorText); ++ } ++ $(".dataTables_empty").show(); ++ if (self.errorCallbacks) { ++ for (let i = 0; i < self.errorCallbacks.length; i++) { ++ self.errorCallbacks[i](jqXHR, statusText, errorThrown); ++ } ++ } ++ }, + }; -+ params.bFilter = $(this).data('filter') || false; -+ self.fmtParams = function (defParams) { -+ var ajaxParams = $.isFunction(self.ajaxParams) ? self.ajaxParams() : self.ajaxParams; -+ for (var p in ajaxParams) { ++ params.searching = $(this).data('filter') || false; ++ self.addHqFiltersToServerSideQuery = function (data) { ++ let ajaxParams = $.isFunction(self.ajaxParams) ? self.ajaxParams() : self.ajaxParams; ++ data.hq = {}; ++ for (let p in ajaxParams) { + if (_.has(ajaxParams, p)) { -+ var currentParam = ajaxParams[p]; -+ if (_.isObject(currentParam.value)) { -+ for (var j = 0; j < currentParam.value.length; j++) { -+ defParams.push({ -+ name: currentParam.name, -+ value: currentParam.value[j], -+ }); -+ } -+ } else { -+ defParams.push(currentParam); -+ } ++ let param = ajaxParams[p]; ++ data.hq[param.name] = _.isArray(param.value) ? _.uniq(param.value) : param.value; + } + } -+ return defParams; ++ return data; + }; -+ params.fnServerData = function (sSource, aoData, fnCallback, oSettings) { -+ var customCallback = function (data) { -+ if (data.warning) { -+ throw new Error(data.warning); -+ } -+ var result = fnCallback(data); // this must be called first because datatables clears the tfoot of the table -+ var i; -+ if ('total_row' in data) { -+ self.render_footer_row('ajax_total_row', data['total_row']); -+ } -+ if ('statistics_rows' in data) { -+ for (i = 0; i < data.statistics_rows.length; i++) { -+ self.render_footer_row('ajax_stat_row-' + i, data.statistics_rows[i]); -+ } ++ ++ params.footerCallback = function (row, data, start, end, display) { ++ if ('total_row' in data) { ++ self.render_footer_row('ajax_total_row', data['total_row']); ++ } ++ if ('statistics_rows' in data) { ++ for (let i = 0; i < data.statistics_rows.length; i++) { ++ self.render_footer_row('ajax_stat_row-' + i, data.statistics_rows[i]); + } -+ applyBootstrapMagic(); -+ if (self.successCallbacks) { -+ for (i = 0; i < self.successCallbacks.length; i++) { -+ self.successCallbacks[i](data); -+ } ++ } ++ }; ++ ++ params.drawCallback = function () { ++ let api = this.api(), ++ data = api.ajax.json(); ++ ++ if (data.warning) { ++ throw new Error(data.warning); ++ } ++ applyBootstrapMagic(); ++ if ('context' in data) { ++ let iconPath = data['icon_path'] || $(".base-maps-data").data("icon_path"); ++ hqRequire(["reports/js/bootstrap5/maps_utils"], function (mapsUtils) { ++ mapsUtils.load(data['context'], iconPath); ++ }); ++ } ++ if (self.successCallbacks) { ++ for (let i = 0; i < self.successCallbacks.length; i++) { ++ self.successCallbacks[i](data); + } -+ return result; -+ }; -+ oSettings.jqXHR = $.ajax({ -+ "url": sSource.url, -+ "method": sSource.method, -+ "data": self.fmtParams(aoData), -+ "success": customCallback, -+ "error": function (jqXHR, textStatus, errorThrown) { -+ $(".dataTables_processing").hide(); -+ if (jqXHR.status === 400) { -+ var errorMessage = self.badRequestErrorText; -+ if (jqXHR.responseText) { -+ errorMessage = "" + gettext("Sorry!") + " " + jqXHR.responseText + "
"; -+ } -+ $(".dataTables_empty").html(errorMessage); -+ } else { -+ $(".dataTables_empty").html(self.errorText); -+ } -+ $(".dataTables_empty").show(); -+ if (self.errorCallbacks) { -+ for (var i = 0; i < self.errorCallbacks.length; i++) { -+ self.errorCallbacks[i](jqXHR, textStatus, errorThrown); -+ } -+ } -+ }, -+ }); ++ } + }; + } -+ params.oLanguage = { -+ sProcessing: self.loadingText, -+ sLoadingRecords: self.loadingText, -+ sZeroRecords: self.emptyText, -+ }; -+ -+ params.fnDrawCallback = function (a,b,c) { -+ /* be able to set fnDrawCallback from outside here later */ -+ if (self.fnDrawCallback) { -+ self.fnDrawCallback(a,b,c); -+ } ++ params.language = { ++ lengthMenu: gettext("_MENU_ per page"), ++ processing: self.loadingText, ++ loadingRecords: self.loadingText, ++ zeroRecords: self.emptyText, + }; + -+ if (self.aoColumns) { -+ params.aoColumns = self.aoColumns; ++ if (self.columns) { ++ params.columns = self.columns; + } + + if (self.forcePageSize) { + // limit the page size option to just the default size + params.lengthMenu = [self.defaultRows]; + } -+ var datatable = $(this).dataTable(params); -+ if (!self.datatable) { -+ self.datatable = datatable; -+ } + + if (self.fixColumns) { + new $.fn.dataTable.FixedColumns(datatable, { + iLeftColumns: self.fixColsNumLeft, + iLeftWidth: self.fixColsWidth, + }); ++ params.fixedColumns = { ++ left: self.fixColsNumLeft, ++ width: self.fixColsWidth, ++ }; ++ } ++ let datatable = $(this).dataTable(params); ++ if (!self.datatable) { ++ self.datatable = datatable; + } + + // This fixes a display bug in some browsers where the pagination @@ -552,19 +542,12 @@ + $inputLabel.html($('').addClass("icon-search")); + } + -+ var $dataTablesLength = $(self.dataTableElem).parents('.dataTables_wrapper').find(".dataTables_length"), -+ $dataTablesInfo = $(self.dataTableElem).parents('.dataTables_wrapper').find(".dataTables_info"); -+ if ($dataTablesLength && $dataTablesInfo) { -+ var $selectField = $dataTablesLength.find("select"), -+ $selectLabel = $dataTablesLength.find("label"); -+ -+ $dataTablesLength.append($selectField); -+ $selectLabel.remove(); -+ $selectField.children().append(" per page"); ++ let $dataTablesLength = $(self.dataTableElem).parents('.dataTables_wrapper').find(".dataTables_length"); ++ if ($dataTablesLength) { ++ let $selectField = $dataTablesLength.find("select"); + if (self.showAllRowsOption) { + $selectField.append($('').text(gettext("All Rows"))); + } -+ $selectField.addClass('form-control'); + $selectField.on("change", function () { + var selectedValue = $selectField.find('option:selected').val(); + googleAnalytics.track.event("Reports", "Changed number of items shown", selectedValue); @@ -579,12 +562,6 @@ + return self; +}; + -+$.extend($.fn.dataTableExt.oStdClasses, { -+ "sSortAsc": "header headerSortAsc", -+ "sSortDesc": "header headerSortDesc", -+ "sSortable": "header headerSort", -+}); -+ +// For sorting rows + +function sortSpecial(a, b, asc, convert) { @@ -662,14 +639,6 @@ + return new Date(m[1]); +} + -+$.fn.dataTableExt.oSort['title-numeric-asc'] = function (a, b) { return sortSpecial(a, b, true, convertNum); }; -+ -+$.fn.dataTableExt.oSort['title-numeric-desc'] = function (a, b) { return sortSpecial(a, b, false, convertNum); }; -+ -+$.fn.dataTableExt.oSort['title-date-asc'] = function (a,b) { return sortSpecial(a, b, true, convertDate); }; -+ -+$.fn.dataTableExt.oSort['title-date-desc'] = function (a,b) { return sortSpecial(a, b, false, convertDate); }; -+ +export default { + HQReportDataTables: function (options) { + return new HQReportDataTables(options); diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/tabular.js.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/tabular.js.diff.txt index 1c116e8c4af7..97946369930a 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/tabular.js.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/javascript/reports/js/tabular.js.diff.txt @@ -1,6 +1,6 @@ --- +++ -@@ -1,105 +1,107 @@ +@@ -1,105 +1,106 @@ -hqDefine("reports/js/bootstrap3/tabular", [ - 'jquery', - 'underscore', @@ -101,7 +101,6 @@ + defaultRows: tableConfig.default_rows, + startAtRowNum: tableConfig.start_at_row, + showAllRowsOption: tableConfig.show_all_rows, -+ loadingTemplateSelector: '#js-template-loading-report', + autoWidth: tableConfig.headers.auto_width, + }; + if (!tableConfig.sortable) { diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/async/basic.html.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/async/basic.html.diff.txt new file mode 100644 index 000000000000..b225ad584407 --- /dev/null +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/async/basic.html.diff.txt @@ -0,0 +1,8 @@ +--- ++++ +@@ -1,4 +1,4 @@ +-{% extends report_base|default:"reports/async/bootstrap3/default.html" %} ++{% extends report_base|default:"reports/async/bootstrap5/default.html" %} + {% load hq_shared_tags %} + + {% block reportcontent %} diff --git a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/datatables/column.html.diff.txt b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/datatables/column.html.diff.txt index 9b861562dcd8..f246fc32bd6e 100644 --- a/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/datatables/column.html.diff.txt +++ b/corehq/apps/hqwebapp/tests/data/bootstrap5_diffs/reports/datatables/column.html.diff.txt @@ -1,16 +1,56 @@ --- +++ -@@ -1,11 +1,11 @@ -" + gettext("Sorry!") + " " + jqXHR.responseText + "
"; + } + $(".dataTables_empty").html(errorMessage); + } else { + $(".dataTables_empty").html(self.errorText); + } + $(".dataTables_empty").show(); + if (self.errorCallbacks) { + for (let i = 0; i < self.errorCallbacks.length; i++) { + self.errorCallbacks[i](jqXHR, statusText, errorThrown); + } + } + }, }; - params.bFilter = $(this).data('filter') || false; - self.fmtParams = function (defParams) { - var ajaxParams = $.isFunction(self.ajaxParams) ? self.ajaxParams() : self.ajaxParams; - for (var p in ajaxParams) { + params.searching = $(this).data('filter') || false; + self.addHqFiltersToServerSideQuery = function (data) { + let ajaxParams = $.isFunction(self.ajaxParams) ? self.ajaxParams() : self.ajaxParams; + data.hq = {}; + for (let p in ajaxParams) { if (_.has(ajaxParams, p)) { - var currentParam = ajaxParams[p]; - if (_.isObject(currentParam.value)) { - for (var j = 0; j < currentParam.value.length; j++) { - defParams.push({ - name: currentParam.name, - value: currentParam.value[j], - }); - } - } else { - defParams.push(currentParam); - } + let param = ajaxParams[p]; + data.hq[param.name] = _.isArray(param.value) ? _.uniq(param.value) : param.value; } } - return defParams; + return data; }; - params.fnServerData = function (sSource, aoData, fnCallback, oSettings) { - var customCallback = function (data) { - if (data.warning) { - throw new Error(data.warning); - } - var result = fnCallback(data); // this must be called first because datatables clears the tfoot of the table - var i; - if ('total_row' in data) { - self.render_footer_row('ajax_total_row', data['total_row']); - } - if ('statistics_rows' in data) { - for (i = 0; i < data.statistics_rows.length; i++) { - self.render_footer_row('ajax_stat_row-' + i, data.statistics_rows[i]); - } + + params.footerCallback = function (row, data, start, end, display) { + if ('total_row' in data) { + self.render_footer_row('ajax_total_row', data['total_row']); + } + if ('statistics_rows' in data) { + for (let i = 0; i < data.statistics_rows.length; i++) { + self.render_footer_row('ajax_stat_row-' + i, data.statistics_rows[i]); } - applyBootstrapMagic(); - if (self.successCallbacks) { - for (i = 0; i < self.successCallbacks.length; i++) { - self.successCallbacks[i](data); - } + } + }; + + params.drawCallback = function () { + let api = this.api(), + data = api.ajax.json(); + + if (data.warning) { + throw new Error(data.warning); + } + applyBootstrapMagic(); + if ('context' in data) { + let iconPath = data['icon_path'] || $(".base-maps-data").data("icon_path"); + hqRequire(["reports/js/bootstrap5/maps_utils"], function (mapsUtils) { + mapsUtils.load(data['context'], iconPath); + }); + } + if (self.successCallbacks) { + for (let i = 0; i < self.successCallbacks.length; i++) { + self.successCallbacks[i](data); } - return result; - }; - oSettings.jqXHR = $.ajax({ - "url": sSource.url, - "method": sSource.method, - "data": self.fmtParams(aoData), - "success": customCallback, - "error": function (jqXHR, textStatus, errorThrown) { - $(".dataTables_processing").hide(); - if (jqXHR.status === 400) { - var errorMessage = self.badRequestErrorText; - if (jqXHR.responseText) { - errorMessage = "" + gettext("Sorry!") + " " + jqXHR.responseText + "
"; - } - $(".dataTables_empty").html(errorMessage); - } else { - $(".dataTables_empty").html(self.errorText); - } - $(".dataTables_empty").show(); - if (self.errorCallbacks) { - for (var i = 0; i < self.errorCallbacks.length; i++) { - self.errorCallbacks[i](jqXHR, textStatus, errorThrown); - } - } - }, - }); + } }; } - params.oLanguage = { - sProcessing: self.loadingText, - sLoadingRecords: self.loadingText, - sZeroRecords: self.emptyText, - }; - - params.fnDrawCallback = function (a,b,c) { - /* be able to set fnDrawCallback from outside here later */ - if (self.fnDrawCallback) { - self.fnDrawCallback(a,b,c); - } + params.language = { + lengthMenu: gettext("_MENU_ per page"), + processing: self.loadingText, + loadingRecords: self.loadingText, + zeroRecords: self.emptyText, }; - if (self.aoColumns) { - params.aoColumns = self.aoColumns; + if (self.columns) { + params.columns = self.columns; } if (self.forcePageSize) { // limit the page size option to just the default size params.lengthMenu = [self.defaultRows]; } - var datatable = $(this).dataTable(params); - if (!self.datatable) { - self.datatable = datatable; - } if (self.fixColumns) { new $.fn.dataTable.FixedColumns(datatable, { iLeftColumns: self.fixColsNumLeft, iLeftWidth: self.fixColsWidth, }); + params.fixedColumns = { + left: self.fixColsNumLeft, + width: self.fixColsWidth, + }; + } + let datatable = $(this).dataTable(params); + if (!self.datatable) { + self.datatable = datatable; } // This fixes a display bug in some browsers where the pagination @@ -252,19 +242,12 @@ var HQReportDataTables = function (options) { $inputLabel.html($('').addClass("icon-search")); } - var $dataTablesLength = $(self.dataTableElem).parents('.dataTables_wrapper').find(".dataTables_length"), - $dataTablesInfo = $(self.dataTableElem).parents('.dataTables_wrapper').find(".dataTables_info"); - if ($dataTablesLength && $dataTablesInfo) { - var $selectField = $dataTablesLength.find("select"), - $selectLabel = $dataTablesLength.find("label"); - - $dataTablesLength.append($selectField); - $selectLabel.remove(); - $selectField.children().append(" per page"); + let $dataTablesLength = $(self.dataTableElem).parents('.dataTables_wrapper').find(".dataTables_length"); + if ($dataTablesLength) { + let $selectField = $dataTablesLength.find("select"); if (self.showAllRowsOption) { $selectField.append($('').text(gettext("All Rows"))); } - $selectField.addClass('form-control'); $selectField.on("change", function () { var selectedValue = $selectField.find('option:selected').val(); googleAnalytics.track.event("Reports", "Changed number of items shown", selectedValue); @@ -279,12 +262,6 @@ var HQReportDataTables = function (options) { return self; }; -$.extend($.fn.dataTableExt.oStdClasses, { - "sSortAsc": "header headerSortAsc", - "sSortDesc": "header headerSortDesc", - "sSortable": "header headerSort", -}); - // For sorting rows function sortSpecial(a, b, asc, convert) { @@ -324,14 +301,6 @@ function convertDate(k) { return new Date(m[1]); } -$.fn.dataTableExt.oSort['title-numeric-asc'] = function (a, b) { return sortSpecial(a, b, true, convertNum); }; - -$.fn.dataTableExt.oSort['title-numeric-desc'] = function (a, b) { return sortSpecial(a, b, false, convertNum); }; - -$.fn.dataTableExt.oSort['title-date-asc'] = function (a,b) { return sortSpecial(a, b, true, convertDate); }; - -$.fn.dataTableExt.oSort['title-date-desc'] = function (a,b) { return sortSpecial(a, b, false, convertDate); }; - export default { HQReportDataTables: function (options) { return new HQReportDataTables(options); diff --git a/corehq/apps/reports/static/reports/js/bootstrap5/tabular.js b/corehq/apps/reports/static/reports/js/bootstrap5/tabular.js index 85daf77160df..d8867b07d55f 100644 --- a/corehq/apps/reports/static/reports/js/bootstrap5/tabular.js +++ b/corehq/apps/reports/static/reports/js/bootstrap5/tabular.js @@ -29,7 +29,6 @@ function renderPage(slug, tableOptions) { defaultRows: tableConfig.default_rows, startAtRowNum: tableConfig.start_at_row, showAllRowsOption: tableConfig.show_all_rows, - loadingTemplateSelector: '#js-template-loading-report', autoWidth: tableConfig.headers.auto_width, }; if (!tableConfig.sortable) { diff --git a/corehq/apps/reports/templates/reports/async/basic.html b/corehq/apps/reports/templates/reports/async/bootstrap3/basic.html similarity index 100% rename from corehq/apps/reports/templates/reports/async/basic.html rename to corehq/apps/reports/templates/reports/async/bootstrap3/basic.html diff --git a/corehq/apps/reports/templates/reports/async/bootstrap5/basic.html b/corehq/apps/reports/templates/reports/async/bootstrap5/basic.html new file mode 100644 index 000000000000..0ef37fa197b9 --- /dev/null +++ b/corehq/apps/reports/templates/reports/async/bootstrap5/basic.html @@ -0,0 +1,6 @@ +{% extends report_base|default:"reports/async/bootstrap5/default.html" %} +{% load hq_shared_tags %} + +{% block reportcontent %} + {% if report_partial %}{% include report_partial %}{% endif %} +{% endblock %} diff --git a/corehq/apps/reports/templates/reports/bootstrap5/tabular.html b/corehq/apps/reports/templates/reports/bootstrap5/tabular.html index 3288d16d304d..f8542d368c28 100644 --- a/corehq/apps/reports/templates/reports/bootstrap5/tabular.html +++ b/corehq/apps/reports/templates/reports/bootstrap5/tabular.html @@ -129,14 +129,6 @@
The migration process begins by either applying @use_bootstrap5
decorator the view function or
@method_decorator(use_bootstrap5, name='dispatch')
to the view class. Also update the template's
@@ -575,6 +580,93 @@
+ If you are migrating a Report, you may realize there is no dispatch
method to apply
+ the @use_bootstrap5
decorator to. That's because reports are rendered by a
+ ReportDispatcher
.
+
+ Migrating a Report View will be different from other Views: +
+use_bootstrap5
class variable to True
+
+ For example, if we want to
+ migrate the SubmitHistory
report, we would do the following:
+
class SubmitHistory(...): + ... + use_bootstrap5 = True + ...+
class SubmitHistory(...): + ... + debug_bootstrap5 = True + ...+
+ You can then view the output in the console for tips on what templates and filters to migrate. + Important: please don't commit this line at the end! +
+ ++ Check to see that your report doesn't have custom templates that override base report templates. + If it does, it should be flagged by the debug tool from step 2—so you will know! +
+
+ If a custom template is set, and this is the only report using that template, you can then follow the procedure
+ above to un-split template files. Prior to doing this, make sure to
+ replace the bootstrap3
path of this template with the bootstrap5
path.
+ You can then update the template's HTML as needed.
+
COMMON_REPORT_TEMPLATES
list in corehq.apps.hqwebapp.utils.bootstrap.reports.debug
+ after migrating it. Other reports will then automatically select the bootstrap5
version
+ of this template when use_bootstrap5
is set to True
.
+ + The report debugging tool should provide a list of filters that haven't been migrated. + You should take the following steps: +
++ Once you are done migrating the report, you must use the following migration + tool to mark the report and its filters as complete: +
+ +./manage.py complete_bootstrap5_report <ReportClassName>+
Once these changes are complete, the @use_bootstrap5
decorator can be removed from all the views.
+ Additionally, the use_bootstrap5
class variable can be removed from all the reports. Cleanup
+ of the bootstrap3
templates listed in COMMON_REPORT_TEMPLATES
in
+ corehq.apps.hqwebapp.utils.bootstrap.reports.debug
can also be completed, as well as the
+ filter templates marked completed by manage.py complete_bootstrap5_report
.
+
Then, build_requirejs
can be updated to only build bootstrap5
version of files, with
the split requirejs_config.js
files no longer needed, and the
diff --git a/corehq/apps/userreports/reports/view.py b/corehq/apps/userreports/reports/view.py
index a10eaf0d5589..b0ad1a9dba3f 100644
--- a/corehq/apps/userreports/reports/view.py
+++ b/corehq/apps/userreports/reports/view.py
@@ -39,7 +39,7 @@
from corehq.apps.locations.permissions import conditionally_location_safe
from corehq.apps.reports.datatables import DataTablesHeader
from corehq.apps.reports.dispatcher import ReportDispatcher
-from corehq.apps.reports.util import DatatablesParams
+from corehq.apps.reports.util import DatatablesPagination
from corehq.apps.reports_core.exceptions import FilterException
from corehq.apps.reports_core.filters import Choice
from corehq.apps.saved_reports.models import ReportConfig
@@ -433,7 +433,8 @@ def get_ajax(self, params):
sort_column = params.get('iSortCol_0')
sort_order = params.get('sSortDir_0', 'ASC')
echo = int(params.get('sEcho', 1))
- datatables_params = DatatablesParams.from_request_dict(params)
+ # todo update this for Bootstrap 5:
+ datatables_params = DatatablesPagination.from_request_dict(params)
try:
data_source = self.data_source
diff --git a/settings.py b/settings.py
index b4b72a073db8..3f6196ce64c8 100755
--- a/settings.py
+++ b/settings.py
@@ -451,7 +451,7 @@
# restyle some templates
BASE_TEMPLATE = "hqwebapp/bootstrap3/base_navigation.html"
-BASE_ASYNC_TEMPLATE = "reports/async/basic.html"
+BASE_ASYNC_TEMPLATE = "reports/async/bootstrap3/basic.html"
LOGIN_TEMPLATE = "login_and_password/bootstrap3/login.html"
LOGGEDOUT_TEMPLATE = LOGIN_TEMPLATE
diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js
index 07aa8f1a792f..1c35b8fee247 100644
--- a/webpack/webpack.common.js
+++ b/webpack/webpack.common.js
@@ -7,8 +7,6 @@ const hqPlugins = require('./plugins');
const aliases = {
"commcarehq": path.resolve(utils.getStaticPathForApp('hqwebapp', 'js/bootstrap5/'),
'commcarehq'),
- "datatables.bootstrap": "datatables.net-bs5",
- "datatables.fixedColumns": "datatables.net-fixedcolumns/js/dataTables.fixedColumns.min",
"jquery": require.resolve('jquery'),
"langcodes/js/langcodes": path.resolve("submodules/langcodes/static/langcodes/js/langcodes"),