From d40a1694102f295928edeb3bf168a7cb0d99a79a Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Fri, 15 Mar 2024 16:40:53 +0100 Subject: [PATCH 01/38] fix: allow page length 2500 (#25062) (cherry picked from commit 21cc09e28a7f0d10b30eeeadd49774e9f900cd14) --- frappe/public/js/frappe/list/base_list.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index a4265ff4589..0458d1c0b5f 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -353,7 +353,7 @@ frappe.views.BaseList = class BaseList { } setup_paging_area() { - const paging_values = [20, 100, 500]; + const paging_values = [20, 100, 500, 2500]; this.$paging_area = $( `
From 9f14d65731e57fb07fde557f73d7c70ba3cac3c2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:54:03 +0200 Subject: [PATCH 02/38] feat: CSV params for report view --- frappe/desk/reportview.py | 8 ++++-- .../js/frappe/views/reports/report_view.js | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index bb89a0ed800..2491dab0989 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -14,7 +14,7 @@ from frappe.model.base_document import get_controller from frappe.model.db_query import DatabaseQuery from frappe.model.utils import is_virtual_doctype -from frappe.utils import add_user_info, cint, format_duration +from frappe.utils import add_user_info, cint, cstr, format_duration from frappe.utils.data import sbool @@ -357,7 +357,11 @@ def export_query(): add_totals_row = None file_format_type = form_params["file_format_type"] title = title or doctype + csv_delimiter = cstr(form_params.get("csv_delimiter", ",")) + csv_quoting = cint(form_params.get("csv_quoting", 2)) + del form_params["csv_delimiter"] + del form_params["csv_quoting"] del form_params["doctype"] del form_params["file_format_type"] @@ -398,7 +402,7 @@ def export_query(): from frappe.utils.xlsxutils import handle_html f = StringIO() - writer = csv.writer(f) + writer = csv.writer(f, quoting=csv_quoting, delimiter=csv_delimiter) for r in data: # encode only unicode type strings and not int, floats etc. writer.writerow([handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r]) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index f19237a5386..f02d30287ed 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1559,6 +1559,27 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { options: ["Excel", "CSV"], default: "Excel", }, + { + fieldtype: "Data", + label: __("Delimiter"), + fieldname: "csv_delimiter", + default: ",", + length: 1, + depends_on: "eval:doc.file_format_type=='CSV'", + }, + { + fieldtype: "Select", + label: __("Quoting"), + fieldname: "csv_quoting", + options: [ + { value: 0, label: "Minimal" }, + { value: 1, label: "All" }, + { value: 2, label: "Non-numeric" }, + { value: 3, label: "None" }, + ], + default: 2, + depends_on: "eval:doc.file_format_type=='CSV'", + }, ]; if (this.total_count > this.count_without_children || args.page_length) { @@ -1578,6 +1599,11 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { args.file_format_type = data.file_format_type; args.title = this.report_name || this.doctype; + if (data.file_format_type == "CSV") { + args.csv_delimiter = data.csv_delimiter; + args.csv_quoting = data.csv_quoting; + } + if (this.add_totals_row) { args.add_totals_row = 1; } From 7493c4e0b86a51f80f1d7d924cc6802ecd683f35 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 28 Oct 2022 19:54:33 +0200 Subject: [PATCH 03/38] feat: CSV params for query report --- frappe/desk/query_report.py | 66 ++++++++++------ .../js/frappe/views/reports/query_report.js | 79 +++++++++++-------- 2 files changed, 85 insertions(+), 60 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 3fa27c7b3ee..9fe706bbea8 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -5,6 +5,7 @@ import json import os from datetime import timedelta +from io import StringIO import frappe import frappe.desk.reportview @@ -318,47 +319,60 @@ def get_report_data(doc, data): @frappe.whitelist() def export_query(): """export from query reports""" - data = frappe._dict(frappe.local.form_dict) - data.pop("cmd", None) - data.pop("csrf_token", None) + form_params = frappe._dict(frappe.local.form_dict) + form_params.pop("cmd", None) + form_params.pop("csrf_token", None) - if isinstance(data.get("filters"), str): - filters = json.loads(data["filters"]) + if isinstance(form_params.get("filters"), str): + filters = json.loads(form_params["filters"]) - if data.get("report_name"): - report_name = data["report_name"] + if form_params.get("report_name"): + report_name = form_params["report_name"] frappe.permissions.can_export( frappe.get_cached_value("Report", report_name, "ref_doctype"), raise_exception=True, ) - file_format_type = data.get("file_format_type") - custom_columns = frappe.parse_json(data.get("custom_columns", "[]")) - include_indentation = data.get("include_indentation") - visible_idx = data.get("visible_idx") + file_format_type = form_params.get("file_format_type") + custom_columns = frappe.parse_json(form_params.get("custom_columns", "[]")) + include_indentation = form_params.get("include_indentation") + visible_idx = form_params.get("visible_idx") + csv_delimiter = cstr(form_params.get("csv_delimiter", ",")) + csv_quoting = cint(form_params.get("csv_quoting", 2)) if isinstance(visible_idx, str): visible_idx = json.loads(visible_idx) - if file_format_type == "Excel": - data = run(report_name, filters, custom_columns=custom_columns, are_default_filters=False) - data = frappe._dict(data) - if not data.columns: - frappe.respond_as_web_page( - _("No data to export"), - _("You can try changing the filters of your report."), - ) - return + data = run(report_name, filters, custom_columns=custom_columns) + data = frappe._dict(data) + if not data.columns: + frappe.respond_as_web_page( + _("No data to export"), + _("You can try changing the filters of your report."), + ) + return + + format_duration_fields(data) + xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) + + if file_format_type == "CSV": + import csv + file = StringIO() + writer = csv.writer(file, quoting=csv_quoting, delimiter=csv_delimiter) + writer.writerows(xlsx_data) + content = file.getvalue().encode("utf-8") + file_extension = "csv" + elif file_format_type == "Excel": from frappe.utils.xlsxutils import make_xlsx - format_duration_fields(data) - xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) - xlsx_file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) + file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) + file_extension = "xlsx" + content = file.getvalue() - frappe.response["filename"] = _(report_name) + ".xlsx" - frappe.response["filecontent"] = xlsx_file.getvalue() - frappe.response["type"] = "binary" + frappe.response["filename"] = f"{report_name}.{file_extension}" + frappe.response["filecontent"] = content + frappe.response["type"] = "binary" def format_duration_fields(data: frappe._dict) -> None: diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index d5ee8c05441..feb62849def 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1481,6 +1481,27 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { default: "Excel", reqd: 1, }, + { + fieldtype: "Data", + label: __("Delimiter"), + fieldname: "csv_delimiter", + default: ",", + length: 1, + depends_on: "eval:doc.file_format=='CSV'", + }, + { + fieldtype: "Select", + label: __("Quoting"), + fieldname: "csv_quoting", + options: [ + { value: 0, label: "Minimal" }, + { value: 1, label: "All" }, + { value: 2, label: "Non-numeric" }, + { value: 3, label: "None" }, + ], + default: 2, + depends_on: "eval:doc.file_format=='CSV'", + }, ]; if (this.tree_report) { @@ -1493,45 +1514,35 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { this.export_dialog = frappe.prompt( export_dialog_fields, - ({ file_format, include_indentation }) => { + ({ file_format, include_indentation, csv_delimiter, csv_quoting }) => { this.make_access_log("Export", file_format); - if (file_format === "CSV") { - const column_row = this.columns.reduce((acc, col) => { - if (!col.hidden) { - acc.push(__(col.label)); - } - return acc; - }, []); - const data = this.get_data_for_csv(include_indentation); - const out = [column_row].concat(data); - frappe.tools.downloadify(out, null, this.report_name); - } else { - let filters = this.get_filter_values(true); - if (frappe.urllib.get_dict("prepared_report_name")) { - filters = Object.assign( - frappe.urllib.get_dict("prepared_report_name"), - filters - ); - } + let filters = this.get_filter_values(true); + if (frappe.urllib.get_dict("prepared_report_name")) { + filters = Object.assign( + frappe.urllib.get_dict("prepared_report_name"), + filters + ); + } - const visible_idx = this.datatable?.bodyRenderer.visibleRowIndices || []; - if (visible_idx.length + 1 === this.data?.length) { - visible_idx.push(visible_idx.length); - } + const visible_idx = this.datatable.bodyRenderer.visibleRowIndices; + if (visible_idx.length + 1 === this.data.length) { + visible_idx.push(visible_idx.length); + } - const args = { - cmd: "frappe.desk.query_report.export_query", - report_name: this.report_name, - custom_columns: this.custom_columns?.length ? this.custom_columns : [], - file_format_type: file_format, - filters: filters, - visible_idx, - include_indentation, - }; + const args = { + cmd: "frappe.desk.query_report.export_query", + report_name: this.report_name, + custom_columns: this.custom_columns.length ? this.custom_columns : [], + file_format_type: file_format, + filters: filters, + visible_idx, + csv_delimiter, + csv_quoting, + include_indentation, + }; - open_url_post(frappe.request.url, args); - } + open_url_post(frappe.request.url, args); }, __("Export Report: {0}", [this.report_name]), __("Download") From f9e08ef86b09c2b6dae08d5cef27dc9ca621c4f7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 29 Oct 2022 17:17:05 +0200 Subject: [PATCH 04/38] refactor: utility function get_export_dialog --- .../js/frappe/views/reports/query_report.js | 63 +++++-------------- .../js/frappe/views/reports/report_utils.js | 44 +++++++++++++ .../js/frappe/views/reports/report_view.js | 62 +++++------------- 3 files changed, 77 insertions(+), 92 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index feb62849def..ff6c0231c42 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1467,53 +1467,20 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return; } - let export_options = ["Excel"]; - if (this.datatable) { - export_options.push("CSV"); - } - - let export_dialog_fields = [ - { - label: __("Select File Format"), - fieldname: "file_format", - fieldtype: "Select", - options: export_options, - default: "Excel", - reqd: 1, - }, - { - fieldtype: "Data", - label: __("Delimiter"), - fieldname: "csv_delimiter", - default: ",", - length: 1, - depends_on: "eval:doc.file_format=='CSV'", - }, - { - fieldtype: "Select", - label: __("Quoting"), - fieldname: "csv_quoting", - options: [ - { value: 0, label: "Minimal" }, - { value: 1, label: "All" }, - { value: 2, label: "Non-numeric" }, - { value: 3, label: "None" }, - ], - default: 2, - depends_on: "eval:doc.file_format=='CSV'", - }, - ]; - + let extra_fields = null; if (this.tree_report) { - export_dialog_fields.push({ - label: __("Include indentation"), - fieldname: "include_indentation", - fieldtype: "Check", - }); + extra_fields = [ + { + label: __("Include indentation"), + fieldname: "include_indentation", + fieldtype: "Check", + }, + ]; } - this.export_dialog = frappe.prompt( - export_dialog_fields, + this.export_dialog = frappe.report_utils.get_export_dialog( + __(this.report_name), + extra_fields, ({ file_format, include_indentation, csv_delimiter, csv_quoting }) => { this.make_access_log("Export", file_format); @@ -1543,10 +1510,12 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }; open_url_post(frappe.request.url, args); - }, - __("Export Report: {0}", [this.report_name]), - __("Download") + + this.export_dialog.hide(); + } ); + + this.export_dialog.show(); } get_data_for_csv(include_indentation) { diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index d40cf07ce6e..3aef05df73d 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -165,4 +165,48 @@ frappe.report_utils = { }; return get_result[fn](values); }, + + get_export_dialog(report_name, extra_fields, callback) { + const fields = [ + { + label: __("Select File Format"), + fieldname: "file_format", + fieldtype: "Select", + options: ["Excel", "CSV"], + default: "Excel", + reqd: 1, + }, + { + fieldtype: "Data", + label: __("Delimiter"), + fieldname: "csv_delimiter", + default: ",", + length: 1, + depends_on: "eval:doc.file_format=='CSV'", + }, + { + fieldtype: "Select", + label: __("Quoting"), + fieldname: "csv_quoting", + options: [ + { value: 0, label: "Minimal" }, + { value: 1, label: "All" }, + { value: 2, label: "Non-numeric" }, + { value: 3, label: "None" }, + ], + default: 2, + depends_on: "eval:doc.file_format=='CSV'", + }, + ]; + if (extra_fields) { + fields.push(...extra_fields); + } + + return new frappe.ui.Dialog({ + title: __("Export Report: {0}", [report_name]), + fields: fields, + primary_action_label: __("Download"), + primary_action: callback, + }); + }, }; diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index f02d30287ed..dd1b9b00740 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1551,55 +1551,27 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { action: () => { const args = this.get_args(); const selected_items = this.get_checked_items(true); - let fields = [ - { - fieldtype: "Select", - label: __("Select File Type"), - fieldname: "file_format_type", - options: ["Excel", "CSV"], - default: "Excel", - }, - { - fieldtype: "Data", - label: __("Delimiter"), - fieldname: "csv_delimiter", - default: ",", - length: 1, - depends_on: "eval:doc.file_format_type=='CSV'", - }, - { - fieldtype: "Select", - label: __("Quoting"), - fieldname: "csv_quoting", - options: [ - { value: 0, label: "Minimal" }, - { value: 1, label: "All" }, - { value: 2, label: "Non-numeric" }, - { value: 3, label: "None" }, - ], - default: 2, - depends_on: "eval:doc.file_format_type=='CSV'", - }, - ]; - if (this.total_count > this.count_without_children || args.page_length) { - fields.push({ - fieldtype: "Check", - fieldname: "export_all_rows", - label: __("Export All {0} rows?", [(this.total_count + "").bold()]), - }); + let extra_fields = null; + if (this.total_count > (this.count_without_children || args.page_length)) { + extra_fields = [ + { + fieldtype: "Check", + fieldname: "export_all_rows", + label: __("Export All {0} rows?", [`${this.total_count}`]), + }, + ]; } - const d = new frappe.ui.Dialog({ - title: __("Export Report: {0}", [__(this.doctype)]), - fields: fields, - primary_action_label: __("Download"), - primary_action: (data) => { + const d = frappe.report_utils.get_export_dialog( + __(this.doctype), + extra_fields, + (data) => { args.cmd = "frappe.desk.reportview.export_query"; - args.file_format_type = data.file_format_type; + args.file_format_type = data.file_format; args.title = this.report_name || this.doctype; - if (data.file_format_type == "CSV") { + if (data.file_format == "CSV") { args.csv_delimiter = data.csv_delimiter; args.csv_quoting = data.csv_quoting; } @@ -1623,8 +1595,8 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { open_url_post(frappe.request.url, args); d.hide(); - }, - }); + } + ); d.show(); }, From 87c6fc615291fe2cf57f8189fdb681e835c6b088 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 29 Oct 2022 18:05:52 +0200 Subject: [PATCH 05/38] fix: don't parse CSV params for Excel --- frappe/desk/reportview.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 2491dab0989..f393006d60f 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -357,11 +357,12 @@ def export_query(): add_totals_row = None file_format_type = form_params["file_format_type"] title = title or doctype - csv_delimiter = cstr(form_params.get("csv_delimiter", ",")) - csv_quoting = cint(form_params.get("csv_quoting", 2)) + if file_format_type == "CSV": + csv_delimiter = cstr(form_params.get("csv_delimiter", ",")) + csv_quoting = cint(form_params.get("csv_quoting", 2)) + del form_params["csv_delimiter"] + del form_params["csv_quoting"] - del form_params["csv_delimiter"] - del form_params["csv_quoting"] del form_params["doctype"] del form_params["file_format_type"] From 18562cef7dca442fb219a03a9c1ca68bf1743581 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 29 Oct 2022 18:07:51 +0200 Subject: [PATCH 06/38] refactor: reuse functions from reportview --- frappe/desk/query_report.py | 39 +++++++++++++++++++------------------ 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 9fe706bbea8..f2e2ae3df9b 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -11,6 +11,7 @@ import frappe.desk.reportview from frappe import _ from frappe.core.utils import ljust_list +from frappe.desk.reportview import clean_params, parse_json from frappe.model.utils import render_include from frappe.modules import get_module_path, scrub from frappe.monitor import add_data_to_monitor @@ -320,30 +321,24 @@ def get_report_data(doc, data): def export_query(): """export from query reports""" form_params = frappe._dict(frappe.local.form_dict) - form_params.pop("cmd", None) - form_params.pop("csrf_token", None) + clean_params(form_params) + parse_json(form_params) - if isinstance(form_params.get("filters"), str): - filters = json.loads(form_params["filters"]) - - if form_params.get("report_name"): - report_name = form_params["report_name"] - frappe.permissions.can_export( - frappe.get_cached_value("Report", report_name, "ref_doctype"), - raise_exception=True, - ) + report_name = form_params.report_name + frappe.permissions.can_export( + frappe.get_cached_value("Report", report_name, "ref_doctype"), + raise_exception=True, + ) - file_format_type = form_params.get("file_format_type") - custom_columns = frappe.parse_json(form_params.get("custom_columns", "[]")) - include_indentation = form_params.get("include_indentation") - visible_idx = form_params.get("visible_idx") - csv_delimiter = cstr(form_params.get("csv_delimiter", ",")) - csv_quoting = cint(form_params.get("csv_quoting", 2)) + file_format_type = form_params.file_format_type + custom_columns = frappe.parse_json(form_params.custom_columns or "[]") + include_indentation = form_params.include_indentation + visible_idx = form_params.visible_idx if isinstance(visible_idx, str): visible_idx = json.loads(visible_idx) - data = run(report_name, filters, custom_columns=custom_columns) + data = run(report_name, form_params.filters, custom_columns=custom_columns) data = frappe._dict(data) if not data.columns: frappe.respond_as_web_page( @@ -359,7 +354,13 @@ def export_query(): import csv file = StringIO() - writer = csv.writer(file, quoting=csv_quoting, delimiter=csv_delimiter) + writer = csv.writer( + file, + quoting=csv.QUOTE_NONNUMERIC + if form_params.csv_quoting is None + else cint(form_params.csv_quoting), + delimiter=cstr(form_params.csv_delimiter or ","), + ) writer.writerows(xlsx_data) content = file.getvalue().encode("utf-8") file_extension = "csv" From 5308728f3a73b631d5cc0d41150fc199ba4d23f3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 3 Nov 2022 12:02:16 +0100 Subject: [PATCH 07/38] test: export query report as CSV --- frappe/tests/test_query_report.py | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 6f77a3380c9..9b8f89157bf 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -188,3 +188,42 @@ def test_report_for_duplicate_column_names(self): except Exception as e: raise e frappe.db.rollback() + + def test_csv(self): + from csv import QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC, DictReader + from io import StringIO + + REPORT_NAME = "Test CSV Report" + REF_DOCTYPE = "DocType" + REPORT_COLUMNS = ["name", "module", "issingle"] + + if not frappe.db.exists("Report", REPORT_NAME): + report = frappe.new_doc("Report") + report.report_name = REPORT_NAME + report.ref_doctype = "User" + report.report_type = "Query Report" + report.query = frappe.qb.from_(REF_DOCTYPE).select(*REPORT_COLUMNS).limit(10).get_sql() + report.is_standard = "No" + report.save() + + for delimiter in (",", ";", "\t", "|"): + for quoting in (QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC): + frappe.local.form_dict = { + "report_name": REPORT_NAME, + "file_format_type": "CSV", + "csv_quoting": quoting, + "csv_delimiter": delimiter, + "include_indentation": 0, + "visible_idx": [0, 1, 2], + } + export_query() + + self.assertTrue(frappe.response["filename"].endswith(".csv")) + self.assertEqual(frappe.response["type"], "binary") + with StringIO(frappe.response["filecontent"].decode("utf-8")) as result: + reader = DictReader(result, delimiter=delimiter, quoting=quoting) + row = reader.__next__() + for column in REPORT_COLUMNS: + self.assertIn(column, row) + + frappe.delete_doc("Report", REPORT_NAME, delete_permanently=True) From 88cfe4bb8e7c7adafcbc4131c879ab73d90ea5b7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 15 Nov 2022 00:06:08 +0100 Subject: [PATCH 08/38] feat: add translation context --- .../js/frappe/views/reports/report_utils.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 3aef05df73d..68f3786c8b5 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -169,7 +169,7 @@ frappe.report_utils = { get_export_dialog(report_name, extra_fields, callback) { const fields = [ { - label: __("Select File Format"), + label: __("Select File Format", null, "Export report"), fieldname: "file_format", fieldtype: "Select", options: ["Excel", "CSV"], @@ -178,7 +178,7 @@ frappe.report_utils = { }, { fieldtype: "Data", - label: __("Delimiter"), + label: __("CSV Delimiter", null, "Export report"), fieldname: "csv_delimiter", default: ",", length: 1, @@ -186,13 +186,13 @@ frappe.report_utils = { }, { fieldtype: "Select", - label: __("Quoting"), + label: __("CSV Quoting", null, "Export report"), fieldname: "csv_quoting", options: [ - { value: 0, label: "Minimal" }, - { value: 1, label: "All" }, - { value: 2, label: "Non-numeric" }, - { value: 3, label: "None" }, + { value: 0, label: __("Minimal", null, "Export report") }, + { value: 1, label: __("All", null, "Export report") }, + { value: 2, label: __("Non-numeric", null, "Export report") }, + { value: 3, label: __("None", null, "Export report") }, ], default: 2, depends_on: "eval:doc.file_format=='CSV'", @@ -203,9 +203,9 @@ frappe.report_utils = { } return new frappe.ui.Dialog({ - title: __("Export Report: {0}", [report_name]), + title: __("Export Report: {0}", [report_name], "Export report"), fields: fields, - primary_action_label: __("Download"), + primary_action_label: __("Download", null, "Export report"), primary_action: callback, }); }, From d89a12681868f697e43f54adfa2162abc3b0dcc2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 15 Nov 2022 00:07:19 +0100 Subject: [PATCH 09/38] feat: add csv preview --- .../js/frappe/views/reports/report_utils.js | 83 ++++++++++++++++++- 1 file changed, 82 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 68f3786c8b5..ab82bad3ed7 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -197,16 +197,97 @@ frappe.report_utils = { default: 2, depends_on: "eval:doc.file_format=='CSV'", }, + { + fieldtype: "Small Text", + label: __("CSV Preview", null, "Export report"), + fieldname: "csv_preview", + read_only: 1, + depends_on: "eval:doc.file_format=='CSV'", + }, ]; + if (extra_fields) { fields.push(...extra_fields); } - return new frappe.ui.Dialog({ + const dialog = new frappe.ui.Dialog({ title: __("Export Report: {0}", [report_name], "Export report"), fields: fields, primary_action_label: __("Download", null, "Export report"), primary_action: callback, }); + + function update_csv_preview(dialog) { + PREVIEW_DATA = [ + [ + __("Text", null, "Export report"), + __("Number", null, "Export report"), + __("Float", null, "Export report"), + __("Check", null, "Export report"), + ], + [__("Hello, World", null, "Export report"), 42, 3.14, 0], + [__("Hello World", null, "Export report"), 0, 99.99, 1], + ]; + dialog.set_value( + "csv_preview", + frappe.report_utils.get_csv_preview( + PREVIEW_DATA, + dialog.get_value("csv_quoting"), + dialog.get_value("csv_delimiter") + ) + ); + } + + dialog.fields_dict["file_format"].df.onchange = () => update_csv_preview(dialog); + dialog.fields_dict["csv_quoting"].df.onchange = () => update_csv_preview(dialog); + dialog.fields_dict["csv_delimiter"].df.onchange = () => update_csv_preview(dialog); + + return dialog; + }, + + get_csv_preview(data, quoting, delimiter) { + // data: array of arrays + // quoting: 0 - minimal, 1 - all, 2 - non-numeric, 3 - none + // delimiter: any single character + quoting = cint(quoting); + const QUOTING = { + Minimal: 0, + All: 1, + NonNumeric: 2, + None: 3, + }; + + if (delimiter.length > 1) { + frappe.throw(__("Delimiter must be a single character")); + } + + if (0 > quoting || quoting > 3) { + frappe.throw(__("Quoting must be between 0 and 3")); + } + + return data + .map((row) => { + return row + .map((col) => { + if (typeof col == "string" && col.includes('"')) { + col = col.replace(/"/g, '""'); + } + + switch (quoting) { + case QUOTING.Minimal: + return typeof col === "string" && col.includes(delimiter) + ? `"${col}"` + : `${col}`; + case QUOTING.All: + return `"${col}"`; + case QUOTING.NonNumeric: + return isNaN(col) ? `"${col}"` : `${col}`; + case QUOTING.None: + return `${col}`; + } + }) + .join(delimiter); + }) + .join("\n"); }, }; From 91b20b8f345b0a2f8373ebdc621b1f64db3ad06d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 15 Nov 2022 00:48:44 +0100 Subject: [PATCH 10/38] feat: preview real data --- .../js/frappe/views/reports/report_utils.js | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index ab82bad3ed7..9f874b1b857 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -218,16 +218,18 @@ frappe.report_utils = { }); function update_csv_preview(dialog) { + const is_query_report = frappe.get_route()[0] === "query-report"; + const report = is_query_report ? frappe.query_report : cur_list; + const columns = report.columns.filter((col) => col.hidden !== 1); PREVIEW_DATA = [ - [ - __("Text", null, "Export report"), - __("Number", null, "Export report"), - __("Float", null, "Export report"), - __("Check", null, "Export report"), - ], - [__("Hello, World", null, "Export report"), 42, 3.14, 0], - [__("Hello World", null, "Export report"), 0, 99.99, 1], + columns.map((col) => __(is_query_report ? col.label : col.name)), + ...report.data + .slice(0, 3) + .map((row) => + columns.map((col) => row[is_query_report ? col.fieldname : col.field]) + ), ]; + dialog.set_value( "csv_preview", frappe.report_utils.get_csv_preview( From 628ed7ae979ac494020eba9f8708e77f1c88ddc7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:00:58 +0100 Subject: [PATCH 11/38] refactor: extract common methods --- frappe/desk/query_report.py | 24 +++++--------------- frappe/desk/reportview.py | 44 +++++++++++++------------------------ frappe/desk/utils.py | 31 ++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 47 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index f2e2ae3df9b..afe70ed2d6a 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -5,7 +5,6 @@ import json import os from datetime import timedelta -from io import StringIO import frappe import frappe.desk.reportview @@ -320,7 +319,10 @@ def get_report_data(doc, data): @frappe.whitelist() def export_query(): """export from query reports""" + from frappe.desk.utils import get_csv_bytes, pop_csv_params, provide_binary_file + form_params = frappe._dict(frappe.local.form_dict) + csv_params = pop_csv_params(form_params) clean_params(form_params) parse_json(form_params) @@ -351,29 +353,15 @@ def export_query(): xlsx_data, column_widths = build_xlsx_data(data, visible_idx, include_indentation) if file_format_type == "CSV": - import csv - - file = StringIO() - writer = csv.writer( - file, - quoting=csv.QUOTE_NONNUMERIC - if form_params.csv_quoting is None - else cint(form_params.csv_quoting), - delimiter=cstr(form_params.csv_delimiter or ","), - ) - writer.writerows(xlsx_data) - content = file.getvalue().encode("utf-8") + content = get_csv_bytes(xlsx_data, csv_params) file_extension = "csv" elif file_format_type == "Excel": from frappe.utils.xlsxutils import make_xlsx - file = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths) file_extension = "xlsx" - content = file.getvalue() + content = make_xlsx(xlsx_data, "Query Report", column_widths=column_widths).getvalue() - frappe.response["filename"] = f"{report_name}.{file_extension}" - frappe.response["filecontent"] = content - frappe.response["type"] = "binary" + provide_binary_file(report_name, file_extension, content) def format_duration_fields(data: frappe._dict) -> None: diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index f393006d60f..78b3ec2fd6f 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -4,7 +4,6 @@ """build query for doclistview and return results""" import json -from io import StringIO import frappe import frappe.permissions @@ -14,7 +13,7 @@ from frappe.model.base_document import get_controller from frappe.model.db_query import DatabaseQuery from frappe.model.utils import is_virtual_doctype -from frappe.utils import add_user_info, cint, cstr, format_duration +from frappe.utils import add_user_info, cint, format_duration from frappe.utils.data import sbool @@ -347,6 +346,8 @@ def delete_report(name): @frappe.read_only() def export_query(): """export from report builder""" + from frappe.desk.utils import get_csv_bytes, pop_csv_params, provide_binary_file + title = frappe.form_dict.title frappe.form_dict.pop("title", None) @@ -357,11 +358,7 @@ def export_query(): add_totals_row = None file_format_type = form_params["file_format_type"] title = title or doctype - if file_format_type == "CSV": - csv_delimiter = cstr(form_params.get("csv_delimiter", ",")) - csv_quoting = cint(form_params.get("csv_quoting", 2)) - del form_params["csv_delimiter"] - del form_params["csv_quoting"] + csv_params = pop_csv_params(form_params) del form_params["doctype"] del form_params["file_format_type"] @@ -390,37 +387,26 @@ def export_query(): if add_totals_row: ret = append_totals_row(ret) - data = [[_("Sr"), *get_labels(db_query.fields, doctype)]] - for i, row in enumerate(ret): - data.append([i + 1, *list(row)]) - + data = [[_("Sr")] + get_labels(db_query.fields, doctype)].extend( + [i + 1] + list(row) for i, row in enumerate(ret) + ) data = handle_duration_fieldtype_values(doctype, data, db_query.fields) if file_format_type == "CSV": - # convert to csv - import csv - from frappe.utils.xlsxutils import handle_html - f = StringIO() - writer = csv.writer(f, quoting=csv_quoting, delimiter=csv_delimiter) - for r in data: - # encode only unicode type strings and not int, floats etc. - writer.writerow([handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r]) - - f.seek(0) - frappe.response["result"] = cstr(f.read()) - frappe.response["type"] = "csv" - frappe.response["doctype"] = title - + file_extension = "csv" + content = get_csv_bytes( + [[handle_html(frappe.as_unicode(v)) if isinstance(v, str) else v for v in r] for r in data], + csv_params, + ) elif file_format_type == "Excel": from frappe.utils.xlsxutils import make_xlsx - xlsx_file = make_xlsx(data, doctype) + file_extension = "xlsx" + content = make_xlsx(data, doctype).getvalue() - frappe.response["filename"] = _(title) + ".xlsx" - frappe.response["filecontent"] = xlsx_file.getvalue() - frappe.response["type"] = "binary" + provide_binary_file(title, file_extension, content) def append_totals_row(data): diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py index a42fe25b910..77edf88d7a4 100644 --- a/frappe/desk/utils.py +++ b/frappe/desk/utils.py @@ -25,3 +25,34 @@ def validate_route_conflict(doctype, name): def slug(name): return name.lower().replace(" ", "-") + + +def pop_csv_params(form_dict): + """Pop csv params from form_dict and return them as a dict.""" + from csv import QUOTE_NONNUMERIC + + from frappe.utils.data import cint, cstr + + return { + "delimiter": cstr(form_dict.pop("csv_delimiter", ","))[0], + "quoting": cint(form_dict.pop("csv_quoting", QUOTE_NONNUMERIC)), + } + + +def get_csv_bytes(data: list[list], csv_params: dict) -> bytes: + """Convert data to csv bytes.""" + from csv import writer + from io import StringIO + + file = StringIO() + csv_writer = writer(file, **csv_params) + csv_writer.writerows(data) + + return file.getvalue().encode("utf-8") + + +def provide_binary_file(filename: str, extension: str, content: bytes) -> None: + """Provide a binary file to the client.""" + frappe.response["type"] = "binary" + frappe.response["filecontent"] = content + frappe.response["filename"] = f"{filename}.{extension}" From 4eb31e023ae4497078fa6e2e8980aee345f2bb13 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:01:48 +0100 Subject: [PATCH 12/38] feat: add test for exporting reportview as CSV --- frappe/tests/test_reportview.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 frappe/tests/test_reportview.py diff --git a/frappe/tests/test_reportview.py b/frappe/tests/test_reportview.py new file mode 100644 index 00000000000..ba3657aacd3 --- /dev/null +++ b/frappe/tests/test_reportview.py @@ -0,0 +1,34 @@ +# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + +import frappe +from frappe.desk.reportview import export_query +from frappe.tests.utils import FrappeTestCase + + +class TestReportview(FrappeTestCase): + def test_csv(self): + from csv import QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC, DictReader + from io import StringIO + + frappe.local.form_dict = frappe._dict( + doctype="DocType", + title="Test Report", + file_format_type="CSV", + ) + + for delimiter in (",", ";", "\t", "|"): + for quoting in (QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC): + frappe.local.form_dict.update( + { + "csv_quoting": quoting, + "csv_delimiter": delimiter, + } + ) + export_query() + + self.assertTrue(frappe.response["filename"].endswith(".csv")) + self.assertEqual(frappe.response["type"], "binary") + with StringIO(frappe.response["filecontent"].decode("utf-8")) as result: + reader = DictReader(result, delimiter=delimiter, quoting=quoting) + reader.__next__() From 09ea753754a4fee6274a4463837bf8c6c1aca323 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 26 Nov 2022 20:08:50 +0100 Subject: [PATCH 13/38] refactor: use provide_binary_file --- frappe/core/doctype/data_export/exporter.py | 11 ++++------- frappe/core/doctype/data_import/exporter.py | 12 ++---------- frappe/utils/xlsxutils.py | 8 +++----- 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index 6561f12f56b..d6d990d2348 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -427,23 +427,20 @@ def add_data_row(self, rows, dt, parentfield, doc, rowidx): row[_column_start_end.start + i + 1] = value def build_response_as_excel(self): + from frappe.desk.utils import provide_binary_file + from frappe.utils.xlsxutils import make_xlsx + filename = frappe.generate_hash(length=10) with open(filename, "wb") as f: f.write(cstr(self.writer.getvalue()).encode("utf-8")) f = open(filename) reader = csv.reader(f) - - from frappe.utils.xlsxutils import make_xlsx - xlsx_file = make_xlsx(reader, "Data Import Template" if self.template else "Data Export") f.close() os.remove(filename) - # write out response as a xlsx type - frappe.response["filename"] = _(self.doctype) + ".xlsx" - frappe.response["filecontent"] = xlsx_file.getvalue() - frappe.response["type"] = "binary" + provide_binary_file(self.doctype, "xlsx", xlsx_file.getvalue()) def _append_name_column(self, dt=None): self.append_field_column( diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index b3a79e46761..a7dcd1acd71 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -1,8 +1,6 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import typing - import frappe from frappe import _ from frappe.model import display_fieldtypes, no_value_fields @@ -243,15 +241,9 @@ def get_csv_array_for_export(self): def build_response(self): if self.file_type == "CSV": - self.build_csv_response() + build_csv_response(self.get_csv_array_for_export(), _(self.doctype)) elif self.file_type == "Excel": - self.build_xlsx_response() - - def build_csv_response(self): - build_csv_response(self.get_csv_array_for_export(), _(self.doctype)) - - def build_xlsx_response(self): - build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) + build_xlsx_response(self.get_csv_array_for_export(), _(self.doctype)) def group_children_data_by_parent(self, children_data: dict[str, list]): return groupby_metric(children_data, key="parent") diff --git a/frappe/utils/xlsxutils.py b/frappe/utils/xlsxutils.py index 506183cf942..94764e7e284 100644 --- a/frappe/utils/xlsxutils.py +++ b/frappe/utils/xlsxutils.py @@ -109,8 +109,6 @@ def read_xls_file_from_attached_file(content): def build_xlsx_response(data, filename): - xlsx_file = make_xlsx(data, filename) - # write out response as a xlsx type - frappe.response["filename"] = _(filename) + ".xlsx" - frappe.response["filecontent"] = xlsx_file.getvalue() - frappe.response["type"] = "binary" + from frappe.desk.utils import provide_binary_file + + provide_binary_file(filename, "xlsx", make_xlsx(data, filename).getvalue()) From efb70bee88fe0c442cb53b045c67395897d45aa0 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:02:25 +0100 Subject: [PATCH 14/38] feat: hide csv settings in collapsible section --- .../js/frappe/views/reports/report_utils.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 9f874b1b857..71f7bd663d5 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -176,6 +176,13 @@ frappe.report_utils = { default: "Excel", reqd: 1, }, + { + fieldtype: "Section Break", + fieldname: "csv_settings", + label: __("Settings", null, "Export report"), + collapsible: 1, + depends_on: "eval:doc.file_format=='CSV'", + }, { fieldtype: "Data", label: __("CSV Delimiter", null, "Export report"), @@ -207,7 +214,14 @@ frappe.report_utils = { ]; if (extra_fields) { - fields.push(...extra_fields); + fields.push( + { + fieldtype: "Section Break", + fieldname: "extra_fields", + collapsible: 0, + }, + ...extra_fields + ); } const dialog = new frappe.ui.Dialog({ From 471ad2649dbe02fc7aa6080beb708cfd0f81ecaa Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:10:53 +0100 Subject: [PATCH 15/38] fix: duplicate translation of field labels --- .../js/frappe/views/reports/report_utils.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 71f7bd663d5..16b02c28a10 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -169,7 +169,7 @@ frappe.report_utils = { get_export_dialog(report_name, extra_fields, callback) { const fields = [ { - label: __("Select File Format", null, "Export report"), + label: "File Format", fieldname: "file_format", fieldtype: "Select", options: ["Excel", "CSV"], @@ -179,13 +179,13 @@ frappe.report_utils = { { fieldtype: "Section Break", fieldname: "csv_settings", - label: __("Settings", null, "Export report"), + label: "Settings", collapsible: 1, depends_on: "eval:doc.file_format=='CSV'", }, { fieldtype: "Data", - label: __("CSV Delimiter", null, "Export report"), + label: "CSV Delimiter", fieldname: "csv_delimiter", default: ",", length: 1, @@ -193,20 +193,20 @@ frappe.report_utils = { }, { fieldtype: "Select", - label: __("CSV Quoting", null, "Export report"), + label: "CSV Quoting", fieldname: "csv_quoting", options: [ - { value: 0, label: __("Minimal", null, "Export report") }, - { value: 1, label: __("All", null, "Export report") }, - { value: 2, label: __("Non-numeric", null, "Export report") }, - { value: 3, label: __("None", null, "Export report") }, + { value: 0, label: "Minimal" }, + { value: 1, label: "All" }, + { value: 2, label: "Non-numeric" }, + { value: 3, label: "None" }, ], default: 2, depends_on: "eval:doc.file_format=='CSV'", }, { fieldtype: "Small Text", - label: __("CSV Preview", null, "Export report"), + label: "CSV Preview", fieldname: "csv_preview", read_only: 1, depends_on: "eval:doc.file_format=='CSV'", From eab535d9ded5e47cfa4942010f12ffb8245e994b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 27 Nov 2022 17:13:22 +0100 Subject: [PATCH 16/38] feat: german translations for export dialog --- frappe/translations/de.csv | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 54da9a309d2..06fa4b45df5 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -158,7 +158,7 @@ No Data,Keine Daten, No address added yet.,Noch keine Adresse hinzugefügt., No contacts added yet.,Noch keine Kontakte hinzugefügt., No items found.,Keine Elemente gefunden., -None,Keiner, +None,Keine, Not Permitted,Nicht zulässig, Not active,Nicht aktiv, Notes,Hinweise, @@ -2220,7 +2220,6 @@ Select Columns,Spalten auswählen, Select Document Type,Dokumenttyp auswählen, Select Document Type or Role to start.,"Dokumententyp oder Rolle auswählen, um zu beginnen.", Select Document Types to set which User Permissions are used to limit access.,"Dokumentenarten auswählen, um die Benutzerrechte, die den Zugriff einschränken, anzuwenden", -Select File Format,Wählen Sie Dateiformat, Select File Type,Dateityp auswählen, Select Language...,Sprache auswählen..., Select Languages,Sprachenauswahl, @@ -4870,3 +4869,9 @@ Filters:,Filter:, {0} is not like {1},{0} ist nicht wie {1}, {0} is set,{0} ist eingetragen, {0} is not set,{0} ist nicht eingetragen, +File Format,Dateiformat, +CSV Delimiter,Trennzeichen, +CSV Quoting,Anführungszeichen, +CSV Preview,Vorschau, +Non-numeric,Nicht-numerische, +Minimal,Minimal, From 95a0db82c3e8a987d0ccc42edca359c189ebf76f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:15:30 +0100 Subject: [PATCH 17/38] fix: fieldname referenced before assignment --- frappe/desk/reportview.py | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 78b3ec2fd6f..edadaa2edf8 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -433,16 +433,12 @@ def get_labels(fields, doctype): """get column labels based on column names""" labels = [] for key in fields: - key = key.split(" as ")[0] - - if key.startswith(("count(", "sum(", "avg(")): + try: + parenttype, fieldname = parse_field(key) + except ValueError: continue - if "." in key: - parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") - else: - parenttype = doctype - fieldname = fieldname.strip("`") + parenttype = parenttype or doctype if parenttype == doctype and fieldname == "name": label = _("ID", context="Label of name column in report") @@ -461,17 +457,12 @@ def get_labels(fields, doctype): def handle_duration_fieldtype_values(doctype, data, fields): for field in fields: - key = field.split(" as ")[0] - - if key.startswith(("count(", "sum(", "avg(")): + try: + parenttype, fieldname = parse_field(field) + except ValueError: continue - if "." in key: - parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") - else: - parenttype = doctype - fieldname = field.strip("`") - + parenttype = parenttype or doctype df = frappe.get_meta(parenttype).get_field(fieldname) if df and df.fieldtype == "Duration": @@ -484,6 +475,19 @@ def handle_duration_fieldtype_values(doctype, data, fields): return data +def parse_field(field: str) -> tuple[str | None, str]: + """Parse a field into parenttype and fieldname.""" + key = field.split(" as ")[0] + + if key.startswith(("count(", "sum(", "avg(")): + raise ValueError + + if "." in key: + return key.split(".")[0][4:-1], key.split(".")[1].strip("`") + + return None, key.strip("`") + + @frappe.whitelist() def delete_items(): """delete selected items""" From 889dcfcbfa53a756c3378977be98f094d068075f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:15:57 +0100 Subject: [PATCH 18/38] refactor: export_query --- frappe/desk/reportview.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index edadaa2edf8..91af77d0360 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -348,31 +348,19 @@ def export_query(): """export from report builder""" from frappe.desk.utils import get_csv_bytes, pop_csv_params, provide_binary_file - title = frappe.form_dict.title - frappe.form_dict.pop("title", None) - form_params = get_form_params() form_params["limit_page_length"] = None form_params["as_list"] = True - doctype = form_params.doctype - add_totals_row = None - file_format_type = form_params["file_format_type"] - title = title or doctype + doctype = form_params.pop("doctype") + file_format_type = form_params.pop("file_format_type") + title = frappe.form_dict.pop("title", doctype) csv_params = pop_csv_params(form_params) - - del form_params["doctype"] - del form_params["file_format_type"] - - if "add_totals_row" in form_params and form_params["add_totals_row"] == "1": - add_totals_row = 1 - del form_params["add_totals_row"] + add_totals_row = 1 if form_params.pop("add_totals_row", None) == "1" else None frappe.permissions.can_export(doctype, raise_exception=True) - if "selected_items" in form_params: - si = json.loads(frappe.form_dict.get("selected_items")) - form_params["filters"] = {"name": ("in", si)} - del form_params["selected_items"] + if selection := form_params.pop("selected_items", None): + form_params["filters"] = {"name": ("in", json.loads(selection))} make_access_log( doctype=doctype, @@ -387,9 +375,8 @@ def export_query(): if add_totals_row: ret = append_totals_row(ret) - data = [[_("Sr")] + get_labels(db_query.fields, doctype)].extend( - [i + 1] + list(row) for i, row in enumerate(ret) - ) + data = [[_("Sr")] + get_labels(db_query.fields, doctype)] + data.extend([i + 1] + list(row) for i, row in enumerate(ret)) data = handle_duration_fieldtype_values(doctype, data, db_query.fields) if file_format_type == "CSV": From bb5c3a1ec13e77adf0a23d5d1194bf3ce80a1336 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:16:18 +0100 Subject: [PATCH 19/38] fix: test reportview --- frappe/tests/test_reportview.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/tests/test_reportview.py b/frappe/tests/test_reportview.py index ba3657aacd3..4a24514acf8 100644 --- a/frappe/tests/test_reportview.py +++ b/frappe/tests/test_reportview.py @@ -13,22 +13,22 @@ def test_csv(self): frappe.local.form_dict = frappe._dict( doctype="DocType", - title="Test Report", file_format_type="CSV", + fields=("name", "module", "issingle"), + filters={"issingle": 1, "module": "Core"}, ) for delimiter in (",", ";", "\t", "|"): + frappe.local.form_dict.csv_delimiter = delimiter for quoting in (QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC): - frappe.local.form_dict.update( - { - "csv_quoting": quoting, - "csv_delimiter": delimiter, - } - ) + frappe.local.form_dict.csv_quoting = quoting + export_query() self.assertTrue(frappe.response["filename"].endswith(".csv")) self.assertEqual(frappe.response["type"], "binary") with StringIO(frappe.response["filecontent"].decode("utf-8")) as result: reader = DictReader(result, delimiter=delimiter, quoting=quoting) - reader.__next__() + for row in reader: + self.assertEqual(int(row["Is Single"]), 1) + self.assertEqual(row["Module"], "Core") From 377b35caeb323d3cb64cbbb1803bf17fb12d7f64 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 28 Nov 2022 12:19:14 +0100 Subject: [PATCH 20/38] fix: pop from form_params --- frappe/desk/reportview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 91af77d0360..574df3b19f0 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -353,7 +353,7 @@ def export_query(): form_params["as_list"] = True doctype = form_params.pop("doctype") file_format_type = form_params.pop("file_format_type") - title = frappe.form_dict.pop("title", doctype) + title = form_params.pop("title", doctype) csv_params = pop_csv_params(form_params) add_totals_row = 1 if form_params.pop("add_totals_row", None) == "1" else None From 9916427dc12f83dba8ce86a22eda10da656be5ae Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 29 Nov 2022 11:53:52 +0530 Subject: [PATCH 21/38] test: use _dict in tests form_dict is usually _dict, otherwise some unrelated tests will fail. --- frappe/tests/test_query_report.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 9b8f89157bf..1c02b9da8b3 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -208,14 +208,16 @@ def test_csv(self): for delimiter in (",", ";", "\t", "|"): for quoting in (QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC): - frappe.local.form_dict = { - "report_name": REPORT_NAME, - "file_format_type": "CSV", - "csv_quoting": quoting, - "csv_delimiter": delimiter, - "include_indentation": 0, - "visible_idx": [0, 1, 2], - } + frappe.local.form_dict = frappe._dict( + { + "report_name": REPORT_NAME, + "file_format_type": "CSV", + "csv_quoting": quoting, + "csv_delimiter": delimiter, + "include_indentation": 0, + "visible_idx": [0, 1, 2], + } + ) export_query() self.assertTrue(frappe.response["filename"].endswith(".csv")) From 8ecc7d96b83a8c6b8356ed190cee657d519e67e2 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 20 Mar 2024 17:15:27 +0530 Subject: [PATCH 22/38] fix: ruff fixes Signed-off-by: Akhil Narang --- frappe/desk/reportview.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 574df3b19f0..f2676ee5443 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -375,8 +375,8 @@ def export_query(): if add_totals_row: ret = append_totals_row(ret) - data = [[_("Sr")] + get_labels(db_query.fields, doctype)] - data.extend([i + 1] + list(row) for i, row in enumerate(ret)) + data = [[_("Sr"), *get_labels(db_query.fields, doctype)]] + data.extend([i + 1, *list(row)] for i, row in enumerate(ret)) data = handle_duration_fieldtype_values(doctype, data, db_query.fields) if file_format_type == "CSV": From a9ee773fbc88917cad1877261fbdb8ab26ed2b78 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:08:27 +0530 Subject: [PATCH 23/38] feat: connect to redis sentinel for redis queue (backport #25506) (#25556) * feat: connect to redis sentinel for redis queue (cherry picked from commit e9ece3b2835a847a6057b5be37ae9acd08ece372) # Conflicts: # frappe/utils/redis_queue.py * refactor: resolve conflict --------- Co-authored-by: Revant Nandgaonkar --- frappe/utils/redis_queue.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/frappe/utils/redis_queue.py b/frappe/utils/redis_queue.py index 31b8ebc39db..791ff12a9b0 100644 --- a/frappe/utils/redis_queue.py +++ b/frappe/utils/redis_queue.py @@ -17,6 +17,21 @@ def add_user(self, username, password=None): @classmethod def get_connection(cls, username=None, password=None): + if frappe.conf.redis_queue_sentinel_enabled: + from frappe.utils.redis_wrapper import get_sentinel_connection + + sentinels = [tuple(node.split(":")) for node in frappe.conf.get("redis_queue_sentinels", [])] + sentinel = get_sentinel_connection( + sentinels=sentinels, + sentinel_username=frappe.conf.get("redis_queue_sentinel_username"), + sentinel_password=frappe.conf.get("redis_queue_sentinel_password"), + master_username=frappe.conf.get("redis_queue_master_username", username), + master_password=frappe.conf.get("redis_queue_master_password", password), + ) + conn = sentinel.master_for(frappe.conf.get("redis_queue_master_service")) + conn.ping() + return conn + rq_url = frappe.local.conf.redis_queue domain = rq_url.split("redis://", 1)[-1] url = (username and f"redis://{username}:{password or ''}@{domain}") or rq_url From e343a32e375df1ab747866e75ab0319393c40170 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Thu, 21 Mar 2024 16:39:53 +0530 Subject: [PATCH 24/38] fix(lint): v14 doesn't have typed doctype classes, so this was never needed (#25578) This just prevented us from discovering actual errors Signed-off-by: Akhil Narang --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 5b2fc6f3bc4..0251efb1fc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -129,7 +129,6 @@ ignore = [ "F403", # can't detect undefined names from * import "F405", # can't detect undefined names from * import "F722", # syntax error in forward type annotation - "F821", # undefined name "W191", # indentation contains tabs "RUF001", # string contains ambiguous unicode character ] From 4474e053543e3c0ab65fbe470a23d07ef206d28c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 18:59:52 +0530 Subject: [PATCH 25/38] chore: add methodname to monitor log (#19388) (#25580) (cherry picked from commit 296ef1f9bd1b452a41aa064ba24e3c5256ea8546) Co-authored-by: Saqib Ansari --- frappe/handler.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/handler.py b/frappe/handler.py index c2f1342ed3b..2b0175ca0d6 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -12,6 +12,7 @@ import frappe.utils from frappe import _, is_whitelisted from frappe.core.doctype.server_script.server_script_utils import get_server_script_map +from frappe.monitor import add_data_to_monitor from frappe.utils import cint from frappe.utils.csvutils import build_csv_response from frappe.utils.image import optimize_image @@ -332,6 +333,8 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): frappe.response["message"] = response + add_data_to_monitor(methodname=method) + # for backwards compatibility runserverobj = run_doc_method From 68d6947d7ecaee846c3686a4763c5e0ec6c4b0ed Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 19:35:12 +0530 Subject: [PATCH 26/38] fix: diff after converting to html to text (#25582) (#25583) (cherry picked from commit a9ebf58bd922fabe91be38284ba19b595c81d9aa) Co-authored-by: Ankush Menat --- .../js/frappe/form/footer/version_timeline_content_builder.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js index f69ddaae60c..33c87e458af 100644 --- a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js +++ b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js @@ -224,6 +224,7 @@ function format_content_for_timeline(content) { // limits content to 40 characters // escapes HTML // and makes it bold + content = frappe.utils.html2text(content); content = frappe.ellipsis(content, 40) || '""'; content = frappe.utils.escape_html(content); return content.bold(); From 5e65cc9202c970802b642bde01c3aba169165b87 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 11:58:32 +0530 Subject: [PATCH 27/38] perf: remove useless sorting on docstatus (#25571) (#25590) (cherry picked from commit a12fc118f4b4ad85e28e7c1a866614d0e50f2c89) Co-authored-by: Ankush Menat --- frappe/model/db_query.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 173314c2d6f..3febf625898 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -1079,11 +1079,6 @@ def set_order_by(self, args): f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}" ) - # draft docs always on top - if hasattr(self.doctype_meta, "is_submittable") and self.doctype_meta.is_submittable: - if self.order_by: - args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}" - def validate_order_by_and_group_by(self, parameters: str): """Check order by, group by so that atleast one column is selected and does not have subquery""" if not parameters: From d201326b19f98b94502e0efd5ba512f5ccb5b604 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 22 Mar 2024 08:34:40 +0000 Subject: [PATCH 28/38] fix: DB Query distinct handling with full table name (#25594) (#25596) (cherry picked from commit 4fe04982bb9bc51147c3bfb239a3d0f8177702ec) Co-authored-by: Ankush Menat --- frappe/model/db_query.py | 2 +- frappe/tests/test_db_query.py | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 3febf625898..43fdee26c9c 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -644,7 +644,7 @@ def apply_fieldlevel_read_permissions(self): # field: 'distinct name' # column: 'name' else: - column = field.split(" ", 2)[1].replace("`", "") + column = field.split(" ", 1)[1].replace("`", "") else: # field: 'count(`tabPhoto`.name) as total_count' # column: 'tabPhoto.name' diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index af2093a11ae..2335fc28f88 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -1220,6 +1220,20 @@ def test_get_count(self): self.assertIsInstance(count, int) self.assertLessEqual(count, limit) + # doctype with space in name + limit = 2 + frappe.local.form_dict = frappe._dict( + { + "doctype": "Role Profile", + "fields": [], + "distinct": "true", + "limit": limit, + } + ) + count = execute_cmd("frappe.desk.reportview.get_count") + self.assertIsInstance(count, int) + self.assertLessEqual(count, limit) + def test_reportview_get(self): user = frappe.get_doc("User", "test@example.com") add_child_table_to_blog_post() From 0bf0cb8bb13f44318f331a98d86c4c9086bf63e1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 22 Mar 2024 16:31:28 +0530 Subject: [PATCH 29/38] Revert "fix: escape text types before setting disp area (#25520) (#25522)" (#25604) This reverts commit e446770f452680ac034bc886a302ca191297cfa5. --- frappe/public/js/frappe/form/controls/base_input.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 0628a8417de..1b67d7acbc7 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -146,12 +146,11 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control } else { value = this.value || value; } - if (["Data", "Long Text", "Small Text", "Text", "Password"].includes(this.df.fieldtype)) { + if (this.df.fieldtype === "Data") { value = frappe.utils.escape_html(value); } let doc = this.doc || (this.frm && this.frm.doc); let display_value = frappe.format(value, this.df, { no_icon: true, inline: true }, doc); - // This is used to display formatted output AND showing values in read only fields this.disp_area && $(this.disp_area).html(display_value); } set_label(label) { From b7a1da5eab31a0966da60f58d7c759a99fa89bc2 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Fri, 22 Mar 2024 15:41:14 +0530 Subject: [PATCH 30/38] feat: allow setting a custom rate limit for `login via email link` feature Signed-off-by: Akhil Narang (cherry picked from commit 766d2ae778b69c67b59989ed30528516301087ae) Signed-off-by: Akhil Narang --- .../system_settings/system_settings.json | 22 +++++++++++++------ frappe/www/login.py | 6 ++++- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 48c1c9f4e25..25c2ee7093b 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -45,6 +45,7 @@ "disable_user_pass_login", "login_with_email_link", "login_with_email_link_expiry", + "rate_limit_email_link_login", "allow_error_traceback", "strip_exif_metadata_from_uploaded_images", "allow_older_web_view_links", @@ -437,11 +438,11 @@ "label": "Include Web View Link in Email" }, { - "collapsible": 1, - "fieldname": "prepared_report_section", - "fieldtype": "Section Break", - "label": "Reports" - }, + "collapsible": 1, + "fieldname": "prepared_report_section", + "fieldtype": "Section Break", + "label": "Reports" + }, { "default": "Frappe", "description": "The application name will be used in the Login page.", @@ -600,12 +601,19 @@ "fieldname": "store_attached_pdf_document", "fieldtype": "Check", "label": "Store Attached PDF Document" + }, + { + "depends_on": "login_with_email_link", + "description": "You can set a high value here if multiple users will be logging in from the same network.", + "fieldname": "rate_limit_email_link_login", + "fieldtype": "Int", + "label": "Rate limit for email link login" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2024-03-14 15:18:01.465057", + "modified": "2024-03-22 16:35:52.338727", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -624,4 +632,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/www/login.py b/frappe/www/login.py index 81d90263c70..791308cd014 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -150,8 +150,12 @@ def _generate_temporary_login_link(email: str, expiry: int): return get_url(f"/api/method/frappe.www.login.login_via_key?key={key}") +def get_login_with_email_link_ratelimit() -> int: + return frappe.get_system_settings("rate_limit_email_link_login") or 5 + + @frappe.whitelist(allow_guest=True, methods=["GET"]) -@rate_limit(limit=5, seconds=60 * 60) +@rate_limit(limit=get_login_with_email_link_ratelimit, seconds=60 * 60) def login_via_key(key: str): cache_key = f"one_time_login_key:{key}" email = frappe.cache().get_value(cache_key) From 1a794df4b03d526f3a2a693b84b7cd304063adc2 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:28:15 +0100 Subject: [PATCH 31/38] fix(Contact form): make email translatable (cherry picked from commit bcdce09dbac60cbf1699f622693d05557a2d7a6f) --- frappe/www/contact.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/www/contact.py b/frappe/www/contact.py index 98aafdef493..bec7c7333a0 100644 --- a/frappe/www/contact.py +++ b/frappe/www/contact.py @@ -34,10 +34,13 @@ def send_message(sender, message, subject="Website Query"): if forward_to_email := frappe.db.get_single_value("Contact Us Settings", "forward_to_email"): frappe.sendmail(recipients=forward_to_email, reply_to=sender, content=message, subject=subject) + reply = _( + "Thank you for reaching out to us. We will get back to you at the earliest.\n\n\nYour query:\n\n{0}" + ).format(message) frappe.sendmail( recipients=sender, - content=f"
Thank you for reaching out to us. We will get back to you at the earliest.\n\n\nYour query:\n\n{message}
", - subject="We've received your query!", + content=f"
{reply}
", + subject=_("We've received your query!"), ) # for clearing outgoing email error message From 6d731faf8c6d1ee8829ea650221af2d1c661f968 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 22 Mar 2024 17:44:37 +0100 Subject: [PATCH 32/38] fix(Contact form): make title and options translatable (cherry picked from commit b440eab24f1a83f8835eacb00dd4b82fb6a347cc) --- frappe/www/contact.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/www/contact.html b/frappe/www/contact.html index a6672d63d2a..d34c63589d8 100644 --- a/frappe/www/contact.html +++ b/frappe/www/contact.html @@ -1,7 +1,7 @@ {% extends "templates/web.html" %} {% set title = heading or "Contact Us" %} -{% block header %}

{{ heading or "Contact Us" }}

{% endblock %} +{% block header %}

{{ heading or _("Contact Us") }}

{% endblock %} {% block page_content %}