From 65122b4ca6a06601c6941beccebbb9bbcd022a8f Mon Sep 17 00:00:00 2001 From: HENRY Florian Date: Wed, 20 Sep 2023 15:22:21 +0200 Subject: [PATCH 001/115] feat: bulk update fields select sorted by translated labels (#22318) * feat: bulk update fields sorted translated alpha sort * chore: implement review from @barredterra * chore: fix linter/prettier * fix: convert to string --------- Co-authored-by: Ankush Menat (cherry picked from commit 42ee7f9b2ddafe1c9b769df4760462be3ee64f12) --- frappe/public/js/frappe/list/bulk_operations.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 5e38c4c25bd6..a0271967b4ec 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -235,7 +235,11 @@ export default class BulkOperations { } edit(docnames, field_mappings, done) { - let field_options = Object.keys(field_mappings).sort(); + let field_options = Object.keys(field_mappings).sort(function (a, b) { + return __(cstr(field_mappings[a].label)).localeCompare( + cstr(__(field_mappings[b].label)) + ); + }); const status_regex = /status/i; const default_field = field_options.find((value) => status_regex.test(value)); From 9c87707129095bebf74898204030de967fc8f2ab Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Thu, 14 Sep 2023 11:32:31 +0530 Subject: [PATCH 002/115] fix: fixed the conflict between fieldname in General Ledger Report (cherry picked from commit 985b9a4ac4580da2195783f0a78e861c843d5901) --- frappe/desk/query_report.py | 17 +++++++++++-- .../js/frappe/views/reports/query_report.js | 25 ++++++++++++++----- 2 files changed, 34 insertions(+), 8 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 0e0a6750e7b1..b1249818375c 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -91,8 +91,8 @@ def generate_report_result( columns, result, message, chart, report_summary, skip_total_row = ljust_list(res, 6) columns = [get_column_as_dict(col) for col in (columns or [])] report_column_names = [col["fieldname"] for col in columns] - # convert to list of dicts + result = normalize_result(result, columns) if report.custom_columns: @@ -108,6 +108,13 @@ def generate_report_result( report_custom_columns = [ column for column in columns if column["fieldname"] not in report_column_names ] + # for column in report_custom_columns: + # column["fieldname"] = column["fieldname"].split("-")[0] + + # for column in columns: + # if (len(column.get("fieldname").split("-")) > 1): + # print(column.get("fieldname")) + # column["fieldname"] = column["fieldname"].split("-")[0] if report_custom_columns: result = add_custom_column_data(report_custom_columns, result) @@ -118,6 +125,7 @@ def generate_report_result( if cint(report.add_total_row) and result and not skip_total_row: result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field) + # breakpoint() return { "result": result, "columns": columns, @@ -248,6 +256,9 @@ def run( def add_custom_column_data(custom_columns, result): + for column in custom_columns: + column["fieldname"] = column["fieldname"].split("-")[0] + custom_column_data = get_data_for_custom_report(custom_columns, result) for column in custom_columns: @@ -265,6 +276,9 @@ def add_custom_column_data(custom_columns, result): # possible if the row is empty if not row_reference: continue + + if custom_column_data.get(key).get(row_reference) is None: + column["fieldname"] = column.get("fieldname") + "-" + frappe.unscrub(key[0]) row[column.get("fieldname")] = custom_column_data.get(key).get(row_reference) return result @@ -525,7 +539,6 @@ def get_data_for_custom_report(columns, result): names = list(set(names)) doc_field_value_map[(doctype, fieldname)] = get_data_for_custom_field(doctype, fieldname, names) - return doc_field_value_map diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index b6c794149d3d..9f0f89e256a4 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -935,7 +935,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { render_datatable() { let data = this.data; + console.log(this.data); let columns = this.columns.filter((col) => !col.hidden); + // columns = this. + // debugger if (this.raw_data.add_total_row && !this.report_settings.tree) { data = data.slice(); @@ -1691,7 +1694,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { (column) => column.label === values.insert_after ); custom_columns.push({ - fieldname: df.fieldname, + fieldname: df.fieldname + "-" + frappe.scrub(values.doctype), fieldtype: df.fieldtype, label: df.label, insert_after_index: insert_after_index, @@ -1713,14 +1716,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }, callback: (r) => { const custom_data = r.message; + console.log(r.message); const link_field = this.doctype_field_map[values.doctype].fieldname; - + console.log(link_field, values.field); this.add_custom_column( custom_columns, custom_data, link_field, - values.field, + values, insert_after_index ); d.hide(); @@ -1801,13 +1805,22 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } } - add_custom_column(custom_column, custom_data, link_field, column_field, insert_after_index) { + add_custom_column( + custom_column, + custom_data, + link_field, + new_column_data, + insert_after_index + ) { + console.log(custom_column); const column = this.prepare_columns(custom_column); + const column_field = new_column_data.field; this.columns.splice(insert_after_index + 1, 0, column[0]); - this.data.forEach((row) => { - row[column_field] = custom_data[row[link_field]]; + console.log(row); + row[column_field + "-" + frappe.scrub(new_column_data.doctype)] = + custom_data[row[link_field]]; }); this.render_datatable(); From be31d56d087ae0e406ec483c8945b6460e8801a5 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Thu, 14 Sep 2023 13:23:00 +0530 Subject: [PATCH 003/115] fix: works for multiple rows now after saving (cherry picked from commit d3178527b3b5e56d2abf59e4d87e2bd41bca7c6e) --- frappe/desk/query_report.py | 18 +++++++-------- .../js/frappe/views/reports/query_report.js | 23 +++++++++++-------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index b1249818375c..f7ab8fe2294d 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -108,13 +108,6 @@ def generate_report_result( report_custom_columns = [ column for column in columns if column["fieldname"] not in report_column_names ] - # for column in report_custom_columns: - # column["fieldname"] = column["fieldname"].split("-")[0] - - # for column in columns: - # if (len(column.get("fieldname").split("-")) > 1): - # print(column.get("fieldname")) - # column["fieldname"] = column["fieldname"].split("-")[0] if report_custom_columns: result = add_custom_column_data(report_custom_columns, result) @@ -125,7 +118,6 @@ def generate_report_result( if cint(report.add_total_row) and result and not skip_total_row: result = add_total_row(result, columns, is_tree=is_tree, parent_field=parent_field) - # breakpoint() return { "result": result, "columns": columns, @@ -256,7 +248,11 @@ def run( def add_custom_column_data(custom_columns, result): + doctype_name_from_custom_field = [] for column in custom_columns: + doctype_name_from_custom_field.append(frappe.unscrub(column["fieldname"].split("-")[1])) if len( + column["fieldname"].split("-") + ) > 1 else None column["fieldname"] = column["fieldname"].split("-")[0] custom_column_data = get_data_for_custom_report(custom_columns, result) @@ -276,8 +272,10 @@ def add_custom_column_data(custom_columns, result): # possible if the row is empty if not row_reference: continue - - if custom_column_data.get(key).get(row_reference) is None: + if ( + custom_column_data.get(key).get(row_reference) is None + and key[0] in doctype_name_from_custom_field + ): column["fieldname"] = column.get("fieldname") + "-" + frappe.unscrub(key[0]) row[column.get("fieldname")] = custom_column_data.get(key).get(row_reference) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 9f0f89e256a4..6c4106d0be3d 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -935,10 +935,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { render_datatable() { let data = this.data; - console.log(this.data); let columns = this.columns.filter((col) => !col.hidden); - // columns = this. - // debugger if (this.raw_data.add_total_row && !this.report_settings.tree) { data = data.slice(); @@ -1693,8 +1690,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { const insert_after_index = this.columns.findIndex( (column) => column.label === values.insert_after ); + custom_columns.push({ - fieldname: df.fieldname + "-" + frappe.scrub(values.doctype), + fieldname: this.columns + .map((column) => column.fieldname) + .includes(df.fieldname) + ? df.fieldname + "-" + frappe.scrub(values.doctype) + : df.fieldname, fieldtype: df.fieldtype, label: df.label, insert_after_index: insert_after_index, @@ -1716,10 +1718,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }, callback: (r) => { const custom_data = r.message; - console.log(r.message); const link_field = this.doctype_field_map[values.doctype].fieldname; - console.log(link_field, values.field); this.add_custom_column( custom_columns, custom_data, @@ -1812,15 +1812,18 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { new_column_data, insert_after_index ) { - console.log(custom_column); const column = this.prepare_columns(custom_column); const column_field = new_column_data.field; this.columns.splice(insert_after_index + 1, 0, column[0]); + this.data.forEach((row) => { - console.log(row); - row[column_field + "-" + frappe.scrub(new_column_data.doctype)] = - custom_data[row[link_field]]; + if (column[0].fieldname.includes("-")) { + row[column_field + "-" + frappe.scrub(new_column_data.doctype)] = + custom_data[row[link_field]]; + } else { + row[column_field] = custom_data[row[link_field]]; + } }); this.render_datatable(); From 9129dde513093e1e8ae111f17f0b7530d02dacb7 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Fri, 15 Sep 2023 12:19:21 +0530 Subject: [PATCH 004/115] chore: code cleanup (cherry picked from commit 4bbed3287eb95ed97c14f658d62fed9236f79676) --- frappe/desk/query_report.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index f7ab8fe2294d..e264ced86f20 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -272,11 +272,8 @@ def add_custom_column_data(custom_columns, result): # possible if the row is empty if not row_reference: continue - if ( - custom_column_data.get(key).get(row_reference) is None - and key[0] in doctype_name_from_custom_field - ): - column["fieldname"] = column.get("fieldname") + "-" + frappe.unscrub(key[0]) + if key[0] in doctype_name_from_custom_field: + column["fieldname"] = column.get("id") row[column.get("fieldname")] = custom_column_data.get(key).get(row_reference) return result From f1be773fc3568eb99e0a60a9fbc29917f95986c3 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Mon, 18 Sep 2023 18:24:43 +0530 Subject: [PATCH 005/115] chore: code cleanup (cherry picked from commit 7ae5ffca198c5fdbdfd2461345c7ba8f414a11f3) --- frappe/desk/query_report.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index e264ced86f20..1ef18c2445df 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -248,11 +248,12 @@ def run( def add_custom_column_data(custom_columns, result): - doctype_name_from_custom_field = [] + doctype_names_from_custom_field = [] for column in custom_columns: - doctype_name_from_custom_field.append(frappe.unscrub(column["fieldname"].split("-")[1])) if len( - column["fieldname"].split("-") - ) > 1 else None + if len(column["fieldname"].split("-")) > 1: + # length greater than 1, means that the column is a custom field with confilicting fieldname + doctype_name = frappe.unscrub(column["fieldname"].split("-")[1]) + doctype_names_from_custom_field.append(doctype_name) column["fieldname"] = column["fieldname"].split("-")[0] custom_column_data = get_data_for_custom_report(custom_columns, result) @@ -272,7 +273,7 @@ def add_custom_column_data(custom_columns, result): # possible if the row is empty if not row_reference: continue - if key[0] in doctype_name_from_custom_field: + if key[0] in doctype_names_from_custom_field: column["fieldname"] = column.get("id") row[column.get("fieldname")] = custom_column_data.get(key).get(row_reference) From 7e0385df83d97b0700f23c12738fe0d7a461fbba Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sun, 24 Sep 2023 01:31:41 +0530 Subject: [PATCH 006/115] test: added test for conflicting column names (cherry picked from commit 34cd9435566dcac6580c47d6b6bb8bc6ca1c4b9d) # Conflicts: # frappe/tests/test_query_report.py --- frappe/tests/test_query_report.py | 181 ++++++++++++++++++++++++++++++ 1 file changed, 181 insertions(+) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 7db4d0b4deb2..945f25553684 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -1,14 +1,33 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import json +import os + import frappe import frappe.utils +<<<<<<< HEAD from frappe.desk.query_report import build_xlsx_data +======= +from frappe.core.doctype.doctype.test_doctype import new_doctype +from frappe.desk.query_report import ( + build_xlsx_data, + export_query, + generate_report_result, + get_data_for_custom_field, + get_data_for_custom_report, + run, +) +from frappe.tests.ui_test_helpers import create_doctype +>>>>>>> 34cd943556 (test: added test for conflicting column names) from frappe.tests.utils import FrappeTestCase from frappe.utils.xlsxutils import make_xlsx class TestQueryReport(FrappeTestCase): + def tearDown(self): + frappe.db.rollback() + def test_xlsx_data_with_multiple_datatypes(self): """Test exporting report using rows with multiple datatypes (list, dict)""" @@ -69,3 +88,165 @@ def test_xlsx_export_with_composite_cell_value(self): for row in xlsx_data: # column_b should be 'str' even with composite cell value self.assertEqual(type(row[1]), str) +<<<<<<< HEAD +======= + + 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 = 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")) + 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) + + def test_report_for_duplicate_column_names(self): + """Test report with duplicate column names""" + + try: + fields = [ + {"label": "First Name", "fieldname": "first_name", "fieldtype": "Data"}, + {"label": "Last Name", "fieldname": "last_name", "fieldtype": "Data"}, + ] + docA = frappe.get_doc( + { + "doctype": "DocType", + "name": "Doc A", + "module": "Core", + "custom": 1, + "autoname": "field:first_name", + "fields": fields, + "permissions": [{"role": "System Manager"}], + } + ).insert(ignore_if_duplicate=True) + + docB = frappe.get_doc( + { + "doctype": "DocType", + "name": "Doc B", + "module": "Core", + "custom": 1, + "autoname": "field:last_name", + "fields": fields, + "permissions": [{"role": "System Manager"}], + } + ).insert(ignore_if_duplicate=True) + + for i in range(1, 3): + frappe.get_doc({"doctype": "Doc A", "first_name": f"John{i}", "last_name": "Doe"}).insert() + frappe.get_doc({"doctype": "Doc B", "last_name": f"Doe{i}", "first_name": "John"}).insert() + + if not frappe.db.exists("Report", "Doc A Report"): + report = frappe.get_doc( + { + "doctype": "Report", + "ref_doctype": "Doc A", + "report_name": "Doc A Report", + "report_type": "Script Report", + "is_standard": "No", + } + ).insert(ignore_permissions=True) + else: + report = frappe.get_doc("Report", "Doc A Report") + + report.report_script = """ +result = [["Ritvik","Sardana", "Doe1"],["Shariq","Ansari", "Doe2"]] +columns = [{ + "label": "First Name", + "fieldname": "first_name", + "fieldtype": "Data", + "width": 180, + }, + { + "label": "Last Name", + "fieldname": "last_name", + "fieldtype": "Data", + "width": 180, + }, + { + "label": "Linked Field", + "fieldname": "linked_field", + "fieldtype": "Link", + "options": "Doc B", + "width": 180, + }, + ] + +data = columns, result + """ + report.save() + data = report.get_data() + + custom_columns = [ + { + "fieldname": "first_name-Doc_B", + "fieldtype": "Data", + "label": "First Name", + "insert_after_index": 1, + "link_field": {"fieldname": "linked_field", "names": {}}, + "doctype": "Doc B", + "width": 100, + "id": "first_name-Doc_B", + "name": "First Name", + "editable": False, + "compareValue": None, + }, + ] + + response = run( + "Doc A Report", + filters={"user": "Administrator", "doctype": "Doc A"}, + custom_columns=custom_columns, + ) + + self.assertListEqual( + ["first_name", "last_name", "first_name-Doc_B", "linked_field"], + [d["fieldname"] for d in response["columns"]], + ) + + self.assertDictEqual( + { + "first_name": "Ritvik", + "last_name": "Sardana", + "linked_field": "Doe1", + "first_name-Doc_B": "John", + }, + response["result"][0], + ) + + except Exception as e: + raise e + frappe.db.rollback() +>>>>>>> 34cd943556 (test: added test for conflicting column names) From 867ff21e3aa987254209a7f6e4d4443b66442b4a Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sun, 24 Sep 2023 02:36:04 +0530 Subject: [PATCH 007/115] chore: code cleanup (cherry picked from commit 0c2c6f2aecb2389940f37e454c56ed85feb78d15) --- frappe/tests/test_query_report.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 945f25553684..ffef81ff872e 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -10,14 +10,7 @@ from frappe.desk.query_report import build_xlsx_data ======= from frappe.core.doctype.doctype.test_doctype import new_doctype -from frappe.desk.query_report import ( - build_xlsx_data, - export_query, - generate_report_result, - get_data_for_custom_field, - get_data_for_custom_report, - run, -) +from frappe.desk.query_report import build_xlsx_data, export_query, run from frappe.tests.ui_test_helpers import create_doctype >>>>>>> 34cd943556 (test: added test for conflicting column names) from frappe.tests.utils import FrappeTestCase From 62be7c5fb343519bc67a20a3e7b4389e64ce567f Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Sun, 24 Sep 2023 11:34:06 +0530 Subject: [PATCH 008/115] test: code cleanup (cherry picked from commit 32e3198c83a9c268fb72776b1de2589293b35e27) # Conflicts: # frappe/tests/test_query_report.py --- frappe/tests/test_query_report.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index ffef81ff872e..ea778b4b1454 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -1,9 +1,6 @@ # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import json -import os - import frappe import frappe.utils <<<<<<< HEAD @@ -11,8 +8,11 @@ ======= from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.desk.query_report import build_xlsx_data, export_query, run +<<<<<<< HEAD from frappe.tests.ui_test_helpers import create_doctype >>>>>>> 34cd943556 (test: added test for conflicting column names) +======= +>>>>>>> 32e3198c83 (test: code cleanup) from frappe.tests.utils import FrappeTestCase from frappe.utils.xlsxutils import make_xlsx From 29c9e4de883bdb7475adbdfe815deb07fef84cb8 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Mon, 25 Sep 2023 12:31:18 +0530 Subject: [PATCH 009/115] test: code cleanup (cherry picked from commit 9ae56289df19149ef69bb95f001035d16960612f) --- frappe/tests/test_query_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index ea778b4b1454..369fa33f5671 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -200,7 +200,6 @@ def test_report_for_duplicate_column_names(self): data = columns, result """ report.save() - data = report.get_data() custom_columns = [ { From 313cf7a7c6867ad17c628f16b24112ebfd34fb8f Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 26 Sep 2023 12:02:25 +0530 Subject: [PATCH 010/115] fix: enable server script while testing (cherry picked from commit 9368f0aee437445b7aaa92156633c4ecd353477e) --- frappe/tests/test_query_report.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 369fa33f5671..0c9b7b97fb30 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -18,6 +18,11 @@ class TestQueryReport(FrappeTestCase): + @classmethod + def setUpClass(cls) -> None: + cls.enable_safe_exec() + return super().setUpClass() + def tearDown(self): frappe.db.rollback() From 8a9728c4b59342b5be0e8fb564aa84007cbf9458 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Sep 2023 16:59:45 +0530 Subject: [PATCH 011/115] fix: Escape filename in sidebar (cherry picked from commit f2c45577d9efb1970762e7e418989be5c237696c) --- frappe/public/js/frappe/form/sidebar/attachments.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index c8785a97c808..541823301705 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -139,7 +139,9 @@ frappe.ui.form.Attachments = class Attachments { var me = this; let file_label = ` - + ${file_name} `; From 5665ce8c9dc03081bbfec4270103163193b61b56 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Sep 2023 20:15:34 +0530 Subject: [PATCH 012/115] Revert "feat: wkhtmltopdf logging (Backport #19935)" --- frappe/utils/logger.py | 28 ---------------------------- frappe/utils/pdf.py | 12 ++---------- 2 files changed, 2 insertions(+), 38 deletions(-) diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index 9f22e39b2eb4..730f95ef175e 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -1,8 +1,6 @@ # imports - standard imports import logging import os -import sys -from contextlib import contextmanager from copy import deepcopy from logging.handlers import RotatingFileHandler @@ -124,29 +122,3 @@ def sanitized_dict(form_dict): if secret_kw in k: sanitized_dict[k] = "********" return sanitized_dict - - -@contextmanager -def pipe_to_log(logger_fn, stream=None): - "Pass an existing logger function e.g. logger.info. Stream defaults to stdout" - # late bind source - if stream is None: - stream = sys.stdout - - stream_int = stream.fileno() - r_int, w_int = os.pipe() - - # copy stream_fd before it is overwritten - with os.fdopen(os.dup(stream_int), "wb") as copied: - stream.flush() - os.dup2(w_int, stream_int) # $ exec >&pipe - try: - with os.fdopen(w_int, "wb"): - yield stream - finally: - # restore stream to its previous value - stream.flush() - os.dup2(copied.fileno(), stream_int) # $ exec >&copied - with os.fdopen(r_int, newline="") as r: - text = r.read() - logger_fn(text) diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index c1c5dcaa06a0..678671bce25d 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -14,7 +14,6 @@ from frappe import _ from frappe.utils import scrub_urls from frappe.utils.jinja_globals import bundled_asset, is_rtl -from frappe.utils.logger import pipe_to_log PDF_CONTENT_ERRORS = [ "ContentNotFoundError", @@ -23,9 +22,6 @@ "RemoteHostClosedError", ] -logger = frappe.logger("wkhtmltopdf", max_size=100000, file_count=3) -logger.setLevel("INFO") - def get_pdf(html, options=None, output: PdfWriter | None = None): html = scrub_urls(html) @@ -38,13 +34,8 @@ def get_pdf(html, options=None, output: PdfWriter | None = None): options.update({"disable-smart-shrinking": ""}) try: - # wkhtmltopdf writes the pdf to stdout and errors to stderr - # pdfkit v1.0.0 writes the pdf to file or returns it - # stderr is written to sys.stdout if verbose=True is supplied # Set filename property to false, so no file is actually created - # defaults to redirecting stdout - with pipe_to_log(logger.info): - filedata = pdfkit.from_string(html, False, options=options or {}, verbose=True) + filedata = pdfkit.from_string(html, options=options or {}, verbose=True) # create in-memory binary streams from filedata and create a PdfReader object reader = PdfReader(io.BytesIO(filedata)) @@ -102,6 +93,7 @@ def prepare_options(html, options): "print-media-type": None, "background": None, "images": None, + "quiet": None, # 'no-outline': None, "encoding": "UTF-8", # 'load-error-handling': 'ignore' From ef5709ad787520683f68af9f15a0638c2e2ccf95 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 29 Sep 2023 12:04:38 +0530 Subject: [PATCH 013/115] fix: improved validation for address and contact (cherry picked from commit 20178bd3eb158d80a0bcb0bdaa56ba5a5fec9f7c) # Conflicts: # frappe/contacts/doctype/contact/contact.py --- frappe/contacts/doctype/address/address.py | 4 +++- frappe/contacts/doctype/contact/contact.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 7658243ba4ab..a4ce308718c7 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -131,7 +131,9 @@ def get_address_display(address_dict: dict | str | None = None) -> str | None: return if not isinstance(address_dict, dict): - address_dict = frappe.db.get_value("Address", address_dict, "*", as_dict=True, cache=True) or {} + address = frappe.get_cached_doc("Address", address_dict) + address.check_permission() + address_dict = address.as_dict() name, template = get_address_templates(address_dict) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 9e9be8e54fd9..9cf18f413dca 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -179,7 +179,13 @@ def invite_user(contact): @frappe.whitelist() def get_contact_details(contact): contact = frappe.get_doc("Contact", contact) +<<<<<<< HEAD out = { +======= + contact.check_permission() + + return { +>>>>>>> 20178bd3eb (fix: improved validation for address and contact) "contact_person": contact.get("name"), "contact_display": " ".join( filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")]) From 833c661e0c5f561c123839375d0b310acb068b1e Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 29 Sep 2023 12:15:08 +0530 Subject: [PATCH 014/115] chore: fix conflicts --- frappe/contacts/doctype/contact/contact.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 9cf18f413dca..6558ce1a0ddd 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -179,13 +179,9 @@ def invite_user(contact): @frappe.whitelist() def get_contact_details(contact): contact = frappe.get_doc("Contact", contact) -<<<<<<< HEAD - out = { -======= contact.check_permission() return { ->>>>>>> 20178bd3eb (fix: improved validation for address and contact) "contact_person": contact.get("name"), "contact_display": " ".join( filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")]) @@ -196,7 +192,6 @@ def get_contact_details(contact): "contact_designation": contact.get("designation"), "contact_department": contact.get("department"), } - return out def update_contact(doc, method): From fda329f2681d52af6cd4caab029ffa3bd13f762c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 28 Sep 2023 11:46:44 +0530 Subject: [PATCH 015/115] fix: avoid double translation (cherry picked from commit c561369330a87aa09cd162f759dfac027120977e) --- frappe/permissions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/permissions.py b/frappe/permissions.py index 1e922cb71173..02569caa8771 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -180,7 +180,7 @@ def is_user_owner(): return (doc.get("owner") or "").lower() == user.lower() if has_controller_permissions(doc, ptype, user=user) is False: - push_perm_check_log("Not allowed via controller permission check") + push_perm_check_log(_("Not allowed via controller permission check")) return {ptype: 0} permissions = copy.deepcopy(get_role_permissions(meta, user=user, is_owner=is_user_owner())) @@ -703,7 +703,7 @@ def push_perm_check_log(log): if frappe.flags.get("has_permission_check_logs") is None: return - frappe.flags.get("has_permission_check_logs").append(_(log)) + frappe.flags.get("has_permission_check_logs").append(log) def has_child_permission( From d283b558f3d3a2a07125dc377784dde3b1c66c29 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Sep 2023 13:09:27 +0530 Subject: [PATCH 016/115] fix: no perm if doc doesn't exist Right now "doc doesn't exist is thrown, instead we can assume that user doesn't have permission to this file. User might have access to other file still. (cherry picked from commit 8242d75bc6ab52050af182209400f635ae391f94) --- frappe/core/doctype/file/file.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 01e6eb121f50..99e0f338f34f 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -732,7 +732,11 @@ def has_permission(doc, ptype=None, user=None): attached_to_doctype = doc.attached_to_doctype attached_to_name = doc.attached_to_name - ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name) + try: + ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name) + except frappe.DoesNotExistError: + frappe.clear_last_message() + return False if ptype in ["write", "create", "delete"]: return ref_doc.has_permission("write") From 2fadf43a17e43fe0b5bf83c31eaa97fcc6af243d Mon Sep 17 00:00:00 2001 From: Corentin Forler <10946971+cogk@users.noreply.github.com> Date: Fri, 29 Sep 2023 16:22:45 +0200 Subject: [PATCH 017/115] fix: Fix breakpoints CSS variables using map-get MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit “Sass parses custom property declarations differently than other property declarations. All tokens are passed through to CSS as-is.” https://sass-lang.com/documentation/style-rules/declarations/#custom-properties (cherry picked from commit b9e16f5fed6d8548522a65e6f3961e27a8d7ae3d) --- frappe/public/scss/desk/css_variables.scss | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss index aceaa3e1e6e6..4d5ad6aed32b 100644 --- a/frappe/public/scss/desk/css_variables.scss +++ b/frappe/public/scss/desk/css_variables.scss @@ -3,12 +3,12 @@ :root, [data-theme="light"] { // breakpoints - --xxl-width: map-get($grid-breakpoints, '2xl'); - --xl-width: map-get($grid-breakpoints, 'xl'); - --lg-width: map-get($grid-breakpoints, 'lg'); - --md-width: map-get($grid-breakpoints, 'md'); - --sm-width: map-get($grid-breakpoints, 'sm'); - --xs-width: map-get($grid-breakpoints, 'xs'); + --xxl-width: #{map-get($grid-breakpoints, '2xl')}; + --xl-width: #{map-get($grid-breakpoints, 'xl')}; + --lg-width: #{map-get($grid-breakpoints, 'lg')}; + --md-width: #{map-get($grid-breakpoints, 'md')}; + --sm-width: #{map-get($grid-breakpoints, 'sm')}; + --xs-width: #{map-get($grid-breakpoints, 'xs')}; --text-bold: 500; From 5fc944a3911530bf6932054bee573db492ec1b00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= <46800703+bosue@users.noreply.github.com> Date: Wed, 27 Sep 2023 23:38:24 +0200 Subject: [PATCH 018/115] fix(UX): Responsive, column-major design for multicheck elements. (cherry picked from commit 7baab4490bdc7425be1223d71f37fb2cb066e18c) --- .../js/frappe/form/controls/multicheck.js | 18 ++++++++---------- frappe/templates/styles/standard.css | 4 ++++ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/multicheck.js b/frappe/public/js/frappe/form/controls/multicheck.js index 7b980299aa4e..4b2987cf4528 100644 --- a/frappe/public/js/frappe/form/controls/multicheck.js +++ b/frappe/public/js/frappe/form/controls/multicheck.js @@ -14,10 +14,13 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for this.$select_buttons = this.get_select_buttons().appendTo(this.wrapper); this.$load_state.appendTo(this.wrapper); - const row = this.get_column_size() === 12 ? "" : "row"; - this.$checkbox_area = $(`
`).appendTo( - this.wrapper - ); + // In your implementation, you may use the 'columns' property to specify either of: + // - minimum column width, e.g. `"15rem"` + // - fixed number of columns, e.g. `3` + // - both minimum column width and maximum number of columns, e.g. `"15rem 5"` + const columns = this.df.columns; + this.$checkbox_area = $('
').appendTo(this.wrapper); + this.$checkbox_area.get(0).style.setProperty("--checkbox-options-columns", columns); } refresh() { @@ -145,9 +148,8 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for } get_checkbox_element(option) { - const column_size = this.get_column_size(); return $(` -
+
`); } - - get_column_size() { - return 12 / (+this.df.columns || 1); - } }; diff --git a/frappe/templates/styles/standard.css b/frappe/templates/styles/standard.css index b6ba34872e54..d800893f0992 100644 --- a/frappe/templates/styles/standard.css +++ b/frappe/templates/styles/standard.css @@ -78,6 +78,10 @@ margin: 20px 0px; } +.checkbox-options { + columns: var(--checkbox-options-columns); +} + .square-image { width: 100%; height: 0; From 3bcf15d8bf355f4b3c0b88a08ad0197ef1fdb7a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= <46800703+bosue@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:31:11 +0200 Subject: [PATCH 019/115] fix(UX): Bring responsive multi-column design to module and role editors (cherry picked from commit 42575d99517fa416338ef4f43725639490ac301f) --- frappe/public/js/frappe/module_editor.js | 2 +- frappe/public/js/frappe/roles_editor.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/module_editor.js b/frappe/public/js/frappe/module_editor.js index 666d1393bf9c..b296dacdbe1e 100644 --- a/frappe/public/js/frappe/module_editor.js +++ b/frappe/public/js/frappe/module_editor.js @@ -9,7 +9,7 @@ frappe.ModuleEditor = class ModuleEditor { fieldname: "block_modules", fieldtype: "MultiCheck", select_all: true, - columns: 3, + columns: "15rem", get_data: () => { return this.frm.doc.__onload.all_modules.map((module) => { return { diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 312a90a82c5b..32f1f07b1fbe 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -10,7 +10,7 @@ frappe.RoleEditor = class { fieldname: "roles", fieldtype: "MultiCheck", select_all: true, - columns: 3, + columns: "15rem", get_data: () => { return frappe .xcall("frappe.core.doctype.user.user.get_all_roles") From e4a7577abf4a793927172b5f5bef401c0bd81ec1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 6 Oct 2023 12:29:09 +0530 Subject: [PATCH 020/115] fix: Give All role permission to read/create own address and contact (#22642) --- frappe/contacts/doctype/address/address.json | 13 +++++++++++-- frappe/contacts/doctype/contact/contact.json | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json index e85a89ff1aae..b29333234422 100644 --- a/frappe/contacts/doctype/address/address.json +++ b/frappe/contacts/doctype/address/address.json @@ -148,7 +148,7 @@ "icon": "fa fa-map-marker", "idx": 5, "links": [], - "modified": "2020-10-21 16:14:37.284830", + "modified": "2023-10-06 11:52:46.990766", "modified_by": "Administrator", "module": "Contacts", "name": "Address", @@ -208,9 +208,18 @@ "set_user_permissions": 1, "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "All", + "write": 1 } ], "search_fields": "country, state", "sort_field": "modified", - "sort_order": "DESC" + "sort_order": "DESC", + "states": [] } \ No newline at end of file diff --git a/frappe/contacts/doctype/contact/contact.json b/frappe/contacts/doctype/contact/contact.json index 696cd61d6ca4..dd6f00e53129 100644 --- a/frappe/contacts/doctype/contact/contact.json +++ b/frappe/contacts/doctype/contact/contact.json @@ -250,7 +250,7 @@ "image_field": "image", "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-27 14:12:09.906719", + "modified": "2023-10-06 11:52:34.088559", "modified_by": "Administrator", "module": "Contacts", "name": "Contact", @@ -378,8 +378,17 @@ "read": 1, "report": 1, "role": "All" + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "All", + "write": 1 } ], "sort_field": "modified", - "sort_order": "ASC" + "sort_order": "ASC", + "states": [] } \ No newline at end of file From 844fd766ed78180fff16decada51359fa8454bac Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 9 Oct 2023 19:13:10 +0530 Subject: [PATCH 021/115] fix: allow larger URLs in Integration Request (cherry picked from commit 6043909fe6519f72877367620ea842a877f1c46d) --- .../doctype/integration_request/integration_request.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/integration_request/integration_request.json b/frappe/integrations/doctype/integration_request/integration_request.json index 98db8ea7485d..8565b2a0fdd9 100644 --- a/frappe/integrations/doctype/integration_request/integration_request.json +++ b/frappe/integrations/doctype/integration_request/integration_request.json @@ -101,7 +101,7 @@ }, { "fieldname": "url", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "URL", "read_only": 1 }, @@ -129,7 +129,7 @@ ], "in_create": 1, "links": [], - "modified": "2022-04-07 11:32:27.557548", + "modified": "2023-10-09 09:36:23.856188", "modified_by": "Administrator", "module": "Integrations", "name": "Integration Request", From 656213945df94661de6923a46ef407db1f94dbfb Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Sun, 8 Oct 2023 17:22:29 +0530 Subject: [PATCH 022/115] fix: keyerror on reports with subtotal (cherry picked from commit f6d9069fb1c45690429c0081d068115b99fd5aaf) --- frappe/desk/query_report.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 0e0a6750e7b1..10e849d18f4d 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -581,7 +581,9 @@ def get_filtered_data(ref_doctype, columns, data, user): if match_filters_per_doctype: for row in data: # Why linked_doctypes.get(ref_doctype)? because if column is empty, linked_doctypes[ref_doctype] is removed - if linked_doctypes.get(ref_doctype) and shared and row[linked_doctypes[ref_doctype]] in shared: + if ( + linked_doctypes.get(ref_doctype) and shared and row.get(linked_doctypes[ref_doctype]) in shared + ): result.append(row) elif has_match( From 6062bb4c0e3d69eff2ae2cc94a0484edd9d83392 Mon Sep 17 00:00:00 2001 From: Florian HENRY Date: Mon, 9 Oct 2023 18:28:44 +0200 Subject: [PATCH 023/115] chore: backport #22562 --- frappe/public/js/frappe/form/grid_row.js | 2 +- frappe/translations/fr.csv | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 242c12438bb2..400bc7d02c31 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -338,7 +338,7 @@ export default class GridRow { this.open_form_button = $(`
${frappe.utils.icon("edit", "xs")} -
${__("Edit")}
+
${__("Edit", "", "Edit grid row")}
`) .appendTo(this.open_form_button) diff --git a/frappe/translations/fr.csv b/frappe/translations/fr.csv index c5f9df134b86..cd70d549c7dd 100644 --- a/frappe/translations/fr.csv +++ b/frappe/translations/fr.csv @@ -4751,3 +4751,4 @@ No Results found,Aucun résultat trouvé Shelf Life in Days,Durée de conservation (en jours) Batch Expiry Date,Date d'expiration du Lot, Edit Full Form,Ouvrir le formulaire complet +Edit,Détail,Edit grid row, From 219b18ee9831537afb3e46407a001e13885f24bd Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Mon, 9 Oct 2023 23:21:59 +0530 Subject: [PATCH 024/115] fix: duplicate column in report conflict --- frappe/tests/test_query_report.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 0c9b7b97fb30..e1a99bcaef52 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -3,25 +3,15 @@ import frappe import frappe.utils -<<<<<<< HEAD -from frappe.desk.query_report import build_xlsx_data -======= from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.desk.query_report import build_xlsx_data, export_query, run -<<<<<<< HEAD from frappe.tests.ui_test_helpers import create_doctype ->>>>>>> 34cd943556 (test: added test for conflicting column names) -======= ->>>>>>> 32e3198c83 (test: code cleanup) from frappe.tests.utils import FrappeTestCase from frappe.utils.xlsxutils import make_xlsx class TestQueryReport(FrappeTestCase): - @classmethod - def setUpClass(cls) -> None: - cls.enable_safe_exec() - return super().setUpClass() + def tearDown(self): frappe.db.rollback() @@ -86,8 +76,6 @@ def test_xlsx_export_with_composite_cell_value(self): for row in xlsx_data: # column_b should be 'str' even with composite cell value self.assertEqual(type(row[1]), str) -<<<<<<< HEAD -======= def test_csv(self): from csv import QUOTE_ALL, QUOTE_MINIMAL, QUOTE_NONE, QUOTE_NONNUMERIC, DictReader @@ -246,4 +234,4 @@ def test_report_for_duplicate_column_names(self): except Exception as e: raise e frappe.db.rollback() ->>>>>>> 34cd943556 (test: added test for conflicting column names) + From 69b4c8d4b8d0a658bf344f9e6b42bb7246668281 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Mon, 9 Oct 2023 23:32:20 +0530 Subject: [PATCH 025/115] chore: code cleanup --- frappe/tests/test_query_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index e1a99bcaef52..26a032ebc30d 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -12,7 +12,6 @@ class TestQueryReport(FrappeTestCase): - def tearDown(self): frappe.db.rollback() From 55c06024a49a9022c748224ef5e9482131df7d8b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 10 Oct 2023 12:32:54 +0530 Subject: [PATCH 026/115] fix: strip html and show text (#22656) (#22677) * fix: strip html and show text Quill editor adds html which is useless for user so strip html using html2text and render Text only. If no text is found it will render No Text Found in + "Field Label" (ex. Description). * fix: remove placeholder text [skip ci] --------- Co-authored-by: Ankush Menat (cherry picked from commit 77521360817d0311bda5b87db346398b29bba057) Co-authored-by: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> --- frappe/public/js/frappe/list/list_view.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 195cbb952de1..2709668dcbaa 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1014,8 +1014,11 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { link.dataset.doctype = this.doctype; link.dataset.name = doc.name; link.href = this.get_form_link(doc); - link.title = value.toString(); - link.textContent = value.toString(); + // "Text Editor" and some other fieldtypes can have html tags in them so strip and show text. + // If no text is found show "No Text Found in {Field Label}" + let textValue = frappe.utils.html2text(value); + link.title = textValue; + link.textContent = textValue; return div.innerHTML; } From 204f485b55df2e06994759602427582af4872157 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 10 Oct 2023 13:38:40 +0530 Subject: [PATCH 027/115] chore: removed v15 test from v14 --- frappe/tests/test_query_report.py | 41 ------------------------------- 1 file changed, 41 deletions(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 26a032ebc30d..149c4dd33282 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -76,47 +76,6 @@ def test_xlsx_export_with_composite_cell_value(self): # column_b should be 'str' even with composite cell value self.assertEqual(type(row[1]), str) - 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 = 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")) - 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) - def test_report_for_duplicate_column_names(self): """Test report with duplicate column names""" From b82d261262a0cc7ae35bd2661f3adc366b3c531f Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 10 Oct 2023 14:49:17 +0530 Subject: [PATCH 028/115] chore: code cleanup --- frappe/tests/test_query_report.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 149c4dd33282..579c9b5d160d 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -11,10 +11,6 @@ class TestQueryReport(FrappeTestCase): - - def tearDown(self): - frappe.db.rollback() - def test_xlsx_data_with_multiple_datatypes(self): """Test exporting report using rows with multiple datatypes (list, dict)""" From 45be3d39b969e8f23be3e6420133fad590a65baf Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 10 Oct 2023 11:47:03 +0530 Subject: [PATCH 029/115] fix: conditional webform with list view enabled (cherry picked from commit fb9c111ed24e0d7bfc78f55676028f4970bb318a) --- .../js/frappe/web_form/web_form_list.js | 6 + .../js/frappe/web_form/webform_script.js | 1 + frappe/website/doctype/web_form/web_form.js | 105 ++++++++++++++++++ frappe/website/doctype/web_form/web_form.json | 28 ++--- frappe/website/doctype/web_form/web_form.py | 16 +-- 5 files changed, 129 insertions(+), 27 deletions(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 6d89b8ac95c3..1db53a4b5136 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -109,6 +109,12 @@ export default class WebFormList { } fetch_data() { + if (this.condition_json && JSON.parse(this.condition_json)) { + let filter = frappe.utils.get_filter_from_json(this.condition_json); + filter = frappe.utils.get_filter_as_json(filter); + this.filters = Object.assign(this.filters, JSON.parse(filter)); + } + let args = { method: "frappe.www.list.get_list_data", args: { diff --git a/frappe/public/js/frappe/web_form/webform_script.js b/frappe/public/js/frappe/web_form/webform_script.js index 8a5d123d2ff9..4c745a39092d 100644 --- a/frappe/public/js/frappe/web_form/webform_script.js +++ b/frappe/public/js/frappe/web_form/webform_script.js @@ -27,6 +27,7 @@ frappe.ready(function () { doctype: web_form_doc.doc_type, web_form_name: web_form_doc.name, list_columns: web_form_doc.list_columns, + condition_json: web_form_doc.condition_json, settings: { allow_delete: web_form_doc.allow_delete, }, diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 277330e67427..ecfea34d8ba6 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -36,6 +36,7 @@ frappe.ui.form.on("Web Form", { frm.trigger("set_fields"); frm.trigger("add_get_fields_button"); frm.trigger("add_publish_button"); + frm.trigger("render_condition_table"); }, login_required: function (frm) { @@ -170,6 +171,110 @@ frappe.ui.form.on("Web Form", { allow_multiple: function (frm) { frm.doc.allow_multiple && frm.set_value("show_list", 1); }, + + before_save: function (frm) { + let static_filters = JSON.parse(frm.doc.condition_json || "[]"); + frm.set_value("condition_json", JSON.stringify(static_filters)); + frm.trigger("render_condition_table"); + }, + + render_condition_table: function (frm) { + // frm.set_df_property("filters_section", "hidden", 0); + let is_document_type = true; + + let wrapper = $(frm.get_field("condition_json").wrapper).empty(); + let table = $(` + + + + + + + + +
${__("Filter")}${__("Condition")}${__("Value")}
`).appendTo(wrapper); + $(`

${__("Click table to edit")}

`).appendTo(wrapper); + + let filters = JSON.parse(frm.doc.condition_json || "[]"); + let filters_set = false; + + let fields = []; + if (is_document_type) { + fields = [ + { + fieldtype: "HTML", + fieldname: "filter_area", + }, + ]; + + if (filters?.length) { + filters.forEach((filter) => { + const filter_row = $(` + ${filter[1]} + ${filter[2] || ""} + ${filter[3]} + `); + + table.find("tbody").append(filter_row); + }); + filters_set = true; + } + } else if (frm.filters.length) { + fields = frm.filters.filter((f) => f.fieldname); + fields.map((f) => { + if (filters[f.fieldname]) { + let condition = "="; + const filter_row = $(` + ${f.label} + ${condition} + ${filters[f.fieldname] || ""} + `); + table.find("tbody").append(filter_row); + if (!filters_set) filters_set = true; + } + }); + } + + if (!filters_set) { + const filter_row = $(` + ${__("Click to Set Filters")}`); + table.find("tbody").append(filter_row); + } + + table.on("click", () => { + let dialog = new frappe.ui.Dialog({ + title: __("Set Filters"), + fields: fields, + primary_action: function () { + let values = this.get_values(); + if (values) { + this.hide(); + if (is_document_type) { + let filters = frm.filter_group.get_filters(); + frm.set_value("condition_json", JSON.stringify(filters)); + } else { + frm.set_value("condition_json", JSON.stringify(values)); + } + frm.trigger("render_condition_table"); + } + }, + primary_action_label: "Set", + }); + + if (is_document_type) { + frm.filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field("filter_area").$wrapper, + doctype: frm.doc.doc_type, + on_change: () => {}, + }); + filters && frm.filter_group.add_filters_to_filter_group(filters); + } + + dialog.show(); + + dialog.set_values(filters); + }); + }, }); frappe.ui.form.on("Web Form List Column", { diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index b01b35409181..648bff9afce4 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -31,9 +31,8 @@ "allow_incomplete", "section_break_2", "max_attachment_size", - "section_break_xzqr", - "condition", - "column_break_tjgl", + "condition_section", + "condition_json", "condition_description", "section_break_3", "list_setting_message", @@ -375,31 +374,26 @@ "label": "Anonymous" }, { - "fieldname": "condition", - "fieldtype": "Code", - "label": "Condition", - "max_height": "150px" + "fieldname": "condition_description", + "fieldtype": "HTML", + "label": "Condition Description", + "options": "

Multiple webforms can be created for a single doctype. Write a condition specific to this webform to display correct record after submission.

For Example:

\n

If you create a separate webform every year to capture feedback from employees add a \n field named year in doctype and add a condition doc.year==\"2023\"

\n" }, { - "fieldname": "section_break_xzqr", + "fieldname": "condition_section", "fieldtype": "Section Break" }, { - "fieldname": "column_break_tjgl", - "fieldtype": "Column Break" - }, - { - "fieldname": "condition_description", - "fieldtype": "HTML", - "label": "Condition Description", - "options": "

Multiple webforms can be created for a single doctype. Write a condition specific to this webform to display correct record after submission.

For Example:

\n

If you create a separate webform every year to capture feedback from employees add a \n field named year in doctype and add a condition doc.year==\"2023\"

\n" + "fieldname": "condition_json", + "fieldtype": "JSON", + "label": "Condition JSON" } ], "has_web_view": 1, "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2023-06-03 19:18:56.760479", + "modified": "2023-10-10 11:31:56.609386", "modified_by": "Administrator", "module": "Website", "name": "Web Form", diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index 930788285a7a..4dd49154dc42 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -153,16 +153,12 @@ def get_context(self, context): and not frappe.form_dict.name and not frappe.form_dict.is_list ): - names = frappe.db.get_values(self.doc_type, {"owner": frappe.session.user}, pluck="name") - for name in names: - if self.condition: - doc = frappe.get_doc(self.doc_type, name) - if frappe.safe_eval(self.condition, None, {"doc": doc.as_dict()}): - context.in_view_mode = True - frappe.redirect(f"/{self.route}/{name}") - else: - context.in_view_mode = True - frappe.redirect(f"/{self.route}/{name}") + condition_json = json.loads(self.condition_json) if self.condition_json else [] + condition_json.append(["owner", "=", frappe.session.user]) + names = frappe.get_all(self.doc_type, filters=condition_json, pluck="name") + if names: + context.in_view_mode = True + frappe.redirect(f"/{self.route}/{names[0]}") # Show new form when # - User is Guest From 7e77bea415108165b5a974f13befe4b31a2964c4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 10 Oct 2023 12:53:21 +0530 Subject: [PATCH 030/115] fix: changed table style to match grid style updated description and some minor fixes (cherry picked from commit 46f3b5b12dc675d76983b04aabe1533173f274dd) --- .../js/frappe/ui/filters/filter_list.js | 2 +- frappe/website/doctype/web_form/web_form.js | 99 ++++++++++--------- frappe/website/doctype/web_form/web_form.json | 6 +- 3 files changed, 55 insertions(+), 52 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 4691e05792f0..447b5311d967 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -36,7 +36,7 @@ frappe.ui.FilterGroup = class { } hide_popover() { - this.filter_button.popover("hide"); + this.filter_button?.popover("hide"); } init_filter_popover() { diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index ecfea34d8ba6..187ee745da50 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -179,60 +179,69 @@ frappe.ui.form.on("Web Form", { }, render_condition_table: function (frm) { - // frm.set_df_property("filters_section", "hidden", 0); - let is_document_type = true; - let wrapper = $(frm.get_field("condition_json").wrapper).empty(); - let table = $(` + let table = $(` + + +
- +
${__("Filter")}${__("Filter")} ${__("Condition")} ${__("Value")}
`).appendTo(wrapper); - $(`

${__("Click table to edit")}

`).appendTo(wrapper); + $(`

${__("Click table to edit")}

`).appendTo(wrapper); let filters = JSON.parse(frm.doc.condition_json || "[]"); let filters_set = false; - let fields = []; - if (is_document_type) { - fields = [ - { - fieldtype: "HTML", - fieldname: "filter_area", - }, - ]; + let fields = [ + { + fieldtype: "HTML", + fieldname: "filter_area", + }, + ]; - if (filters?.length) { - filters.forEach((filter) => { - const filter_row = $(` + if (filters?.length) { + filters.forEach((filter) => { + const filter_row = $(` ${filter[1]} ${filter[2] || ""} ${filter[3]} `); - table.find("tbody").append(filter_row); - }); - filters_set = true; - } - } else if (frm.filters.length) { - fields = frm.filters.filter((f) => f.fieldname); - fields.map((f) => { - if (filters[f.fieldname]) { - let condition = "="; - const filter_row = $(` - ${f.label} - ${condition} - ${filters[f.fieldname] || ""} - `); - table.find("tbody").append(filter_row); - if (!filters_set) filters_set = true; - } + table.find("tbody").append(filter_row); }); + filters_set = true; } if (!filters_set) { @@ -249,26 +258,20 @@ frappe.ui.form.on("Web Form", { let values = this.get_values(); if (values) { this.hide(); - if (is_document_type) { - let filters = frm.filter_group.get_filters(); - frm.set_value("condition_json", JSON.stringify(filters)); - } else { - frm.set_value("condition_json", JSON.stringify(values)); - } + let filters = frm.filter_group.get_filters(); + frm.set_value("condition_json", JSON.stringify(filters)); frm.trigger("render_condition_table"); } }, primary_action_label: "Set", }); - if (is_document_type) { - frm.filter_group = new frappe.ui.FilterGroup({ - parent: dialog.get_field("filter_area").$wrapper, - doctype: frm.doc.doc_type, - on_change: () => {}, - }); - filters && frm.filter_group.add_filters_to_filter_group(filters); - } + frm.filter_group = new frappe.ui.FilterGroup({ + parent: dialog.get_field("filter_area").$wrapper, + doctype: frm.doc.doc_type, + on_change: () => {}, + }); + filters && frm.filter_group.add_filters_to_filter_group(filters); dialog.show(); diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json index 648bff9afce4..ad9193628fc1 100644 --- a/frappe/website/doctype/web_form/web_form.json +++ b/frappe/website/doctype/web_form/web_form.json @@ -32,8 +32,8 @@ "section_break_2", "max_attachment_size", "condition_section", - "condition_json", "condition_description", + "condition_json", "section_break_3", "list_setting_message", "show_list", @@ -377,7 +377,7 @@ "fieldname": "condition_description", "fieldtype": "HTML", "label": "Condition Description", - "options": "

Multiple webforms can be created for a single doctype. Write a condition specific to this webform to display correct record after submission.

For Example:

\n

If you create a separate webform every year to capture feedback from employees add a \n field named year in doctype and add a condition doc.year==\"2023\"

\n" + "options": "

Multiple webforms can be created for a single doctype. Add filters specific to this webform to display correct record after submission.

For Example:

\n

If you create a separate webform every year to capture feedback from employees add a \n field named year in doctype and add a filter year = 2023

\n" }, { "fieldname": "condition_section", @@ -393,7 +393,7 @@ "icon": "icon-edit", "is_published_field": "published", "links": [], - "modified": "2023-10-10 11:31:56.609386", + "modified": "2023-10-10 12:51:18.341792", "modified_by": "Administrator", "module": "Website", "name": "Web Form", From 9049a4f04448ec700593d790ccd60765125bcd4f Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 5 Aug 2023 12:46:31 +0530 Subject: [PATCH 031/115] fix: add rate limiting and type hints for `add_feedback` --- frappe/website/doctype/help_article/help_article.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/website/doctype/help_article/help_article.py b/frappe/website/doctype/help_article/help_article.py index e70de0770340..df83a5abf24e 100644 --- a/frappe/website/doctype/help_article/help_article.py +++ b/frappe/website/doctype/help_article/help_article.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.rate_limiter import rate_limit from frappe.utils import cint, is_markdown, markdown from frappe.website.utils import get_comment_list from frappe.website.website_generator import WebsiteGenerator @@ -110,10 +111,11 @@ def clear_website_cache(path=None): @frappe.whitelist(allow_guest=True) -def add_feedback(article, helpful): - field = "helpful" - if helpful == "No": - field = "not_helpful" +@rate_limit(key="article", limit=5, seconds=60 * 60) +def add_feedback(article: str, helpful: str): + if not isinstance("article", str): + frappe.throw(_("Invalid Article Name")) + field = "not_helpful" if helpful == "No" else "helpful" value = cint(frappe.db.get_value("Help Article", article, field)) frappe.db.set_value("Help Article", article, field, value + 1, update_modified=False) From 850b8c8f5af11f7758ed307dba39964e56eeb7eb Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Wed, 11 Oct 2023 11:11:52 +0530 Subject: [PATCH 032/115] chore: code cleanup --- frappe/tests/test_query_report.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 579c9b5d160d..99cf50b2348e 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -188,4 +188,3 @@ def test_report_for_duplicate_column_names(self): except Exception as e: raise e frappe.db.rollback() - From 37e128af0e4d4d2c2451dd0a725380228dc67c74 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 11 Oct 2023 11:39:26 +0530 Subject: [PATCH 033/115] chore: fix bad translation --- frappe/translations/fr.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translations/fr.csv b/frappe/translations/fr.csv index cd70d549c7dd..87686affecba 100644 --- a/frappe/translations/fr.csv +++ b/frappe/translations/fr.csv @@ -4751,4 +4751,4 @@ No Results found,Aucun résultat trouvé Shelf Life in Days,Durée de conservation (en jours) Batch Expiry Date,Date d'expiration du Lot, Edit Full Form,Ouvrir le formulaire complet -Edit,Détail,Edit grid row, +Edit,Détail,Edit grid row From de7f13f1dbd22f90db45a431bacf0128099b753b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 11 Oct 2023 11:48:51 +0530 Subject: [PATCH 034/115] fix: Apply address all perm only if owner (cherry picked from commit bac5f76247d8bd0fc10962e52a21bb04052d3577) --- frappe/contacts/doctype/address/address.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json index b29333234422..4f90d1549da9 100644 --- a/frappe/contacts/doctype/address/address.json +++ b/frappe/contacts/doctype/address/address.json @@ -148,7 +148,7 @@ "icon": "fa fa-map-marker", "idx": 5, "links": [], - "modified": "2023-10-06 11:52:46.990766", + "modified": "2023-10-11 11:48:26.954934", "modified_by": "Administrator", "module": "Contacts", "name": "Address", @@ -211,7 +211,7 @@ }, { "create": 1, - "email": 1, + "if_owner": 1, "print": 1, "read": 1, "role": "All", From 330f8c48d23061e277136adda8d794f2b88c4a23 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Wed, 11 Oct 2023 09:54:55 +0000 Subject: [PATCH 035/115] chore(release): Bumped to Version 14.52.0 # [14.52.0](https://github.com/frappe/frappe/compare/v14.51.0...v14.52.0) (2023-10-11) ### Bug Fixes * add rate limiting and type hints for `add_feedback` ([9049a4f](https://github.com/frappe/frappe/commit/9049a4f04448ec700593d790ccd60765125bcd4f)) * allow larger URLs in Integration Request ([844fd76](https://github.com/frappe/frappe/commit/844fd766ed78180fff16decada51359fa8454bac)) * Apply address all perm only if owner ([de7f13f](https://github.com/frappe/frappe/commit/de7f13f1dbd22f90db45a431bacf0128099b753b)) * avoid double translation ([fda329f](https://github.com/frappe/frappe/commit/fda329f2681d52af6cd4caab029ffa3bd13f762c)) * changed table style to match grid style ([7e77bea](https://github.com/frappe/frappe/commit/7e77bea415108165b5a974f13befe4b31a2964c4)) * conditional webform with list view enabled ([45be3d3](https://github.com/frappe/frappe/commit/45be3d39b969e8f23be3e6420133fad590a65baf)) * Escape filename in sidebar ([8a9728c](https://github.com/frappe/frappe/commit/8a9728c4b59342b5be0e8fb564aa84007cbf9458)) * Fix breakpoints CSS variables using map-get ([2fadf43](https://github.com/frappe/frappe/commit/2fadf43a17e43fe0b5bf83c31eaa97fcc6af243d)) * Give All role permission to read/create own address and contact ([#22642](https://github.com/frappe/frappe/issues/22642)) ([e4a7577](https://github.com/frappe/frappe/commit/e4a7577abf4a793927172b5f5bef401c0bd81ec1)) * improved validation for address and contact ([ef5709a](https://github.com/frappe/frappe/commit/ef5709ad787520683f68af9f15a0638c2e2ccf95)) * keyerror on reports with subtotal ([6562139](https://github.com/frappe/frappe/commit/656213945df94661de6923a46ef407db1f94dbfb)) * no perm if doc doesn't exist ([d283b55](https://github.com/frappe/frappe/commit/d283b558f3d3a2a07125dc377784dde3b1c66c29)) * strip html and show text ([#22656](https://github.com/frappe/frappe/issues/22656)) ([#22677](https://github.com/frappe/frappe/issues/22677)) ([55c0602](https://github.com/frappe/frappe/commit/55c06024a49a9022c748224ef5e9482131df7d8b)) * **UX:** Bring responsive multi-column design to module and role editors ([3bcf15d](https://github.com/frappe/frappe/commit/3bcf15d8bf355f4b3c0b88a08ad0197ef1fdb7a2)) * **UX:** Responsive, column-major design for multicheck elements. ([5fc944a](https://github.com/frappe/frappe/commit/5fc944a3911530bf6932054bee573db492ec1b00)) ### Features * bulk update fields select sorted by translated labels ([#22318](https://github.com/frappe/frappe/issues/22318)) ([65122b4](https://github.com/frappe/frappe/commit/65122b4ca6a06601c6941beccebbb9bbcd022a8f)) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 001c29c70934..3898a22997d2 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.51.0" +__version__ = "14.52.0" __title__ = "Frappe Framework" controllers = {} From c2c2db8aa74e4b7eeffae9213059bf796919de94 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Sun, 8 Oct 2023 16:42:17 +0530 Subject: [PATCH 036/115] fix: workspace don't strip filter info on save. currently on save of filter ( get_filter_as_json ) is used to convert it to object with following structure: ``` { priority: (2) ['=', 'Medium'], status: (2) ['=', 'Open'] } ``` Doing this we lose information of parent doctype. Missing parent doctype causes field not permitted error. To fix this, I saved filter as it is and converted it to object when required. (cherry picked from commit 27e4901e0b60eded3053d41ae5436b6c0297a79c) --- frappe/public/js/frappe/utils/utils.js | 32 +++++++++++-------- .../js/frappe/widgets/quick_list_widget.js | 6 ++-- .../js/frappe/widgets/shortcut_widget.js | 2 +- .../public/js/frappe/widgets/widget_dialog.js | 4 +-- 4 files changed, 24 insertions(+), 20 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 54a8314a12bf..d27d412f4092 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1603,19 +1603,8 @@ Object.assign(frappe.utils, { } }, - get_filter_as_json(filters) { - // convert filter array to json - let filter = null; - - if (filters.length) { - filter = {}; - filters.forEach((arr) => { - filter[arr[1]] = [arr[2], arr[3]]; - }); - filter = JSON.stringify(filter); - } - - return filter; + process_filter_expression(filter) { + return new Function(`return ${filter}`)(); }, get_filter_from_json(filter_json, doctype) { @@ -1625,12 +1614,22 @@ Object.assign(frappe.utils, { return []; } - const filters_json = new Function(`return ${filter_json}`)(); + const filters_json = this.process_filter_expression(filter_json); if (!doctype) { // e.g. return { // priority: (2) ['=', 'Medium'], // status: (2) ['=', 'Open'] // } + + // don't remove unless patch is created to convert all existing filters from object to array + // backward compatibility + if (Array.isArray(filters_json)) { + let filter = {}; + filters_json.forEach((arr) => { + filter[arr[1]] = [arr[2], arr[3]]; + }); + return filter || []; + } return filters_json || []; } @@ -1638,6 +1637,11 @@ Object.assign(frappe.utils, { // ['ToDo', 'status', '=', 'Open', false], // ['ToDo', 'priority', '=', 'Medium', false] // ] + if (Array.isArray(filters_json)) { + return filters_json; + } + // don't remove unless patch is created to convert all existing filters from object to array + // backward compatibility return Object.keys(filters_json).map((filter) => { let val = filters_json[filter]; return [doctype, filter, val[0], val[1], false]; diff --git a/frappe/public/js/frappe/widgets/quick_list_widget.js b/frappe/public/js/frappe/widgets/quick_list_widget.js index a4b7c83e5446..3e6b1beaa26a 100644 --- a/frappe/public/js/frappe/widgets/quick_list_widget.js +++ b/frappe/public/js/frappe/widgets/quick_list_widget.js @@ -76,7 +76,7 @@ export default class QuickListWidget extends Widget { delete this.filter_group; } - this.filters = frappe.utils.get_filter_from_json(this.quick_list_filter, doctype); + this.filters = frappe.utils.process_filter_expression(this.quick_list_filter); this.filter_group = new frappe.ui.FilterGroup({ parent: this.dialog.get_field("filter_area").$wrapper, @@ -104,7 +104,7 @@ export default class QuickListWidget extends Widget { primary_action: function () { let old_filter = me.quick_list_filter; let filters = me.filter_group.get_filters(); - me.quick_list_filter = frappe.utils.get_filter_as_json(filters); + me.quick_list_filter = JSON.parse(filters); this.hide(); @@ -203,7 +203,7 @@ export default class QuickListWidget extends Widget { fields.push("modified"); - let quick_list_filter = frappe.utils.get_filter_from_json(this.quick_list_filter); + let quick_list_filter = frappe.utils.process_filter_expression(this.quick_list_filter); let args = { method: "frappe.desk.reportview.get", diff --git a/frappe/public/js/frappe/widgets/shortcut_widget.js b/frappe/public/js/frappe/widgets/shortcut_widget.js index d0937f3b46c6..27c430c5d044 100644 --- a/frappe/public/js/frappe/widgets/shortcut_widget.js +++ b/frappe/public/js/frappe/widgets/shortcut_widget.js @@ -67,7 +67,7 @@ export default class ShortcutWidget extends Widget { this.widget.addClass("shortcut-widget-box"); - let filters = frappe.utils.get_filter_from_json(this.stats_filter); + let filters = frappe.utils.process_filter_expression(this.stats_filter); if (this.type == "DocType" && filters) { frappe.db .count(this.link_to, { diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 113ec0712c45..970c81f4560a 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -182,7 +182,7 @@ class QuickListDialog extends WidgetDialog { process_data(data) { if (this.filter_group) { let filters = this.filter_group.get_filters(); - data.quick_list_filter = frappe.utils.get_filter_as_json(filters); + data.quick_list_filter = JSON.stringify(filters); } data.label = data.label ? data.label : data.document_type; @@ -540,7 +540,7 @@ class ShortcutDialog extends WidgetDialog { process_data(data) { if (this.dialog.get_value("type") == "DocType" && this.filter_group) { let filters = this.filter_group.get_filters(); - data.stats_filter = frappe.utils.get_filter_as_json(filters); + data.stats_filter = JSON.stringify(filters); } data.label = data.label ? data.label : frappe.model.unscrub(data.link_to); From 05fe80486806a49b7bc42a9a41ebd54a95d68071 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Tue, 10 Oct 2023 21:18:20 +0530 Subject: [PATCH 037/115] chore: deprecated get_filter_as_json Instead of removing get_filter_as_json add console warn. (cherry picked from commit 8550e7bdedc2d9e67cafa9a5cea323deb1de57cc) --- frappe/public/js/frappe/utils/utils.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index d27d412f4092..7238009c7643 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1602,6 +1602,20 @@ Object.assign(frappe.utils, { return string; } }, + // deprecated! + get_filter_as_json(filters) { + console.warn("frappe.utils.get_filter_as_json is deprecated."); + // convert filter array to json + let filter = null; + if (filters.length) { + filter = {}; + filters.forEach((arr) => { + filter[arr[1]] = [arr[2], arr[3]]; + }); + filter = JSON.stringify(filter); + } + return filter; + }, process_filter_expression(filter) { return new Function(`return ${filter}`)(); From 052423ead7b7139cf57a30aaa55f08ce37c4a445 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:35:53 +0530 Subject: [PATCH 038/115] chore: remove deprecated message (cherry picked from commit 7d4de5675cdb77c272f134d2a53e2fdcab1d4200) --- frappe/public/js/frappe/utils/utils.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 7238009c7643..4722ea104504 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1602,9 +1602,8 @@ Object.assign(frappe.utils, { return string; } }, - // deprecated! + get_filter_as_json(filters) { - console.warn("frappe.utils.get_filter_as_json is deprecated."); // convert filter array to json let filter = null; if (filters.length) { From 589e26d62b00669da8d91094d62b029260200d55 Mon Sep 17 00:00:00 2001 From: Corentin Forler <10946971+cogk@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:16:49 +0200 Subject: [PATCH 039/115] fix: Don't mutate `df` object in frappe.format When customizing the **Communication** DocType by setting the property `in_list_view` for the field `_user_tags` to `1`, the *fieldtype* of the `_user_tags` docfield can be mutated to `"Tag"` by the `frappe.format` function called during the list view rendering. Then, when opening a Communication form view, the layout tries to render the `_user_tags` field which has the invalid fieldtype `"Tag"`, which crashes the rendering of the form. (cherry picked from commit 7c9954e7242323eab6538d1108b6dd3bc7e1d164) --- frappe/public/js/frappe/form/formatters.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 1cc67aed97dd..64ec341f7f29 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -388,7 +388,7 @@ frappe.form.get_formatter = function (fieldtype) { frappe.format = function (value, df, options, doc) { if (!df) df = { fieldtype: "Data" }; - if (df.fieldname == "_user_tags") df.fieldtype = "Tag"; + if (df.fieldname == "_user_tags") df = { ...df, fieldtype: "Tag" }; var fieldtype = df.fieldtype || "Data"; // format Dynamic Link as a Link From b30826d248228b8ce03d08593df2340c1e25ddcb Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:05:36 +0530 Subject: [PATCH 040/115] fix: email from 'Table Multiselect' field (#22733) (#22740) * feat (notificaion): table multiselect field * refactor: use model.table_fields instead of duplicating --------- Co-authored-by: Ankush Menat (cherry picked from commit 94263bf4f21ea15c7087717a50c313643d9f7fbe) Co-authored-by: Sayed Ayman --- frappe/email/doctype/notification/notification.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index eb3e2e8634ad..5be70b14b03f 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -50,7 +50,7 @@ frappe.notification = { if (frm.doc.channel === "Email") { receiver_fields = $.map(fields, function (d) { // Add User and Email fields from child into select dropdown - if (d.fieldtype == "Table") { + if (frappe.model.table_fields.includes(d.fieldtype)) { let child_fields = frappe.get_doc("DocType", d.options).fields; return $.map(child_fields, function (df) { return df.options == "Email" || From fa2a88e306c07c76912583bc175f3ede51cb1ccf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:22:07 +0530 Subject: [PATCH 041/115] fix: sync doctype layout on update standard field (#22699) (#22742) * fix: sync doctype layout on update standard field When standard field is deleted & it is not updated in the doctype layout, Error occurs as it will try to render fields that doesn't exist and layout won't render. to fix this, sync doctype layout on update standard field * refactor: use savepoint decorator --------- Co-authored-by: Ankush Menat (cherry picked from commit bbf91b8afc3aa08b1194c6a704259dda34d5b0ef) Co-authored-by: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> --- frappe/core/doctype/doctype/doctype.py | 14 ++++++++++++++ .../doctype/doctype_layout/doctype_layout.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index c2fdcf905ba2..281ff8ca364a 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -16,6 +16,7 @@ from frappe.cache_manager import clear_controller_cache, clear_user_cache from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.custom.doctype.property_setter.property_setter import make_property_setter +from frappe.database import savepoint from frappe.database.schema import validate_column_length, validate_column_name from frappe.desk.notifications import delete_notification_count_for, get_filters_for from frappe.desk.utils import validate_route_conflict @@ -408,7 +409,9 @@ def on_update(self): if self.flags.in_insert: self.run_module_method("after_doctype_insert") + self.sync_doctype_layouts() delete_notification_count_for(doctype=self.name) + frappe.clear_cache(doctype=self.name) # clear user cache so that on the next reload this doctype is included in boot @@ -419,6 +422,17 @@ def on_update(self): clear_linked_doctype_cache() + @savepoint(catch=Exception) + def sync_doctype_layouts(self): + """Sync Doctype Layout""" + doctype_layouts = frappe.get_all( + "DocType Layout", filters={"document_type": self.name}, pluck="name", ignore_ddl=True + ) + for layout in doctype_layouts: + layout_doc = frappe.get_doc("DocType Layout", layout) + layout_doc.sync_fields() + layout_doc.save() + def setup_autoincrement_and_sequence(self): """Changes name type and makes sequence on change (if required)""" diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py index f712853ccd78..08f1e71df4af 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.py +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -18,7 +18,7 @@ def validate(self): @frappe.whitelist() def sync_fields(self): - doctype_fields = frappe.get_meta(self.document_type).fields + doctype_fields = frappe.get_meta(self.document_type, cached=False).fields if self.is_new(): added_fields = [field.fieldname for field in doctype_fields] From 4430798faaf2222522d21de89f4cda4b10864b7d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 12:31:49 +0530 Subject: [PATCH 042/115] fix: work-around to fix issue with syncing Google Contacts (#22649) (#22743) Issue #22648 (cherry picked from commit aff3f66366eb81e6974903ae983e6dc226fc6922) Co-authored-by: Anand Chitipothu --- .../integrations/doctype/google_contacts/google_contacts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 9a20d5e90538..43cb32498fd1 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -142,6 +142,10 @@ def sync_contacts_from_google_contacts(g_contact): frappe.publish_realtime( "import_google_contacts", dict(progress=idx + 1, total=len(results)), user=frappe.session.user ) + # Work-around to fix + # https://github.com/frappe/frappe/issues/22648 + if not connection.get("names"): + continue for name in connection.get("names"): if name.get("metadata").get("primary"): From cdc16ab59d22ef643893a7fd670f4e724cbd0c55 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 16 Oct 2023 18:10:03 +0530 Subject: [PATCH 043/115] fix: validation permission on `tag` creation (#22753) (#22755) * fix: validation permission on `tag` creation * refactor: apply checks on all operations Add/remove both are controlled by update --------- Co-authored-by: Ankush Menat (cherry picked from commit d06a5808cc2241a7198e8234307745e9e33ffa16) Co-authored-by: ruthra kumar --- frappe/desk/doctype/tag/tag.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 84239fae6d2c..c6cabdf5226c 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -38,8 +38,6 @@ def add_tags(tags, dt, docs, color=None): for tag in tags: DocTags(dt).add(doc, tag) - # return tag - @frappe.whitelist() def remove_tag(tag, dt, dn): @@ -143,6 +141,7 @@ def update_tags(doc, tags): :param doc: Document to be added to global tags """ + doc.check_permission("write") new_tags = {tag.strip() for tag in tags.split(",") if tag} existing_tags = [ tag.tag From 5acecf1c352431cac6c308ffd25e53424b9a711f Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 10:59:00 +0530 Subject: [PATCH 044/115] feat: create single doctype for comparator (cherry picked from commit 513f74ffa6f0319fd3fdd35554879359d77a181c) --- .../doctype/document_comparator/__init__.py | 0 .../document_comparator.json | 63 +++++++++++++++++++ .../test_document_comparator.py | 9 +++ 3 files changed, 72 insertions(+) create mode 100644 frappe/core/doctype/document_comparator/__init__.py create mode 100644 frappe/core/doctype/document_comparator/document_comparator.json create mode 100644 frappe/core/doctype/document_comparator/test_document_comparator.py diff --git a/frappe/core/doctype/document_comparator/__init__.py b/frappe/core/doctype/document_comparator/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/frappe/core/doctype/document_comparator/document_comparator.json b/frappe/core/doctype/document_comparator/document_comparator.json new file mode 100644 index 000000000000..3a60d866e556 --- /dev/null +++ b/frappe/core/doctype/document_comparator/document_comparator.json @@ -0,0 +1,63 @@ +{ + "actions": [], + "creation": "2023-08-14 13:06:24.520160", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "doctype_name", + "column_break_peck", + "document", + "section_break_gppi", + "version_table" + ], + "fields": [ + { + "fieldname": "doctype_name", + "fieldtype": "Link", + "label": "Doctype", + "options": "DocType" + }, + { + "fieldname": "document", + "fieldtype": "Dynamic Link", + "label": "Document", + "options": "doctype_name" + }, + { + "fieldname": "column_break_peck", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_gppi", + "fieldtype": "Section Break" + }, + { + "fieldname": "version_table", + "fieldtype": "HTML", + "label": "version_table" + } + ], + "hide_toolbar": 1, + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2023-08-14 14:08:19.031748", + "modified_by": "Administrator", + "module": "Core", + "name": "Document Comparator", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/document_comparator/test_document_comparator.py b/frappe/core/doctype/document_comparator/test_document_comparator.py new file mode 100644 index 000000000000..937df8eadd3e --- /dev/null +++ b/frappe/core/doctype/document_comparator/test_document_comparator.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestDocumentComparator(FrappeTestCase): + pass From 9c7fa7ca612bb45d59ffa16a6fa6bf016c90c649 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 11:00:52 +0530 Subject: [PATCH 045/115] feat: add logic for changed values and rows (cherry picked from commit e53a78544c174a7420dfe55bd0db6b21fa7e65de) --- .../document_comparator.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 frappe/core/doctype/document_comparator/document_comparator.py diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py new file mode 100644 index 000000000000..5e5b412afcdf --- /dev/null +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -0,0 +1,91 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +import json + +import frappe +from frappe.core.doctype.version.version import get_diff +from frappe.model.document import Document + + +class DocumentComparator(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + doctype_name: DF.Link | None + document: DF.DynamicLink | None + # end: auto-generated types + pass + + @frappe.whitelist() + def compare_document(self): + amended_document_names = frappe.db.get_list( + self.doctype_name, + filters={"name": ("like", "%" + self.document + "%")}, + order_by="modified", + pluck="name", + limit=5, + ) + amended_docs = [frappe.get_doc(self.doctype_name, name) for name in amended_document_names] + + changed = {} + row_changed = {} + for i in range(1, len(amended_docs)): + diff = get_diff(amended_docs[i - 1], amended_docs[i], compare_cancelled=True) + changed = get_diff_grid(amended_docs, i, diff, "changed", changed) + row_changed = get_rows_updated_grid(amended_docs, i, diff, "row_changed", row_changed) + + return amended_document_names, { + "changed": changed, + "row_changed": row_changed, + } + + +def get_diff_grid(amended_docs, i, diff, key, changed_fields): + for change in diff[key]: + fieldname = get_field_label(change[0], doctype=amended_docs[0].doctype) + value = change[-1] + if fieldname not in changed_fields: + changed_fields[fieldname] = [""] * len(amended_docs) + changed_fields[fieldname][i] = value if value else "" + + if i == 1: + value = change[1] + changed_fields[fieldname][i - 1] = value if value else "" + + return changed_fields + + +def get_rows_updated_grid(amended_docs, i, diff, key, changed_fields): + for change in diff[key]: + table_name = get_field_label(change[0], doctype=amended_docs[0].doctype) + changed_fields[table_name] = {"index": change[1]} + for field in change[-1]: + fieldname = get_field_label(field[0], is_child=True) + value = field[-1] + if fieldname not in changed_fields[table_name]: + changed_fields[table_name][fieldname] = [""] * len(amended_docs) + changed_fields[table_name][fieldname][i] = value if value else "" + + if i == 1: + value = field[1] + changed_fields[table_name][fieldname][i - 1] = value if value else "" + + return changed_fields + + +def get_field_label(fieldname, doctype=None, is_child=False): + if is_child: + label = frappe.db.get_value("DocField", {"fieldname": fieldname}, "label") + return label + + meta = frappe.get_meta(doctype) + label = meta.get_label(fieldname) + if label not in ["No Label", "None", ""]: + return label + return fieldname From 3d692d5a95fe61e2b3d71fc96356fde95283cec6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 11:01:55 +0530 Subject: [PATCH 046/115] fix: return df label only when not none (cherry picked from commit 0d80ffc988c1559713f428229170edf7a109ffa3) --- frappe/model/meta.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 01db2e92cef0..d47506ad3e3c 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -246,7 +246,8 @@ def get_label(self, fieldname): """Get label of the given fieldname""" if df := self.get_field(fieldname): - return df.label + if df.label: + return df.label if fieldname in DEFAULT_FIELD_LABELS: return DEFAULT_FIELD_LABELS[fieldname]() From d79e0b9ae827e279630afde6a2f303e55c4808be Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 11:03:34 +0530 Subject: [PATCH 047/115] fix: child rows for cancelled docs in getdiff (cherry picked from commit ef7af5e849e7df08e2eb18b18edb28892a792e88) --- frappe/core/doctype/version/version.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 40ee006a5882..d22a08ce7de2 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -47,7 +47,7 @@ def get_data(self): return json.loads(self.data) -def get_diff(old, new, for_child=False): +def get_diff(old, new, for_child=False, compare_cancelled=False): """Get diff between 2 document objects If there is a change, then returns a dict like: @@ -99,7 +99,19 @@ def get_diff(old, new, for_child=False): # check rows for additions, changes for i, d in enumerate(new_value): - old_row_name = getattr(d, old_row_name_field, None) + old_row_name = None + + if compare_cancelled: + amended_from = frappe.db.get_value(d.parenttype, d.parent, "amended_from") + if amended_from: + parent_doc = frappe.get_doc(d.parenttype, amended_from) + old_table = parent_doc.get(d.parentfield) + if old_table and len(old_table) > i: + old_row_name = old_table[i].name + + if not old_row_name: + old_row_name = getattr(d, old_row_name_field, None) + if old_row_name and old_row_name in old_rows_by_name: found_rows.add(old_row_name) From b4fd3817361bb291cfa9f858d81a524ef42e6c76 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 11:04:31 +0530 Subject: [PATCH 048/115] feat: add html template for comparator (cherry picked from commit c3d0ab5f9a4ef4c961bf11e7574fb2175bc7d3a7) --- .../document_comparator.html | 74 +++++++++++++++++++ .../document_comparator.js | 35 +++++++++ 2 files changed, 109 insertions(+) create mode 100644 frappe/core/doctype/document_comparator/document_comparator.html create mode 100644 frappe/core/doctype/document_comparator/document_comparator.js diff --git a/frappe/core/doctype/document_comparator/document_comparator.html b/frappe/core/doctype/document_comparator/document_comparator.html new file mode 100644 index 000000000000..3b4950adea5a --- /dev/null +++ b/frappe/core/doctype/document_comparator/document_comparator.html @@ -0,0 +1,74 @@ +
+ +
+ {% if documents.length > 1 %} +

Documents to Compare : {{ documents.length }}

+ {% else %} +

Documents to Compare : {{ documents.length - 1 }}

+ {% endif %} +
+ + {% var field_keys = Object.keys(changed).sort(); %} + {% if field_keys.length > 0 %} +
+
Changes
+ + + + {% for doc in documents %} + + {% endfor %} + + + {% for fieldname in field_keys %} + + + {% var values = changed[fieldname] %} + + {% for value in values %} + + {% endfor %} + + {% endfor %} + +
Fields {{ doc }}
{{ fieldname }} {{ value }}
+
+ {% endif %} + + {% var tables = Object.keys(row_changed).sort(); %} + {% if tables.length > 0 %} +
+
Rows Updated
+ + + + {% for doc in documents %} + + {% endfor %} + + + {% for table in tables %} + + + {% var row_index = row_changed[table]["index"] %} + + {% var fields = Object.keys(row_changed[table]).sort(); %} + {% for field in fields %} + {% if field != "index" %} + + + {% var values = row_changed[table][field] %} + {% for value in values %} + + {% endfor %} + + {% endfor %} + {% endfor %} + + {% endfor %} + +
Fields {{ doc }}
{{ table }}
idx : {{ row_index }}
{{ field }} {{ value }}
+
+ {% endif %} + +
\ No newline at end of file diff --git a/frappe/core/doctype/document_comparator/document_comparator.js b/frappe/core/doctype/document_comparator/document_comparator.js new file mode 100644 index 000000000000..22e73a848f85 --- /dev/null +++ b/frappe/core/doctype/document_comparator/document_comparator.js @@ -0,0 +1,35 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Document Comparator", { + refresh(frm) { + frm.disable_save(); + + frm.set_query("doctype_name", () => { + return { + filters: { + track_changes: 1, + }, + }; + }); + + frm.page.set_primary_action("Compare", () => { + frm.call({ + doc: frm.doc, + method: "compare_document", + callback: function (r) { + let document_names = r.message[0]; + let changed_fields = r.message[1]; + let render_dict = { + documents: document_names, + changed: changed_fields.changed, + row_changed: changed_fields.row_changed, + }; + $(frappe.render_template("document_comparator", render_dict)).appendTo( + frm.fields_dict.version_table.$wrapper.empty() + ); + }, + }); + }); + }, +}); From 4b8d6e88613c4f06d7f887dcdf7bb35f4a1efbd6 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 11:27:34 +0530 Subject: [PATCH 049/115] fix: remove indicator (cherry picked from commit 0b00aabc6aac5518e9423217bc01e8d61f887518) --- frappe/core/doctype/document_comparator/document_comparator.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/core/doctype/document_comparator/document_comparator.js b/frappe/core/doctype/document_comparator/document_comparator.js index 22e73a848f85..4119af7378af 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.js +++ b/frappe/core/doctype/document_comparator/document_comparator.js @@ -3,6 +3,8 @@ frappe.ui.form.on("Document Comparator", { refresh(frm) { + frm.page.clear_indicator(); + frm.disable_save(); frm.set_query("doctype_name", () => { From e7a326494a9604a99e0ffcfe63a5c331efc3a50d Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 17:27:11 +0530 Subject: [PATCH 050/115] fix: multiple rows changed in each table (cherry picked from commit 53f5280af06817dadfe1a2a0bb8e87157eb44b30) --- .../document_comparator.html | 12 ++++++------ .../document_comparator/document_comparator.js | 2 ++ .../document_comparator.json | 3 ++- .../document_comparator/document_comparator.py | 17 ++++++++++++----- 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.html b/frappe/core/doctype/document_comparator/document_comparator.html index 3b4950adea5a..6a5ec2f2b8fa 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.html +++ b/frappe/core/doctype/document_comparator/document_comparator.html @@ -50,14 +50,14 @@
Rows Updated
{% for table in tables %} {{ table }} - {% var row_index = row_changed[table]["index"] %} - idx : {{ row_index }} - {% var fields = Object.keys(row_changed[table]).sort(); %} - {% for field in fields %} - {% if field != "index" %} + {% var rows = Object.keys(row_changed[table]).sort(); %} + {% for idx in rows %} + idx : {{ idx }} + {% var fields = Object.keys(row_changed[table][idx]).sort(); %} + {% for field in fields %} {{ field }} - {% var values = row_changed[table][field] %} + {% var values = row_changed[table][idx][field] %} {% for value in values %} {{ value }} {% endfor %} diff --git a/frappe/core/doctype/document_comparator/document_comparator.js b/frappe/core/doctype/document_comparator/document_comparator.js index 4119af7378af..45f70c45e94d 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.js +++ b/frappe/core/doctype/document_comparator/document_comparator.js @@ -11,6 +11,7 @@ frappe.ui.form.on("Document Comparator", { return { filters: { track_changes: 1, + is_submittable: 1, }, }; }); @@ -30,6 +31,7 @@ frappe.ui.form.on("Document Comparator", { $(frappe.render_template("document_comparator", render_dict)).appendTo( frm.fields_dict.version_table.$wrapper.empty() ); + frm.set_df_property("version_table", "hidden", 0); }, }); }); diff --git a/frappe/core/doctype/document_comparator/document_comparator.json b/frappe/core/doctype/document_comparator/document_comparator.json index 3a60d866e556..fc0644731f70 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.json +++ b/frappe/core/doctype/document_comparator/document_comparator.json @@ -36,6 +36,7 @@ { "fieldname": "version_table", "fieldtype": "HTML", + "hidden": 1, "label": "version_table" } ], @@ -43,7 +44,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-14 14:08:19.031748", + "modified": "2023-08-18 14:07:50.848659", "modified_by": "Administrator", "module": "Core", "name": "Document Comparator", diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index 5e5b412afcdf..4857b8e5b652 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -62,19 +62,26 @@ def get_diff_grid(amended_docs, i, diff, key, changed_fields): def get_rows_updated_grid(amended_docs, i, diff, key, changed_fields): + # set an empty dictionary for each table + # so it does not get overwritten for every change in same table + for table in diff[key]: + table_name = get_field_label(table[0], doctype=amended_docs[0].doctype) + changed_fields[table_name] = {} + for change in diff[key]: table_name = get_field_label(change[0], doctype=amended_docs[0].doctype) - changed_fields[table_name] = {"index": change[1]} + index = change[1] + changed_fields[table_name][index] = {} for field in change[-1]: fieldname = get_field_label(field[0], is_child=True) value = field[-1] - if fieldname not in changed_fields[table_name]: - changed_fields[table_name][fieldname] = [""] * len(amended_docs) - changed_fields[table_name][fieldname][i] = value if value else "" + if fieldname not in changed_fields[table_name][index]: + changed_fields[table_name][index][fieldname] = [""] * len(amended_docs) + changed_fields[table_name][index][fieldname][i] = value if value else "" if i == 1: value = field[1] - changed_fields[table_name][fieldname][i - 1] = value if value else "" + changed_fields[table_name][index][fieldname][i - 1] = value if value else "" return changed_fields From 301a698cc85232a2672b254a1209dd5691300924 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 18:14:23 +0530 Subject: [PATCH 051/115] refactor: code cleanup (cherry picked from commit 6dbbdd0afe77f69d8dad3af6fee5df6fb2336ce1) --- .../document_comparator.json | 10 +- .../document_comparator.py | 93 +++++++++---------- 2 files changed, 50 insertions(+), 53 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.json b/frappe/core/doctype/document_comparator/document_comparator.json index fc0644731f70..a41bab478f82 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.json +++ b/frappe/core/doctype/document_comparator/document_comparator.json @@ -16,14 +16,18 @@ { "fieldname": "doctype_name", "fieldtype": "Link", + "in_list_view": 1, "label": "Doctype", - "options": "DocType" + "options": "DocType", + "reqd": 1 }, { "fieldname": "document", "fieldtype": "Dynamic Link", + "in_list_view": 1, "label": "Document", - "options": "doctype_name" + "options": "doctype_name", + "reqd": 1 }, { "fieldname": "column_break_peck", @@ -44,7 +48,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-18 14:07:50.848659", + "modified": "2023-08-18 17:57:02.576653", "modified_by": "Administrator", "module": "Core", "name": "Document Comparator", diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index 4857b8e5b652..dcf6537e64f1 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -17,8 +17,8 @@ class DocumentComparator(Document): if TYPE_CHECKING: from frappe.types import DF - doctype_name: DF.Link | None - document: DF.DynamicLink | None + doctype_name: DF.Link + document: DF.DynamicLink # end: auto-generated types pass @@ -32,58 +32,51 @@ def compare_document(self): limit=5, ) amended_docs = [frappe.get_doc(self.doctype_name, name) for name in amended_document_names] + self.docs_to_compare = len(amended_docs) - changed = {} - row_changed = {} - for i in range(1, len(amended_docs)): + self.changed = {} + self.row_changed = {} + + for i in range(1, self.docs_to_compare): diff = get_diff(amended_docs[i - 1], amended_docs[i], compare_cancelled=True) - changed = get_diff_grid(amended_docs, i, diff, "changed", changed) - row_changed = get_rows_updated_grid(amended_docs, i, diff, "row_changed", row_changed) - - return amended_document_names, { - "changed": changed, - "row_changed": row_changed, - } - - -def get_diff_grid(amended_docs, i, diff, key, changed_fields): - for change in diff[key]: - fieldname = get_field_label(change[0], doctype=amended_docs[0].doctype) - value = change[-1] - if fieldname not in changed_fields: - changed_fields[fieldname] = [""] * len(amended_docs) - changed_fields[fieldname][i] = value if value else "" - - if i == 1: - value = change[1] - changed_fields[fieldname][i - 1] = value if value else "" - - return changed_fields - - -def get_rows_updated_grid(amended_docs, i, diff, key, changed_fields): - # set an empty dictionary for each table - # so it does not get overwritten for every change in same table - for table in diff[key]: - table_name = get_field_label(table[0], doctype=amended_docs[0].doctype) - changed_fields[table_name] = {} - - for change in diff[key]: - table_name = get_field_label(change[0], doctype=amended_docs[0].doctype) - index = change[1] - changed_fields[table_name][index] = {} - for field in change[-1]: - fieldname = get_field_label(field[0], is_child=True) - value = field[-1] - if fieldname not in changed_fields[table_name][index]: - changed_fields[table_name][index][fieldname] = [""] * len(amended_docs) - changed_fields[table_name][index][fieldname][i] = value if value else "" + self.get_diff_grid(i, diff) + self.get_rows_updated_grid(i, diff) - if i == 1: - value = field[1] - changed_fields[table_name][index][fieldname][i - 1] = value if value else "" + return amended_document_names, {"changed": self.changed, "row_changed": self.row_changed} - return changed_fields + def get_diff_grid(self, i, diff): + for change in diff.changed: + fieldname = get_field_label(change[0], doctype=self.doctype_name) + value = change[-1] + if fieldname not in self.changed: + self.changed[fieldname] = [""] * self.docs_to_compare + self.changed[fieldname][i] = value or "" + + if i == 1: + value = change[1] + self.changed[fieldname][i - 1] = value or "" + + def get_rows_updated_grid(self, i, diff): + # set an empty dictionary for each table + # so it does not get overwritten for every change in same table + for table in diff.row_changed: + table_name = get_field_label(table[0], doctype=self.doctype_name) + self.row_changed[table_name] = {} + + for change in diff.row_changed: + table_name = get_field_label(change[0], doctype=self.doctype_name) + index = change[1] + self.row_changed[table_name][index] = {} + for field in change[-1]: + fieldname = get_field_label(field[0], is_child=True) + value = field[-1] + if fieldname not in self.row_changed[table_name][index]: + self.row_changed[table_name][index][fieldname] = [""] * self.docs_to_compare + self.row_changed[table_name][index][fieldname][i] = value or "" + + if i == 1: + value = field[1] + self.row_changed[table_name][index][fieldname][i - 1] = value or "" def get_field_label(fieldname, doctype=None, is_child=False): From 53b37835330ee8de4bdc0f5e888f5f207ac95e5a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 18 Aug 2023 18:25:25 +0530 Subject: [PATCH 052/115] feat: add validation for mandatory fields (cherry picked from commit 4bc3d2e4b212d1ab20ee00d99ee1087b745234ee) --- .../document_comparator/document_comparator.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index dcf6537e64f1..36f4d2876df6 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -4,6 +4,7 @@ import json import frappe +from frappe import _ from frappe.core.doctype.version.version import get_diff from frappe.model.document import Document @@ -22,8 +23,21 @@ class DocumentComparator(Document): # end: auto-generated types pass + def validate(self): + self.validate_doctype_name() + self.validate_document() + + def validate_doctype_name(self): + if not self.doctype_name: + frappe.throw(_("{} field cannot be empty.".format(frappe.bold("Doctype")))) + + def validate_document(self): + if not self.document: + frappe.throw(_("{} field cannot be empty.".format(frappe.bold("Document")))) + @frappe.whitelist() def compare_document(self): + self.validate() amended_document_names = frappe.db.get_list( self.doctype_name, filters={"name": ("like", "%" + self.document + "%")}, From 39ec51380bb7d9fc0436109c2edd3064cf90c1dd Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 21 Aug 2023 13:07:49 +0530 Subject: [PATCH 053/115] test: changed fields and rows (cherry picked from commit f916c6821e57e6eda154e4b0d505781835f26cbd) --- .../document_comparator.py | 4 +- .../test_document_comparator.py | 114 +++++++++++++++++- 2 files changed, 114 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index 36f4d2876df6..856e4fcbd036 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -29,11 +29,11 @@ def validate(self): def validate_doctype_name(self): if not self.doctype_name: - frappe.throw(_("{} field cannot be empty.".format(frappe.bold("Doctype")))) + frappe.throw(_("{} field cannot be empty.").format(frappe.bold("Doctype"))) def validate_document(self): if not self.document: - frappe.throw(_("{} field cannot be empty.".format(frappe.bold("Document")))) + frappe.throw(_("{} field cannot be empty.").format(frappe.bold("Document"))) @frappe.whitelist() def compare_document(self): diff --git a/frappe/core/doctype/document_comparator/test_document_comparator.py b/frappe/core/doctype/document_comparator/test_document_comparator.py index 937df8eadd3e..596a6648f66a 100644 --- a/frappe/core/doctype/document_comparator/test_document_comparator.py +++ b/frappe/core/doctype/document_comparator/test_document_comparator.py @@ -1,9 +1,119 @@ # Copyright (c) 2023, Frappe Technologies and Contributors # See license.txt -# import frappe +import frappe from frappe.tests.utils import FrappeTestCase class TestDocumentComparator(FrappeTestCase): - pass + def setUp(self): + self.child_doctype = create_custom_child_doctype() + self.custom_doctype = create_custom_doctype() + + def test_compare_changed_fields(self): + doc = frappe.new_doc("Test Custom Doctype for Doc Comparator") + doc.test_field = "first value" + doc.submit() + doc.cancel() + + changed_fields = frappe._dict(test_field="second value") + amended_doc = amend_document(doc, changed_fields, {}, 1) + amended_doc.cancel() + + changed_fields = frappe._dict(test_field="third value") + re_amended_doc = amend_document(amended_doc, changed_fields, {}, 1) + + comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", doc.name) + documents, results = comparator.compare_document() + + test_field_values = results["changed"]["Field"] + self.check_expected_values(test_field_values, ["first value", "second value", "third value"]) + + def test_compare_rows_updated(self): + doc = frappe.new_doc("Test Custom Doctype for Doc Comparator") + doc.append("child_table_field", {"test_table_field": "old row value"}) + doc.submit() + doc.cancel() + + rows_updated = frappe._dict(child_table_field={"test_table_field": "new row value"}) + amended_doc = amend_document(doc, {}, rows_updated, 1) + + comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", doc.name) + documents, results = comparator.compare_document() + + table_field_values = results["row_changed"]["Child Table Field"][0]["Table Field"] + self.check_expected_values(table_field_values, ["old row value", "new row value"]) + + def check_expected_values(self, values_to_check, expected_values): + for i in range(len(values_to_check)): + self.assertEqual(values_to_check[i], expected_values[i]) + + def tearDown(self): + self.child_doctype.delete() + self.custom_doctype.delete() + + +def create_custom_child_doctype(): + child_doctype = frappe.get_doc( + { + "doctype": "DocType", + "module": "Core", + "name": "Test Custom Child for Doc Comparator", + "custom": 1, + "istable": 1, + "fields": [ + { + "label": "Table Field", + "fieldname": "test_table_field", + "fieldtype": "Data", + }, + ], + } + ).insert(ignore_if_duplicate=True) + return child_doctype + + +def create_custom_doctype(): + custom_doctype = frappe.get_doc( + { + "doctype": "DocType", + "module": "Core", + "name": "Test Custom Doctype for Doc Comparator", + "custom": 1, + "is_submittable": 1, + "fields": [ + { + "label": "Field", + "fieldname": "test_field", + "fieldtype": "Data", + }, + { + "label": "Child Table Field", + "fieldname": "child_table_field", + "fieldtype": "Table", + "options": "Test Custom Child for Doc Comparator", + }, + ], + "permissions": [{"role": "System Manager", "read": 1}], + } + ).insert(ignore_if_duplicate=True) + return custom_doctype + + +def amend_document(amend_from, changed_fields, rows_updated, submit=False): + amended_doc = frappe.copy_doc(amend_from) + amended_doc.amended_from = amend_from.name + amended_doc.update(changed_fields) + for child_table in rows_updated: + amended_doc.set(child_table, []) + amended_doc.append(child_table, rows_updated[child_table]) + if submit: + amended_doc.submit() + return amended_doc + + +def create_comparator_doc(doctype_name, document): + comparator = frappe.new_doc("Document Comparator") + comparator.doctype_name = doctype_name + comparator.document = document + return comparator From 426733989a7e590c6a340356b5816e05cf05068d Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 23 Aug 2023 12:26:35 +0530 Subject: [PATCH 054/115] feat: add collapsible sections for row additions and deletions (cherry picked from commit a0fe974530da0c30fb66030034ffc77368d743d7) --- .../document_comparator.json | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.json b/frappe/core/doctype/document_comparator/document_comparator.json index a41bab478f82..821772fae887 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.json +++ b/frappe/core/doctype/document_comparator/document_comparator.json @@ -10,7 +10,11 @@ "column_break_peck", "document", "section_break_gppi", - "version_table" + "version_table", + "rows_added_section", + "rows_added", + "rows_removed_section", + "rows_removed" ], "fields": [ { @@ -42,13 +46,35 @@ "fieldtype": "HTML", "hidden": 1, "label": "version_table" + }, + { + "fieldname": "rows_added", + "fieldtype": "HTML" + }, + { + "fieldname": "rows_removed", + "fieldtype": "HTML" + }, + { + "collapsible": 1, + "fieldname": "rows_added_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Rows Added" + }, + { + "collapsible": 1, + "fieldname": "rows_removed_section", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Rows Removed" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2023-08-18 17:57:02.576653", + "modified": "2023-08-22 12:12:59.780845", "modified_by": "Administrator", "module": "Core", "name": "Document Comparator", From 38dbb93e66ff5e7cbf25d0bafd8e722ad9b54dc0 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 23 Aug 2023 12:27:34 +0530 Subject: [PATCH 055/115] feat: add logic for addition and deletion grids (cherry picked from commit 98437f1c3091b52c83c612763cc9596dd8a4dc40) --- .../document_comparator.py | 64 +++++++++++++------ 1 file changed, 44 insertions(+), 20 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index 856e4fcbd036..4079ead137e5 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -45,47 +45,58 @@ def compare_document(self): pluck="name", limit=5, ) - amended_docs = [frappe.get_doc(self.doctype_name, name) for name in amended_document_names] - self.docs_to_compare = len(amended_docs) + self.amended_docs = [frappe.get_doc(self.doctype_name, name) for name in amended_document_names] + self.docs_to_compare = len(self.amended_docs) self.changed = {} self.row_changed = {} + self.added = {} + self.removed = {} for i in range(1, self.docs_to_compare): - diff = get_diff(amended_docs[i - 1], amended_docs[i], compare_cancelled=True) + diff = get_diff(self.amended_docs[i - 1], self.amended_docs[i], compare_cancelled=True) self.get_diff_grid(i, diff) + self.get_rows_added_removed_grid(i, diff, "added", self.added) + self.get_rows_added_removed_grid(i, diff, "removed", self.removed) self.get_rows_updated_grid(i, diff) - return amended_document_names, {"changed": self.changed, "row_changed": self.row_changed} + return amended_document_names, { + "changed": self.changed, + "row_changed": self.row_changed, + "added": self.added, + "removed": self.removed, + } def get_diff_grid(self, i, diff): for change in diff.changed: fieldname = get_field_label(change[0], doctype=self.doctype_name) value = change[-1] - if fieldname not in self.changed: - self.changed[fieldname] = [""] * self.docs_to_compare + value_list = [""] * self.docs_to_compare + self.changed.setdefault(fieldname, value_list) self.changed[fieldname][i] = value or "" if i == 1: value = change[1] self.changed[fieldname][i - 1] = value or "" - def get_rows_updated_grid(self, i, diff): - # set an empty dictionary for each table - # so it does not get overwritten for every change in same table - for table in diff.row_changed: - table_name = get_field_label(table[0], doctype=self.doctype_name) - self.row_changed[table_name] = {} + def get_rows_added_removed_grid(self, i, diff, key, changed_dict): + doc_name = self.amended_docs[i].name + changed_dict[doc_name] = {} + for change in diff[key]: + tablename = get_field_label(change[0], doctype=self.doctype_name) + value_dict = filter_fields_for_gridview(change[-1]) + changed_dict[doc_name].setdefault(tablename, []).append(value_dict) + def get_rows_updated_grid(self, i, diff): for change in diff.row_changed: table_name = get_field_label(change[0], doctype=self.doctype_name) index = change[1] - self.row_changed[table_name][index] = {} + self.row_changed.setdefault(table_name, {}).setdefault(index, {}) for field in change[-1]: - fieldname = get_field_label(field[0], is_child=True) + fieldname = get_field_label(field[0], doctype=self.doctype_name, child_field=change[0]) value = field[-1] - if fieldname not in self.row_changed[table_name][index]: - self.row_changed[table_name][index][fieldname] = [""] * self.docs_to_compare + value_list = [""] * self.docs_to_compare + self.row_changed[table_name][index].setdefault(fieldname, value_list) self.row_changed[table_name][index][fieldname][i] = value or "" if i == 1: @@ -93,13 +104,26 @@ def get_rows_updated_grid(self, i, diff): self.row_changed[table_name][index][fieldname][i - 1] = value or "" -def get_field_label(fieldname, doctype=None, is_child=False): - if is_child: - label = frappe.db.get_value("DocField", {"fieldname": fieldname}, "label") - return label +def get_field_label(fieldname, doctype, child_field=None): + if child_field: + meta = frappe.get_meta(doctype) + for field in meta.fields: + if field.fieldname == child_field: + doctype = field.options meta = frappe.get_meta(doctype) label = meta.get_label(fieldname) if label not in ["No Label", "None", ""]: return label return fieldname + + +def filter_fields_for_gridview(row): + grid_row = {} + meta = frappe.get_meta(row.doctype) + for field in meta.fields: + if field.in_list_view == 1: + fieldlabel = get_field_label(field.fieldname, row.doctype) + grid_row[fieldlabel] = row[field.fieldname] or "" + + return grid_row From 56d89112bf967de133928677527d73b658d8b1ce Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 23 Aug 2023 12:29:19 +0530 Subject: [PATCH 056/115] feat: add js for rendering html grids (cherry picked from commit 57e4e0bc30c502dc1d12cbc193f864ddf722708f) --- .../document_comparator.js | 46 +++++++++++++++---- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.js b/frappe/core/doctype/document_comparator/document_comparator.js index 45f70c45e94d..ec31200573d6 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.js +++ b/frappe/core/doctype/document_comparator/document_comparator.js @@ -23,17 +23,45 @@ frappe.ui.form.on("Document Comparator", { callback: function (r) { let document_names = r.message[0]; let changed_fields = r.message[1]; - let render_dict = { - documents: document_names, - changed: changed_fields.changed, - row_changed: changed_fields.row_changed, - }; - $(frappe.render_template("document_comparator", render_dict)).appendTo( - frm.fields_dict.version_table.$wrapper.empty() - ); - frm.set_df_property("version_table", "hidden", 0); + frm.events.render_changed_fields(frm, document_names, changed_fields); + frm.events.render_rows_added_or_removed(frm, changed_fields); }, }); }); }, + + render_changed_fields(frm, document_names, changed_fields) { + let render_dict = { + documents: document_names, + changed: changed_fields.changed, + row_changed: changed_fields.row_changed, + }; + $(frappe.render_template("document_comparator", render_dict)).appendTo( + frm.fields_dict.version_table.$wrapper.empty() + ); + frm.set_df_property("version_table", "hidden", 0); + }, + + render_rows_added_or_removed(frm, changed_fields) { + let added_or_removed = { + rows_added: changed_fields.added, + rows_removed: changed_fields.removed, + }; + + let hide_section = 0; + let section_dict = {}; + + for (let key in added_or_removed) { + hide_section = 0; + section_dict = { + added_or_removed: added_or_removed[key], + }; + $( + frappe.render_template("document_comparator_rows_added_removed", section_dict) + ).appendTo(frm.fields_dict[key].$wrapper.empty()); + + if (!frm.fields_dict[key].disp_area.innerHTML.includes(" Date: Wed, 23 Aug 2023 12:30:08 +0530 Subject: [PATCH 057/115] feat: add html template for additions and deletions (cherry picked from commit c598fde22e7fa4b4192963a92c5bc1026277fcc3) --- .../document_comparator.html | 15 ++++---- ...ocument_comparator_rows_added_removed.html | 34 +++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 frappe/core/doctype/document_comparator/document_comparator_rows_added_removed.html diff --git a/frappe/core/doctype/document_comparator/document_comparator.html b/frappe/core/doctype/document_comparator/document_comparator.html index 6a5ec2f2b8fa..74ed663e655b 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.html +++ b/frappe/core/doctype/document_comparator/document_comparator.html @@ -11,7 +11,7 @@ {% var field_keys = Object.keys(changed).sort(); %} {% if field_keys.length > 0 %}
-
Changes
+
Fields Changed
@@ -38,18 +38,21 @@
Changes
{% var tables = Object.keys(row_changed).sort(); %} {% if tables.length > 0 %}
-
Rows Updated
+
Rows Updated
Fields
- {% for doc in documents %} - - {% endfor %} {% for table in tables %} - + + + {% for doc in documents %} + + {% endfor %} + + {% var rows = Object.keys(row_changed[table]).sort(); %} {% for idx in rows %} diff --git a/frappe/core/doctype/document_comparator/document_comparator_rows_added_removed.html b/frappe/core/doctype/document_comparator/document_comparator_rows_added_removed.html new file mode 100644 index 000000000000..355ac9f70b91 --- /dev/null +++ b/frappe/core/doctype/document_comparator/document_comparator_rows_added_removed.html @@ -0,0 +1,34 @@ +
+ {% var docs = Object.keys(added_or_removed) %} + {% for doc in docs %} +
+ {% if Object.keys(added_or_removed[doc]).length > 0 %} +
{{ doc }}
+
+ {% var tables = Object.keys(added_or_removed[doc]) %} + {% for table in tables %} +
{{ table }}
+
Fields {{ doc }}
{{ table }}
{{ table }}{{ doc }}
idx : {{ idx }}
+ + {% var fieldnames = Object.keys(added_or_removed[doc][table][0]) %} + {% for fieldname in fieldnames %} + + {% endfor %} + + + {% var rows = Object.keys(added_or_removed[doc][table]) %} + {% for row in rows %} + + {% var field_keys = Object.keys(added_or_removed[doc][table][row]) %} + {% for key in field_keys %} + + {% endfor %} + + {% endfor %} + +
{{ fieldname }}
{{ added_or_removed[doc][table][row][key] }}
+ {% endfor %} + {% endif %} +
+ {% endfor %} +
\ No newline at end of file From 25587dd32d510ce06fbe2ad65159ab5463419f51 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Wed, 23 Aug 2023 15:51:33 +0530 Subject: [PATCH 058/115] test: rows added dict (cherry picked from commit 9630df16a5859f953a77ef4956004946250e04bc) --- .../test_document_comparator.py | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/document_comparator/test_document_comparator.py b/frappe/core/doctype/document_comparator/test_document_comparator.py index 596a6648f66a..56e1bf891933 100644 --- a/frappe/core/doctype/document_comparator/test_document_comparator.py +++ b/frappe/core/doctype/document_comparator/test_document_comparator.py @@ -29,20 +29,35 @@ def test_compare_changed_fields(self): test_field_values = results["changed"]["Field"] self.check_expected_values(test_field_values, ["first value", "second value", "third value"]) - def test_compare_rows_updated(self): + def test_compare_rows(self): doc = frappe.new_doc("Test Custom Doctype for Doc Comparator") - doc.append("child_table_field", {"test_table_field": "old row value"}) + doc.append("child_table_field", {"test_table_field": "old row 1 value"}) doc.submit() doc.cancel() - rows_updated = frappe._dict(child_table_field={"test_table_field": "new row value"}) + child_table_new = [{"test_table_field": "new row 1 value"}, {"test_table_field": "row 2 value"}] + rows_updated = frappe._dict(child_table_field=child_table_new) amended_doc = amend_document(doc, {}, rows_updated, 1) comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", doc.name) documents, results = comparator.compare_document() - table_field_values = results["row_changed"]["Child Table Field"][0]["Table Field"] - self.check_expected_values(table_field_values, ["old row value", "new row value"]) + results = frappe._dict(results) + self.check_rows_updated(results.row_changed) + self.check_rows_added(results.added[amended_doc.name]) + + def check_rows_updated(self, row_changed): + self.assertIn("Child Table Field", row_changed) + self.assertIn(0, row_changed["Child Table Field"]) + self.assertIn("Table Field", row_changed["Child Table Field"][0]) + table_field_values = row_changed["Child Table Field"][0]["Table Field"] + self.check_expected_values(table_field_values, ["old row 1 value", "new row 1 value"]) + + def check_rows_added(self, rows_added): + self.assertIn("Child Table Field", rows_added) + child_table = rows_added["Child Table Field"] + self.assertIn("Table Field", child_table[0]) + self.check_expected_values(child_table[0]["Table Field"], "row 2 value") def check_expected_values(self, values_to_check, expected_values): for i in range(len(values_to_check)): @@ -66,6 +81,7 @@ def create_custom_child_doctype(): "label": "Table Field", "fieldname": "test_table_field", "fieldtype": "Data", + "in_list_view": 1, }, ], } @@ -105,8 +121,7 @@ def amend_document(amend_from, changed_fields, rows_updated, submit=False): amended_doc.amended_from = amend_from.name amended_doc.update(changed_fields) for child_table in rows_updated: - amended_doc.set(child_table, []) - amended_doc.append(child_table, rows_updated[child_table]) + amended_doc.set(child_table, rows_updated[child_table]) if submit: amended_doc.submit() return amended_doc From 4fbda9b9c57a8b228b4ec4aeef3dd704c52e6319 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 11 Sep 2023 11:06:01 +0530 Subject: [PATCH 059/115] fix: check label for fields (cherry picked from commit 177955a12f18d0ded4234dc5c7d41f7c3dd2b842) --- frappe/core/doctype/document_comparator/document_comparator.py | 2 +- frappe/model/meta.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index 4079ead137e5..64fe701622ae 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -113,7 +113,7 @@ def get_field_label(fieldname, doctype, child_field=None): meta = frappe.get_meta(doctype) label = meta.get_label(fieldname) - if label not in ["No Label", "None", ""]: + if label not in ["No Label", None, ""]: return label return fieldname diff --git a/frappe/model/meta.py b/frappe/model/meta.py index d47506ad3e3c..7d7ceb3567a6 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -246,8 +246,7 @@ def get_label(self, fieldname): """Get label of the given fieldname""" if df := self.get_field(fieldname): - if df.label: - return df.label + return df.get("label") if fieldname in DEFAULT_FIELD_LABELS: return DEFAULT_FIELD_LABELS[fieldname]() From 9b12aa6d939d39d4dedb5b112cec36ecbd956ee7 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 11 Sep 2023 11:10:06 +0530 Subject: [PATCH 060/115] fix: fetch prev docs from last amended doc (cherry picked from commit 0b722906dbcfa4d8c47ae846a8c988720931e458) --- .../document_comparator.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/document_comparator/document_comparator.py index 64fe701622ae..e795417b26b6 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/document_comparator/document_comparator.py @@ -38,20 +38,10 @@ def validate_document(self): @frappe.whitelist() def compare_document(self): self.validate() - amended_document_names = frappe.db.get_list( - self.doctype_name, - filters={"name": ("like", "%" + self.document + "%")}, - order_by="modified", - pluck="name", - limit=5, - ) + amended_document_names = self.get_amended_documents() self.amended_docs = [frappe.get_doc(self.doctype_name, name) for name in amended_document_names] self.docs_to_compare = len(self.amended_docs) - - self.changed = {} - self.row_changed = {} - self.added = {} - self.removed = {} + self.changed, self.row_changed, self.added, self.removed = {}, {}, {}, {} for i in range(1, self.docs_to_compare): diff = get_diff(self.amended_docs[i - 1], self.amended_docs[i], compare_cancelled=True) @@ -67,6 +57,16 @@ def compare_document(self): "removed": self.removed, } + def get_amended_documents(self): + amended_document_names = [] + curr_doc = self.document + while curr_doc and len(amended_document_names) < 5: + amended_document_names.append(curr_doc) + curr_doc = frappe.db.get_value(self.doctype_name, curr_doc, "amended_from") + amended_document_names = amended_document_names[::-1] + + return amended_document_names + def get_diff_grid(self, i, diff): for change in diff.changed: fieldname = get_field_label(change[0], doctype=self.doctype_name) From b6eeeb39d11d2b7f095ac69704aaa33613f4b007 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 11 Sep 2023 11:15:24 +0530 Subject: [PATCH 061/115] refactor: use old value instead of fetching parent doc (cherry picked from commit 4add076285081acd50c02764fdb1448fbb968b24) --- frappe/core/doctype/version/version.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index d22a08ce7de2..2ae6f9b1a496 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -99,18 +99,11 @@ def get_diff(old, new, for_child=False, compare_cancelled=False): # check rows for additions, changes for i, d in enumerate(new_value): - old_row_name = None - + old_row_name = getattr(d, old_row_name_field, None) if compare_cancelled: - amended_from = frappe.db.get_value(d.parenttype, d.parent, "amended_from") - if amended_from: - parent_doc = frappe.get_doc(d.parenttype, amended_from) - old_table = parent_doc.get(d.parentfield) - if old_table and len(old_table) > i: - old_row_name = old_table[i].name - - if not old_row_name: - old_row_name = getattr(d, old_row_name_field, None) + if amended_from := frappe.db.get_value(d.parenttype, d.parent, "amended_from"): + if len(old_value) > i: + old_row_name = old_value[i].name if old_row_name and old_row_name in old_rows_by_name: found_rows.add(old_row_name) From d14613267b026153fcaacc8ac29e5e6eef5cf245 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 11 Sep 2023 11:49:40 +0530 Subject: [PATCH 062/115] fix: use latest doc name for test (cherry picked from commit a7a6f438beef92fb1884131ea06ab718f37aa8d5) --- .../doctype/document_comparator/test_document_comparator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/document_comparator/test_document_comparator.py b/frappe/core/doctype/document_comparator/test_document_comparator.py index 56e1bf891933..af1f19874bcd 100644 --- a/frappe/core/doctype/document_comparator/test_document_comparator.py +++ b/frappe/core/doctype/document_comparator/test_document_comparator.py @@ -23,7 +23,7 @@ def test_compare_changed_fields(self): changed_fields = frappe._dict(test_field="third value") re_amended_doc = amend_document(amended_doc, changed_fields, {}, 1) - comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", doc.name) + comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", re_amended_doc.name) documents, results = comparator.compare_document() test_field_values = results["changed"]["Field"] @@ -39,7 +39,7 @@ def test_compare_rows(self): rows_updated = frappe._dict(child_table_field=child_table_new) amended_doc = amend_document(doc, {}, rows_updated, 1) - comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", doc.name) + comparator = create_comparator_doc("Test Custom Doctype for Doc Comparator", amended_doc.name) documents, results = comparator.compare_document() results = frappe._dict(results) From 56da0ba0a594ff9f08297412fb0bcc244edc32a7 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 12 Sep 2023 12:56:48 +0530 Subject: [PATCH 063/115] fix: rename doctype (cherry picked from commit bda868b0c29b152c0dbf2d56b0ef82d64afee7b9) --- .../doctype/{document_comparator => audit_trail}/__init__.py | 0 .../document_comparator.html => audit_trail/audit_trail.html} | 0 .../document_comparator.js => audit_trail/audit_trail.js} | 2 +- .../document_comparator.json => audit_trail/audit_trail.json} | 2 +- .../document_comparator.py => audit_trail/audit_trail.py} | 2 +- .../audit_trail_rows_added_removed.html} | 0 .../test_audit_trail.py} | 2 +- 7 files changed, 4 insertions(+), 4 deletions(-) rename frappe/core/doctype/{document_comparator => audit_trail}/__init__.py (100%) rename frappe/core/doctype/{document_comparator/document_comparator.html => audit_trail/audit_trail.html} (100%) rename frappe/core/doctype/{document_comparator/document_comparator.js => audit_trail/audit_trail.js} (97%) rename frappe/core/doctype/{document_comparator/document_comparator.json => audit_trail/audit_trail.json} (98%) rename frappe/core/doctype/{document_comparator/document_comparator.py => audit_trail/audit_trail.py} (99%) rename frappe/core/doctype/{document_comparator/document_comparator_rows_added_removed.html => audit_trail/audit_trail_rows_added_removed.html} (100%) rename frappe/core/doctype/{document_comparator/test_document_comparator.py => audit_trail/test_audit_trail.py} (98%) diff --git a/frappe/core/doctype/document_comparator/__init__.py b/frappe/core/doctype/audit_trail/__init__.py similarity index 100% rename from frappe/core/doctype/document_comparator/__init__.py rename to frappe/core/doctype/audit_trail/__init__.py diff --git a/frappe/core/doctype/document_comparator/document_comparator.html b/frappe/core/doctype/audit_trail/audit_trail.html similarity index 100% rename from frappe/core/doctype/document_comparator/document_comparator.html rename to frappe/core/doctype/audit_trail/audit_trail.html diff --git a/frappe/core/doctype/document_comparator/document_comparator.js b/frappe/core/doctype/audit_trail/audit_trail.js similarity index 97% rename from frappe/core/doctype/document_comparator/document_comparator.js rename to frappe/core/doctype/audit_trail/audit_trail.js index ec31200573d6..173ac86519fc 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.js +++ b/frappe/core/doctype/audit_trail/audit_trail.js @@ -1,7 +1,7 @@ // Copyright (c) 2023, Frappe Technologies and contributors // For license information, please see license.txt -frappe.ui.form.on("Document Comparator", { +frappe.ui.form.on("Audit Trail", { refresh(frm) { frm.page.clear_indicator(); diff --git a/frappe/core/doctype/document_comparator/document_comparator.json b/frappe/core/doctype/audit_trail/audit_trail.json similarity index 98% rename from frappe/core/doctype/document_comparator/document_comparator.json rename to frappe/core/doctype/audit_trail/audit_trail.json index 821772fae887..8ad5a88c37d0 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.json +++ b/frappe/core/doctype/audit_trail/audit_trail.json @@ -77,7 +77,7 @@ "modified": "2023-08-22 12:12:59.780845", "modified_by": "Administrator", "module": "Core", - "name": "Document Comparator", + "name": "Audit Trail", "owner": "Administrator", "permissions": [ { diff --git a/frappe/core/doctype/document_comparator/document_comparator.py b/frappe/core/doctype/audit_trail/audit_trail.py similarity index 99% rename from frappe/core/doctype/document_comparator/document_comparator.py rename to frappe/core/doctype/audit_trail/audit_trail.py index e795417b26b6..b8e4e48b430a 100644 --- a/frappe/core/doctype/document_comparator/document_comparator.py +++ b/frappe/core/doctype/audit_trail/audit_trail.py @@ -9,7 +9,7 @@ from frappe.model.document import Document -class DocumentComparator(Document): +class AuditTrail(Document): # begin: auto-generated types # This code is auto-generated. Do not modify anything in this block. diff --git a/frappe/core/doctype/document_comparator/document_comparator_rows_added_removed.html b/frappe/core/doctype/audit_trail/audit_trail_rows_added_removed.html similarity index 100% rename from frappe/core/doctype/document_comparator/document_comparator_rows_added_removed.html rename to frappe/core/doctype/audit_trail/audit_trail_rows_added_removed.html diff --git a/frappe/core/doctype/document_comparator/test_document_comparator.py b/frappe/core/doctype/audit_trail/test_audit_trail.py similarity index 98% rename from frappe/core/doctype/document_comparator/test_document_comparator.py rename to frappe/core/doctype/audit_trail/test_audit_trail.py index af1f19874bcd..f977e4500b06 100644 --- a/frappe/core/doctype/document_comparator/test_document_comparator.py +++ b/frappe/core/doctype/audit_trail/test_audit_trail.py @@ -5,7 +5,7 @@ from frappe.tests.utils import FrappeTestCase -class TestDocumentComparator(FrappeTestCase): +class TestAuditTrail(FrappeTestCase): def setUp(self): self.child_doctype = create_custom_child_doctype() self.custom_doctype = create_custom_doctype() From a600e855f5d351b2ad7c5eeaff9843a811dbd8d1 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 12 Sep 2023 12:57:33 +0530 Subject: [PATCH 064/115] refactor: avoid fetch for amended field again (cherry picked from commit 99df38a26492ed37fd352e996bf39fc1cd094677) --- frappe/core/doctype/version/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/version/version.py b/frappe/core/doctype/version/version.py index 2ae6f9b1a496..38e813683662 100644 --- a/frappe/core/doctype/version/version.py +++ b/frappe/core/doctype/version/version.py @@ -101,7 +101,7 @@ def get_diff(old, new, for_child=False, compare_cancelled=False): for i, d in enumerate(new_value): old_row_name = getattr(d, old_row_name_field, None) if compare_cancelled: - if amended_from := frappe.db.get_value(d.parenttype, d.parent, "amended_from"): + if amended_from: if len(old_value) > i: old_row_name = old_value[i].name From fbb663d98054c356beadaf32316015849411f302 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Tue, 12 Sep 2023 13:56:23 +0530 Subject: [PATCH 065/115] fix: rename imports (cherry picked from commit aa18c4f751ed2883e8f1cc092a701b7e1bee09cc) --- frappe/core/doctype/audit_trail/audit_trail.js | 8 ++++---- frappe/core/doctype/audit_trail/test_audit_trail.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/audit_trail/audit_trail.js b/frappe/core/doctype/audit_trail/audit_trail.js index 173ac86519fc..ffd289257ec2 100644 --- a/frappe/core/doctype/audit_trail/audit_trail.js +++ b/frappe/core/doctype/audit_trail/audit_trail.js @@ -36,7 +36,7 @@ frappe.ui.form.on("Audit Trail", { changed: changed_fields.changed, row_changed: changed_fields.row_changed, }; - $(frappe.render_template("document_comparator", render_dict)).appendTo( + $(frappe.render_template("audit_trail", render_dict)).appendTo( frm.fields_dict.version_table.$wrapper.empty() ); frm.set_df_property("version_table", "hidden", 0); @@ -56,9 +56,9 @@ frappe.ui.form.on("Audit Trail", { section_dict = { added_or_removed: added_or_removed[key], }; - $( - frappe.render_template("document_comparator_rows_added_removed", section_dict) - ).appendTo(frm.fields_dict[key].$wrapper.empty()); + $(frappe.render_template("audit_trail_rows_added_removed", section_dict)).appendTo( + frm.fields_dict[key].$wrapper.empty() + ); if (!frm.fields_dict[key].disp_area.innerHTML.includes(" Date: Tue, 10 Oct 2023 11:47:49 +0530 Subject: [PATCH 066/115] fix: Use same filter in report view and list view (cherry picked from commit fbaefddaa1b5df109a0674e1f9f6ac38e0481aab) --- frappe/public/js/frappe/views/reports/report_view.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 29ce7b1ba1e2..51726190b2f8 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -76,6 +76,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { this.setup_charts_area(); this.$datatable_wrapper = $('
'); this.$result.append(this.$datatable_wrapper); + this.settings.onload && this.settings.onload(this); } setup_charts_area() { From d973f832ea7c754e76924b480179d381676c4d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bernd=20Oliver=20S=C3=BCnderhauf?= <46800703+bosue@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:49:59 +0200 Subject: [PATCH 067/115] fix(UX): Add 'Due Date' to Quick Entry Form, default to Today. (cherry picked from commit da6c42bb63608ab6d6e05d9f0d9cfb3d5dc2c085) --- frappe/desk/doctype/todo/todo.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/desk/doctype/todo/todo.json b/frappe/desk/doctype/todo/todo.json index 518ca00374ab..c3b534d272f0 100644 --- a/frappe/desk/doctype/todo/todo.json +++ b/frappe/desk/doctype/todo/todo.json @@ -62,8 +62,11 @@ "label": "Color" }, { + "allow_in_quick_entry": 1, + "default": "Today", "fieldname": "date", "fieldtype": "Date", + "in_list_view": 1, "in_standard_filter": 1, "label": "Due Date", "oldfieldname": "date", @@ -158,7 +161,7 @@ "icon": "fa fa-check", "idx": 2, "links": [], - "modified": "2021-09-16 11:36:34.586898", + "modified": "2023-10-05 07:44:38.476400", "modified_by": "Administrator", "module": "Desk", "name": "ToDo", @@ -196,4 +199,4 @@ "title_field": "description", "track_changes": 1, "track_seen": 1 -} \ No newline at end of file +} From 5f23775bba92432709d4a3f6c5c0dc979e3bb950 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 17 Oct 2023 14:54:35 +0530 Subject: [PATCH 068/115] fix: number card shorten number is not formatted correctly if number format is not default (cherry picked from commit 51185af12fd523852079c1d8b4da75549980a73b) --- .../public/js/frappe/utils/number_format.js | 26 +++++++++++++++++++ .../js/frappe/widgets/number_card_widget.js | 1 + 2 files changed, 27 insertions(+) diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index ef4c7366f72a..a95ca65e9cdd 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -51,6 +51,31 @@ function strip_number_groups(v, number_format) { return v; } +function convert_old_to_new_number_format(v, old_number_format, new_number_format) { + if (!new_number_format) new_number_format = get_number_format(); + let new_info = get_number_format_info(new_number_format); + + if (!old_number_format) old_number_format = "#,###.##"; + let old_info = get_number_format_info(old_number_format); + + if (old_number_format === new_number_format) return v; + + if (new_info.decimal_str == "") { + return strip_number_groups(v); + } + + let v_parts = v.split(old_info.decimal_str); + let v_before_decimal = v_parts[0]; + let v_after_decimal = v_parts[1] || ""; + + // replace old group separator with new group separator in v_before_decimal + let old_group_regex = new RegExp(old_info.group_sep === "." ? "\\." : old_info.group_sep, "g"); + v_before_decimal = v_before_decimal.replace(old_group_regex, new_info.group_sep); + + v = v_before_decimal + new_info.decimal_str + v_after_decimal; + return v; +} + frappe.number_format_info = { "#,###.##": { decimal_str: ".", group_sep: "," }, "#.###,##": { decimal_str: ",", group_sep: "." }, @@ -289,6 +314,7 @@ Object.assign(window, { flt, cint, strip_number_groups, + convert_old_to_new_number_format, format_currency, fmt_money, get_currency_symbol, diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index 23018a0a2d44..042667c9d6d5 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -223,6 +223,7 @@ export default class NumberCardWidget extends Widget { let number_parts = shortened_number.split(" "); const symbol = number_parts[1] || ""; + number_parts[0] = window.convert_old_to_new_number_format(number_parts[0]); const formatted_number = $(frappe.format(number_parts[0], df)).text(); this.formatted_number = formatted_number + " " + __(symbol); From 3dc38e3a166b05837654d988898f7ed633891eba Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 17 Oct 2023 12:14:06 +0000 Subject: [PATCH 069/115] chore(release): Bumped to Version 14.52.1 ## [14.52.1](https://github.com/frappe/frappe/compare/v14.52.0...v14.52.1) (2023-10-17) ### Bug Fixes * Don't mutate `df` object in frappe.format ([589e26d](https://github.com/frappe/frappe/commit/589e26d62b00669da8d91094d62b029260200d55)) * duplicate column in report conflict ([219b18e](https://github.com/frappe/frappe/commit/219b18ee9831537afb3e46407a001e13885f24bd)) * email from 'Table Multiselect' field ([#22733](https://github.com/frappe/frappe/issues/22733)) ([#22740](https://github.com/frappe/frappe/issues/22740)) ([b30826d](https://github.com/frappe/frappe/commit/b30826d248228b8ce03d08593df2340c1e25ddcb)) * enable server script while testing ([313cf7a](https://github.com/frappe/frappe/commit/313cf7a7c6867ad17c628f16b24112ebfd34fb8f)) * fixed the conflict between fieldname in General Ledger Report ([9c87707](https://github.com/frappe/frappe/commit/9c87707129095bebf74898204030de967fc8f2ab)) * sync doctype layout on update standard field ([#22699](https://github.com/frappe/frappe/issues/22699)) ([#22742](https://github.com/frappe/frappe/issues/22742)) ([fa2a88e](https://github.com/frappe/frappe/commit/fa2a88e306c07c76912583bc175f3ede51cb1ccf)) * Use same filter in report view and list view ([196a71e](https://github.com/frappe/frappe/commit/196a71e0cee6454fa8af81c769fd2c2e5c12f7df)) * **UX:** Add 'Due Date' to Quick Entry Form, default to Today. ([d973f83](https://github.com/frappe/frappe/commit/d973f832ea7c754e76924b480179d381676c4d14)) * validation permission on `tag` creation ([#22753](https://github.com/frappe/frappe/issues/22753)) ([#22755](https://github.com/frappe/frappe/issues/22755)) ([cdc16ab](https://github.com/frappe/frappe/commit/cdc16ab59d22ef643893a7fd670f4e724cbd0c55)) * work-around to fix issue with syncing Google Contacts ([#22649](https://github.com/frappe/frappe/issues/22649)) ([#22743](https://github.com/frappe/frappe/issues/22743)) ([4430798](https://github.com/frappe/frappe/commit/4430798faaf2222522d21de89f4cda4b10864b7d)), closes [#22648](https://github.com/frappe/frappe/issues/22648) * works for multiple rows now after saving ([be31d56](https://github.com/frappe/frappe/commit/be31d56d087ae0e406ec483c8945b6460e8801a5)) * workspace don't strip filter info on save. ([c2c2db8](https://github.com/frappe/frappe/commit/c2c2db8aa74e4b7eeffae9213059bf796919de94)) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 3898a22997d2..f7ca02314c72 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.52.0" +__version__ = "14.52.1" __title__ = "Frappe Framework" controllers = {} From 87aa9e6da6cea4c7760ff624cfc2a31a56777b4d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 17:53:04 +0530 Subject: [PATCH 070/115] fix: filename xss (#22778) (#22782) (cherry picked from commit 73b58a42b06d83ca9a0fddd5f2b35d5851d933c4) Co-authored-by: ranjit-git --- frappe/public/js/frappe/views/file/file_view.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js index ab0241caf728..a4bd8214f163 100644 --- a/frappe/public/js/frappe/views/file/file_view.js +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -364,8 +364,8 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { - - + + ${file.subject_html} From de27391e34005caa64bbacfd38aa83e6d8a12302 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:52:33 +0530 Subject: [PATCH 071/115] feat: rate limit logins based on IP too (backport #22774) (#22780) * fix!: Enable login tracker by default (cherry picked from commit 27d50bb0d157cd3e015e01b1b67ce77e266e5905) # Conflicts: # frappe/core/doctype/system_settings/system_settings.json * feat: rate limit logins based on IP too Co-Authored-By: Aditya Hase (cherry picked from commit 768d4ba4b0af75d014c9fb5cad6496063842975f) * refactor: make login tracker support arbitrary keys (cherry picked from commit f4f6d97d06e1a8e3f9301c8d80298339e8494f8f) # Conflicts: # frappe/auth.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- frappe/auth.py | 41 ++++++++++++------- .../doctype/activity_log/test_activity_log.py | 2 + .../system_settings/system_settings.json | 3 +- frappe/tests/test_auth.py | 6 +-- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/frappe/auth.py b/frappe/auth.py index 66d9c34ec4c9..eec6f03dd442 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -18,6 +18,7 @@ should_run_2fa, ) from frappe.utils import cint, date_diff, datetime, get_datetime, today +from frappe.utils.deprecations import deprecation_warning from frappe.utils.password import check_password from frappe.website.utils import get_home_page @@ -237,23 +238,28 @@ def authenticate(self, user: str = None, pwd: str = None): _raw_user_name = user user = User.find_by_credentials(user, pwd) + ip_tracker = get_login_attempt_tracker(frappe.local.request_ip) if not user: + ip_tracker and ip_tracker.add_failure_attempt() self.fail("Invalid login credentials", user=_raw_user_name) # Current login flow uses cached credentials for authentication while checking OTP. # Incase of OTP check, tracker for auth needs to be disabled(If not, it can remove tracker history as it is going to succeed anyway) # Tracker is activated for 2FA incase of OTP. ignore_tracker = should_run_2fa(user.name) and ("otp" in frappe.form_dict) - tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) + user_tracker = None if ignore_tracker else get_login_attempt_tracker(user.name) if not user.is_authenticated: - tracker and tracker.add_failure_attempt() + user_tracker and user_tracker.add_failure_attempt() + ip_tracker and ip_tracker.add_failure_attempt() self.fail("Invalid login credentials", user=user.name) elif not (user.name == "Administrator" or user.enabled): - tracker and tracker.add_failure_attempt() + user_tracker and user_tracker.add_failure_attempt() + ip_tracker and ip_tracker.add_failure_attempt() self.fail("User disabled or missing", user=user.name) else: - tracker and tracker.add_success_attempt() + user_tracker and user_tracker.add_success_attempt() + ip_tracker and ip_tracker.add_success_attempt() self.user = user.name def force_user_to_reset_password(self): @@ -441,7 +447,7 @@ def validate_ip_address(user): frappe.throw(_("Access not allowed from this IP Address"), frappe.AuthenticationError) -def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = True): +def get_login_attempt_tracker(key: str, raise_locked_exception: bool = True): """Get login attempt tracker instance. :param user_name: Name of the loggedin user @@ -455,7 +461,7 @@ def get_login_attempt_tracker(user_name: str, raise_locked_exception: bool = Tru tracker_kwargs["lock_interval"] = sys_settings.allow_login_after_fail tracker_kwargs["max_consecutive_login_attempts"] = sys_settings.allow_consecutive_login_attempts - tracker = LoginAttemptTracker(user_name, **tracker_kwargs) + tracker = LoginAttemptTracker(key, **tracker_kwargs) if raise_locked_exception and track_login_attempts and not tracker.is_user_allowed(): frappe.throw( @@ -474,7 +480,12 @@ class LoginAttemptTracker: """ def __init__( - self, user_name: str, max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60 + self, + key: str, + max_consecutive_login_attempts: int = 3, + lock_interval: int = 5 * 60, + *, + user_name: str = None, ): """Initialize the tracker. @@ -482,21 +493,23 @@ def __init__( :param max_consecutive_login_attempts: Maximum allowed consecutive failed login attempts :param lock_interval: Locking interval incase of maximum failed attempts """ - self.user_name = user_name + if user_name: + deprecation_warning("`username` parameter is deprecated, use `key` instead.") + self.key = key or user_name self.lock_interval = datetime.timedelta(seconds=lock_interval) self.max_failed_logins = max_consecutive_login_attempts @property def login_failed_count(self): - return frappe.cache().hget("login_failed_count", self.user_name) + return frappe.cache().hget("login_failed_count", self.key) @login_failed_count.setter def login_failed_count(self, count): - frappe.cache().hset("login_failed_count", self.user_name, count) + frappe.cache().hset("login_failed_count", self.key, count) @login_failed_count.deleter def login_failed_count(self): - frappe.cache().hdel("login_failed_count", self.user_name) + frappe.cache().hdel("login_failed_count", self.key) @property def login_failed_time(self): @@ -504,15 +517,15 @@ def login_failed_time(self): For every user we track only First failed login attempt time within lock interval of time. """ - return frappe.cache().hget("login_failed_time", self.user_name) + return frappe.cache().hget("login_failed_time", self.key) @login_failed_time.setter def login_failed_time(self, timestamp): - frappe.cache().hset("login_failed_time", self.user_name, timestamp) + frappe.cache().hset("login_failed_time", self.key, timestamp) @login_failed_time.deleter def login_failed_time(self): - frappe.cache().hdel("login_failed_time", self.user_name) + frappe.cache().hdel("login_failed_time", self.key) def add_failure_attempt(self): """Log user failure attempts into the system. diff --git a/frappe/core/doctype/activity_log/test_activity_log.py b/frappe/core/doctype/activity_log/test_activity_log.py index df3f113a8550..4a2d80481cd1 100644 --- a/frappe/core/doctype/activity_log/test_activity_log.py +++ b/frappe/core/doctype/activity_log/test_activity_log.py @@ -20,6 +20,7 @@ def test_activity_log(self): } ) + frappe.local.request_ip = "127.0.0.1" frappe.local.cookie_manager = CookieManager() frappe.local.login_manager = LoginManager() @@ -61,6 +62,7 @@ def test_brute_security(self): {"cmd": "login", "sid": "Guest", "pwd": "admin", "usr": "Administrator"} ) + frappe.local.request_ip = "127.0.0.1" frappe.local.cookie_manager = CookieManager() frappe.local.login_manager = LoginManager() diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 926cab41f1e8..dc6c467c6655 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -298,6 +298,7 @@ "label": "Brute Force Security" }, { + "default": "10", "fieldname": "allow_consecutive_login_attempts", "fieldtype": "Int", "label": "Allow Consecutive Login Attempts " @@ -596,7 +597,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-09-13 12:49:32.309521", + "modified": "2023-10-17 16:12:28.145496", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py index e466660df4b1..47a092cb54f5 100644 --- a/frappe/tests/test_auth.py +++ b/frappe/tests/test_auth.py @@ -161,9 +161,7 @@ def test_login_with_email_link(self): class TestLoginAttemptTracker(FrappeTestCase): def test_account_lock(self): """Make sure that account locks after `n consecutive failures""" - tracker = LoginAttemptTracker( - user_name="tester", max_consecutive_login_attempts=3, lock_interval=60 - ) + tracker = LoginAttemptTracker("tester", max_consecutive_login_attempts=3, lock_interval=60) # Clear the cache by setting attempt as success tracker.add_success_attempt() @@ -183,7 +181,7 @@ def test_account_unlock(self): """Make sure that locked account gets unlocked after lock_interval of time.""" lock_interval = 2 # In sec tracker = LoginAttemptTracker( - user_name="tester", max_consecutive_login_attempts=1, lock_interval=lock_interval + "tester", max_consecutive_login_attempts=1, lock_interval=lock_interval ) # Clear the cache by setting attempt as success tracker.add_success_attempt() From 729d842bc3c2c446400df8bbae60ee69c96c65b9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:06:47 +0530 Subject: [PATCH 072/115] fix: Check perms before sharing linked docs (#22783) (#22786) (cherry picked from commit 440612f3b9061e56331c3a66d3c4875a6e4708a5) Co-authored-by: Ankush Menat --- frappe/desk/form/linked_with.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 9bc7b138ddb9..35ce5afd9f31 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -38,6 +38,7 @@ def get_submitted_linked_docs(doctype: str, name: str) -> list[tuple]: 3. Searching for links is going to be a tree like structure where at every level, you will be finding documents using parent document and parent document links. """ + frappe.has_permission(doctype, doc=name) tree = SubmittableDocumentTree(doctype, name) visited_documents = tree.get_all_children() docs = [] @@ -518,6 +519,7 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di @frappe.whitelist() def get(doctype, docname): + frappe.has_permission(doctype, doc=docname) linked_doctypes = get_linked_doctypes(doctype=doctype) return get_linked_docs(doctype=doctype, name=docname, linkinfo=linked_doctypes) From 61056c1ca815ce232ecf5e86b9fa1a1477681983 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 19:07:18 +0530 Subject: [PATCH 073/115] fix: check read before assigning/removing (#22779) (#22785) (cherry picked from commit 691eae8e84d7d5f7bc70c83b4e5bc7b6e3be0ba8) Co-authored-by: Ankush Menat --- frappe/desk/form/assign_to.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index b1b14ee28bf5..a02d35414f4d 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -63,6 +63,8 @@ def add(args=None): "status": "Open", "allocated_to": assign_to, } + parent_doc = frappe.get_doc(args["doctype"], args["name"]) + parent_doc.check_permission() if frappe.get_all("ToDo", filters=filters): users_with_duplicate_todo.append(assign_to) @@ -174,6 +176,9 @@ def close(doctype: str, name: str, assign_to: str): def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"): """remove from todo""" + + doc = frappe.get_doc(doctype, name) + doc.check_permission() try: if not todo: todo = frappe.db.get_value( From fa9f67146c05424b054743559d241eb7ab3932c6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 17 Oct 2023 22:10:18 +0530 Subject: [PATCH 074/115] refactor: Split address render function (backport #22784) (#22788) * refactor: Split address render function (#22784) This function can be used as utility where permssion checks might not be required. (cherry picked from commit 3e19fb36a70b56b8f636cee7aa9ae72b16d24657) * refactor: change kwarg name frappe.call is oversmart about this particular name (cherry picked from commit 8df5402b1fda5df915782c1f97351e55282bcf3d) --------- Co-authored-by: Ankush Menat --- frappe/contacts/doctype/address/address.py | 23 ++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index a4ce308718c7..05687c5dd4bd 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -1,8 +1,6 @@ # Copyright (c) 2015, Frappe Technologies and contributors # License: MIT. See LICENSE -from typing import Optional - from jinja2 import TemplateSyntaxError import frappe @@ -127,18 +125,23 @@ def get_default_address( @frappe.whitelist() def get_address_display(address_dict: dict | str | None = None) -> str | None: - if not address_dict: + return render_address(address_dict) + + +def render_address(address: dict | str | None, check_permissions=True) -> str | None: + if not address: return - if not isinstance(address_dict, dict): - address = frappe.get_cached_doc("Address", address_dict) - address.check_permission() - address_dict = address.as_dict() + if not isinstance(address, dict): + address = frappe.get_cached_doc("Address", address) + if check_permissions: + address.check_permission() + address = address.as_dict() - name, template = get_address_templates(address_dict) + name, template = get_address_templates(address) try: - return frappe.render_template(template, address_dict) + return frappe.render_template(template, address) except TemplateSyntaxError: frappe.throw(_("There is an error in your Address Template {0}").format(name)) @@ -219,7 +222,7 @@ def get_company_address(company): if company: ret.company_address = get_default_address("Company", company) - ret.company_address_display = get_address_display(ret.company_address) + ret.company_address_display = render_address(ret.company_address, check_permissions=False) return ret From 745b79c77420af13abacbe78e5b8f593ed39efab Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 17 Oct 2023 19:13:56 +0530 Subject: [PATCH 075/115] fix: do not allow deleting other's private workspaces (cherry picked from commit 1b1c11ad5e8e92bc74665c5d6767be1f590edab6) --- frappe/desk/doctype/workspace/workspace.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index b564c2d3153e..ac3fee095b40 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -334,7 +334,17 @@ def delete_page(page): page = loads(page) if page.get("public") and not is_workspace_manager(): - return + frappe.throw( + _("Cannot delete public workspace without Workspace Manager role"), + frappe.PermissionError, + ) + elif not page.get("public") and not is_workspace_manager(): + workspace_owner = frappe.get_value("Workspace", page.get("name"), "for_user") + if workspace_owner != frappe.session.user: + frappe.throw( + _("Cannot delete private workspace of other users"), + frappe.PermissionError, + ) if frappe.db.exists("Workspace", page.get("name")): frappe.get_doc("Workspace", page.get("name")).delete(ignore_permissions=True) From caa28b848621f11f13cc4c6a2a7d02e614a183f3 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 17 Oct 2023 19:38:42 +0530 Subject: [PATCH 076/115] fix: do not allow creating private workspace for other users (cherry picked from commit f1c394cafc755a721ae8eeda54bd4ef5a41f18b6) --- frappe/desk/doctype/workspace/workspace.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index ac3fee095b40..d926b0422757 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -192,6 +192,12 @@ def new_page(new_page): if page.get("public") and not is_workspace_manager(): return + elif ( + not page.get("public") + and page.get("for_user") != frappe.session.user + and not is_workspace_manager() + ): + frappe.throw(_("Cannot create private workspace of other users"), frappe.PermissionError) doc = frappe.new_doc("Workspace") doc.title = page.get("title") From 11dd3a79ea5c30b0caa967c83f6dd0ec5007839c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 17 Oct 2023 19:39:53 +0530 Subject: [PATCH 077/115] fix: do not allow editing other's private workspaces (cherry picked from commit 7343c83838dc1937a192966e57660e599b77e6ed) --- frappe/desk/doctype/workspace/workspace.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index d926b0422757..4d7d1304a8e5 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -238,6 +238,16 @@ def update_page(name, title, icon, parent, public): public = frappe.parse_json(public) doc = frappe.get_doc("Workspace", name) + if ( + not doc.get("public") + and doc.get("for_user") != frappe.session.user + and not is_workspace_manager() + ): + frappe.throw( + _("Need Workspace Manager role to edit private workspace of other users"), + frappe.PermissionError, + ) + if doc: doc.title = title doc.icon = icon From 89ebb9d56154fb7b3d19ed88cea7844589fb532f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 17 Oct 2023 19:40:57 +0530 Subject: [PATCH 078/115] fix: allow hiding/unhiding others private workspace if you are workspace manager (cherry picked from commit c955cba8946dfea0d154f903908037acdfbd8635) --- frappe/desk/doctype/workspace/workspace.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 4d7d1304a8e5..9fdd8d287fd9 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -292,7 +292,11 @@ def hide_unhide_page(page_name: str, is_hidden: bool): _("Need Workspace Manager role to hide/unhide public workspaces"), frappe.PermissionError ) - if not page.get("public") and page.get("for_user") != frappe.session.user: + if ( + not page.get("public") + and page.get("for_user") != frappe.session.user + and not is_workspace_manager() + ): frappe.throw(_("Cannot update private workspace of other users"), frappe.PermissionError) page.is_hidden = int(is_hidden) From 7a3dbe0809fd2b76a2da99d7bdd70abe76626d9d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 18 Oct 2023 18:31:29 +0530 Subject: [PATCH 079/115] chore: remove dumb test --- .../doctype/help_article/test_help_article.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/frappe/website/doctype/help_article/test_help_article.py b/frappe/website/doctype/help_article/test_help_article.py index a7576c7168c0..51b8c7908b07 100644 --- a/frappe/website/doctype/help_article/test_help_article.py +++ b/frappe/website/doctype/help_article/test_help_article.py @@ -25,20 +25,6 @@ def setUpClass(cls) -> None: } ).insert() - def test_article_is_helpful(self): - from frappe.website.doctype.help_article.help_article import add_feedback - - self.help_article.load_from_db() - self.assertEqual(self.help_article.helpful, 0) - self.assertEqual(self.help_article.not_helpful, 0) - - add_feedback(self.help_article.name, "Yes") - add_feedback(self.help_article.name, "No") - - self.help_article.load_from_db() - self.assertEqual(self.help_article.helpful, 1) - self.assertEqual(self.help_article.not_helpful, 1) - @classmethod def tearDownClass(cls) -> None: frappe.delete_doc(cls.help_article.doctype, cls.help_article.name) From f7c43e140f2f9b8081dc9d4b1e9bbb8f3a7466fc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 09:23:06 +0530 Subject: [PATCH 080/115] fix: cron job firing immediately after save (backport #22795) (#22806) * fix: cron job firing immediately after save (cherry picked from commit ec0817d1821e6aa72b23820a61224b70136277d1) * chore: adding test case (cherry picked from commit 6193f23fd60c88855d515ade1f63818a668c897d) * chore: linter fix (cherry picked from commit a8452484a05da182d1a2588d990e9149bef6ba71) * fix: handle cold start edge case (cherry picked from commit d4c49a70c20c083ffe1ecd2cb83d88a52065a712) * test: Add cold start tests (cherry picked from commit 05d6f5cc8a4f6eff1039bc2e158cfe5451348937) # Conflicts: # frappe/tests/utils.py # pyproject.toml * chore: conflicts --------- Co-authored-by: Sambasiva Suda Co-authored-by: Ankush Menat --- .../scheduled_job_type/scheduled_job_type.py | 9 +++-- .../test_scheduled_job_type.py | 28 ++++++++++++++++ frappe/tests/test_test_utils.py | 10 ++++++ frappe/tests/utils.py | 33 +++++++++++++++++++ pyproject.toml | 1 + 5 files changed, 78 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 4699d4416264..1d0a77824d5e 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -80,9 +80,12 @@ def get_next_execution(self): if not self.cron_format: self.cron_format = CRON_MAP[self.frequency] - return croniter( - self.cron_format, get_datetime(self.last_execution or datetime(2000, 1, 1)) - ).get_next(datetime) + # If this is a cold start then last_execution will not be set. + # Creation is set as fallback because if very old fallback is set job might trigger + # immediately, even when it's meant to be daily. + # A dynamic fallback like current time might miss the scheduler interval and job will never start. + last_execution = get_datetime(self.last_execution or self.creation) + return croniter(self.cron_format, last_execution).get_next(datetime) def execute(self): self.scheduler_log = None diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py index bbc92dfbc9bf..689cf6fed38c 100644 --- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -1,9 +1,12 @@ # Copyright (c) 2019, Frappe Technologies and Contributors # License: MIT. See LICENSE +from datetime import timedelta + import frappe from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs from frappe.tests.utils import FrappeTestCase from frappe.utils import get_datetime +from frappe.utils.data import add_to_date, now_datetime class TestScheduledJobType(FrappeTestCase): @@ -65,9 +68,34 @@ def test_monthly_job(self): self.assertFalse(job.is_event_due(get_datetime("2019-01-31 23:59:59"))) def test_cron_job(self): + # Daily but offset by 45 minutes + job = frappe.get_doc( + "Scheduled Job Type", + dict(method="frappe.core.doctype.log_settings.log_settings.run_log_clean_up"), + ) + self.assertEqual( + job.next_execution, + add_to_date(None, days=1).replace(hour=0, minute=45, second=0, microsecond=0), + ) # runs every 15 mins job = frappe.get_doc("Scheduled Job Type", dict(method="frappe.oauth.delete_oauth2_data")) job.db_set("last_execution", "2019-01-01 00:00:00") + self.assertEqual(job.next_execution, get_datetime("2019-01-01 00:15:00")) self.assertTrue(job.is_event_due(get_datetime("2019-01-01 00:15:01"))) self.assertFalse(job.is_event_due(get_datetime("2019-01-01 00:05:06"))) self.assertFalse(job.is_event_due(get_datetime("2019-01-01 00:14:59"))) + + def test_cold_start(self): + now = now_datetime() + just_before_12_am = now.replace(hour=11, minute=59, second=30) + just_after_12_am = now.replace(hour=0, minute=0, second=30) + timedelta(days=1) + + job = frappe.new_doc("Scheduled Job Type") + job.frequency = "Daily" + job.set_user_and_timestamp() + + with self.freeze_time(just_before_12_am): + self.assertFalse(job.is_event_due()) + + with self.freeze_time(just_after_12_am): + self.assertTrue(job.is_event_due()) diff --git a/frappe/tests/test_test_utils.py b/frappe/tests/test_test_utils.py index 4e5c424ca6cf..de004ac0286b 100644 --- a/frappe/tests/test_test_utils.py +++ b/frappe/tests/test_test_utils.py @@ -1,5 +1,8 @@ +from datetime import timedelta + import frappe from frappe.tests.utils import FrappeTestCase, change_settings +from frappe.utils.data import now_datetime class TestTestUtils(FrappeTestCase): @@ -27,6 +30,13 @@ def test_temp_setting_changes(self): restored_settings = frappe.get_system_settings("logout_on_password_reset") self.assertEqual(current_setting, restored_settings) + def test_time_freezing(self): + now = now_datetime() + + tomorrow = now + timedelta(days=1) + with self.freeze_time(tomorrow): + self.assertEqual(now_datetime(), tomorrow) + def tearDownModule(): """assertions for ensuring tests didn't leave state behind""" diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 552e3ddd5fb0..e9e4db863407 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -6,9 +6,12 @@ from typing import Sequence from unittest.mock import patch +import pytz + import frappe from frappe.model.base_document import BaseDocument from frappe.utils import cint +from frappe.utils.data import convert_utc_to_timezone, get_datetime, get_system_timezone datetime_like_types = (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) @@ -112,6 +115,36 @@ def _sql_with_count(*args, **kwargs): finally: frappe.db.sql = orig_sql + @contextmanager + def set_user(self, user: str): + old_user = frappe.session.user + frappe.set_user(user) + yield + frappe.set_user(old_user) + + @contextmanager + def switch_site(self, site: str): + """Switch connection to different site. + Note: Drops current site connection completely.""" + + old_site = frappe.local.site + frappe.init(site, force=True) + frappe.connect() + yield + frappe.init(old_site, force=True) + frappe.connect() + + @contextmanager + def freeze_time(self, time_to_freeze, *args, **kwargs): + from freezegun import freeze_time + + # Freeze time expects UTC or tzaware objects. We have neither, so convert to UTC. + timezone = pytz.timezone(get_system_timezone()) + fake_time_with_tz = timezone.localize(get_datetime(time_to_freeze)).astimezone(pytz.utc) + + with freeze_time(fake_time_with_tz, *args, **kwargs): + yield + def _commit_watcher(): import traceback diff --git a/pyproject.toml b/pyproject.toml index 477d364c62ac..8c335c792432 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -108,3 +108,4 @@ pyngrok = "~=5.0.5" unittest-xml-reporting = "~=3.0.4" watchdog = "~=2.1.9" hypothesis = "~=6.68.2" +freezegun = "~=1.2.2" From de6d27166829b0f543ad85893dc886d73ae2e2f2 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 3 Mar 2023 11:39:57 +0530 Subject: [PATCH 081/115] fix: compare with content value for Link fieldtypes (cherry picked from commit e589ef87f6dcf843f35b3c8a9dd4f259961edd76) --- frappe/public/js/frappe/views/reports/query_report.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 6c4106d0be3d..be41a20d2a52 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1243,7 +1243,10 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { width: parseInt(column.width) || null, editable: false, compareValue: compareFn, - format: (value, row, column, data) => { + format: (value, row, column, data, for_filter = false) => { + if (for_filter && column?.fieldtype === "Link") { + return value || ""; + } if (this.report_settings.formatter) { return this.report_settings.formatter( value, From 484f18451e4c35d4feab9fea607c7c4f4c1b8cc1 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 19 Oct 2023 13:37:59 +0530 Subject: [PATCH 082/115] fix: amount calculation issue (#22820) --- frappe/public/js/frappe/utils/number_format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index a95ca65e9cdd..eebfcf8fc3b0 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -200,7 +200,7 @@ function get_number_format_info(format) { function _round(num, precision, rounding_method) { rounding_method = - rounding_method || frappe.boot.sysdefaults.rounding_method || "Banker's Rounding (legacy)"; + rounding_method || frappe.boot.sysdefaults?.rounding_method || "Banker's Rounding (legacy)"; let is_negative = num < 0 ? true : false; From fe06b7c8d98f0dfeda39b58ba4c9ff3de5bb96f6 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:11:19 +0530 Subject: [PATCH 083/115] refactor: Use client.set_value instead of db.set_value (#22814) (#22819) (cherry picked from commit bdc5aba7ad9864edc5e9fdc16ece6283a09819e4) Co-authored-by: Ankush Menat --- frappe/email/inbox.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/frappe/email/inbox.py b/frappe/email/inbox.py index 69b49d8413a0..b2432602627b 100644 --- a/frappe/email/inbox.py +++ b/frappe/email/inbox.py @@ -1,6 +1,7 @@ import json import frappe +from frappe.client import set_value def get_email_accounts(user=None): @@ -97,32 +98,32 @@ def mark_as_seen_unseen(name, action): @frappe.whitelist() -def mark_as_closed_open(communication, status): +def mark_as_closed_open(communication: str, status: str): """Set status to open or close""" - frappe.db.set_value("Communication", communication, "status", status) + set_value("Communication", communication, "status", status) @frappe.whitelist() -def move_email(communication, email_account): +def move_email(communication: str, email_account: str): """Move email to another email account.""" - frappe.db.set_value("Communication", communication, "email_account", email_account) + set_value("Communication", communication, "email_account", email_account) @frappe.whitelist() -def mark_as_trash(communication): +def mark_as_trash(communication: str): """Set email status to trash.""" - frappe.db.set_value("Communication", communication, "email_status", "Trash") + set_value("Communication", communication, "email_status", "Trash") @frappe.whitelist() -def mark_as_spam(communication, sender): +def mark_as_spam(communication: str, sender: str): """Set email status to spam.""" email_rule = frappe.db.get_value("Email Rule", {"email_id": sender}) if not email_rule: frappe.get_doc({"doctype": "Email Rule", "email_id": sender, "is_spam": 1}).insert( ignore_permissions=True ) - frappe.db.set_value("Communication", communication, "email_status", "Spam") + set_value("Communication", communication, "email_status", "Spam") def link_communication_to_document( From 4563b86971456028a0262d20a29148b6c6cea797 Mon Sep 17 00:00:00 2001 From: Vishnu VS Date: Sun, 22 Oct 2023 15:55:11 +0530 Subject: [PATCH 084/115] fix: resolve page not found error (cherry picked from commit 2e431f75fbf81ffc8bc89f70332f43a37d20eb13) --- frappe/public/js/frappe/views/reports/query_report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index be41a20d2a52..b8baa33405b9 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -362,7 +362,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { frappe.xcall(method, { args: args }).then(() => { let message; if (dashboard_name) { - let dashboard_route_html = `${dashboard_name}`; + let dashboard_route_html = `${dashboard_name}`; message = __("New {0} {1} added to Dashboard {2}", [ __(doctype), name, From a258d84c9b0e5f630a6f208c1f73bdaaf7245344 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 09:51:13 +0530 Subject: [PATCH 085/115] fix: ignore duplicate perm check on assign hooks (backport #22832) (#22833) * fix: ignore duplicate perm check on assign hooks (#22832) * fix: Ignore permissions while assigning if flag set * fix: Avoid double permission checks on assignment rule When it's triggered via doc events either: - Permission check is done or - Permission checks are not applicable (cherry picked from commit 08b92858a3f2105fd3b05c25b2d63ad1f74b05af) # Conflicts: # frappe/automation/doctype/assignment_rule/test_assignment_rule.py * chore: conflicts --------- Co-authored-by: Ankush Menat --- frappe/__init__.py | 1 + .../assignment_rule/assignment_rule.py | 11 +++-- .../assignment_rule/test_assignment_rule.py | 18 +++++++- frappe/desk/form/assign_to.py | 46 +++++++++++++------ frappe/tests/utils.py | 24 ++++++---- 5 files changed, 70 insertions(+), 30 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 6a03e4e2be3d..085eee1484a8 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1653,6 +1653,7 @@ def get_newargs(fn: Callable, kwargs: dict[str, Any]) -> dict[str, Any]: if (a in fnargs) or varkw_exist: newargs[a] = kwargs.get(a) + # WARNING: This behaviour is now part of business logic in places, never remove. newargs.pop("ignore_permissions", None) newargs.pop("flags", None) diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index 508ed317c650..ac9f95e6f312 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -52,7 +52,7 @@ def apply_assign(self, doc): def do_assignment(self, doc): # clear existing assignment, to reassign - assign_to.clear(doc.get("doctype"), doc.get("name")) + assign_to.clear(doc.get("doctype"), doc.get("name"), ignore_permissions=True) user = self.get_user(doc) @@ -66,7 +66,8 @@ def do_assignment(self, doc): assignment_rule=self.name, notify=True, date=doc.get(self.due_date_based_on) if self.due_date_based_on else None, - ) + ), + ignore_permissions=True, ) # set for reference in round robin @@ -78,12 +79,14 @@ def do_assignment(self, doc): def clear_assignment(self, doc): """Clear assignments""" if self.safe_eval("unassign_condition", doc): - return assign_to.clear(doc.get("doctype"), doc.get("name")) + return assign_to.clear(doc.get("doctype"), doc.get("name"), ignore_permissions=True) def close_assignments(self, doc): """Close assignments""" if self.safe_eval("close_condition", doc): - return assign_to.close_all_assignments(doc.get("doctype"), doc.get("name")) + return assign_to.close_all_assignments( + doc.get("doctype"), doc.get("name"), ignore_permissions=True + ) def get_user(self, doc): """ diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py index 879a082978d5..e174da1ad5a1 100644 --- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py @@ -106,6 +106,20 @@ def test_load_balancing(self): len(frappe.get_all("ToDo", dict(allocated_to=user, reference_type="Note"))), 10 ) + def test_assingment_on_guest_submissions(self): + """Sometimes documents are inserted as guest, check if assignment rules run on them. Use case: Web Forms""" + with self.set_user("Guest"): + doc = make_note({"public": 1}, ignore_permissions=True) + + # check assignment to *anyone* + self.assertTrue( + frappe.db.get_value( + "ToDo", + {"reference_type": "Note", "reference_name": doc.name, "status": "Open"}, + "allocated_to", + ), + ) + def test_based_on_field(self): self.assignment_rule.rule = "Based on Field" self.assignment_rule.field = "owner" @@ -375,13 +389,13 @@ def get_assignment_rule(days, assign=None): return assignment_rule -def make_note(values=None): +def make_note(values=None, *, ignore_permissions=False): note = frappe.get_doc(dict(doctype="Note", title=random_string(10), content=random_string(20))) if values: note.update(values) - note.insert() + note.insert(ignore_permissions=ignore_permissions) return note diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index a02d35414f4d..dc8dbc7cf5a3 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -39,7 +39,7 @@ def get(args=None): @frappe.whitelist() -def add(args=None): +def add(args=None, *, ignore_permissions=False): """add in someone's to do list args = { "assign_to": [], @@ -63,8 +63,8 @@ def add(args=None): "status": "Open", "allocated_to": assign_to, } - parent_doc = frappe.get_doc(args["doctype"], args["name"]) - parent_doc.check_permission() + if not ignore_permissions: + frappe.get_doc(args["doctype"], args["name"]).check_permission() if frappe.get_all("ToDo", filters=filters): users_with_duplicate_todo.append(assign_to) @@ -146,7 +146,7 @@ def add_multiple(args=None): add(args) -def close_all_assignments(doctype, name): +def close_all_assignments(doctype, name, ignore_permissions=False): assignments = frappe.get_all( "ToDo", fields=["allocated_to", "name"], @@ -156,29 +156,42 @@ def close_all_assignments(doctype, name): return False for assign_to in assignments: - set_status(doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Closed") + set_status( + doctype, + name, + todo=assign_to.name, + assign_to=assign_to.allocated_to, + status="Closed", + ignore_permissions=ignore_permissions, + ) return True @frappe.whitelist() -def remove(doctype, name, assign_to): - return set_status(doctype, name, "", assign_to, status="Cancelled") +def remove(doctype, name, assign_to, ignore_permissions=False): + return set_status( + doctype, name, "", assign_to, status="Cancelled", ignore_permissions=ignore_permissions + ) @frappe.whitelist() -def close(doctype: str, name: str, assign_to: str): +def close(doctype: str, name: str, assign_to: str, ignore_permissions=False): if assign_to != frappe.session.user: frappe.throw(_("Only the assignee can complete this to-do.")) - return set_status(doctype, name, "", assign_to, status="Closed") + return set_status( + doctype, name, "", assign_to, status="Closed", ignore_permissions=ignore_permissions + ) -def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"): +def set_status( + doctype, name, todo=None, assign_to=None, status="Cancelled", ignore_permissions=False +): """remove from todo""" - doc = frappe.get_doc(doctype, name) - doc.check_permission() + if not ignore_permissions: + frappe.get_doc(doctype, name).check_permission() try: if not todo: todo = frappe.db.get_value( @@ -206,7 +219,7 @@ def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"): return get({"doctype": doctype, "name": name}) -def clear(doctype, name): +def clear(doctype, name, ignore_permissions=False): """ Clears assignments, return False if not assigned. """ @@ -220,7 +233,12 @@ def clear(doctype, name): for assign_to in assignments: set_status( - doctype, name, todo=assign_to.name, assign_to=assign_to.allocated_to, status="Cancelled" + doctype, + name, + todo=assign_to.name, + assign_to=assign_to.allocated_to, + status="Cancelled", + ignore_permissions=ignore_permissions, ) return True diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index e9e4db863407..22c783a9dc4a 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -117,22 +117,26 @@ def _sql_with_count(*args, **kwargs): @contextmanager def set_user(self, user: str): - old_user = frappe.session.user - frappe.set_user(user) - yield - frappe.set_user(old_user) + try: + old_user = frappe.session.user + frappe.set_user(user) + yield + finally: + frappe.set_user(old_user) @contextmanager def switch_site(self, site: str): """Switch connection to different site. Note: Drops current site connection completely.""" - old_site = frappe.local.site - frappe.init(site, force=True) - frappe.connect() - yield - frappe.init(old_site, force=True) - frappe.connect() + try: + old_site = frappe.local.site + frappe.init(site, force=True) + frappe.connect() + yield + finally: + frappe.init(old_site, force=True) + frappe.connect() @contextmanager def freeze_time(self, time_to_freeze, *args, **kwargs): From ba226fb849647c9e91b1b9b8313f903399e4178f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 10:14:32 +0530 Subject: [PATCH 086/115] fix: escape print format code field(#22852) (#22865) (cherry picked from commit 68c28318dda013da83174cdb3a30e1fa541f44b8) Co-authored-by: surayp <96818210+surayp@users.noreply.github.com> --- frappe/templates/print_formats/standard_macros.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html index 2bfcf074ab70..5554958c3e61 100644 --- a/frappe/templates/print_formats/standard_macros.html +++ b/frappe/templates/print_formats/standard_macros.html @@ -109,7 +109,7 @@
{%- if df.fieldtype in ("Text", "Code", "Long Text") %}{%- endif %} {%- if df.fieldtype=="Code" %} -
{{ doc.get(df.fieldname) }}
+
{{ doc.get(df.fieldname)|e }}
{% else -%} {{ doc.get_formatted(df.fieldname, parent_doc or doc, translated=df.translatable) }} {% endif -%} From 2ab788d711361bd5105c8abfbbb9ecade5422e97 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 11:54:20 +0530 Subject: [PATCH 087/115] fix: amount calculation issue (#22820) (#22867) (cherry picked from commit 484f18451e4c35d4feab9fea607c7c4f4c1b8cc1) Co-authored-by: rohitwaghchaure --- frappe/public/js/frappe/utils/number_format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index ef4c7366f72a..df6123b5e40b 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -175,7 +175,7 @@ function get_number_format_info(format) { function _round(num, precision, rounding_method) { rounding_method = - rounding_method || frappe.boot.sysdefaults.rounding_method || "Banker's Rounding (legacy)"; + rounding_method || frappe.boot.sysdefaults?.rounding_method || "Banker's Rounding (legacy)"; let is_negative = num < 0 ? true : false; From 1b3359dda171e7af31f5c8b44f1bac75e29a328e Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Mon, 23 Oct 2023 06:25:44 +0000 Subject: [PATCH 088/115] chore(release): Bumped to Version 14.52.2 ## [14.52.2](https://github.com/frappe/frappe/compare/v14.52.1...v14.52.2) (2023-10-23) ### Bug Fixes * amount calculation issue ([#22820](https://github.com/frappe/frappe/issues/22820)) ([#22867](https://github.com/frappe/frappe/issues/22867)) ([2ab788d](https://github.com/frappe/frappe/commit/2ab788d711361bd5105c8abfbbb9ecade5422e97)) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index f7ca02314c72..a93b30e0260e 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.52.1" +__version__ = "14.52.2" __title__ = "Frappe Framework" controllers = {} From 4ce39ecb05135805eeeb82abdf40859339ca7051 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 17:48:46 +0530 Subject: [PATCH 089/115] fix: don't encode `/` in html escaping (#22871) (#22874) This isn't really required and `/` is used in user facing text. (cherry picked from commit 9abb964d3305ddb87542fd4548b6b6120374e6f1) Co-authored-by: Ankush Menat --- frappe/public/js/frappe/utils/utils.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 4722ea104504..93a99c60cb3f 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -253,12 +253,11 @@ Object.assign(frappe.utils, { ">": ">", '"': """, "'": "'", - "/": "/", "`": "`", "=": "=", }; - return String(txt).replace(/[&<>"'`=/]/g, (char) => escape_html_mapping[char] || char); + return String(txt).replace(/[&<>"'`=]/g, (char) => escape_html_mapping[char] || char); }, unescape_html: function (txt) { @@ -268,13 +267,12 @@ Object.assign(frappe.utils, { ">": ">", """: '"', "'": "'", - "/": "/", "`": "`", "=": "=", }; return String(txt).replace( - /&|<|>|"|'|/|`|=/g, + /&|<|>|"|'|`|=/g, (char) => unescape_html_mapping[char] || char ); }, From 25b5861a94902aee435e2bde5a2421f9d39cb84d Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 24 Oct 2023 09:58:05 +0000 Subject: [PATCH 090/115] chore(release): Bumped to Version 14.53.0 # [14.53.0](https://github.com/frappe/frappe/compare/v14.52.2...v14.53.0) (2023-10-24) ### Bug Fixes * allow hiding/unhiding others private workspace if you are workspace manager ([89ebb9d](https://github.com/frappe/frappe/commit/89ebb9d56154fb7b3d19ed88cea7844589fb532f)) * amount calculation issue ([#22820](https://github.com/frappe/frappe/issues/22820)) ([484f184](https://github.com/frappe/frappe/commit/484f18451e4c35d4feab9fea607c7c4f4c1b8cc1)) * Check perms before sharing linked docs ([#22783](https://github.com/frappe/frappe/issues/22783)) ([#22786](https://github.com/frappe/frappe/issues/22786)) ([729d842](https://github.com/frappe/frappe/commit/729d842bc3c2c446400df8bbae60ee69c96c65b9)) * check read before assigning/removing ([#22779](https://github.com/frappe/frappe/issues/22779)) ([#22785](https://github.com/frappe/frappe/issues/22785)) ([61056c1](https://github.com/frappe/frappe/commit/61056c1ca815ce232ecf5e86b9fa1a1477681983)) * compare with content value for Link fieldtypes ([de6d271](https://github.com/frappe/frappe/commit/de6d27166829b0f543ad85893dc886d73ae2e2f2)) * cron job firing immediately after save (backport [#22795](https://github.com/frappe/frappe/issues/22795)) ([#22806](https://github.com/frappe/frappe/issues/22806)) ([f7c43e1](https://github.com/frappe/frappe/commit/f7c43e140f2f9b8081dc9d4b1e9bbb8f3a7466fc)) * do not allow creating private workspace for other users ([caa28b8](https://github.com/frappe/frappe/commit/caa28b848621f11f13cc4c6a2a7d02e614a183f3)) * do not allow deleting other's private workspaces ([745b79c](https://github.com/frappe/frappe/commit/745b79c77420af13abacbe78e5b8f593ed39efab)) * do not allow editing other's private workspaces ([11dd3a7](https://github.com/frappe/frappe/commit/11dd3a79ea5c30b0caa967c83f6dd0ec5007839c)) * don't encode `/` in html escaping ([#22871](https://github.com/frappe/frappe/issues/22871)) ([#22874](https://github.com/frappe/frappe/issues/22874)) ([4ce39ec](https://github.com/frappe/frappe/commit/4ce39ecb05135805eeeb82abdf40859339ca7051)) * escape print format code field([#22852](https://github.com/frappe/frappe/issues/22852)) ([#22865](https://github.com/frappe/frappe/issues/22865)) ([ba226fb](https://github.com/frappe/frappe/commit/ba226fb849647c9e91b1b9b8313f903399e4178f)) * filename xss ([#22778](https://github.com/frappe/frappe/issues/22778)) ([#22782](https://github.com/frappe/frappe/issues/22782)) ([87aa9e6](https://github.com/frappe/frappe/commit/87aa9e6da6cea4c7760ff624cfc2a31a56777b4d)) * ignore duplicate perm check on assign hooks (backport [#22832](https://github.com/frappe/frappe/issues/22832)) ([#22833](https://github.com/frappe/frappe/issues/22833)) ([a258d84](https://github.com/frappe/frappe/commit/a258d84c9b0e5f630a6f208c1f73bdaaf7245344)) * number card shorten number is not formatted correctly if number format is not default ([5f23775](https://github.com/frappe/frappe/commit/5f23775bba92432709d4a3f6c5c0dc979e3bb950)) * resolve page not found error ([4563b86](https://github.com/frappe/frappe/commit/4563b86971456028a0262d20a29148b6c6cea797)) ### Features * rate limit logins based on IP too (backport [#22774](https://github.com/frappe/frappe/issues/22774)) ([#22780](https://github.com/frappe/frappe/issues/22780)) ([de27391](https://github.com/frappe/frappe/commit/de27391e34005caa64bbacfd38aa83e6d8a12302)) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 9d2ba34db934..fadec88182e2 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.52.2" +__version__ = "14.53.0" __title__ = "Frappe Framework" controllers = {} From 1fcb7bc7cd48e097a4091957aa54744bfd443ade Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Tue, 24 Oct 2023 15:33:21 +0530 Subject: [PATCH 091/115] fix: relink attachments before saving doc ( backport #22693 ) (#22831) * fix: relink attachments before saving doc Certain people add attachment, before filling mandatory fields which will raise Missing Fields error. Or any other kind of errors raised by different validators due to which file is uploaded but doc is not saved. This will lead to orphaned/mislinked files. ex. new-purchase-receipt-1 This fix changes name of new docs to new---<10digithash> after saving the document we can use this new name to find any mislinked files created in past hour and relink them to the new doc on save. * fix(minor): don't update docname instead of updating docname use docname_title. https://github.com/frappe/frappe/pull/22693#discussion_r1354297835 * fix: don't update unnecessary fields removed attached_to_doctype and attached_to_field from set_value as those are not required. Co-authored-by: Ankush Menat * chore: rename __temporary_name & check temp_doc_name * renamed file_relink_temp_docname to __temporary_name * added check for temp_doc_name in relink_files in case someone calls the function it directly * fix(minor): remove __temporary_name after relink removed __temporary_name as after relink file there is no need to keep it * fix(minor): remove count from url Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * fix: attach file from library Previously, the attach from library only shared file_url, problem is we need more information such as is_private. fix: send name instead of file_url on file uploader and use that in upload_file handler to get more information about the file then attach that info to newly created file. * fix: don't overwrite name for library files name is used in many places and should not be overwritten, so i have sent name in upload_via_file_browser as library_file_name which can be used to get existing file. * fix: cypress and add check for __islocal changed check for __islocal as well in savedocs As new route ends with random hash instead of numbers, updated cypress test to just check start of data-route instead of entire path * fix: cypress tests as per new hash naming Some Cypress Tests are using hardcoded numbers in them. As new naming is based on random hash i have changed them accordingly. * fix: cypress attach control library button * As on clear of attach field file is deleted as well in `Checking functionality for "Link"` test. * Instead of assuming that file is present from previous test. * create a new doc and attach a file (link) to it. then create another doc and check library button functionality and then delete both docs. * fix(minor): set is_private as int instead of bool * fix: cypress add test for attach before doc save added cypress test where attachment is added before mandatory fields are filled so doc save will fail once and then succeed after mandatory fields are filled then check if file is attached to correct document and no orphaned files exist for that doc. * chore: use new_form command instead of visit Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> * chore: typo and replace cy.visit with cy.new_form * chore: fix linter --------- Co-authored-by: Ankush Menat Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- cypress/integration/control_attach.js | 118 +++++++++++++++++- cypress/integration/control_data.js | 7 +- cypress/integration/dashboard_chart.js | 2 +- cypress/integration/grid_keyboard_shortcut.js | 17 ++- cypress/integration/number_card.js | 2 +- cypress/integration/timeline.js | 2 +- cypress/support/commands.js | 5 +- frappe/core/doctype/file/utils.py | 48 ++++++- frappe/desk/form/save.py | 3 + frappe/handler.py | 11 ++ frappe/model/document.py | 2 + .../js/frappe/file_uploader/FileUploader.vue | 6 +- frappe/public/js/frappe/model/create_new.js | 7 +- .../public/js/frappe/utils/number_format.js | 4 +- frappe/public/js/frappe/views/breadcrumbs.js | 14 ++- 15 files changed, 223 insertions(+), 25 deletions(-) diff --git a/cypress/integration/control_attach.js b/cypress/integration/control_attach.js index 6714f6c24ee9..250cdc509832 100644 --- a/cypress/integration/control_attach.js +++ b/cypress/integration/control_attach.js @@ -64,6 +64,25 @@ context("Attach Control", () => { //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype cy.findByRole("button", { name: "Attach" }).click(); + //Clicking on "Link" button to attach a file using the "Link" button + cy.findByRole("button", { name: "Link" }).click(); + cy.findByPlaceholderText("Attach a web link").type( + "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg", + { force: true } + ); + + //Clicking on the Upload button to upload the file + cy.intercept("POST", "/api/method/upload_file").as("upload_image"); + cy.get(".modal-footer").findByRole("button", { name: "Upload" }).click({ delay: 500 }); + cy.wait("@upload_image"); + cy.findByRole("button", { name: "Save" }).click(); + + //Navigating to the new form for the newly created doctype to check Library button + cy.new_form("Test Attach Control"); + + //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype + cy.findByRole("button", { name: "Attach" }).click(); + //Clicking on "Library" button to attach a file using the "Library" button cy.findByRole("button", { name: "Library" }).click(); cy.contains("72402.jpg").click(); @@ -85,9 +104,10 @@ context("Attach Control", () => { //Checking if clicking on the clear button clears the field of the doctype form and again displays the attach button cy.get(".control-input > .btn-sm").should("contain", "Attach"); - //Deleting the doc + //Deleting both docs cy.go_to_list("Test Attach Control"); cy.get(".list-row-checkbox").eq(0).click(); + cy.get(".list-row-checkbox").eq(1).click(); cy.get(".actions-btn-group > .btn").contains("Actions").click(); cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click(); cy.click_modal_primary_button("Yes"); @@ -106,7 +126,10 @@ context("Attach Control", () => { }; }, }); - cy.get("body").should("have.attr", "data-route", `Form/${doctype}/new-${dt_in_route}-1`); + cy.get("body").should(($body) => { + const dataRoute = $body.attr("data-route"); + expect(dataRoute).to.match(new RegExp(`^Form/${doctype}/new-${dt_in_route}-`)); + }); cy.get("body").should("have.attr", "data-ajax-state", "complete"); //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype @@ -126,7 +149,10 @@ context("Attach Control", () => { delete win.navigator.mediaDevices; }, }); - cy.get("body").should("have.attr", "data-route", `Form/${doctype}/new-${dt_in_route}-1`); + cy.get("body").should(($body) => { + const dataRoute = $body.attr("data-route"); + expect(dataRoute).to.match(new RegExp(`^Form/${doctype}/new-${dt_in_route}-`)); + }); cy.get("body").should("have.attr", "data-ajax-state", "complete"); //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype @@ -136,3 +162,89 @@ context("Attach Control", () => { cy.findByRole("button", { name: "Camera" }).should("not.exist"); }); }); +context("Attach Control with Failed Document Save", () => { + before(() => { + cy.login(); + cy.visit("/app/doctype"); + return cy + .window() + .its("frappe") + .then((frappe) => { + return frappe.xcall("frappe.tests.ui_test_helpers.create_doctype", { + name: "Test Mandatory Attach Control", + fields: [ + { + label: "Attach File or Image", + fieldname: "attach", + fieldtype: "Attach", + in_list_view: 1, + }, + { + label: "Mandatory Text Field", + fieldname: "text_field", + fieldtype: "Text Editor", + in_list_view: 1, + reqd: 1, + }, + ], + }); + }); + }); + let temp_name = ""; + let docname = ""; + it("Attaching a file on an unsaved document", () => { + //Navigating to the new form for the newly created doctype + cy.new_form("Test Mandatory Attach Control"); + cy.get("body").should(($body) => { + temp_name = $body.attr("data-route").split("/")[2]; + }); + + //Clicking on the attach button which is displayed as part of creating a doctype with "Attach" fieldtype + cy.findByRole("button", { name: "Attach" }).click(); + + //Clicking on "Link" button to attach a file using the "Link" button + cy.findByRole("button", { name: "Link" }).click(); + cy.findByPlaceholderText("Attach a web link").type( + "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg", + { force: true } + ); + + //Clicking on the Upload button to upload the file + cy.intercept("POST", "/api/method/upload_file").as("upload_image"); + cy.get(".modal-footer").findByRole("button", { name: "Upload" }).click({ delay: 500 }); + cy.wait("@upload_image"); + cy.get(".msgprint-dialog .modal-title").contains("Missing Fields").should("be.visible"); + cy.hide_dialog(); + cy.fill_field("text_field", "Random value", "Text Editor").wait(500); + cy.findByRole("button", { name: "Save" }).click().wait(500); + + //Checking if the URL of the attached image is getting displayed in the field of the newly created doctype + cy.get(".attached-file > .ellipsis > .attached-file-link") + .should("have.attr", "href") + .and("equal", "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg"); + + cy.get(".title-text").then(($value) => { + docname = $value.text(); + }); + }); + + it("Check if file was uploaded correctly", () => { + cy.go_to_list("File"); + cy.open_list_filter(); + cy.get(".fieldname-select-area .form-control") + .click() + .type("Attached To Name{enter}") + .blur() + .wait(500); + cy.get('input[data-fieldname="attached_to_name"]').click().type(docname).blur(); + cy.get(".filter-popover .apply-filters").click({ force: true }); + cy.get("header .level-right .list-count").should("contain.text", "1 of 1"); + }); + + it("Check if file exists with temporary name", () => { + cy.open_list_filter(); + cy.get('input[data-fieldname="attached_to_name"]').click().clear().type(temp_name).blur(); + cy.get(".filter-popover .apply-filters").click({ force: true }); + cy.get(".frappe-list > .no-result").should("be.visible"); + }); +}); diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js index ee6dfbca95d3..ce2cf4ba1114 100644 --- a/cypress/integration/control_data.js +++ b/cypress/integration/control_data.js @@ -49,7 +49,7 @@ context("Data Control", () => { cy.new_form("Test Data Control"); //Checking the URL for the new form of the doctype - cy.location("pathname").should("eq", "/app/test-data-control/new-test-data-control-1"); + cy.location("pathname").should("contains", "/app/test-data-control/new-test-data-control"); cy.get(".title-text").should("have.text", "New Test Data Control"); cy.get('.frappe-control[data-fieldname="name1"]') .find("label") @@ -128,7 +128,10 @@ context("Data Control", () => { cy.fill_field("phone", "9432380001", "Data"); cy.findByRole("button", { name: "Save" }).click({ force: true }); //Checking if the fields contains the data which has been filled in - cy.location("pathname").should("not.be", "/app/test-data-control/new-test-data-control-1"); + cy.location("pathname").should( + "not.contains", + "/app/test-data-control/new-test-data-control" + ); cy.get_field("name1").should("have.value", "Komal"); cy.get_field("email").should("have.value", "komal@test.com"); cy.get_field("phone").should("have.value", "9432380001"); diff --git a/cypress/integration/dashboard_chart.js b/cypress/integration/dashboard_chart.js index 6023a50abe7c..f2a837e4b390 100644 --- a/cypress/integration/dashboard_chart.js +++ b/cypress/integration/dashboard_chart.js @@ -5,7 +5,7 @@ context("Dashboard Chart", () => { }); it("Check filter populate for child table doctype", () => { - cy.visit("/app/dashboard-chart/new-dashboard-chart-1"); + cy.new_form("Dashboard Chart"); cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none"); cy.get_field("document_type", "Link"); diff --git a/cypress/integration/grid_keyboard_shortcut.js b/cypress/integration/grid_keyboard_shortcut.js index 414e822516da..1bd11603b6eb 100644 --- a/cypress/integration/grid_keyboard_shortcut.js +++ b/cypress/integration/grid_keyboard_shortcut.js @@ -1,18 +1,25 @@ context("Grid Keyboard Shortcut", () => { let total_count = 0; + let contact_email_name = null; before(() => { cy.login(); }); beforeEach(() => { cy.reload(); - cy.visit("/app/contact/new-contact-1"); + cy.new_form("Contact"); cy.get('.frappe-control[data-fieldname="email_ids"]').find(".grid-add-row").click(); + // as new names uses hash instead of numbers get row's data-name dynamically. + cy.get('.frappe-control[data-fieldname="email_ids"]') + .find(".grid-body .grid-row") + .should(($row) => { + contact_email_name = $row.attr("data-name"); + }); }); it("Insert new row at the end", () => { cy.add_new_row_in_grid( "{ctrl}{shift}{downarrow}", (cy, total_count) => { - cy.get('[data-name="new-contact-email-1"]').should( + cy.get(`[data-name="${contact_email_name}"]`).should( "have.attr", "data-idx", `${total_count + 1}` @@ -23,17 +30,17 @@ context("Grid Keyboard Shortcut", () => { }); it("Insert new row at the top", () => { cy.add_new_row_in_grid("{ctrl}{shift}{uparrow}", (cy) => { - cy.get('[data-name="new-contact-email-1"]').should("have.attr", "data-idx", "2"); + cy.get(`[data-name="${contact_email_name}"]`).should("have.attr", "data-idx", "2"); }); }); it("Insert new row below", () => { cy.add_new_row_in_grid("{ctrl}{downarrow}", (cy) => { - cy.get('[data-name="new-contact-email-1"]').should("have.attr", "data-idx", "1"); + cy.get(`[data-name^="${contact_email_name}"]`).should("have.attr", "data-idx", "1"); }); }); it("Insert new row above", () => { cy.add_new_row_in_grid("{ctrl}{uparrow}", (cy) => { - cy.get('[data-name="new-contact-email-1"]').should("have.attr", "data-idx", "2"); + cy.get(`[data-name^="${contact_email_name}"]`).should("have.attr", "data-idx", "2"); }); }); }); diff --git a/cypress/integration/number_card.js b/cypress/integration/number_card.js index eb0f19be26dc..24227fe27b74 100644 --- a/cypress/integration/number_card.js +++ b/cypress/integration/number_card.js @@ -5,7 +5,7 @@ context("Number Card", () => { }); it("Check filter populate for child table doctype", () => { - cy.visit("/app/number-card/new-number-card-1"); + cy.new_form("Number Card"); cy.get('[data-fieldname="parent_document_type"]').should("have.css", "display", "none"); cy.get_field("document_type", "Link"); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js index 783581933466..ce5cae8f5d0e 100644 --- a/cypress/integration/timeline.js +++ b/cypress/integration/timeline.js @@ -8,7 +8,7 @@ context("Timeline", () => { it("Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo", () => { //Adding new ToDo - cy.visit("/app/todo/new-todo-1"); + cy.new_form("ToDo"); cy.get('[data-fieldname="description"] .ql-editor.ql-blank') .type("Test ToDo", { force: true }) .wait(200); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 88b4d1b5890d..81347c10d2ee 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -254,7 +254,10 @@ Cypress.Commands.add("awesomebar", (text) => { Cypress.Commands.add("new_form", (doctype) => { let dt_in_route = doctype.toLowerCase().replace(/ /g, "-"); cy.visit(`/app/${dt_in_route}/new`); - cy.get("body").should("have.attr", "data-route", `Form/${doctype}/new-${dt_in_route}-1`); + cy.get("body").should(($body) => { + const dataRoute = $body.attr("data-route"); + expect(dataRoute).to.match(new RegExp(`^Form/${doctype}/new-${dt_in_route}-`)); + }); cy.get("body").should("have.attr", "data-ajax-state", "complete"); }); diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index 240355c998a6..a1aee64c6610 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -13,7 +13,7 @@ import frappe from frappe import _, safe_decode -from frappe.utils import cstr, encode, get_files_path, random_string, strip +from frappe.utils import cint, cstr, encode, get_files_path, random_string, strip from frappe.utils.file_manager import safe_b64decode from frappe.utils.image import optimize_image @@ -337,6 +337,7 @@ def attach_files_to_document(doc: "Document", event) -> None: "attached_to_name": doc.name, "attached_to_doctype": doc.doctype, "attached_to_field": df.fieldname, + "is_private": cint(value.startswith("/private")), }, ) return @@ -355,6 +356,51 @@ def attach_files_to_document(doc: "Document", event) -> None: doc.log_error("Error Attaching File") +def relink_files(doc, fieldname, temp_doc_name): + if not temp_doc_name: + return + from frappe.utils.data import add_to_date, now_datetime + + """ + Relink files attached to incorrect document name to the new document name + by check if file with temp name exists that was created in last 60 minutes + """ + mislinked_file = frappe.db.exists( + "File", + { + "file_url": doc.get(fieldname), + "attached_to_name": temp_doc_name, + "attached_to_doctype": doc.doctype, + "attached_to_field": fieldname, + "creation": ( + "between", + [now_datetime() - add_to_date(date=now_datetime(), minutes=-60), now_datetime()], + ), + }, + ) + """If file exists, attach it to the new docname""" + if mislinked_file: + frappe.db.set_value( + "File", + mislinked_file, + field={ + "attached_to_name": doc.name, + }, + ) + return + + +def relink_mismatched_files(doc: "Document") -> None: + if not doc.get("__temporary_name", None): + return + attach_fields = doc.meta.get("fields", {"fieldtype": ["in", ["Attach", "Attach Image"]]}) + for df in attach_fields: + if doc.get(df.fieldname): + relink_files(doc, df.fieldname, doc.__temporary_name) + # delete temporary name after relinking is done + doc.delete_key("__temporary_name") + + def decode_file_content(content: bytes) -> bytes: if isinstance(content, str): content = content.encode("utf-8") diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 7e9dc035199d..b19d0bcadc8d 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -15,6 +15,9 @@ def savedocs(doc, action): """save / submit / update doclist""" doc = frappe.get_doc(json.loads(doc)) capture_doc(doc, action) + if doc.get("__islocal") and doc.name.startswith("new-" + doc.doctype.lower().replace(" ", "-")): + # required to relink missing attachments if they exist. + doc.__temporary_name = doc.name set_local_name(doc) # action diff --git a/frappe/handler.py b/frappe/handler.py index de59a22957c1..5215c45ebbf7 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -191,6 +191,17 @@ def upload_file(): optimize = frappe.form_dict.optimize content = None + if frappe.form_dict.get("library_file_name", False): + doc = frappe.get_value( + "File", + frappe.form_dict.library_file_name, + ["is_private", "file_url", "file_name"], + as_dict=True, + ) + is_private = doc.is_private + file_url = doc.file_url + filename = doc.file_name + if "file" in files: file = files["file"] content = file.stream.read() diff --git a/frappe/model/document.py b/frappe/model/document.py index cc74bc7687b3..c134aee162be 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -8,6 +8,7 @@ import frappe from frappe import _, is_whitelisted, msgprint +from frappe.core.doctype.file.utils import relink_mismatched_files from frappe.core.doctype.server_script.server_script_utils import run_server_script_for_doc_event from frappe.desk.form.document_follow import follow_document from frappe.integrations.doctype.webhook import run_webhooks @@ -284,6 +285,7 @@ def insert( # flag to prevent creation of event update log for create and update both # during document creation self.flags.update_log_for_doc_creation = True + relink_mismatched_files(self) self.run_post_save_methods() self.flags.in_insert = False diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 61ebfa210c1c..35e49bc40b45 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -414,7 +414,7 @@ export default { } this.close_dialog = true; return this.upload_file({ - file_url: selected_file.file_url + library_file_name: selected_file.value, }); }, upload_via_web_link() { @@ -530,6 +530,10 @@ export default { form_data.append('file_name', file.file_name); } + if (file.library_file_name) { + form_data.append('library_file_name', file.library_file_name); + } + if (this.doctype && this.docname) { form_data.append('doctype', this.doctype); form_data.append('docname', this.docname); diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 7acb707752bd..df8aacd1bea2 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -5,7 +5,6 @@ frappe.provide("frappe.model"); $.extend(frappe.model, { new_names: {}, - new_name_count: {}, get_new_doc: function (doctype, parent_doc, parentfield, with_mandatory_children) { frappe.provide("locals." + doctype); @@ -78,10 +77,8 @@ $.extend(frappe.model, { }, get_new_name: function (doctype) { - var cnt = frappe.model.new_name_count; - if (!cnt[doctype]) cnt[doctype] = 0; - cnt[doctype]++; - return frappe.router.slug(`new-${doctype}-${cnt[doctype]}`); + // random hash is added to idenity mislinked files when doc is not saved and file is uploaded. + return frappe.router.slug(`new-${doctype}-${frappe.utils.get_random(10)}`); }, set_default_values: function (doc, parent_doc) { diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index eebfcf8fc3b0..e340ac315371 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -200,7 +200,9 @@ function get_number_format_info(format) { function _round(num, precision, rounding_method) { rounding_method = - rounding_method || frappe.boot.sysdefaults?.rounding_method || "Banker's Rounding (legacy)"; + rounding_method || + frappe.boot.sysdefaults?.rounding_method || + "Banker's Rounding (legacy)"; let is_negative = num < 0 ? true : false; diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 673c68b4cb59..8920bb2f1e3e 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -187,9 +187,17 @@ frappe.breadcrumbs = { set_form_breadcrumb(breadcrumbs, view) { const doctype = breadcrumbs.doctype; - const docname = frappe.get_route().slice(2).join("/"); - let form_route = `/app/${frappe.router.slug(doctype)}/${docname}`; - this.append_breadcrumb_element(form_route, __(docname)); + let docname = frappe.get_route().slice(2).join("/"); + let docname_title = docname; + if (docname.startsWith("new-" + doctype.toLowerCase().replace(/ /g, "-"))) { + // using docname instead of doctype to include No like Doctype Name + 1, 2, 3 + docname_title = docname_title + .slice(0, -10) + .replace(/-/g, " ") + .replace(/\b\w/g, (l) => l.toUpperCase()); + } + let form_route = `/app/${frappe.router.slug(doctype)}/${docname_title}`; + this.append_breadcrumb_element(form_route, __(docname_title)); if (view === "form") { let last_crumb = this.$breadcrumbs.find("li").last(); From c37c9bf302ba71a4d3affbd7cc00b4373d635f7c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:08:40 +0530 Subject: [PATCH 092/115] fix: kanban remove unnecessary get_docfield (#22630) * Label will be same for all documents so there is no need to call get_docfield. * As it internally calls docfield_copy and makes "deep copy" for each document. which causes massive memory usage as number of document increases. * Instead get meta from docfield_map and use it. (cherry picked from commit 68221f4d9ee2f10b5c6ad995381c69ae414056e7) Co-authored-by: Maharshi Patel --- frappe/public/js/frappe/views/kanban/kanban_board.bundle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js b/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js index 8626807fef1b..404eb8beaf7e 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.bundle.js @@ -729,7 +729,7 @@ frappe.provide("frappe.views"); let fields = []; for (let field_name of cur_list.board.fields) { let field = - frappe.meta.get_docfield(card.doctype, field_name, card.name) || + frappe.meta.docfield_map[card.doctype]?.[field_name] || frappe.model.get_std_field(field_name); let label = cur_list.board.show_labels ? `${__(field.label)}: ` : ""; let value = frappe.format(card.doc[field_name], field); From aef832261f2e963914a1060b0be1042d1c57361c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:09:53 +0530 Subject: [PATCH 093/115] fix(minor): hide side section on new doc. (#22629) * on new doc, hide sidebar is hidden. however, layout-side-section is still visible. * added hide-sidebar class to layout-side-section when doc is new. (cherry picked from commit 658f58d1e0cabef6045d597fc0e7fdb6cafb9ca5) Co-authored-by: Maharshi Patel Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/form/sidebar/form_sidebar.js | 2 ++ frappe/public/scss/desk/sidebar.scss | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index 430ff487e76c..d7b12c3ef1de 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -66,7 +66,9 @@ frappe.ui.form.Sidebar = class { refresh() { if (this.frm.doc.__islocal) { this.sidebar.toggle(false); + this.page.sidebar.addClass("hide-sidebar"); } else { + this.page.sidebar.removeClass("hide-sidebar"); this.sidebar.toggle(true); this.frm.assign_to.refresh(); this.frm.attachments.refresh(); diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index a4d713986c3b..0105182cacda 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -161,6 +161,10 @@ body[data-route^="Module"] .main-menu { font-size: var(--text-md); padding-right: 30px; + &.hide-sidebar { + display: none; + } + > .divider { display: none !important; } From 3dbb79e5a9b820af3c063bfcb47f13c499279a32 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 24 Oct 2023 10:57:28 +0000 Subject: [PATCH 094/115] chore(release): Bumped to Version 14.53.1 ## [14.53.1](https://github.com/frappe/frappe/compare/v14.53.0...v14.53.1) (2023-10-24) ### Bug Fixes * kanban remove unnecessary get_docfield ([#22630](https://github.com/frappe/frappe/issues/22630)) ([c37c9bf](https://github.com/frappe/frappe/commit/c37c9bf302ba71a4d3affbd7cc00b4373d635f7c)) * **minor:** hide side section on new doc. ([#22629](https://github.com/frappe/frappe/issues/22629)) ([aef8322](https://github.com/frappe/frappe/commit/aef832261f2e963914a1060b0be1042d1c57361c)) * relink attachments before saving doc ( backport [#22693](https://github.com/frappe/frappe/issues/22693) ) ([#22831](https://github.com/frappe/frappe/issues/22831)) ([1fcb7bc](https://github.com/frappe/frappe/commit/1fcb7bc7cd48e097a4091957aa54744bfd443ade)), closes [/github.com/frappe/frappe/pull/22693#discussion_r1354297835](https://github.com//github.com/frappe/frappe/pull/22693/issues/discussion_r1354297835) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index fadec88182e2..093abec46d46 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.53.0" +__version__ = "14.53.1" __title__ = "Frappe Framework" controllers = {} From 4ac08b8a4d94361958e5988d8e7d0011768fbfa8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 14:08:30 +0530 Subject: [PATCH 095/115] fix: ignore workspace links on delete (#22895) (#22896) These can be manually modified later. It's not important to completely block and prevent deletion. (cherry picked from commit 287e13522cbae1f2e3656f9906381b877105b207) Co-authored-by: Ankush Menat --- frappe/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/hooks.py b/frappe/hooks.py index 1b450b3e8b31..df0976a24edc 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -414,6 +414,7 @@ "Integration Request", "Unhandled Email", "Webhook Request Log", + "Workspace", ] # Request Hooks From 11e5c002a5ed6c413b118776368a52ca6b54c9c4 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 25 Oct 2023 17:25:14 +0530 Subject: [PATCH 096/115] fix: Footer should show up once at end if not repeating header/footer (#22902) (#22909) * fix: Footer should show up once at end if not repeating header/footer * fix: only show page number if repeating header [skip ci] (cherry picked from commit a643098a26fcd82b352d8fa49ac22c1803daa448) Co-authored-by: Ankush Menat --- frappe/templates/print_formats/standard.html | 26 ++++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/frappe/templates/print_formats/standard.html b/frappe/templates/print_formats/standard.html index f5ce1d3f5a99..642e74415e64 100644 --- a/frappe/templates/print_formats/standard.html +++ b/frappe/templates/print_formats/standard.html @@ -6,19 +6,6 @@ {{ add_header(loop.index, layout|len, doc, letter_head, no_letterhead, footer, print_settings, print_heading_template) }}
- {% if print_settings.repeat_header_footer %} - - {% endif %} - {% for section in page %}
{%- if doc.print_line_breaks and loop.index != 1 -%}
{%- endif -%} @@ -35,5 +22,18 @@

{{ _(section.label) }}

{% endfor %}
{% endfor %} + +
{% endfor %} From 717402f09e478dbf4b6d3707f3cfa72db1bde815 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 25 Oct 2023 16:07:40 +0530 Subject: [PATCH 097/115] fix!: revert UI behavior of `fetch_if_empty` (cherry picked from commit 99ec771e8b4dfbb64284f14d0cddbbf976fff73c) --- frappe/public/js/frappe/form/controls/link.js | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 50e678f199dd..0adf4e71298f 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -589,19 +589,13 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat let field_value = ""; for (const [target_field, source_field] of Object.entries(fetch_map)) { if (value) field_value = response[source_field]; - let target_df = frappe.meta.get_docfield(df.parent, target_field); - let target_value = frappe.model.get_value(df.parent, docname, target_field); - if (target_df?.fetch_if_empty && target_value) { - continue; - } else { - frappe.model.set_value( - df.parent, - docname, - target_field, - field_value, - df.fieldtype - ); - } + frappe.model.set_value( + df.parent, + docname, + target_field, + field_value, + df.fieldtype + ); } } From b0e747914a70b83937111d442bbf4769cb253258 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 26 Oct 2023 15:02:03 +0530 Subject: [PATCH 098/115] fix: improve label and description for `fetch_if_empty` (cherry picked from commit f08f128b1c61b9d0663203cfeb812e79e1aeac58) --- frappe/core/doctype/docfield/docfield.json | 5 +++-- frappe/custom/doctype/custom_field/custom_field.json | 6 +++--- .../doctype/customize_form_field/customize_form_field.json | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index 4ad888b80ee6..cee6467d6c24 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -244,9 +244,10 @@ }, { "default": "0", + "description": "If unchecked, the value will always be re-fetched on save.", "fieldname": "fetch_if_empty", "fieldtype": "Check", - "label": "Fetch only if value is not set" + "label": "Fetch on Save if Empty" }, { "fieldname": "permissions", @@ -564,7 +565,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-06-08 19:05:10.778371", + "modified": "2023-10-25 06:53:45.194081", "modified_by": "Administrator", "module": "Core", "name": "DocField", diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index b685d6919283..53a003c88ea8 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -153,10 +153,10 @@ }, { "default": "0", - "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", + "description": "If unchecked, the value will always be re-fetched on save.", "fieldname": "fetch_if_empty", "fieldtype": "Check", - "label": "Fetch If Empty" + "label": "Fetch on Save if Empty" }, { "fieldname": "options_help", @@ -450,7 +450,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-06-08 19:05:51.737234", + "modified": "2023-10-25 06:55:10.713382", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index fa338d7e3791..f89956a6f935 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -193,10 +193,10 @@ }, { "default": "0", - "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", + "description": "If unchecked, the value will always be re-fetched on save.", "fieldname": "fetch_if_empty", "fieldtype": "Check", - "label": "Fetch If Empty" + "label": "Fetch on Save if Empty" }, { "fieldname": "permissions", @@ -477,7 +477,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-06-08 19:05:37.767838", + "modified": "2023-10-25 06:55:50.718441", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", From b4ebde7386d3efb3790b24b4045e33755e2bbbc0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 14:16:56 +0530 Subject: [PATCH 099/115] fix: ignore stale custom fields while checking back-links (#22940) (#22942) * fix: ignore non-existing back-links * refactor: less indentation, guard clauses (cherry picked from commit 1560fa86104b2c4bfbed7ddf54072cda6dbaff02) Co-authored-by: Ankush Menat --- frappe/model/delete_doc.py | 64 +++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 39dc567ff394..ca49e66c3594 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -248,37 +248,45 @@ def check_if_doc_is_linked(doc, method="Delete"): for lf in link_fields: link_dt, link_field, issingle = lf["parent"], lf["fieldname"], lf["issingle"] - if not issingle: - fields = ["name", "docstatus"] - if frappe.get_meta(link_dt).istable: - fields.extend(["parent", "parenttype"]) - - for item in frappe.db.get_values(link_dt, {link_field: doc.name}, fields, as_dict=True): - # available only in child table cases - item_parent = getattr(item, "parent", None) - linked_doctype = item.parenttype if item_parent else link_dt - - if linked_doctype in frappe.get_hooks("ignore_links_on_delete") or ( - linked_doctype in ignore_linked_doctypes and method == "Cancel" - ): - # don't check for communication and todo! - continue - - if method != "Delete" and (method != "Cancel" or not DocStatus(item.docstatus).is_submitted()): - # don't raise exception if not - # linked to a non-cancelled doc when deleting or to a submitted doc when cancelling - continue - elif link_dt == doc.doctype and (item_parent or item.name) == doc.name: - # don't raise exception if not - # linked to same item or doc having same name as the item - continue - else: - reference_docname = item_parent or item.name - raise_link_exists_exception(doc, linked_doctype, reference_docname) + try: + meta = frappe.get_meta(link_dt) + except frappe.DoesNotExistError: + # This mostly happens when app do not remove their customizations, we shouldn't + # prevent link checks from failing in those cases + continue - else: + if issingle: if frappe.db.get_value(link_dt, None, link_field) == doc.name: raise_link_exists_exception(doc, link_dt, link_dt) + continue + + fields = ["name", "docstatus"] + + if meta.istable: + fields.extend(["parent", "parenttype"]) + + for item in frappe.db.get_values(link_dt, {link_field: doc.name}, fields, as_dict=True): + # available only in child table cases + item_parent = getattr(item, "parent", None) + linked_doctype = item.parenttype if item_parent else link_dt + + if linked_doctype in frappe.get_hooks("ignore_links_on_delete") or ( + linked_doctype in ignore_linked_doctypes and method == "Cancel" + ): + # don't check for communication and todo! + continue + + if method != "Delete" and (method != "Cancel" or not DocStatus(item.docstatus).is_submitted()): + # don't raise exception if not + # linked to a non-cancelled doc when deleting or to a submitted doc when cancelling + continue + elif link_dt == doc.doctype and (item_parent or item.name) == doc.name: + # don't raise exception if not + # linked to same item or doc having same name as the item + continue + else: + reference_docname = item_parent or item.name + raise_link_exists_exception(doc, linked_doctype, reference_docname) def check_if_doc_is_dynamically_linked(doc, method="Delete"): From cb10eb0afd64da37268f08638aced7e9e5a4ac33 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 27 Oct 2023 16:21:18 +0530 Subject: [PATCH 100/115] Revert "refactor: force ipv4 localhost (#22394) (#22396)" (#22946) This reverts commit 69811a76c3c5199426a22dda93e729d3e7a576c5. --- frappe/app.py | 2 +- frappe/commands/site.py | 2 +- frappe/email/test_smtp.py | 2 +- .../network_printer_settings/network_printer_settings.py | 2 +- frappe/tests/test_db.py | 2 +- frappe/utils/backups.py | 2 +- frappe/utils/connections.py | 2 +- frappe/utils/data.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index c817a5e66ea1..8fd2bf8380a6 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -417,7 +417,7 @@ def serve( application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(sites_path))}) application.debug = True - application.config = {"SERVER_NAME": "127.0.0.1:8000"} + application.config = {"SERVER_NAME": "localhost:8000"} log = logging.getLogger("werkzeug") log.propagate = False diff --git a/frappe/commands/site.py b/frappe/commands/site.py index e0f9c30bdca8..cf5164df846e 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1092,7 +1092,7 @@ def start_ngrok(context, bind_tls): port = frappe.conf.http_port or frappe.conf.webserver_port tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls) print(f"Public URL: {tunnel.public_url}") - print("Inspect logs at http://127.0.0.1:4040") + print("Inspect logs at http://localhost:4040") ngrok_process = ngrok.get_ngrok_process() try: diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py index 0495fd8596af..b101f610a18d 100644 --- a/frappe/email/test_smtp.py +++ b/frappe/email/test_smtp.py @@ -73,7 +73,7 @@ def create_email_account(email_id, password, enable_outgoing, default_outgoing=0 "enable_incoming": 1, "append_to": append_to, "is_dummy_password": 1, - "smtp_server": "127.0.0.1", + "smtp_server": "localhost", "use_imap": 0, } diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py index 23403e543e8a..3e0b8f0c2ee0 100644 --- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py @@ -8,7 +8,7 @@ class NetworkPrinterSettings(Document): @frappe.whitelist() - def get_printers_list(self, ip="127.0.0.1", port=631): + def get_printers_list(self, ip="localhost", port=631): printer_list = [] try: import cups diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index d710e25e57b6..5739ae1829a9 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -918,7 +918,7 @@ def _get_transaction_id(): # Treat same DB as replica for tests, a separate connection will be opened class TestReplicaConnections(FrappeTestCase): def test_switching_to_replica(self): - with patch.dict(frappe.local.conf, {"read_from_replica": 1, "replica_host": "127.0.0.1"}): + with patch.dict(frappe.local.conf, {"read_from_replica": 1, "replica_host": "localhost"}): def db_id(): return id(frappe.local.db) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 366eb7ec53da..266e7c2d5e10 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -31,7 +31,7 @@ class BackupGenerator: """ This class contains methods to perform On Demand Backup - To initialize, specify (db_name, user, password, db_file_name=None, db_host="127.0.0.1") + To initialize, specify (db_name, user, password, db_file_name=None, db_host="localhost") If specifying db_file_name, also append ".sql.gz" """ diff --git a/frappe/utils/connections.py b/frappe/utils/connections.py index 341e92fea806..6660e6ce19a3 100644 --- a/frappe/utils/connections.py +++ b/frappe/utils/connections.py @@ -22,7 +22,7 @@ def is_open(ip, port, timeout=10): def check_database(): config = get_conf() db_type = config.get("db_type", "mariadb") - db_host = config.get("db_host", "127.0.0.1") + db_host = config.get("db_host", "localhost") db_port = config.get("db_port", 3306 if db_type == "mariadb" else 5432) return {db_type: is_open(db_host, db_port)} diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 487bed803cea..2e213d2fa8b2 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1623,7 +1623,7 @@ def get_url(uri: str | None = None, full_address: bool = False) -> str: host_name = frappe.db.get_single_value("Website Settings", "subdomain") if not host_name: - host_name = "http://127.0.0.1" + host_name = "http://localhost" if host_name and not (host_name.startswith("http://") or host_name.startswith("https://")): host_name = "http://" + host_name From 0e684ccb66c6b52b00082950b99122e633c0b81f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 27 Oct 2023 16:23:44 +0530 Subject: [PATCH 101/115] Revert "refactor: force ipv4 localhost (#22394) (#22396)" (#22946) (#22948) This reverts commit 69811a76c3c5199426a22dda93e729d3e7a576c5. (cherry picked from commit cb10eb0afd64da37268f08638aced7e9e5a4ac33) Co-authored-by: Ankush Menat --- frappe/app.py | 2 +- frappe/commands/site.py | 2 +- frappe/email/test_smtp.py | 2 +- .../network_printer_settings/network_printer_settings.py | 2 +- frappe/tests/test_db.py | 2 +- frappe/utils/backups.py | 2 +- frappe/utils/connections.py | 2 +- frappe/utils/data.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index c817a5e66ea1..8fd2bf8380a6 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -417,7 +417,7 @@ def serve( application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(sites_path))}) application.debug = True - application.config = {"SERVER_NAME": "127.0.0.1:8000"} + application.config = {"SERVER_NAME": "localhost:8000"} log = logging.getLogger("werkzeug") log.propagate = False diff --git a/frappe/commands/site.py b/frappe/commands/site.py index e0f9c30bdca8..cf5164df846e 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1092,7 +1092,7 @@ def start_ngrok(context, bind_tls): port = frappe.conf.http_port or frappe.conf.webserver_port tunnel = ngrok.connect(addr=str(port), host_header=site, bind_tls=bind_tls) print(f"Public URL: {tunnel.public_url}") - print("Inspect logs at http://127.0.0.1:4040") + print("Inspect logs at http://localhost:4040") ngrok_process = ngrok.get_ngrok_process() try: diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py index 0495fd8596af..b101f610a18d 100644 --- a/frappe/email/test_smtp.py +++ b/frappe/email/test_smtp.py @@ -73,7 +73,7 @@ def create_email_account(email_id, password, enable_outgoing, default_outgoing=0 "enable_incoming": 1, "append_to": append_to, "is_dummy_password": 1, - "smtp_server": "127.0.0.1", + "smtp_server": "localhost", "use_imap": 0, } diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py index 23403e543e8a..3e0b8f0c2ee0 100644 --- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py @@ -8,7 +8,7 @@ class NetworkPrinterSettings(Document): @frappe.whitelist() - def get_printers_list(self, ip="127.0.0.1", port=631): + def get_printers_list(self, ip="localhost", port=631): printer_list = [] try: import cups diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index d710e25e57b6..5739ae1829a9 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -918,7 +918,7 @@ def _get_transaction_id(): # Treat same DB as replica for tests, a separate connection will be opened class TestReplicaConnections(FrappeTestCase): def test_switching_to_replica(self): - with patch.dict(frappe.local.conf, {"read_from_replica": 1, "replica_host": "127.0.0.1"}): + with patch.dict(frappe.local.conf, {"read_from_replica": 1, "replica_host": "localhost"}): def db_id(): return id(frappe.local.db) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 366eb7ec53da..266e7c2d5e10 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -31,7 +31,7 @@ class BackupGenerator: """ This class contains methods to perform On Demand Backup - To initialize, specify (db_name, user, password, db_file_name=None, db_host="127.0.0.1") + To initialize, specify (db_name, user, password, db_file_name=None, db_host="localhost") If specifying db_file_name, also append ".sql.gz" """ diff --git a/frappe/utils/connections.py b/frappe/utils/connections.py index 341e92fea806..6660e6ce19a3 100644 --- a/frappe/utils/connections.py +++ b/frappe/utils/connections.py @@ -22,7 +22,7 @@ def is_open(ip, port, timeout=10): def check_database(): config = get_conf() db_type = config.get("db_type", "mariadb") - db_host = config.get("db_host", "127.0.0.1") + db_host = config.get("db_host", "localhost") db_port = config.get("db_port", 3306 if db_type == "mariadb" else 5432) return {db_type: is_open(db_host, db_port)} diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 487bed803cea..2e213d2fa8b2 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1623,7 +1623,7 @@ def get_url(uri: str | None = None, full_address: bool = False) -> str: host_name = frappe.db.get_single_value("Website Settings", "subdomain") if not host_name: - host_name = "http://127.0.0.1" + host_name = "http://localhost" if host_name and not (host_name.startswith("http://") or host_name.startswith("https://")): host_name = "http://" + host_name From 23201a3477b3f4027b4988978e52990b38399f55 Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Fri, 27 Oct 2023 10:55:13 +0000 Subject: [PATCH 102/115] chore(release): Bumped to Version 14.53.2 ## [14.53.2](https://github.com/frappe/frappe/compare/v14.53.1...v14.53.2) (2023-10-27) ### Reverts * Revert "refactor: force ipv4 localhost (#22394) (#22396)" (#22946) (#22948) ([0e684cc](https://github.com/frappe/frappe/commit/0e684ccb66c6b52b00082950b99122e633c0b81f)), closes [#22394](https://github.com/frappe/frappe/issues/22394) [#22396](https://github.com/frappe/frappe/issues/22396) [#22946](https://github.com/frappe/frappe/issues/22946) [#22948](https://github.com/frappe/frappe/issues/22948) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 093abec46d46..8bba78757ffe 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.53.1" +__version__ = "14.53.2" __title__ = "Frappe Framework" controllers = {} From 1cc4ce329628dc69634f7c1c80146dde2092c451 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 15:22:18 +0530 Subject: [PATCH 103/115] fix: group by didn't work for the frappe.client.get_value (#22969) (#22972) (cherry picked from commit aa0f0d51e7264f9f402651e201c140a95a25dd6f) Co-authored-by: rohitwaghchaure --- frappe/client.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/client.py b/frappe/client.py index d48e5e2ab98c..23f73fea8e8b 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -27,6 +27,7 @@ def get_list( doctype, fields=None, filters=None, + group_by=None, order_by=None, limit_start=None, limit_page_length=20, @@ -52,6 +53,7 @@ def get_list( fields=fields, filters=filters, or_filters=or_filters, + group_by=group_by, order_by=order_by, limit_start=limit_start, limit_page_length=limit_page_length, From 70745b294348b7f6e8639708a5488bd8f227ab6d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 30 Oct 2023 15:23:25 +0530 Subject: [PATCH 104/115] fix: revert unintentional quick entry default for Address (#22974) (cherry picked from commit e8cbf61e6ae6ca7e8808270c6bdc7b74f6e8007f) --- frappe/contacts/doctype/address/address.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/contacts/doctype/address/address.json b/frappe/contacts/doctype/address/address.json index 4f90d1549da9..a269ba68629d 100644 --- a/frappe/contacts/doctype/address/address.json +++ b/frappe/contacts/doctype/address/address.json @@ -148,7 +148,7 @@ "icon": "fa fa-map-marker", "idx": 5, "links": [], - "modified": "2023-10-11 11:48:26.954934", + "modified": "2023-10-30 05:50:23.912366", "modified_by": "Administrator", "module": "Contacts", "name": "Address", From dbbfa17f9e2f6063339bd612acd80bcc2e294a01 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 16:46:00 +0530 Subject: [PATCH 105/115] feat: webhook timeout (backport #21410) (#22978) * feat: webhook timeout (#21410) * feat: webhook timeout * fix: ensure default timeout 5 seconds Co-authored-by: Ankush Menat (cherry picked from commit 62a3a70bf8d81445648928003f335e532e92ad7c) # Conflicts: # frappe/integrations/doctype/webhook/webhook.json * chore: conflicts --------- Co-authored-by: Devin Slauenwhite Co-authored-by: Ankush Menat --- frappe/integrations/doctype/webhook/webhook.json | 11 ++++++++++- frappe/integrations/doctype/webhook/webhook.py | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index a21e46065956..ca0eb747c271 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -18,6 +18,7 @@ "html_condition", "sb_webhook", "request_url", + "timeout", "request_method", "cb_webhook", "request_structure", @@ -200,10 +201,18 @@ { "fieldname": "section_break_28", "fieldtype": "Section Break" + }, + { + "default": "5", + "description": "The number of seconds until the request expires", + "fieldname": "timeout", + "fieldtype": "Int", + "label": "Request Timeout", + "reqd": 1 } ], "links": [], - "modified": "2022-07-11 08:54:10.740512", + "modified": "2023-06-16 10:21:00.971833", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index f7e5d38c2213..1d3a869bea29 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -136,7 +136,7 @@ def enqueue_webhook(doc, webhook) -> None: url=webhook.request_url, data=json.dumps(data, default=str), headers=headers, - timeout=5, + timeout=webhook.timeout or 5, ) r.raise_for_status() frappe.logger().debug({"webhook_success": r.text}) From 8e928943436f0f1d198ced7b3609f0037d067f25 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:24:38 +0530 Subject: [PATCH 106/115] perf: query fields only once (#22982) (#22987) Signed-off-by: Akhil Narang [skip ci] (cherry picked from commit c4544e89893ec21672ab24d778e51ee75979ddfc) Co-authored-by: Akhil Narang --- frappe/core/doctype/doctype/doctype.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 281ff8ca364a..a7edc705b1d0 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -245,8 +245,10 @@ def setup_fields_to_fetch(self): self.flags.update_fields_to_fetch_queries = [] - if set(old_fields_to_fetch) != {df.fieldname for df in new_meta.get_fields_to_fetch()}: - for df in new_meta.get_fields_to_fetch(): + new_fields_to_fetch = [df for df in new_meta.get_fields_to_fetch()] + + if set(old_fields_to_fetch) != {df.fieldname for df in new_fields_to_fetch}: + for df in new_fields_to_fetch: if df.fieldname not in old_fields_to_fetch: link_fieldname, source_fieldname = df.fetch_from.split(".", 1) link_df = new_meta.get_field(link_fieldname) From e2016fe77182c0bf6042524a8ad3bbd910a3f1c2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:34:53 +0530 Subject: [PATCH 107/115] refactor: Writing multi-pdf (backport #22981) (#22990) * refactor: Writing multi-pdf (#22981) - rename variables, output -> pdf_writer - write to in memory stream instead of disk [skip ci] (cherry picked from commit d3225a6df9d87264288cc11009b3bd9aafc4ea1e) * chore: keep deprecated function slated for removal in future This is used in ERPNext. [skip ci] (cherry picked from commit daca4dd31c42ac203cca666d1d4239d6d1de4db3) --------- Co-authored-by: Ankush Menat --- frappe/utils/print_format.py | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index b96809f2c2aa..cea89a0a90d7 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -1,4 +1,5 @@ import os +from io import BytesIO from PyPDF2 import PdfWriter @@ -6,6 +7,7 @@ from frappe import _ from frappe.core.doctype.access_log.access_log import make_access_log from frappe.translate import print_language +from frappe.utils.deprecations import deprecated from frappe.utils.pdf import get_pdf no_cache = 1 @@ -61,7 +63,7 @@ def download_multi_pdf( import json - output = PdfWriter() + pdf_writer = PdfWriter() if isinstance(options, str): options = json.loads(options) @@ -71,12 +73,12 @@ def download_multi_pdf( # Concatenating pdf files for i, ss in enumerate(result): - output = frappe.get_print( + pdf_writer = frappe.get_print( doctype, ss, format, as_pdf=True, - output=output, + output=pdf_writer, no_letterhead=no_letterhead, letterhead=letterhead, pdf_options=options, @@ -88,12 +90,12 @@ def download_multi_pdf( for doctype_name in doctype: for doc_name in doctype[doctype_name]: try: - output = frappe.get_print( + pdf_writer = frappe.get_print( doctype_name, doc_name, format, as_pdf=True, - output=output, + output=pdf_writer, no_letterhead=no_letterhead, letterhead=letterhead, pdf_options=options, @@ -107,19 +109,18 @@ def download_multi_pdf( ) frappe.local.response.filename = f"{name}.pdf" - frappe.local.response.filecontent = read_multi_pdf(output) - frappe.local.response.type = "pdf" - + with BytesIO() as merged_pdf: + pdf_writer.write(merged_pdf) + frappe.local.response.filecontent = merged_pdf.getvalue() -def read_multi_pdf(output): - # Get the content of the merged pdf files - fname = os.path.join("/tmp", f"frappe-pdf-{frappe.generate_hash()}.pdf") - output.write(open(fname, "wb")) + frappe.local.response.type = "pdf" - with open(fname, "rb") as fileobj: - filedata = fileobj.read() - return filedata +@deprecated +def read_multi_pdf(output: PdfWriter) -> bytes: + with BytesIO() as merged_pdf: + output.write(merged_pdf) + return merged_pdf.getvalue() @frappe.whitelist(allow_guest=True) From abc3cca306519878cc5bcbd1051bd79110da4d71 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 17:47:06 +0530 Subject: [PATCH 108/115] fix(UX): Improve search relevance for link field (backport #22729) (#22966) * fix: Improve search relevance for search_link When `locate` returns 0 it's shown on top instead it should be shown last or not shown at all. This is math hack to avoid using any complex SQL functionality which isn't allowed in DB query. (cherry picked from commit 31444228c3793095b918a7fb8aac1bf02c5b2341) * fix: Give idx higher preference than meta order Meta order in most cases is default "modified" which doesn't quite help. idx is # of times a document is referred to somewhere else, which is more likely to be relevant. (cherry picked from commit fec7759d00d5036c8a9ad6704a4f3c6b8509e997) * fix: Make search_link query postgres compatible (cherry picked from commit 55a444959e2287f57f9f8ffa324de945ea26ba4f) --------- Co-authored-by: Ankush Menat --- frappe/desk/search.py | 18 ++++++++++-------- frappe/tests/test_search.py | 22 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 39e08453dfee..453990f0912c 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -188,16 +188,18 @@ def search_widget( order_by_based_on_meta = get_order_by(doctype, meta) # 2 is the index of _relevance column - order_by = f"{order_by_based_on_meta}, `tab{doctype}`.idx desc" + order_by = f"`tab{doctype}`.idx desc, {order_by_based_on_meta}" if not meta.translated_doctype: - formatted_fields.append( - """locate({_txt}, `tab{doctype}`.`name`) as `_relevance`""".format( - _txt=frappe.db.escape((txt or "").replace("%", "").replace("@", "")), - doctype=doctype, - ) - ) - order_by = f"_relevance, {order_by}" + _txt = frappe.db.escape((txt or "").replace("%", "").replace("@", "")) + _relevance = f"(1 / nullif(locate({_txt}, `tab{doctype}`.`name`), 0))" + formatted_fields.append(f"""{_relevance} as `_relevance`""") + # Since we are sorting by alias postgres needs to know number of column we are sorting + if frappe.db.db_type == "mariadb": + order_by = f"ifnull(_relevance, -9999) desc, {order_by}" + elif frappe.db.db_type == "postgres": + # Since we are sorting by alias postgres needs to know number of column we are sorting + order_by = f"{len(formatted_fields)} desc nulls last, {order_by}" ignore_permissions = ( True diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index 116e741ad7ab..5d1d9672287b 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import re +from functools import partial import frappe from frappe.app import make_form_dict @@ -132,7 +133,7 @@ def test_validate_and_sanitize_search_inputs(self): def test_reference_doctype(self): """search query methods should get reference_doctype if they want""" - search_link( + results = test_search( doctype="User", txt="", filters=None, @@ -140,7 +141,24 @@ def test_reference_doctype(self): reference_doctype="ToDo", query="frappe.tests.test_search.query_with_reference_doctype", ) - self.assertListEqual(frappe.response["results"], []) + self.assertListEqual(results, []) + + def test_search_relevance(self): + search = partial(test_search, doctype="Language", filters=None, page_length=10) + for row in search(txt="e"): + self.assertTrue(row["value"].startswith("e")) + + for row in search(txt="es"): + self.assertIn("es", row["value"]) + + # Assume that "es" is used at least 10 times, it should now be first + frappe.db.set_value("Language", "es", "idx", 10) + self.assertEqual("es", search(txt="es")[0]["value"]) + + +def test_search(*args, **kwargs): + search_link(*args, **kwargs) + return frappe.response["results"] @frappe.validate_and_sanitize_search_inputs From e14f3009a041c20f51c4cde4fc17b33304bf60d5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 Oct 2023 17:48:57 +0530 Subject: [PATCH 109/115] chore: remove unnecessary test --- .../scheduled_job_type/test_scheduled_job_type.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py index 689cf6fed38c..8066a5c50004 100644 --- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py @@ -68,15 +68,6 @@ def test_monthly_job(self): self.assertFalse(job.is_event_due(get_datetime("2019-01-31 23:59:59"))) def test_cron_job(self): - # Daily but offset by 45 minutes - job = frappe.get_doc( - "Scheduled Job Type", - dict(method="frappe.core.doctype.log_settings.log_settings.run_log_clean_up"), - ) - self.assertEqual( - job.next_execution, - add_to_date(None, days=1).replace(hour=0, minute=45, second=0, microsecond=0), - ) # runs every 15 mins job = frappe.get_doc("Scheduled Job Type", dict(method="frappe.oauth.delete_oauth2_data")) job.db_set("last_execution", "2019-01-01 00:00:00") From 62841ebe348e9a546ccf663a85e6905fa263cf09 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:10:46 +0530 Subject: [PATCH 110/115] fix: developer mode in website theme (#22958) (#22997) [skip ci] (cherry picked from commit 36ad7e053e3beb0d0ede27f747799a5c0b9ce919) Co-authored-by: Thomas Fojan --- frappe/website/doctype/website_theme/website_theme.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/doctype/website_theme/website_theme.js b/frappe/website/doctype/website_theme/website_theme.js index 5c0524f357f2..6bedf78cd17d 100644 --- a/frappe/website/doctype/website_theme/website_theme.js +++ b/frappe/website/doctype/website_theme/website_theme.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Website Theme", { refresh(frm) { frm.clear_custom_buttons(); - frm.toggle_display(["module", "custom"], !frappe.boot.developer_mode); + frm.toggle_display(["module", "custom"], frappe.boot.developer_mode); frm.trigger("set_default_theme_button_and_indicator"); frm.trigger("make_app_theme_selector"); From 0f03ddd527c9015cb8811ff1c4b06c263b5c0067 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:33:24 +0530 Subject: [PATCH 111/115] fix(migrate): raise exception even if db is not available (#22922) (#22999) * fix(migrate): raise exception even if db is not available * fix: wrap correctly (cherry picked from commit c354b31b1fb67d4c32e69c620cb9800d8aeba991) Co-authored-by: Ankush Menat --- frappe/migrate.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frappe/migrate.py b/frappe/migrate.py index 692b5c2ca3a3..455edab81e60 100644 --- a/frappe/migrate.py +++ b/frappe/migrate.py @@ -1,6 +1,8 @@ # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import contextlib +import functools import json import os from textwrap import dedent @@ -36,14 +38,18 @@ def atomic(method): + @functools.wraps(method) def wrapper(*args, **kwargs): try: ret = method(*args, **kwargs) frappe.db.commit() return ret - except Exception: - frappe.db.rollback() - raise + except Exception as e: + # database itself can be gone while attempting rollback. + # We should preserve original exception in this case. + with contextlib.suppress(Exception): + frappe.db.rollback() + raise e return wrapper From 91b87daddd21006cd38824c5fca45646e7bcf6d5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:52:35 +0530 Subject: [PATCH 112/115] fix: list view formatting logic (backport #22646) (#22848) Co-authored-by: barredterra <14891507+barredterra@users.noreply.github.com> --- frappe/public/js/frappe/list/list_view.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 2709668dcbaa..5d705ad14f28 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -827,15 +827,18 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { frappe.model.is_numeric_field(df) ? "text-right" : "", ].join(" "); - const html_map = { - Subject: this.get_subject_html(doc), - Field: field_html(), - }; - let column_html = html_map[col.type]; - - // listview_setting formatter - if (this.settings.formatters && this.settings.formatters[fieldname]) { + let column_html; + if ( + this.settings.formatters && + this.settings.formatters[fieldname] && + col.type !== "Subject" + ) { column_html = this.settings.formatters[fieldname](value, df, doc); + } else { + column_html = { + Subject: this.get_subject_html(doc), + Field: field_html(), + }[col.type]; } return ` From 48ffb1fcfa67d083ec1fd09855f5a45b3f5a52dc Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:55:03 +0530 Subject: [PATCH 113/115] fix: skip invalid numbers on SMS `receiver_list` (backport #22879) (#23002) Co-authored-by: Kevin Shenk --- frappe/core/doctype/sms_settings/sms_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/sms_settings/sms_settings.py b/frappe/core/doctype/sms_settings/sms_settings.py index 0a5536eb9bb5..df5c4bf5e8e2 100644 --- a/frappe/core/doctype/sms_settings/sms_settings.py +++ b/frappe/core/doctype/sms_settings/sms_settings.py @@ -15,7 +15,7 @@ def validate_receiver_nos(receiver_list): validated_receiver_list = [] for d in receiver_list: if not d: - break + continue # remove invalid character for x in [" ", "-", "(", ")"]: From 254d2e5fe26fc7b6100b1d074777345f91aa557b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 19:29:50 +0530 Subject: [PATCH 114/115] fix(query_doctypes): Allow search in translated name (backport #22590) (#22748) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> fix(query_doctypes): Allow search in translated name (#22590) --- .../permitted_documents_for_user.py | 14 +++++----- frappe/tests/test_search.py | 27 +++++++++++++++++++ 2 files changed, 33 insertions(+), 8 deletions(-) diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index af7768c26d61..ab7ca43fc557 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -56,11 +56,9 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters): single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})] - out = [] - for dt in can_read: - if txt.lower().replace("%", "") in dt.lower() and ( - include_single_doctypes or dt not in single_doctypes - ): - out.append([dt]) - - return out + return [ + [dt] + for dt in can_read + if txt.lower().replace("%", "") in frappe._(dt).lower() + and (include_single_doctypes or dt not in single_doctypes) + ] diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index 5d1d9672287b..422e2e01ad98 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -95,6 +95,33 @@ def test_link_search_in_foreign_language(self): finally: frappe.local.lang = "en" + def test_doctype_search_in_foreign_language(self): + def do_search(txt: str): + search_link( + doctype="DocType", + txt=txt, + query="frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes", + filters={"user": "Administrator"}, + page_length=20, + searchfield=None, + ) + return frappe.response["results"] + + try: + frappe.local.lang = "en" + results = do_search("user") + self.assertIn("User", [x["value"] for x in results]) + + frappe.local.lang = "fr" + results = do_search("utilisateur") + self.assertIn("User", [x["value"] for x in results]) + + frappe.local.lang = "de" + results = do_search("nutzer") + self.assertIn("User", [x["value"] for x in results]) + finally: + frappe.local.lang = "en" + def test_validate_and_sanitize_search_inputs(self): # should raise error if searchfield is injectable From 64c5705372b418a92f49f34f63e215c7f8a918bb Mon Sep 17 00:00:00 2001 From: Frappe PR Bot Date: Tue, 31 Oct 2023 10:03:43 +0000 Subject: [PATCH 115/115] chore(release): Bumped to Version 14.54.0 # [14.54.0](https://github.com/frappe/frappe/compare/v14.53.2...v14.54.0) (2023-10-31) ### Bug Fixes * check label for fields ([4fbda9b](https://github.com/frappe/frappe/commit/4fbda9b9c57a8b228b4ec4aeef3dd704c52e6319)) * child rows for cancelled docs in getdiff ([d79e0b9](https://github.com/frappe/frappe/commit/d79e0b9ae827e279630afde6a2f303e55c4808be)) * developer mode in website theme ([#22958](https://github.com/frappe/frappe/issues/22958)) ([#22997](https://github.com/frappe/frappe/issues/22997)) ([62841eb](https://github.com/frappe/frappe/commit/62841ebe348e9a546ccf663a85e6905fa263cf09)) * fetch prev docs from last amended doc ([9b12aa6](https://github.com/frappe/frappe/commit/9b12aa6d939d39d4dedb5b112cec36ecbd956ee7)) * Footer should show up once at end if not repeating header/footer ([#22902](https://github.com/frappe/frappe/issues/22902)) ([#22909](https://github.com/frappe/frappe/issues/22909)) ([11e5c00](https://github.com/frappe/frappe/commit/11e5c002a5ed6c413b118776368a52ca6b54c9c4)) * group by didn't work for the frappe.client.get_value ([#22969](https://github.com/frappe/frappe/issues/22969)) ([#22972](https://github.com/frappe/frappe/issues/22972)) ([1cc4ce3](https://github.com/frappe/frappe/commit/1cc4ce329628dc69634f7c1c80146dde2092c451)) * ignore stale custom fields while checking back-links ([#22940](https://github.com/frappe/frappe/issues/22940)) ([#22942](https://github.com/frappe/frappe/issues/22942)) ([b4ebde7](https://github.com/frappe/frappe/commit/b4ebde7386d3efb3790b24b4045e33755e2bbbc0)) * ignore workspace links on delete ([#22895](https://github.com/frappe/frappe/issues/22895)) ([#22896](https://github.com/frappe/frappe/issues/22896)) ([4ac08b8](https://github.com/frappe/frappe/commit/4ac08b8a4d94361958e5988d8e7d0011768fbfa8)) * improve label and description for `fetch_if_empty` ([b0e7479](https://github.com/frappe/frappe/commit/b0e747914a70b83937111d442bbf4769cb253258)) * list view formatting logic (backport [#22646](https://github.com/frappe/frappe/issues/22646)) ([#22848](https://github.com/frappe/frappe/issues/22848)) ([91b87da](https://github.com/frappe/frappe/commit/91b87daddd21006cd38824c5fca45646e7bcf6d5)) * **migrate:** raise exception even if db is not available ([#22922](https://github.com/frappe/frappe/issues/22922)) ([#22999](https://github.com/frappe/frappe/issues/22999)) ([0f03ddd](https://github.com/frappe/frappe/commit/0f03ddd527c9015cb8811ff1c4b06c263b5c0067)) * multiple rows changed in each table ([e7a3264](https://github.com/frappe/frappe/commit/e7a326494a9604a99e0ffcfe63a5c331efc3a50d)) * **query_doctypes:** Allow search in translated name (backport [#22590](https://github.com/frappe/frappe/issues/22590)) ([#22748](https://github.com/frappe/frappe/issues/22748)) ([254d2e5](https://github.com/frappe/frappe/commit/254d2e5fe26fc7b6100b1d074777345f91aa557b)) * remove indicator ([4b8d6e8](https://github.com/frappe/frappe/commit/4b8d6e88613c4f06d7f887dcdf7bb35f4a1efbd6)) * rename doctype ([56da0ba](https://github.com/frappe/frappe/commit/56da0ba0a594ff9f08297412fb0bcc244edc32a7)) * rename imports ([fbb663d](https://github.com/frappe/frappe/commit/fbb663d98054c356beadaf32316015849411f302)) * return df label only when not none ([3d692d5](https://github.com/frappe/frappe/commit/3d692d5a95fe61e2b3d71fc96356fde95283cec6)) * revert unintentional quick entry default for Address ([#22974](https://github.com/frappe/frappe/issues/22974)) ([70745b2](https://github.com/frappe/frappe/commit/70745b294348b7f6e8639708a5488bd8f227ab6d)) * skip invalid numbers on SMS `receiver_list` (backport [#22879](https://github.com/frappe/frappe/issues/22879)) ([#23002](https://github.com/frappe/frappe/issues/23002)) ([48ffb1f](https://github.com/frappe/frappe/commit/48ffb1fcfa67d083ec1fd09855f5a45b3f5a52dc)) * use latest doc name for test ([d146132](https://github.com/frappe/frappe/commit/d14613267b026153fcaacc8ac29e5e6eef5cf245)) * **UX:** Improve search relevance for link field (backport [#22729](https://github.com/frappe/frappe/issues/22729)) ([#22966](https://github.com/frappe/frappe/issues/22966)) ([abc3cca](https://github.com/frappe/frappe/commit/abc3cca306519878cc5bcbd1051bd79110da4d71)) ### Features * add collapsible sections for row additions and deletions ([4267339](https://github.com/frappe/frappe/commit/426733989a7e590c6a340356b5816e05cf05068d)) * add html template for additions and deletions ([902c168](https://github.com/frappe/frappe/commit/902c168b5e58f4b43a67e5973ebe1c531aae791f)) * add html template for comparator ([b4fd381](https://github.com/frappe/frappe/commit/b4fd3817361bb291cfa9f858d81a524ef42e6c76)) * add js for rendering html grids ([56d8911](https://github.com/frappe/frappe/commit/56d89112bf967de133928677527d73b658d8b1ce)) * add logic for addition and deletion grids ([38dbb93](https://github.com/frappe/frappe/commit/38dbb93e66ff5e7cbf25d0bafd8e722ad9b54dc0)) * add logic for changed values and rows ([9c7fa7c](https://github.com/frappe/frappe/commit/9c7fa7ca612bb45d59ffa16a6fa6bf016c90c649)) * add validation for mandatory fields ([53b3783](https://github.com/frappe/frappe/commit/53b37835330ee8de4bdc0f5e888f5f207ac95e5a)) * create single doctype for comparator ([5acecf1](https://github.com/frappe/frappe/commit/5acecf1c352431cac6c308ffd25e53424b9a711f)) * webhook timeout (backport [#21410](https://github.com/frappe/frappe/issues/21410)) ([#22978](https://github.com/frappe/frappe/issues/22978)) ([dbbfa17](https://github.com/frappe/frappe/commit/dbbfa17f9e2f6063339bd612acd80bcc2e294a01)) ### Performance Improvements * query fields only once ([#22982](https://github.com/frappe/frappe/issues/22982)) ([#22987](https://github.com/frappe/frappe/issues/22987)) ([8e92894](https://github.com/frappe/frappe/commit/8e928943436f0f1d198ced7b3609f0037d067f25)) ### Reverts * Revert "refactor: force ipv4 localhost (#22394) (#22396)" (#22946) ([cb10eb0](https://github.com/frappe/frappe/commit/cb10eb0afd64da37268f08638aced7e9e5a4ac33)), closes [#22394](https://github.com/frappe/frappe/issues/22394) [#22396](https://github.com/frappe/frappe/issues/22396) [#22946](https://github.com/frappe/frappe/issues/22946) --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 8bba78757ffe..ed4d1163683a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -44,7 +44,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.53.2" +__version__ = "14.54.0" __title__ = "Frappe Framework" controllers = {}