From ea4c11b1647245d2b37d381822f28527c142b6c6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Feb 2024 13:50:48 +0530 Subject: [PATCH 01/32] fix: Use current language in attachment prints (cherry picked from commit 870c92f7ea43218803ad27d89e6251b48fd968b2) --- frappe/core/doctype/communication/mixins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index d9290bdfb2a..e613497d6b6 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -194,6 +194,7 @@ def mail_attachments(self, print_format=None, print_html=None): "print_format_attachment": 1, "doctype": self.reference_doctype, "name": self.reference_name, + "lang": frappe.local.lang, } final_attachments.append(d) From 4e53b12a85cbb3429b6d42bed91cf313da3eafda Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Feb 2024 14:14:58 +0530 Subject: [PATCH 02/32] fix: specify print_language in communication attachments (cherry picked from commit a4ddb7491dd754269b6434e235fe66c60099901c) --- frappe/core/doctype/communication/email.py | 4 ++++ frappe/core/doctype/communication/mixins.py | 11 ++++++++--- frappe/public/js/frappe/views/communication.js | 9 +++++++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 111d18e1471..4a5e0a1d622 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -47,6 +47,7 @@ def make( email_template=None, communication_type=None, send_after=None, + print_language=None, **kwargs, ) -> dict[str, str]: """Make a new communication. Checks for email permissions for specified Document. @@ -102,6 +103,7 @@ def make( communication_type=communication_type, add_signature=False, send_after=send_after, + print_language=print_language, ) @@ -128,6 +130,7 @@ def _make( communication_type=None, add_signature=True, send_after=None, + print_language=None, ) -> dict[str, str]: """Internal method to make a new communication that ignores Permission checks.""" @@ -181,6 +184,7 @@ def _make( print_format=print_format, send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead, + print_language=print_language, ) emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index e613497d6b6..3dc2486ccb9 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -184,7 +184,7 @@ def get_incoming_email_account(self): ) return self._incoming_email_account - def mail_attachments(self, print_format=None, print_html=None): + def mail_attachments(self, print_format=None, print_html=None, print_language=None): final_attachments = [] if print_format or print_html: @@ -194,7 +194,7 @@ def mail_attachments(self, print_format=None, print_html=None): "print_format_attachment": 1, "doctype": self.reference_doctype, "name": self.reference_name, - "lang": frappe.local.lang, + "lang": print_language or frappe.local.lang, } final_attachments.append(d) @@ -257,6 +257,7 @@ def sendmail_input_dict( send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None, + print_language=None, ) -> dict: outgoing_email_account = self.get_outgoing_email_account() if not outgoing_email_account: @@ -273,7 +274,9 @@ def sendmail_input_dict( if not (recipients or cc): return {} - final_attachments = self.mail_attachments(print_format=print_format, print_html=print_html) + final_attachments = self.mail_attachments( + print_format=print_format, print_html=print_html, print_language=print_language + ) incoming_email_account = self.get_incoming_email_account() return { "recipients": recipients, @@ -304,6 +307,7 @@ def send_email( send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None, + print_language=None, ): if input_dict := self.sendmail_input_dict( print_html=print_html, @@ -311,5 +315,6 @@ def send_email( send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead, is_inbound_mail_communcation=is_inbound_mail_communcation, + print_language=print_language, ): frappe.sendmail(**input_dict) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index fbd08627d13..79103486828 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -145,6 +145,14 @@ frappe.views.CommunicationComposer = class { fieldtype: "Select", fieldname: "select_print_format", }, + { + label: __("Print Language"), + fieldtype: "Link", + options: "Language", + fieldname: "print_language", + default: frappe.boot.lang, + depends_on: "attach_document_print", + }, { fieldtype: "Column Break" }, { label: __("Select Attachments"), @@ -728,6 +736,7 @@ frappe.views.CommunicationComposer = class { read_receipt: form_values.send_read_receipt, print_letterhead: me.is_print_letterhead_checked(), send_after: form_values.send_after ? form_values.send_after : null, + print_language: form_values.print_language, }, btn, callback(r) { From c11f14012efef44ba1563b1da5018cb2df499412 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Feb 2024 15:25:28 +0530 Subject: [PATCH 03/32] fix(UX): set default print language from print format (cherry picked from commit c00a34d023dcd2547ec952f9f994592c3a6396cf) --- frappe/public/js/frappe/views/communication.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 79103486828..5ef62cbffd6 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -47,6 +47,7 @@ frappe.views.CommunicationComposer = class { } get_fields() { + let me = this; const fields = [ { label: __("To"), @@ -144,6 +145,9 @@ frappe.views.CommunicationComposer = class { label: __("Select Print Format"), fieldtype: "Select", fieldname: "select_print_format", + onchange: function () { + me.guess_language(); + }, }, { label: __("Print Language"), @@ -195,6 +199,19 @@ frappe.views.CommunicationComposer = class { return fields; } + guess_language() { + // when attach print for print format changes try to guess language + // if print format has language then set that else boot lang. + let lang = frappe.boot.lang; + + let print_format = this.dialog.get_value("select_print_format"); + + if (print_format != "Standard") { + lang = frappe.get_doc("Print Format", print_format)?.default_print_language || lang; + } + this.dialog.set_value("print_language", lang); + } + toggle_more_options(show_options) { show_options = show_options || this.dialog.fields_dict.more_options.df.hidden; this.dialog.set_df_property("more_options", "hidden", !show_options); @@ -504,6 +521,7 @@ frappe.views.CommunicationComposer = class { } else { $(fields.attach_document_print.wrapper).toggle(false); } + this.guess_language(); } setup_attach() { From a987c2d7517aec75542b75187cfc04f24a23b3b4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Feb 2024 16:13:52 +0530 Subject: [PATCH 04/32] fix: escalate print failures Print failures shouldn't generate PDF with failure message but instead escalate the error. This prevent all the PDFs that just contain "PermissionError" from being sent. (cherry picked from commit dbc2e092f181c1248952c5faf6105d6bfece0eab) --- frappe/__init__.py | 33 ++++++++++++++++++--------------- frappe/tests/test_printview.py | 13 +++++++++++++ frappe/website/serve.py | 18 ++++++++++++++++-- 3 files changed, 47 insertions(+), 17 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 354d954964d..7c3b2968ff6 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2089,24 +2089,27 @@ def get_print( :param as_pdf: Return as PDF. Default False. :param password: Password to encrypt the pdf with. Default None""" from frappe.utils.pdf import get_pdf - from frappe.website.serve import get_response_content + from frappe.website.serve import get_response_without_exception_handling original_form_dict = copy.deepcopy(local.form_dict) + try: + local.form_dict.doctype = doctype + local.form_dict.name = name + local.form_dict.format = print_format + local.form_dict.style = style + local.form_dict.doc = doc + local.form_dict.no_letterhead = no_letterhead + local.form_dict.letterhead = letterhead + + pdf_options = pdf_options or {} + if password: + pdf_options["password"] = password + + response = get_response_without_exception_handling("printview", 200) + html = str(response.data, "utf-8") + finally: + local.form_dict = original_form_dict - local.form_dict.doctype = doctype - local.form_dict.name = name - local.form_dict.format = print_format - local.form_dict.style = style - local.form_dict.doc = doc - local.form_dict.no_letterhead = no_letterhead - local.form_dict.letterhead = letterhead - - pdf_options = pdf_options or {} - if password: - pdf_options["password"] = password - - html = get_response_content("printview") - local.form_dict = original_form_dict return get_pdf(html, options=pdf_options, output=output) if as_pdf else html diff --git a/frappe/tests/test_printview.py b/frappe/tests/test_printview.py index 8fa7ad76cea..9dc53dce349 100644 --- a/frappe/tests/test_printview.py +++ b/frappe/tests/test_printview.py @@ -1,4 +1,5 @@ import frappe +from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.tests.utils import FrappeTestCase from frappe.www.printview import get_html_and_style @@ -17,3 +18,15 @@ def test_print_view_without_errors(self): # html should exist self.assertTrue(bool(ret["html"])) + + def test_print_error(self): + """Print failures shouldn't generate PDF with failure message but instead escalate the error""" + doctype = new_doctype(is_submittable=1).insert() + + doc = frappe.new_doc(doctype.name) + doc.insert() + doc.submit() + doc.cancel() + + # cancelled doc can't be printed by default + self.assertRaises(frappe.PermissionError, frappe.attach_print, doc.doctype, doc.name) diff --git a/frappe/website/serve.py b/frappe/website/serve.py index acae44940e3..a10e319154e 100644 --- a/frappe/website/serve.py +++ b/frappe/website/serve.py @@ -1,3 +1,5 @@ +from werkzeug.wrappers import Response + import frappe from frappe.website.page_renderers.error_page import ErrorPage from frappe.website.page_renderers.not_found_page import NotFoundPage @@ -6,7 +8,7 @@ from frappe.website.path_resolver import PathResolver -def get_response(path=None, http_status_code=200): +def get_response(path=None, http_status_code=200) -> Response: """Resolves path and renders page""" response = None path = path or frappe.local.request.path @@ -28,6 +30,18 @@ def get_response(path=None, http_status_code=200): return response -def get_response_content(path=None, http_status_code=200): +def get_response_content(path=None, http_status_code=200) -> str: response = get_response(path, http_status_code) return str(response.data, "utf-8") + + +def get_response_without_exception_handling(path=None, http_status_code=200) -> Response: + """Resolves path and renders page. + + Note: This doesn't do any exception handling and assumes you'll implement the exception + handling that's required.""" + path = path or frappe.local.request.path + + path_resolver = PathResolver(path, http_status_code) + _endpoint, renderer_instance = path_resolver.resolve() + return renderer_instance.render() From 2d907e71ece079ca72dde163f4daf232a6d71154 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Feb 2024 17:33:46 +0530 Subject: [PATCH 05/32] fix(UX): correctly disable standard web form form (#25143) (cherry picked from commit 99a6883e5c2b8d0453c5ad91fea1411c2600ae1b) --- frappe/website/doctype/web_form/web_form.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index dd73f09a0c9..41c0e4a6dee 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -28,8 +28,10 @@ frappe.ui.form.on("Web Form", { frm.get_field("is_standard").toggle(frappe.boot.developer_mode); if (frm.doc.is_standard && !frappe.boot.developer_mode) { - frm.set_read_only(); - frm.disable_save(); + frm.disable_form(); + frappe.show_alert( + __("Standard Web Forms can not be modified, duplicate the Web Form instead.") + ); } render_list_settings_message(frm); From 196483b25b9553bc40279613be72c5528ad56457 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:10:13 +0000 Subject: [PATCH 06/32] fix: always show is_standard on web form (#25144) (#25148) This causes more confusion when it's hidden. (cherry picked from commit bab3ee33f05c69f2cfebd0dab677a68678996818) Co-authored-by: Ankush Menat --- frappe/website/doctype/web_form/web_form.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 41c0e4a6dee..0f57998a03b 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -24,9 +24,6 @@ frappe.ui.form.on("Web Form", { }, refresh: function (frm) { - // show is-standard only if developer mode - frm.get_field("is_standard").toggle(frappe.boot.developer_mode); - if (frm.doc.is_standard && !frappe.boot.developer_mode) { frm.disable_form(); frappe.show_alert( From 73f1a738a697b716729f5782accc3673445ea0ab Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 17:42:27 +0530 Subject: [PATCH 07/32] fix: update file attached_to details in submitted doc (#25141) (cherry picked from commit 5ef208d1f1e5196ae0fdeaa7c264159e4541ed8a) Co-authored-by: Shankarv19bcr --- frappe/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/hooks.py b/frappe/hooks.py index 4fef5469f6f..ddb3fa31c94 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -174,6 +174,7 @@ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", "frappe.automation.doctype.assignment_rule.assignment_rule.apply", "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date", + "frappe.core.doctype.file.utils.attach_files_to_document", ], "on_change": [ "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points", From 338c895718d33be6973ab511430c1faf8e266769 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 28 Feb 2024 16:53:02 +0530 Subject: [PATCH 08/32] fix(setup_module_map): fix caching Use a separate cache key depending on the arguments passed Signed-off-by: Akhil Narang (cherry picked from commit e6be7d664851f7b30b989cf866835eff1a33d79e) --- frappe/__init__.py | 19 +++++++++++++++---- frappe/cache_manager.py | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 7c3b2968ff6..8dbc0436494 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1611,11 +1611,19 @@ def append_hook(target, key, value): target[key].extend(value) -def setup_module_map(include_all_apps=True): - """Rebuild map of all modules (internal).""" - if conf.db_name: +def setup_module_map(include_all_apps: bool = True) -> None: + """ + Function to rebuild map of all modules + + :param: include_all_apps: Include all apps on bench, or just apps installed on the site. + :return: Nothing + """ + if include_all_apps: local.app_modules = cache.get_value("app_modules") local.module_app = cache.get_value("module_app") + else: + local.app_modules = cache.get_value("installed_app_modules") + local.module_app = cache.get_value("module_installed_app") if not (local.app_modules and local.module_app): local.module_app, local.app_modules = {}, {} @@ -1634,9 +1642,12 @@ def setup_module_map(include_all_apps=True): local.module_app[module] = app local.app_modules[app].append(module) - if conf.db_name: + if include_all_apps: cache.set_value("app_modules", local.app_modules) cache.set_value("module_app", local.module_app) + else: + cache.set_value("installed_app_modules", local.app_modules) + cache.set_value("module_installed_app", local.module_app) def get_file_items(path, raise_not_found=False, ignore_empty_lines=True): diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 484916c12b8..b03e20de972 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -26,7 +26,9 @@ def get_doctype_map_key(doctype): "installed_apps", "all_apps", "app_modules", + "installed_app_modules", "module_app", + "module_installed_app", "system_settings", "scheduler_events", "time_zone", From 72da497f109782f59232ab5b35092368a8f2c91b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Feb 2024 20:42:49 +0530 Subject: [PATCH 09/32] fix: Only validate fetch from when user modifies it (cherry picked from commit b044ffedf12989242d29e92fb069518cb306bdfc) --- frappe/core/doctype/doctype/doctype.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index a0d19ca06d5..eaf7407e717 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -1590,6 +1590,9 @@ def check_no_of_ratings(docfield): frappe.throw(_("Options for Rating field can range from 3 to 10")) def check_fetch_from(docfield): + if not frappe.request: + return + fetch_from = docfield.fetch_from fieldname = docfield.fieldname if not fetch_from: From adb7e387439bce6daca585f5fdbe8562f633d818 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:36:00 +0530 Subject: [PATCH 10/32] fix: escape single quotes (#25104) (#25154) Resolves https://github.com/frappe/frappe/pull/25078#discussion_r1504084483 (cherry picked from commit ac05c7db6e9ece05d8fd1c87ffa74358bac8db54) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- frappe/public/js/frappe/list/list_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index c10209f633f..ccb6b47d0de 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1474,7 +1474,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.data = this.data.filter((d) => !names.includes(d.name)); for (let name of names) { this.$result - .find(`.list-row-checkbox[data-name='${name}']`) + .find(`.list-row-checkbox[data-name='${name.replace(/'/g, "\\'")}']`) .closest(".list-row-container") .remove(); } From 72647340c63bab3ebc5417fb9ca579be692cffe2 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 14:54:43 +0000 Subject: [PATCH 11/32] fix: use name for RQ worker instead of PID (#25175) (#25177) Also avoid complex naming schemes. (cherry picked from commit f7bff5893554ca9c714c9d2533b2b575ef510e81) Co-authored-by: Ankush Menat --- frappe/core/doctype/rq_worker/rq_worker.json | 5 +++-- frappe/core/doctype/rq_worker/rq_worker.py | 4 ++-- frappe/core/doctype/rq_worker/test_rq_worker.py | 2 +- frappe/utils/background_jobs.py | 3 +-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/rq_worker/rq_worker.json b/frappe/core/doctype/rq_worker/rq_worker.json index 841c01ddece..cc301be5b31 100644 --- a/frappe/core/doctype/rq_worker/rq_worker.json +++ b/frappe/core/doctype/rq_worker/rq_worker.json @@ -114,7 +114,7 @@ "in_create": 1, "is_virtual": 1, "links": [], - "modified": "2024-01-13 10:36:13.034784", + "modified": "2024-02-29 19:31:08.502527", "modified_by": "Administrator", "module": "Core", "name": "RQ Worker", @@ -141,5 +141,6 @@ "color": "Yellow", "title": "busy" } - ] + ], + "title_field": "pid" } \ No newline at end of file diff --git a/frappe/core/doctype/rq_worker/rq_worker.py b/frappe/core/doctype/rq_worker/rq_worker.py index a9d94559d7c..79ab52b4ad5 100644 --- a/frappe/core/doctype/rq_worker/rq_worker.py +++ b/frappe/core/doctype/rq_worker/rq_worker.py @@ -37,7 +37,7 @@ class RQWorker(Document): def load_from_db(self): all_workers = get_workers() - workers = [w for w in all_workers if w.pid == cint(self.name)] + workers = [w for w in all_workers if w.name == self.name] if not workers: raise frappe.DoesNotExistError d = serialize_worker(workers[0]) @@ -84,7 +84,7 @@ def serialize_worker(worker: Worker) -> frappe._dict: current_job = None return frappe._dict( - name=worker.pid, + name=worker.name, queue=queue, queue_type=queue_types, worker_name=worker.name, diff --git a/frappe/core/doctype/rq_worker/test_rq_worker.py b/frappe/core/doctype/rq_worker/test_rq_worker.py index f07338d6303..5803b1a8759 100644 --- a/frappe/core/doctype/rq_worker/test_rq_worker.py +++ b/frappe/core/doctype/rq_worker/test_rq_worker.py @@ -14,4 +14,4 @@ def test_get_worker_list(self): def test_worker_serialization(self): workers = RQWorker.get_list({}) - frappe.get_doc("RQ Worker", workers[0].pid) + frappe.get_doc("RQ Worker", workers[0].name) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 65986abb4c7..8388ab27657 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -275,7 +275,6 @@ def start_worker( if queue: queue = [q.strip() for q in queue.split(",")] queues = get_queue_list(queue, build_queue_name=True) - queue_name = queue and generate_qname(queue) if os.environ.get("CI"): setup_loghandlers("ERROR") @@ -286,7 +285,7 @@ def start_worker( if quiet: logging_level = "WARNING" - worker = Worker(queues, name=get_worker_name(queue_name), connection=redis_connection) + worker = Worker(queues, connection=redis_connection) worker.work( logging_level=logging_level, burst=burst, From 1657d55ce288cf532de929ca9b67f58391a88144 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 29 Feb 2024 21:17:32 +0530 Subject: [PATCH 12/32] fix: race condition on deletes (backport #25170) (#25171) * fix: lock the doc before deleting Locking only prevents this kinda race conditions: - User A deletes doc - User B modifies doc so that it's not deletable anymore. (cherry picked from commit 8f00aae1603c63f58adaf260b094ef887a16c207) * feat: nowait to skip blocking locks (cherry picked from commit e810fb7eca1d62937ebbed6d5f69d47e237cf81e) * fix: prevent deletion if document is locked (cherry picked from commit fc5ce044e6835e5197aa541ad2dba2c0915c1109) * test: utils for simulating two connections (cherry picked from commit 5116768a54184b3e0269b22e3b257973a35a90ac) * test: NOWAIT functionality (cherry picked from commit 0c9cc2e6cedc19e00138f5ee29afa67318fd34da) * fix(postgres): treat LockNotAvailable as timeout It's a lock timeout in a way. (cherry picked from commit b4fe7223c1dd18a6d9a8b21b67557216111af632) * test: separate out risky tests --------- Co-authored-by: Ankush Menat --- frappe/database/database.py | 10 ++++++ frappe/database/postgres/database.py | 9 +++-- frappe/database/query.py | 3 +- frappe/model/delete_doc.py | 10 ++++++ frappe/tests/test_db.py | 51 +++++++++++++++++++++------- frappe/tests/test_document.py | 2 +- frappe/tests/utils.py | 33 ++++++++++++++++++ 7 files changed, 102 insertions(+), 16 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 6e8ade72ca2..9bc241f8a1a 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -484,6 +484,7 @@ def get_value( pluck=False, distinct=False, skip_locked=False, + wait=True, ): """Returns a document property or list of properties. @@ -498,6 +499,7 @@ def get_value( :param pluck: pluck first column instead of returning as nested list or dict. :param for_update: All the affected/read rows will be locked. :param skip_locked: Skip selecting currently locked rows. + :param wait: Wait for aquiring lock Example: @@ -529,6 +531,7 @@ def get_value( distinct=distinct, limit=1, skip_locked=skip_locked, + wait=wait, ) if not run: @@ -562,6 +565,7 @@ def get_values( distinct=False, limit=None, skip_locked=False, + wait=True, ): """Returns multiple document properties. @@ -602,6 +606,7 @@ def get_values( limit=limit, as_dict=as_dict, skip_locked=skip_locked, + wait=True, for_update=for_update, ) @@ -629,6 +634,7 @@ def get_values( limit=limit, for_update=for_update, skip_locked=skip_locked, + wait=wait, ) except Exception as e: if ignore and ( @@ -866,6 +872,7 @@ def _get_values_from_table( update=None, for_update=False, skip_locked=False, + wait=True, run=True, pluck=False, distinct=False, @@ -877,6 +884,7 @@ def _get_values_from_table( order_by=order_by, for_update=for_update, skip_locked=skip_locked, + wait=wait, fields=fields, distinct=distinct, limit=limit, @@ -902,6 +910,7 @@ def _get_value_for_many_names( as_dict=False, for_update=False, skip_locked=False, + wait=True, ): if names := list(filter(None, names)): return frappe.qb.get_query( @@ -914,6 +923,7 @@ def _get_value_for_many_names( validate_filters=True, for_update=for_update, skip_locked=skip_locked, + wait=wait, ).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck) return {} diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 39812488bf1..7545cfee427 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -12,7 +12,12 @@ UNDEFINED_TABLE, UNIQUE_VIOLATION, ) -from psycopg2.errors import ReadOnlySqlTransaction, SequenceGeneratorLimitExceeded, SyntaxError +from psycopg2.errors import ( + LockNotAvailable, + ReadOnlySqlTransaction, + SequenceGeneratorLimitExceeded, + SyntaxError, +) from psycopg2.extensions import ISOLATION_LEVEL_REPEATABLE_READ import frappe @@ -53,7 +58,7 @@ def is_deadlocked(e): @staticmethod def is_timedout(e): # http://initd.org/psycopg/docs/extensions.html?highlight=datatype#psycopg2.extensions.QueryCanceledError - return isinstance(e, psycopg2.extensions.QueryCanceledError) + return isinstance(e, (psycopg2.extensions.QueryCanceledError | LockNotAvailable)) @staticmethod def is_read_only_mode_error(e) -> bool: diff --git a/frappe/database/query.py b/frappe/database/query.py index 3e881ef7ca2..d2325928841 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -48,6 +48,7 @@ def get_query( *, validate_filters: bool = False, skip_locked: bool = False, + wait: bool = True, ) -> QueryBuilder: self.is_mariadb = frappe.db.db_type == "mariadb" self.is_postgres = frappe.db.db_type == "postgres" @@ -84,7 +85,7 @@ def get_query( self.query = self.query.distinct() if for_update: - self.query = self.query.for_update(skip_locked=skip_locked) + self.query = self.query.for_update(skip_locked=skip_locked, nowait=not wait) if group_by: self.query = self.query.groupby(group_by) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index c6cce21a06e..7dc6c4acde7 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -104,6 +104,16 @@ def delete_doc( pass else: + # Lock the doc without waiting + try: + frappe.db.get_value(doctype, name, for_update=True, wait=False) + except frappe.QueryTimeoutError: + frappe.throw( + _( + "This document can not be deleted right now as it's being modified by another user. Please try again after some time." + ), + exc=frappe.QueryTimeoutError, + ) doc = frappe.get_doc(doctype, name) if not for_reload: diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index fcdef0ca7e5..c1abd1798ed 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -15,7 +15,7 @@ from frappe.query_builder import Field from frappe.query_builder.functions import Concat_ws from frappe.tests.test_query_builder import db_type_is, run_only_if -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import add_days, now, random_string, set_request from frappe.utils.testutils import clear_custom_fields @@ -56,17 +56,6 @@ def test_db_statement_execution_timeout(self): frappe.db.rollback(save_point=savepoint) self.fail("Long running queries not timing out") - def test_skip_locking(self): - first_conn = frappe.local.db - name = frappe.db.get_value("User", "Administrator", "name", for_update=True, skip_locked=True) - self.assertEqual(name, "Administrator") - - frappe.connect() # Create a 2nd connection - second_conn = frappe.local.db - self.assertIsNot(first_conn, second_conn) - name = frappe.db.get_value("User", "Administrator", "name", for_update=True, skip_locked=True) - self.assertFalse(name) - @patch.dict(frappe.conf, {"http_timeout": 20, "enable_db_statement_timeout": 1}) def test_db_timeout_computation(self): set_request(method="GET", path="/") @@ -980,6 +969,44 @@ def inner(): self.assertEqual(write_connection, db_id()) +class TestConcurrency(FrappeTestCase): + @timeout(5, "There shouldn't be any lock wait") + def test_skip_locking(self): + with self.primary_connection(): + name = frappe.db.get_value("User", "Administrator", for_update=True, skip_locked=True) + self.assertEqual(name, "Administrator") + + with self.secondary_connection(): + name = frappe.db.get_value("User", "Administrator", for_update=True, skip_locked=True) + self.assertFalse(name) + + @timeout(5, "Lock timeout should have been 0") + def test_no_wait(self): + with self.primary_connection(): + name = frappe.db.get_value("User", "Administrator", for_update=True) + self.assertEqual(name, "Administrator") + + with self.secondary_connection(): + self.assertRaises( + frappe.QueryTimeoutError, + lambda: frappe.db.get_value("User", "Administrator", for_update=True, wait=False), + ) + + @timeout(5, "Deletion stuck on lock timeout") + def test_delete_race_condition(self): + note = frappe.new_doc("Note") + note.title = note.content = frappe.generate_hash() + note.insert() + frappe.db.commit() # ensure that second connection can see the document + + with self.primary_connection(): + n1 = frappe.get_doc(note.doctype, note.name) + n1.save() + + with self.secondary_connection(): + self.assertRaises(frappe.QueryTimeoutError, frappe.delete_doc, note.doctype, note.name) + + class TestSqlIterator(FrappeTestCase): def test_db_sql_iterator(self): test_queries = [ diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 233d3a0b23a..3d77d2d389d 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -9,7 +9,7 @@ from frappe.core.doctype.doctype.test_doctype import new_doctype from frappe.desk.doctype.note.note import Note from frappe.model.naming import make_autoname, parse_naming_series, revert_series_if_last -from frappe.tests.utils import FrappeTestCase +from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import cint, now_datetime, set_request from frappe.website.serve import get_response diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 088218596c7..1e60070f8ae 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -34,6 +34,8 @@ class FrappeTestCase(unittest.TestCase): def setUpClass(cls) -> None: cls.TEST_SITE = getattr(frappe.local, "site", None) or cls.TEST_SITE cls.ADMIN_PASSWORD = frappe.get_conf(cls.TEST_SITE).admin_password + cls._primary_connection = frappe.local.db + cls._secondary_connection = None # flush changes done so far to avoid flake frappe.db.commit() if cls.SHOW_TRANSACTION_COMMIT_WARNINGS: @@ -92,6 +94,37 @@ def normalize_sql(self, query: str) -> str: return (sqlparse.format(query.strip(), keyword_case="upper", reindent=True, strip_comments=True),) + @contextmanager + def primary_connection(self): + """Switch to primary DB connection + + This is used for simulating multiple users performing actions by simulating two DB connections""" + try: + current_conn = frappe.local.db + frappe.local.db = self._primary_connection + yield + finally: + frappe.local.db = current_conn + + @contextmanager + def secondary_connection(self): + """Switch to secondary DB connection.""" + if self._secondary_connection is None: + frappe.connect() # get second connection + self._secondary_connection = frappe.local.db + + try: + current_conn = frappe.local.db + frappe.local.db = self._secondary_connection + yield + finally: + frappe.local.db = current_conn + self.addCleanup(self._rollback_connections) + + def _rollback_connections(self): + self._primary_connection.rollback() + self._secondary_connection.rollback() + def assertQueryEqual(self, first: str, second: str): self.assertEqual(self.normalize_sql(first), self.normalize_sql(second)) From 0beab773b465b12b4d18302c457fbae77b7b3d06 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 1 Mar 2024 13:31:34 +0100 Subject: [PATCH 13/32] refactor: validate_link_and_fetch Remove redundant parameters (cherry picked from commit 1f969a40ebd6264586634d73afbd3f5b7611160f) --- frappe/public/js/frappe/form/controls/link.js | 34 +++++++++++-------- .../frappe/form/controls/table_multiselect.js | 9 +---- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index d5485fab373..51eb63e88d3 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -577,32 +577,36 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat return value; } - return this.validate_link_and_fetch(this.df, this.get_options(), this.docname, value); + return this.validate_link_and_fetch(value); } - validate_link_and_fetch(df, options, docname, value) { - if (!options) return; + validate_link_and_fetch(value) { + const options = this.get_options(); + if (!options) { + return; + } - const fetch_map = this.fetch_map; - const columns_to_fetch = Object.values(fetch_map); + const columns_to_fetch = Object.values(this.fetch_map); // if default and no fetch, no need to validate - if (!columns_to_fetch.length && df.__default_value === value) { + if (!columns_to_fetch.length && this.df.__default_value === value) { return value; } - function update_dependant_fields(response) { + const update_dependant_fields = (response) => { let field_value = ""; - for (const [target_field, source_field] of Object.entries(fetch_map)) { - if (value) field_value = response[source_field]; + for (const [target_field, source_field] of Object.entries(this.fetch_map)) { + if (value) { + field_value = response[source_field]; + } frappe.model.set_value( - df.parent, - docname, + this.df.parent, + this.docname, target_field, field_value, - df.fieldtype + this.df.fieldtype ); } - } + }; // to avoid unnecessary request if (value) { @@ -613,7 +617,9 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat fields: columns_to_fetch, }) .then((response) => { - if (!docname || !columns_to_fetch.length) return response.name; + if (!this.docname || !columns_to_fetch.length) { + return response.name; + } update_dependant_fields(response); return response.name; }); diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js index 50e94651eb0..a550efbe357 100644 --- a/frappe/public/js/frappe/form/controls/table_multiselect.js +++ b/frappe/public/js/frappe/form/controls/table_multiselect.js @@ -110,14 +110,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends ( return all_rows_except_last; } - const validate_promise = this.validate_link_and_fetch( - this.df, - this.get_options(), - this.docname, - link_value - ); - - return validate_promise.then((validated_value) => { + return this.validate_link_and_fetch(link_value).then((validated_value) => { if (validated_value === link_value) { return rows; } else { From 9f53d863c0e23d63743b4007c83b2809fdc0f000 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 13:02:05 +0530 Subject: [PATCH 14/32] test: add test case for in ("") (#25189) (#25191) (cherry picked from commit daf43cff79ced88479fe19f6b48cc13cf9e101d1) Co-authored-by: Ankush Menat --- frappe/tests/test_db_query.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py index 862e49bef4d..25744079a18 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -1108,6 +1108,8 @@ def test_coalesce_with_in_ops(self): # primary key is never nullable self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", None])}, run=0)) self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", ["a", ""])}, run=0)) + self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", (""))}, run=0)) + self.assertNotIn("ifnull", frappe.get_all("User", {"name": ("in", ())}, run=0)) def test_ambiguous_linked_tables(self): from frappe.desk.reportview import get From 3dafbf900aaba34fb25dee7b2d78e7569fab7297 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 14:20:56 +0000 Subject: [PATCH 15/32] fix(search): Don't break when query doesn't return title (#25168) (#25197) (cherry picked from commit f4b6f95832dc500425ad29709fb0bc4df1f0a52b) Co-authored-by: Corentin Flr <10946971+cogk@users.noreply.github.com> --- frappe/desk/search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 1523e9326b2..85b4a9fdfb0 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -260,6 +260,8 @@ def to_string(parts): if meta.show_title_field_in_link: for item in res: item = list(item) + if len(item) == 1: + item = [item[0], item[0]] label = item[1] # use title as label item[1] = item[0] # show name in description instead of title if len(item) >= 3 and item[2] == label: From 20a7879e8dbb5ad4d4cb80d460fa6d86b568576a Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 2 Mar 2024 14:29:06 +0000 Subject: [PATCH 16/32] fix: filter Implementation is set operator (#25182) (#25195) * Implementation is set operator. fix issue #25180 * Refactored filtrer operator `is`, Add tests * fix: Correct implementation for `is set` --------- Co-authored-by: Ankush Menat (cherry picked from commit eff50e1cd3926a4e80b8155c871a941bce8fb689) # Conflicts: # frappe/tests/test_utils.py Co-authored-by: Maxim Sysoev --- frappe/tests/test_utils.py | 18 +++++++++++++++--- frappe/utils/data.py | 20 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 4eedae62fb3..06fe0b146b3 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -175,8 +175,15 @@ def test_date_time(self): ) ) - def test_like_not_like(self): - doc = {"doctype": "User", "username": "test_abc", "prefix": "startswith", "suffix": "endswith"} + def test_filter_evaluation(self): + doc = { + "doctype": "User", + "username": "test_abc", + "prefix": "startswith", + "suffix": "endswith", + "empty": None, + "number": 0, + } test_cases = [ ([["username", "like", "test"]], True), @@ -187,10 +194,15 @@ def test_like_not_like(self): ([["prefix", "not like", "end%"]], True), ([["suffix", "like", "%with"]], True), ([["suffix", "not like", "%end"]], True), + ([["suffix", "is", "set"]], True), + ([["suffix", "is", "not set"]], False), + ([["empty", "is", "set"]], False), + ([["empty", "is", "not set"]], True), + ([["number", "is", "set"]], True), ] for filter, expected_result in test_cases: - self.assertEqual(evaluate_filters(doc, filter), expected_result) + self.assertEqual(evaluate_filters(doc, filter), expected_result, msg=f"{filter}") class TestMoney(FrappeTestCase): diff --git a/frappe/utils/data.py b/frappe/utils/data.py index d2137b6d49d..6b5e79d3900 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -1693,6 +1693,25 @@ def sql_like(value: str, pattern: str) -> bool: return pattern in value +def filter_operator_is(value: str, pattern: str) -> bool: + """Operator `is` can have two values: 'set' or 'not set'.""" + pattern = pattern.lower() + + def is_set(): + if value is None: + return False + elif isinstance(value, str) and not value: + return False + return True + + if pattern == "set": + return is_set() + elif pattern == "not set": + return not is_set() + else: + frappe.throw(frappe._(f"Invalid argument for operator 'IS': {pattern}")) + + operator_map = { # startswith "^": lambda a, b: (a or "").startswith(b), @@ -1710,6 +1729,7 @@ def sql_like(value: str, pattern: str) -> bool: "None": lambda a, b: a is None, "like": sql_like, "not like": lambda a, b: not sql_like(a, b), + "is": filter_operator_is, } From 7754e4acedf55482bf0922cea87769a9ab1762e5 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sun, 3 Mar 2024 15:43:02 +0000 Subject: [PATCH 17/32] fix(UX): reload form after renaming field (#25159) (#25161) (cherry picked from commit af69dab130e623ea16b35c7c0ac07bc0a7b7e322) # Conflicts: # frappe/custom/doctype/custom_field/custom_field.js Co-authored-by: Ankush Menat --- .../doctype/custom_field/custom_field.js | 48 ++++++++++--------- .../doctype/custom_field/custom_field.py | 1 + 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index 031d53de202..c7c86c00774 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -112,28 +112,32 @@ frappe.ui.form.on("Custom Field", { } }, add_rename_field(frm) { - frm.add_custom_button(__("Rename Fieldname"), () => { - frappe.prompt( - { - fieldtype: "Data", - label: __("Fieldname"), - fieldname: "fieldname", - reqd: 1, - default: frm.doc.fieldname, - }, - function (data) { - frappe.call({ - method: "frappe.custom.doctype.custom_field.custom_field.rename_fieldname", - args: { - custom_field: frm.doc.name, - fieldname: data.fieldname, - }, - }); - }, - __("Rename Fieldname"), - __("Rename") - ); - }); + if (!frm.is_new()) { + frm.add_custom_button(__("Rename Fieldname"), () => { + frappe.prompt( + { + fieldtype: "Data", + label: __("Fieldname"), + fieldname: "fieldname", + reqd: 1, + default: frm.doc.fieldname, + }, + function (data) { + frappe + .xcall( + "frappe.custom.doctype.custom_field.custom_field.rename_fieldname", + { + custom_field: frm.doc.name, + fieldname: data.fieldname, + } + ) + .then(() => frm.reload()); + }, + __("Rename Fieldname"), + __("Rename") + ); + }); + } }, }); diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index d0f27ca66e5..7bd6f86b83b 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -369,6 +369,7 @@ def rename_fieldname(custom_field: str, fieldname: str): field.db_set("fieldname", field.fieldname, notify=True) _update_fieldname_references(field, old_fieldname, new_fieldname) + frappe.msgprint(_("Custom field renamed to {0} successfully.").format(fieldname), alert=True) frappe.db.commit() frappe.clear_cache() From a471499d309c15586fc332fd4a5cef2f0014860d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:36:11 +0530 Subject: [PATCH 18/32] fix: No need to sort keys while saving JSON to DB (#25205) (#25206) * fix: No need to sort keys while saving JSON to DB Also, adding indent is unnecessary. (cherry picked from commit 9a1bd6c1acd3b1548ead3d39732d91d5453ffa2c) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/model/base_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 1a1e96370a2..e3d95dcc94e 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -384,7 +384,7 @@ def get_valid_dict( value = cint(value) elif df.fieldtype == "JSON" and isinstance(value, dict): - value = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": ")) + value = json.dumps(value, separators=(",", ":")) elif df.fieldtype in float_like_fields and not isinstance(value, float): value = flt(value) From c659eca80698de0cf2ad31af19a0e10ca91677a0 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 12:00:39 +0530 Subject: [PATCH 19/32] fix: dont translate numbers (#25208) (#25210) This is not shown in UI and only used for UI logic of toggling button. closes https://github.com/frappe/frappe/issues/25202 (cherry picked from commit 2639bfe9451a6273c761e37dd5b3b0d333196c6a) Co-authored-by: Ankush Menat --- frappe/public/js/frappe/list/list_view.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index ccb6b47d0de..24d34182172 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -989,9 +989,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { - + `; const like = div.querySelector(".like-action"); From 79a8afdaa9f97320815da7c2eba6f22a8b8baa38 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 07:02:35 +0000 Subject: [PATCH 20/32] fix: Export `None` as type if select as no options (#25211) (#25212) Select options can be dynamic, in that case we should at least the default value `None` as a value `DF.Literal` otherwise is invalid type annotation. (cherry picked from commit b1a8bc9312c94a7b145f1d3cde595ebaa52fc29b) # Conflicts: # frappe/automation/doctype/milestone_tracker/milestone_tracker.py # frappe/email/doctype/notification/notification.py # frappe/social/doctype/energy_point_rule/energy_point_rule.py Co-authored-by: Ankush Menat --- .../doctype/assignment_rule/assignment_rule.py | 4 ++-- .../doctype/milestone_tracker/milestone_tracker.py | 4 ++-- frappe/core/doctype/doctype/doctype.py | 2 +- .../document_naming_rule_condition.py | 2 +- frappe/core/doctype/module_def/module_def.py | 2 +- .../core/doctype/system_settings/system_settings.py | 2 +- frappe/core/doctype/user_type/user_type.py | 2 +- frappe/custom/doctype/custom_field/custom_field.py | 2 +- .../custom/doctype/customize_form/customize_form.py | 4 ++-- .../doctype_layout_field/doctype_layout_field.py | 2 +- frappe/desk/doctype/bulk_update/bulk_update.py | 2 +- frappe/desk/doctype/calendar_view/calendar_view.py | 6 +++--- .../desk/doctype/dashboard_chart/dashboard_chart.py | 12 ++++++------ .../dashboard_chart_field/dashboard_chart_field.py | 2 +- frappe/desk/doctype/form_tour_step/form_tour_step.py | 4 ++-- frappe/desk/doctype/kanban_board/kanban_board.py | 2 +- frappe/desk/doctype/number_card/number_card.py | 4 ++-- .../desk/doctype/onboarding_step/onboarding_step.py | 2 +- .../doctype/auto_email_report/auto_email_report.py | 4 ++-- frappe/email/doctype/notification/notification.py | 8 ++++---- .../notification_recipient/notification_recipient.py | 2 +- .../doctype/webhook_data/webhook_data.py | 2 +- .../network_printer_settings.py | 2 +- .../doctype/energy_point_rule/energy_point_rule.py | 8 ++++---- frappe/types/exporter.py | 2 +- frappe/website/doctype/top_bar_item/top_bar_item.py | 2 +- .../website/doctype/web_form_field/web_form_field.py | 2 +- .../web_form_list_column/web_form_list_column.py | 2 +- .../workflow_document_state.py | 2 +- 29 files changed, 48 insertions(+), 48 deletions(-) diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index 7227085fdde..059b5711bb7 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -31,8 +31,8 @@ class AssignmentRule(Document): description: DF.SmallText disabled: DF.Check document_type: DF.Link - due_date_based_on: DF.Literal - field: DF.Literal + due_date_based_on: DF.Literal[None] + field: DF.Literal[None] last_user: DF.Link | None priority: DF.Int rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field"] diff --git a/frappe/automation/doctype/milestone_tracker/milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/milestone_tracker.py index 903aa6f2218..b1281b6d5c4 100644 --- a/frappe/automation/doctype/milestone_tracker/milestone_tracker.py +++ b/frappe/automation/doctype/milestone_tracker/milestone_tracker.py @@ -18,9 +18,9 @@ class MilestoneTracker(Document): disabled: DF.Check document_type: DF.Link - track_field: DF.Literal - + track_field: DF.Literal[None] # end: auto-generated types + def on_update(self): frappe.cache_manager.clear_doctype_map("Milestone Tracker", self.document_type) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index eaf7407e717..5cd180e58d4 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -111,7 +111,7 @@ class DocType(Document): custom: DF.Check default_email_template: DF.Link | None default_print_format: DF.Data | None - default_view: DF.Literal + default_view: DF.Literal[None] description: DF.SmallText | None document_type: DF.Literal["", "Document", "Setup", "System", "Other"] documentation: DF.Data | None diff --git a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py index 0b8d540448d..02889c86b3d 100644 --- a/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py +++ b/frappe/core/doctype/document_naming_rule_condition/document_naming_rule_condition.py @@ -15,7 +15,7 @@ class DocumentNamingRuleCondition(Document): from frappe.types import DF condition: DF.Literal["=", "!=", ">", "<", ">=", "<="] - field: DF.Literal + field: DF.Literal[None] parent: DF.Data parentfield: DF.Data parenttype: DF.Data diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 43fb71c8a7a..617836e6a21 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -19,7 +19,7 @@ class ModuleDef(Document): if TYPE_CHECKING: from frappe.types import DF - app_name: DF.Literal + app_name: DF.Literal[None] custom: DF.Check module_name: DF.Data package: DF.Link | None diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 3b4636e4f81..37d3a106cb3 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -89,7 +89,7 @@ class SystemSettings(Document): setup_complete: DF.Check strip_exif_metadata_from_uploaded_images: DF.Check time_format: DF.Literal["HH:mm:ss", "HH:mm"] - time_zone: DF.Literal + time_zone: DF.Literal[None] two_factor_method: DF.Literal["OTP App", "SMS", "Email"] welcome_email_template: DF.Link | None # end: auto-generated types diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index f739fa22c11..76b0fbd0573 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -29,7 +29,7 @@ class UserType(Document): role: DF.Link | None select_doctypes: DF.Table[UserSelectDocumentType] user_doctypes: DF.Table[UserDocumentType] - user_id_field: DF.Literal + user_id_field: DF.Literal[None] user_type_modules: DF.Table[UserTypeModule] # end: auto-generated types diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 7bd6f86b83b..4617bdc96a1 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -89,7 +89,7 @@ class CustomField(Document): in_list_view: DF.Check in_preview: DF.Check in_standard_filter: DF.Check - insert_after: DF.Literal + insert_after: DF.Literal[None] is_system_generated: DF.Check is_virtual: DF.Check label: DF.Data | None diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 58cdd45d2d4..2da26a29d26 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -44,7 +44,7 @@ class CustomizeForm(Document): autoname: DF.Data | None default_email_template: DF.Link | None default_print_format: DF.Link | None - default_view: DF.Literal + default_view: DF.Literal[None] doc_type: DF.Link | None editable_grid: DF.Check email_append_to: DF.Check @@ -74,7 +74,7 @@ class CustomizeForm(Document): sender_name_field: DF.Data | None show_preview_popup: DF.Check show_title_field_in_link: DF.Check - sort_field: DF.Literal + sort_field: DF.Literal[None] sort_order: DF.Literal["ASC", "DESC"] states: DF.Table[DocTypeState] subject_field: DF.Data | None diff --git a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py index 470517b589a..699178bd198 100644 --- a/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py +++ b/frappe/custom/doctype/doctype_layout_field/doctype_layout_field.py @@ -14,7 +14,7 @@ class DocTypeLayoutField(Document): if TYPE_CHECKING: from frappe.types import DF - fieldname: DF.Literal + fieldname: DF.Literal[None] label: DF.Data | None parent: DF.Data parentfield: DF.Data diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index e05e159ee05..2afaf2ea904 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -20,7 +20,7 @@ class BulkUpdate(Document): condition: DF.SmallText | None document_type: DF.Link - field: DF.Literal + field: DF.Literal[None] limit: DF.Int update_value: DF.SmallText diff --git a/frappe/desk/doctype/calendar_view/calendar_view.py b/frappe/desk/doctype/calendar_view/calendar_view.py index e5ce017669c..c4c50c47e18 100644 --- a/frappe/desk/doctype/calendar_view/calendar_view.py +++ b/frappe/desk/doctype/calendar_view/calendar_view.py @@ -14,9 +14,9 @@ class CalendarView(Document): from frappe.types import DF all_day: DF.Check - end_date_field: DF.Literal + end_date_field: DF.Literal[None] reference_doctype: DF.Link - start_date_field: DF.Literal - subject_field: DF.Literal + start_date_field: DF.Literal[None] + subject_field: DF.Literal[None] # end: auto-generated types pass diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 20ac8da2601..08659546e85 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -335,8 +335,8 @@ class DashboardChart(Document): from frappe.desk.doctype.dashboard_chart_field.dashboard_chart_field import DashboardChartField from frappe.types import DF - aggregate_function_based_on: DF.Literal - based_on: DF.Literal + aggregate_function_based_on: DF.Literal[None] + based_on: DF.Literal[None] chart_name: DF.Data chart_type: DF.Literal["Count", "Sum", "Average", "Group By", "Custom", "Report"] color: DF.Color | None @@ -345,9 +345,9 @@ class DashboardChart(Document): dynamic_filters_json: DF.Code | None filters_json: DF.Code from_date: DF.Date | None - group_by_based_on: DF.Literal + group_by_based_on: DF.Literal[None] group_by_type: DF.Literal["Count", "Sum", "Average"] - heatmap_year: DF.Literal + heatmap_year: DF.Literal[None] is_public: DF.Check is_standard: DF.Check last_synced_on: DF.Datetime | None @@ -363,8 +363,8 @@ class DashboardChart(Document): to_date: DF.Date | None type: DF.Literal["Line", "Bar", "Percentage", "Pie", "Donut", "Heatmap"] use_report_chart: DF.Check - value_based_on: DF.Literal - x_field: DF.Literal + value_based_on: DF.Literal[None] + x_field: DF.Literal[None] y_axis: DF.Table[DashboardChartField] # end: auto-generated types diff --git a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py index 0c0dfb6f64e..bf9d7b6efc7 100644 --- a/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py +++ b/frappe/desk/doctype/dashboard_chart_field/dashboard_chart_field.py @@ -18,6 +18,6 @@ class DashboardChartField(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - y_field: DF.Literal + y_field: DF.Literal[None] # end: auto-generated types pass diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.py b/frappe/desk/doctype/form_tour_step/form_tour_step.py index 47f9f96025d..2387ec0317d 100644 --- a/frappe/desk/doctype/form_tour_step/form_tour_step.py +++ b/frappe/desk/doctype/form_tour_step/form_tour_step.py @@ -17,7 +17,7 @@ class FormTourStep(Document): child_doctype: DF.Data | None description: DF.HTMLEditor element_selector: DF.Data | None - fieldname: DF.Literal + fieldname: DF.Literal[None] fieldtype: DF.Data | None has_next_condition: DF.Check hide_buttons: DF.Check @@ -32,7 +32,7 @@ class FormTourStep(Document): ondemand_description: DF.HTMLEditor | None parent: DF.Data parent_element_selector: DF.Data | None - parent_fieldname: DF.Literal + parent_fieldname: DF.Literal[None] parentfield: DF.Data parenttype: DF.Data popover_element: DF.Check diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index dee58e14188..ea8ac945110 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -19,7 +19,7 @@ class KanbanBoard(Document): from frappe.types import DF columns: DF.Table[KanbanBoardColumn] - field_name: DF.Literal + field_name: DF.Literal[None] fields: DF.Code | None filters: DF.Code | None kanban_board_name: DF.Data diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 29f90f3da62..d69187fa8bb 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -22,7 +22,7 @@ class NumberCard(Document): if TYPE_CHECKING: from frappe.types import DF - aggregate_function_based_on: DF.Literal + aggregate_function_based_on: DF.Literal[None] color: DF.Color | None document_type: DF.Link | None dynamic_filters_json: DF.Code | None @@ -35,7 +35,7 @@ class NumberCard(Document): method: DF.Data | None module: DF.Link | None parent_document_type: DF.Link | None - report_field: DF.Literal + report_field: DF.Literal[None] report_function: DF.Literal["Sum", "Average", "Minimum", "Maximum"] report_name: DF.Link | None show_percentage_stats: DF.Check diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.py b/frappe/desk/doctype/onboarding_step/onboarding_step.py index 29b40b899ff..222d3055db1 100644 --- a/frappe/desk/doctype/onboarding_step/onboarding_step.py +++ b/frappe/desk/doctype/onboarding_step/onboarding_step.py @@ -24,7 +24,7 @@ class OnboardingStep(Document): callback_message: DF.SmallText | None callback_title: DF.Data | None description: DF.MarkdownEditor | None - field: DF.Literal + field: DF.Literal[None] form_tour: DF.Link | None intro_video_url: DF.Data | None is_complete: DF.Check diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 48babce7755..4c96dacf913 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -53,14 +53,14 @@ class AutoEmailReport(Document): filters: DF.Text | None format: DF.Literal["HTML", "XLSX", "CSV"] frequency: DF.Literal["Daily", "Weekdays", "Weekly", "Monthly"] - from_date_field: DF.Literal + from_date_field: DF.Literal[None] no_of_rows: DF.Int reference_report: DF.Data | None report: DF.Link report_type: DF.ReadOnly | None send_if_data: DF.Check sender: DF.Link | None - to_date_field: DF.Literal + to_date_field: DF.Literal[None] use_first_day_of_period: DF.Check user: DF.Link # end: auto-generated types diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 6a4a271cef4..23776fb81f1 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -32,7 +32,7 @@ class Notification(Document): attach_print: DF.Check channel: DF.Literal["Email", "Slack", "System Notification", "SMS"] condition: DF.Code | None - date_changed: DF.Literal + date_changed: DF.Literal[None] days_in_advance: DF.Int document_type: DF.Link enabled: DF.Check @@ -59,12 +59,12 @@ class Notification(Document): send_to_all_assignees: DF.Check sender: DF.Link | None sender_email: DF.Data | None - set_property_after_alert: DF.Literal + set_property_after_alert: DF.Literal[None] slack_webhook_url: DF.Link | None subject: DF.Data | None - value_changed: DF.Literal - + value_changed: DF.Literal[None] # end: auto-generated types + def onload(self): """load message""" if self.is_standard: diff --git a/frappe/email/doctype/notification_recipient/notification_recipient.py b/frappe/email/doctype/notification_recipient/notification_recipient.py index 1a1b397a808..5f9f519a203 100644 --- a/frappe/email/doctype/notification_recipient/notification_recipient.py +++ b/frappe/email/doctype/notification_recipient/notification_recipient.py @@ -19,7 +19,7 @@ class NotificationRecipient(Document): parent: DF.Data parentfield: DF.Data parenttype: DF.Data - receiver_by_document_field: DF.Literal + receiver_by_document_field: DF.Literal[None] receiver_by_role: DF.Link | None # end: auto-generated types pass diff --git a/frappe/integrations/doctype/webhook_data/webhook_data.py b/frappe/integrations/doctype/webhook_data/webhook_data.py index 7461115dc55..1f11daee5f2 100644 --- a/frappe/integrations/doctype/webhook_data/webhook_data.py +++ b/frappe/integrations/doctype/webhook_data/webhook_data.py @@ -14,7 +14,7 @@ class WebhookData(Document): if TYPE_CHECKING: from frappe.types import DF - fieldname: DF.Literal + fieldname: DF.Literal[None] key: DF.Data parent: DF.Data parentfield: DF.Data 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 52451fe1099..c491963cb83 100644 --- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py @@ -16,7 +16,7 @@ class NetworkPrinterSettings(Document): from frappe.types import DF port: DF.Int - printer_name: DF.Literal + printer_name: DF.Literal[None] server_ip: DF.Data # end: auto-generated types diff --git a/frappe/social/doctype/energy_point_rule/energy_point_rule.py b/frappe/social/doctype/energy_point_rule/energy_point_rule.py index a59a55e3f2d..d81ac5e7a90 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.py +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.py @@ -25,17 +25,17 @@ class EnergyPointRule(Document): apply_only_once: DF.Check condition: DF.Code | None enabled: DF.Check - field_to_check: DF.Literal + field_to_check: DF.Literal[None] for_assigned_users: DF.Check for_doc_event: DF.Literal["New", "Submit", "Cancel", "Value Change", "Custom"] max_points: DF.Int - multiplier_field: DF.Literal + multiplier_field: DF.Literal[None] points: DF.Int reference_doctype: DF.Link rule_name: DF.Data - user_field: DF.Literal - + user_field: DF.Literal[None] # end: auto-generated types + def on_update(self): frappe.cache_manager.clear_doctype_map("Energy Point Rule", self.reference_doctype) diff --git a/frappe/types/exporter.py b/frappe/types/exporter.py index 97551e0c017..5dd5b4a3b71 100644 --- a/frappe/types/exporter.py +++ b/frappe/types/exporter.py @@ -178,7 +178,7 @@ def _generic_parameters(self, field) -> str | None: elif field.fieldtype == "Select": if not field.options: # Could be dynamic - return + return "None" options = [o.strip() for o in field.options.split("\n")] return json.dumps(options) diff --git a/frappe/website/doctype/top_bar_item/top_bar_item.py b/frappe/website/doctype/top_bar_item/top_bar_item.py index 61401458d82..5d91d2a135b 100644 --- a/frappe/website/doctype/top_bar_item/top_bar_item.py +++ b/frappe/website/doctype/top_bar_item/top_bar_item.py @@ -17,7 +17,7 @@ class TopBarItem(Document): label: DF.Data open_in_new_tab: DF.Check parent: DF.Data - parent_label: DF.Literal + parent_label: DF.Literal[None] parentfield: DF.Data parenttype: DF.Data right: DF.Check diff --git a/frappe/website/doctype/web_form_field/web_form_field.py b/frappe/website/doctype/web_form_field/web_form_field.py index 208d31096a4..b2b5abad0af 100644 --- a/frappe/website/doctype/web_form_field/web_form_field.py +++ b/frappe/website/doctype/web_form_field/web_form_field.py @@ -18,7 +18,7 @@ class WebFormField(Document): default: DF.Data | None depends_on: DF.Code | None description: DF.Text | None - fieldname: DF.Literal + fieldname: DF.Literal[None] fieldtype: DF.Literal[ "Attach", "Attach Image", diff --git a/frappe/website/doctype/web_form_list_column/web_form_list_column.py b/frappe/website/doctype/web_form_list_column/web_form_list_column.py index 210159bb5b9..3028e6ed4b2 100644 --- a/frappe/website/doctype/web_form_list_column/web_form_list_column.py +++ b/frappe/website/doctype/web_form_list_column/web_form_list_column.py @@ -14,7 +14,7 @@ class WebFormListColumn(Document): if TYPE_CHECKING: from frappe.types import DF - fieldname: DF.Literal + fieldname: DF.Literal[None] fieldtype: DF.Data | None label: DF.Data | None name: DF.Int | None diff --git a/frappe/workflow/doctype/workflow_document_state/workflow_document_state.py b/frappe/workflow/doctype/workflow_document_state/workflow_document_state.py index d1644578d8c..089cad08c9c 100644 --- a/frappe/workflow/doctype/workflow_document_state/workflow_document_state.py +++ b/frappe/workflow/doctype/workflow_document_state/workflow_document_state.py @@ -24,7 +24,7 @@ class WorkflowDocumentState(Document): parentfield: DF.Data parenttype: DF.Data state: DF.Link - update_field: DF.Literal + update_field: DF.Literal[None] update_value: DF.Data | None workflow_builder_id: DF.Data | None # end: auto-generated types From adf6a2a8cffc98d00c5053ab2c1fca05a06ce48b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 15:42:43 +0530 Subject: [PATCH 21/32] fix: add "If Owner" column to roles viewer (#25218) (#25220) (cherry picked from commit bfb1c3e7e1ccbd9c17bc06da3b7c721f012e6801) Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com> --- frappe/public/js/frappe/roles_editor.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 691acb035d8..c53c7976ea5 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -68,6 +68,7 @@ frappe.RoleEditor = class { ${__("Document Type")} ${__("Level")} + ${__("If Owner")} ${frappe.perm.rights.map((p) => ` ${__(frappe.unscrub(p))}`).join("")} @@ -79,6 +80,7 @@ frappe.RoleEditor = class { ${__(perm.parent)} ${perm.permlevel} + ${perm.if_owner ? frappe.utils.icon("check", "xs") : "-"} ${frappe.perm.rights .map( (p) => From 963ff8986e2c86ff4864341a9972830caa0bebfe Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:22:14 +0000 Subject: [PATCH 22/32] fix(UX): list filter take zero as null (#25156) (#25222) * fix: list filter take zero as null * chore: fallback if null only --------- Co-authored-by: Ankush Menat (cherry picked from commit 7d7468c45f1aa450f849db2caba9763f5a738cd2) Co-authored-by: Kunhi --- frappe/public/js/frappe/ui/filters/filter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index d45e44df357..24bb78ec04e 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -420,7 +420,7 @@ frappe.ui.filter_utils = { get_selected_value(field, condition) { if (!field) return; - let val = field.get_value() || field.value; + let val = field.get_value() ?? field.value; if (typeof val === "string") { val = strip(val); From b729c03fe328603522fdef30e58f2b4852b0f372 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 10:38:24 +0000 Subject: [PATCH 23/32] feat: Add doc rename hook in server script (#25085) (#25110) * feat: Add doc rename hook in server script * feat: Add test case for doc event in server script (cherry picked from commit 911846368f2a09b086fddca431ca9b5bd4dc3284) # Conflicts: # frappe/core/doctype/server_script/server_script.json Co-authored-by: Niraj Gautam --- .../doctype/server_script/server_script.json | 12 +++++---- .../doctype/server_script/server_script.py | 2 ++ .../server_script/server_script_utils.py | 2 ++ .../server_script/test_server_script.py | 26 +++++++++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 50f5bfcfe84..9f072c11dbe 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -49,14 +49,15 @@ "in_list_view": 1, "in_standard_filter": 1, "label": "Reference Document Type", - "options": "DocType" + "options": "DocType", + "search_index": 1 }, { "depends_on": "eval:doc.script_type==='DocType Event'", "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Insert\nAfter Save\nBefore Rename\nAfter Rename\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)\nOn Payment Authorization" }, { "depends_on": "eval:doc.script_type==='API'", @@ -106,7 +107,8 @@ "fieldname": "module", "fieldtype": "Link", "label": "Module (for export)", - "options": "Module Def" + "options": "Module Def", + "search_index": 1 }, { "depends_on": "eval:doc.script_type==='API'", @@ -149,7 +151,7 @@ "link_fieldname": "server_script" } ], - "modified": "2023-10-14 11:24:46.478533", + "modified": "2024-02-27 11:44:46.397495", "modified_by": "Administrator", "module": "Core", "name": "Server Script", @@ -173,4 +175,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 02df54967ac..82b16876abb 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -36,6 +36,8 @@ class ServerScript(Document): "Before Save", "After Insert", "After Save", + "Before Rename", + "After Rename", "Before Submit", "After Submit", "Before Cancel", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index d0ae253d29f..eeac8c23626 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -9,6 +9,8 @@ "before_validate": "Before Validate", "validate": "Before Save", "on_update": "After Save", + "before_rename": "Before Rename", + "after_rename": "After Rename", "before_submit": "Before Submit", "on_submit": "After Submit", "before_cancel": "Before Cancel", diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index e7735c8fc9f..a16e9d8ad99 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -82,6 +82,26 @@ disabled=1, script=""" frappe.db.add_index("Todo", ["color", "date"]) +""", + ), + dict( + name="test_before_rename", + script_type="DocType Event", + doctype_event="After Rename", + reference_doctype="Role", + script=""" +doc.desk_access =0 +doc.save() +""", + ), + dict( + name="test_after_rename", + script_type="DocType Event", + doctype_event="After Rename", + reference_doctype="Role", + script=""" +doc.disabled =1 +doc.save() """, ), ] @@ -121,6 +141,12 @@ def test_doctype_event(self): frappe.ValidationError, frappe.get_doc(dict(doctype="ToDo", description="validate me")).insert ) + role = frappe.get_doc(doctype="Role", role_name="_Test Role 9").insert(ignore_if_duplicate=True) + role.rename("_Test Role 10") + role.reload() + self.assertEqual(role.disabled, 1) + self.assertEqual(role.desk_access, 0) + def test_api(self): response = requests.post(get_site_url(frappe.local.site) + "/api/method/test_server_script") self.assertEqual(response.status_code, 200) From b0db64106a788b3c6c3a94213a8e24b1474fb2a8 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:36:34 +0000 Subject: [PATCH 24/32] feat: let users unlock stuck documents (backport #24782) (#25225) * fix: better file locking (cherry picked from commit 1f9efb7b3f222e6dd4aefebc8467eb6b5b3cb517) # Conflicts: # frappe/model/document.py * fix: Auto delete very old document locks locks older than 12 hours are most likely from dead processes. They can be (mostly) safely ignored. (cherry picked from commit d616341ad4b73acd100ee9ae916579c0f608ecdd) # Conflicts: # frappe/model/document.py * fix!: Accept flat arguments for server_action (cherry picked from commit eb1b1b4d6b9271715efc9315ea25715ee8fef845) * feat: support primary_action for `frappe.throw` (cherry picked from commit 40f1ae1cce3e8b0358c2daf469e57fd23670e662) * feat: let users unlock stuck documents (cherry picked from commit d89e0e7e4c470161f56bc8fb747aa7190a9c6b70) * refactor: simplify code Co-authored-by: Akhil Narang (cherry picked from commit 2c19e846df3d0989d90e1d830f67eb1e72e21b59) * chore: conflicts * Revert "fix!: Accept flat arguments for server_action" This reverts commit e7f231954600ff34a0e19b285bcc77c38bb036a7. * fix: make unlocking Backward compatibile * fix: wrong variable assignment --------- Co-authored-by: Ankush Menat Co-authored-by: Ankush Menat Co-authored-by: Akhil Narang --- frappe/__init__.py | 10 +++++++- frappe/model/document.py | 39 ++++++++++++++++++++++++++--- frappe/tests/test_document_locks.py | 10 ++++++++ frappe/utils/file_lock.py | 6 +++++ 4 files changed, 61 insertions(+), 4 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 8dbc0436494..1c01e4bffb2 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -585,11 +585,18 @@ def throw( is_minimizable: bool = False, wide: bool = False, as_list: bool = False, + primary_action=None, ) -> None: """Throw execption and show message (`msgprint`). :param msg: Message. - :param exc: Exception class. Default `frappe.ValidationError`""" + :param exc: Exception class. Default `frappe.ValidationError` + :param title: [optional] Message title. Default: "Message". + :param is_minimizable: [optional] Allow users to minimize the modal + :param wide: [optional] Show wide modal + :param as_list: [optional] If `msg` is a list, render as un-ordered list. + :param primary_action: [optional] Bind a primary server/client side action. + """ msgprint( msg, raise_exception=exc, @@ -598,6 +605,7 @@ def throw( is_minimizable=is_minimizable, wide=wide, as_list=as_list, + primary_action=primary_action, ) diff --git a/frappe/model/document.py b/frappe/model/document.py index 7f9b399cd41..8a3cab5bb0f 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -29,6 +29,10 @@ from frappe.core.doctype.docfield.docfield import DocField +DOCUMENT_LOCK_EXPIRTY = 12 * 60 * 60 # All locks expire in 12 hours automatically +DOCUMENT_LOCK_SOFT_EXPIRY = 60 * 60 # Let users force-unlock after 60 minutes + + def get_doc(*args, **kwargs): """returns a frappe.model.Document object. @@ -1452,8 +1456,8 @@ def log_error(self, title=None, message=None): ) def get_signature(self): - """Returns signature (hash) for private URL.""" - return hashlib.sha224(get_datetime_str(self.creation).encode()).hexdigest() + """Return signature (hash) for private URL.""" + return hashlib.sha224(f"{self.doctype}:{self.name}".encode(), usedforsecurity=False).hexdigest() def get_document_share_key(self, expires_on=None, no_expiry=False): if no_expiry: @@ -1511,9 +1515,25 @@ def queue_action(self, action, **kwargs): try: self.lock() except frappe.DocumentLockedError: + # Allow unlocking if created more than 60 minutes ago + primary_action = None + if file_lock.lock_age(self.get_signature()) > DOCUMENT_LOCK_SOFT_EXPIRY: + primary_action = { + "label": "Force Unlock", + "server_action": "frappe.model.document.unlock_document", + "hide_on_success": True, + "args": { + "doctype": self.doctype, + "name": self.name, + }, + } + frappe.throw( - _("This document is currently queued for execution. Please try again"), + _( + "This document is currently locked and queued for execution. Please try again after some time." + ), title=_("Document Queued"), + primary_action=primary_action, ) return enqueue( @@ -1532,6 +1552,9 @@ def lock(self, timeout=None): signature = self.get_signature() if file_lock.lock_exists(signature): lock_exists = True + if file_lock.lock_age(signature) > DOCUMENT_LOCK_EXPIRTY: + file_lock.delete_lock(signature) + lock_exists = False if timeout: for _ in range(timeout): time.sleep(1) @@ -1695,3 +1718,13 @@ def _document_values_generator( ignore_virtual=True, ) yield tuple(doc_values.get(col) for col in columns) + + +@frappe.whitelist() +def unlock_document(doctype: str | None = None, name: str | None = None, args=None): + if not doctype and not name and args: + # Backward compatibility + doctype = str(args["doctype"]) + name = str(args["name"]) + frappe.get_doc(doctype, name).unlock() + frappe.msgprint(frappe._("Document Unlocked"), alert=True) diff --git a/frappe/tests/test_document_locks.py b/frappe/tests/test_document_locks.py index 5d19f75050c..0fde5b55436 100644 --- a/frappe/tests/test_document_locks.py +++ b/frappe/tests/test_document_locks.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import frappe from frappe.tests.utils import FrappeTestCase +from frappe.utils.data import add_to_date, today class TestDocumentLocks(FrappeTestCase): @@ -36,3 +37,12 @@ def test_operations_on_locked_documents(self): doc.unlock() self.assertEqual(doc.is_locked, False) self.assertEqual(todo.is_locked, False) + + def test_locks_auto_expiry(self): + todo = frappe.get_doc(dict(doctype="ToDo", description=frappe.generate_hash())).insert() + todo.lock() + + self.assertRaises(frappe.DocumentLockedError, todo.lock) + + with self.freeze_time(add_to_date(today(), days=3)): + todo.lock() diff --git a/frappe/utils/file_lock.py b/frappe/utils/file_lock.py index cb86d2f3de0..0e659460d21 100644 --- a/frappe/utils/file_lock.py +++ b/frappe/utils/file_lock.py @@ -9,6 +9,7 @@ """ import os +from pathlib import Path from time import time import frappe @@ -41,6 +42,11 @@ def lock_exists(name): return os.path.exists(get_lock_path(name)) +def lock_age(name) -> float: + """Return time in seconds since lock was created.""" + return time() - Path(get_lock_path(name)).stat().st_mtime + + def check_lock(path, timeout=600): if not os.path.exists(path): return False From 4a4056b53b1738f0ba9de8665f998a687ab7cbed Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 4 Mar 2024 17:46:08 +0530 Subject: [PATCH 25/32] ci(version-15): use 3.10 (#25227) Avoid possible compatibility issues by checking against msv. Develop will continue to use latest python version for faster CI and future proofing. --- .github/workflows/linters.yml | 2 +- .github/workflows/server-tests.yml | 2 +- .github/workflows/ui-tests.yml | 2 +- frappe/tests/test_auth.py | 4 +++- 4 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index c67fdcac63d..f1d5553c07d 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -40,7 +40,7 @@ jobs: - name: 'Setup Environment' uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.10' - uses: actions/checkout@v4 - name: Validate Docs diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 927de9be333..4ef2066be65 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -83,7 +83,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.10" - name: Check for valid Python & Merge Conflicts run: | diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 1efb45a5696..02b2e6f6d24 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -69,7 +69,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.10' - name: Check for valid Python & Merge Conflicts run: | diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py index 2a25ba6a4a2..eb34f694fb1 100644 --- a/frappe/tests/test_auth.py +++ b/frappe/tests/test_auth.py @@ -158,10 +158,12 @@ def test_login_with_email_link(self): self.fail("Rate limting not working") def test_correct_cookie_expiry_set(self): + import pytz + client = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) expiry_time = next(x for x in client.session.cookies if x.name == "sid").expires - current_time = datetime.datetime.now(tz=datetime.UTC).timestamp() + current_time = datetime.datetime.now(tz=pytz.UTC).timestamp() self.assertAlmostEqual(get_expiry_in_seconds(), expiry_time - current_time, delta=60 * 60) From 5f3726ed9738746f9223a4310285360932e72214 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Mon, 4 Mar 2024 17:50:47 +0530 Subject: [PATCH 26/32] chore: don't allow printing more than 50 documents for now (#25229) (#25231) The 2 minute timeout gets exhausted pretty quickly, until we can move this to the background, this will atleast prevent people from seeing an error after the request times out. Signed-off-by: Akhil Narang (cherry picked from commit 7b259fd75c57521a9a0663d24706c740cbfc7589) Co-authored-by: Akhil Narang --- frappe/public/js/frappe/list/bulk_operations.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index a0271967b4e..6e33f81a51a 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -35,6 +35,11 @@ export default class BulkOperations { return; } + if (valid_docs.length > 50) { + frappe.msgprint(__("You can only print upto 50 documents at a time")); + return; + } + const dialog = new frappe.ui.Dialog({ title: __("Print Documents"), fields: [ From 30a2f2743df74edefeeafc6efe12ac6f2dedf246 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:16:27 +0100 Subject: [PATCH 27/32] fix: task_id parameter for publish_progress (cherry picked from commit a9c4894ccfef40f404ca62c253f13ec7f790a1b0) --- frappe/realtime.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frappe/realtime.py b/frappe/realtime.py index 78726314988..59d0351304e 100644 --- a/frappe/realtime.py +++ b/frappe/realtime.py @@ -9,13 +9,16 @@ from frappe.utils.data import cstr -def publish_progress(percent, title=None, doctype=None, docname=None, description=None): +def publish_progress( + percent, title=None, doctype=None, docname=None, description=None, task_id=None +): publish_realtime( "progress", {"percent": percent, "title": title, "description": description}, user=None if doctype and docname else frappe.session.user, doctype=doctype, docname=docname, + task_id=task_id, ) @@ -41,8 +44,11 @@ def publish_realtime( if message is None: message = {} + if not task_id and hasattr(frappe.local, "task_id"): + task_id = frappe.local.task_id + if event is None: - event = "task_progress" if frappe.local.task_id else "global" + event = "task_progress" if task_id else "global" elif event == "msgprint" and not user: user = frappe.session.user elif event == "list_update": @@ -51,9 +57,6 @@ def publish_realtime( elif event == "docinfo_update": room = get_doc_room(doctype, docname) - if not task_id and hasattr(frappe.local, "task_id"): - task_id = frappe.local.task_id - if not room: if task_id: after_commit = False From 0122a69e98371574ec921a33435d49d3f60f5401 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:19:56 +0100 Subject: [PATCH 28/32] refactor: better variable names (cherry picked from commit 105c4a20fb4d5e2546dc8ea1fc22eb72a96c5295) --- frappe/desk/doctype/bulk_update/bulk_update.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 2afaf2ea904..448dd0a0b0e 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -73,8 +73,8 @@ def _bulk_action(doctype, docnames, action, data): failed = [] - for i, d in enumerate(docnames, 1): - doc = frappe.get_doc(doctype, d) + for idx, docname in enumerate(docnames, 1): + doc = frappe.get_doc(doctype, docname) try: message = "" if action == "submit" and doc.docstatus.is_draft(): @@ -92,12 +92,12 @@ def _bulk_action(doctype, docnames, action, data): doc.save() message = _("Updating {0}").format(doctype) else: - failed.append(d) + failed.append(docname) frappe.db.commit() - show_progress(docnames, message, i, d) + show_progress(docnames, message, idx, docname) except Exception: - failed.append(d) + failed.append(docname) frappe.db.rollback() return failed From bf8d102f6149b4b54f3735a9ded07ce5adc6b142 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:30:13 +0100 Subject: [PATCH 29/32] feat: task_id for submit_cancel_or_update_docs (cherry picked from commit 09395420b855ecfa8f77138ea03303a35248a6f3) --- .../desk/doctype/bulk_update/bulk_update.py | 16 ++++++++--- .../public/js/frappe/list/bulk_operations.js | 28 +++++++++---------- 2 files changed, 26 insertions(+), 18 deletions(-) diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 448dd0a0b0e..1728278e5c4 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -25,6 +25,7 @@ class BulkUpdate(Document): update_value: DF.SmallText # end: auto-generated types + @frappe.whitelist() def bulk_update(self): self.check_permission("write") @@ -46,12 +47,12 @@ def bulk_update(self): @frappe.whitelist() -def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): +def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None, task_id=None): if isinstance(docnames, str): docnames = frappe.parse_json(docnames) if len(docnames) < 20: - return _bulk_action(doctype, docnames, action, data) + return _bulk_action(doctype, docnames, action, data, task_id) elif len(docnames) <= 500: frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True) frappe.enqueue( @@ -60,6 +61,7 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): docnames=docnames, action=action, data=data, + task_id=task_id, queue="short", timeout=1000, ) @@ -67,11 +69,12 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): frappe.throw(_("Bulk operations only support up to 500 documents."), title=_("Too Many Documents")) -def _bulk_action(doctype, docnames, action, data): +def _bulk_action(doctype, docnames, action, data, task_id=None): if data: data = frappe.parse_json(data) failed = [] + num_documents = len(docnames) for idx, docname in enumerate(docnames, 1): doc = frappe.get_doc(doctype, docname) @@ -94,7 +97,12 @@ def _bulk_action(doctype, docnames, action, data): else: failed.append(docname) frappe.db.commit() - show_progress(docnames, message, idx, docname) + frappe.publish_progress( + percent=idx / num_documents * 100, + title=message, + description=docname, + task_id=task_id, + ) except Exception: failed.append(docname) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 6e33f81a51a..08b625c110d 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -214,28 +214,28 @@ export default class BulkOperations { submit_or_cancel(docnames, action = "submit", done = null) { action = action.toLowerCase(); - frappe - .call({ - method: "frappe.desk.doctype.bulk_update.bulk_update.submit_cancel_or_update_docs", - args: { - doctype: this.doctype, - action: action, - docnames: docnames, - }, + const task_id = Math.random().toString(36).slice(-5); + frappe.realtime.task_subscribe(task_id); + return frappe + .xcall("frappe.desk.doctype.bulk_update.bulk_update.submit_cancel_or_update_docs", { + doctype: this.doctype, + action: action, + docnames: docnames, + task_id: task_id, }) - .then((r) => { - let failed = r.message; - if (!failed) failed = []; - - if (failed.length && !r._server_messages) { + .then((failed) => { + if (failed?.length) { frappe.throw( __("Cannot {0} {1}", [action, failed.map((f) => f.bold()).join(", ")]) ); } - if (failed.length < docnames.length) { + if (failed?.length < docnames.length) { frappe.utils.play_sound(action); if (done) done(); } + }) + .finally(() => { + frappe.realtime.task_unsubscribe(task_id); }); } From b79f133ed60e5b79de60c22a04a30805f7514dfd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:30:43 +0100 Subject: [PATCH 30/32] chore: deprecate old show_progress (cherry picked from commit 9ada76df6c4ed7ba0d5545f2ff95f69f11ef9739) --- frappe/desk/doctype/bulk_update/bulk_update.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 1728278e5c4..6e5a31068e0 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -6,6 +6,7 @@ from frappe.core.doctype.submission_queue.submission_queue import queue_submission from frappe.model.document import Document from frappe.utils import cint +from frappe.utils.deprecations import deprecated from frappe.utils.scheduler import is_scheduler_inactive @@ -111,6 +112,7 @@ def _bulk_action(doctype, docnames, action, data, task_id=None): return failed +@deprecated def show_progress(docnames, message, i, description): n = len(docnames) frappe.publish_progress(float(i) * 100 / n, title=message, description=description) From b94e9784b6a39fb0a224f7d5ee280347eab3e020 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:37:02 +0100 Subject: [PATCH 31/32] fix: better error message (cherry picked from commit 54a83892bbb3d2d7cb546729678ee3e4465f283a) --- frappe/public/js/frappe/list/bulk_operations.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 08b625c110d..d9577412897 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -225,9 +225,17 @@ export default class BulkOperations { }) .then((failed) => { if (failed?.length) { - frappe.throw( - __("Cannot {0} {1}", [action, failed.map((f) => f.bold()).join(", ")]) - ); + const comma_separated_records = frappe.utils.comma_and(failed); + switch (action) { + case "submit": + frappe.throw(__("Cannot submit {0}.", [comma_separated_records])); + break; + case "cancel": + frappe.throw(__("Cannot cancel {0}.", [comma_separated_records])); + break; + default: + frappe.throw(__("Cannot {0} {1}.", [action, comma_separated_records])); + } } if (failed?.length < docnames.length) { frappe.utils.play_sound(action); From 37e0db75f50096e0b32d554caad7984a948d752b Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 5 Dec 2023 19:39:47 +0100 Subject: [PATCH 32/32] refactor: better parameter name (cherry picked from commit 68c0e6f85f36be71928593a2fbaf756115f22462) --- frappe/public/js/frappe/list/bulk_operations.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index d9577412897..a258ac50521 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -223,9 +223,9 @@ export default class BulkOperations { docnames: docnames, task_id: task_id, }) - .then((failed) => { - if (failed?.length) { - const comma_separated_records = frappe.utils.comma_and(failed); + .then((failed_docnames) => { + if (failed_docnames?.length) { + const comma_separated_records = frappe.utils.comma_and(failed_docnames); switch (action) { case "submit": frappe.throw(__("Cannot submit {0}.", [comma_separated_records])); @@ -237,7 +237,7 @@ export default class BulkOperations { frappe.throw(__("Cannot {0} {1}.", [action, comma_separated_records])); } } - if (failed?.length < docnames.length) { + if (failed_docnames?.length < docnames.length) { frappe.utils.play_sound(action); if (done) done(); }