diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 03ef96783a..dff04a5693 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -2,7 +2,11 @@ context("Awesome Bar", () => { before(() => { cy.visit("/login"); cy.login(); - cy.visit("/app/website"); + cy.visit("/app/todo"); // Make sure ToDo filters are cleared. + cy.clear_filters(); + cy.visit("/app/blog-post"); // Make sure Blog Post filters are cleared. + cy.clear_filters(); + cy.visit("/app/website"); // Go to some other page. }); beforeEach(() => { @@ -11,36 +15,61 @@ context("Awesome Bar", () => { cy.get("@awesome_bar").type("{selectall}"); }); + after(() => { + cy.visit("/app/todo"); // Make sure we're not bleeding any filters to the next spec. + cy.clear_filters(); + }); + it("navigates to doctype list", () => { cy.get("@awesome_bar").type("todo"); - cy.wait(100); + cy.wait(100); // Wait a bit before hitting enter. cy.get(".awesomplete").findByRole("listbox").should("be.visible"); cy.get("@awesome_bar").type("{enter}"); cy.get(".title-text").should("contain", "To Do"); cy.location("pathname").should("eq", "/app/todo"); }); - it("find text in doctype list", () => { + it("finds text in doctype list", () => { cy.get("@awesome_bar").type("test in todo"); - cy.wait(100); + cy.wait(150); // Wait a bit before hitting enter. cy.get("@awesome_bar").type("{enter}"); cy.get(".title-text").should("contain", "To Do"); - cy.wait(200); - const name_filter = cy.get('[data-original-title="ID"] > input'); - name_filter.should("have.value", "%test%"); - cy.clear_filters(); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.get('[data-original-title="ID"] > input').should("have.value", "%test%"); + }); + + it("filter preserved, now finds something else", () => { + cy.visit("/app/todo"); + cy.get(".title-text").should("contain", "To Do"); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.get('[data-original-title="ID"] > input').as("filter"); + cy.get("@filter").should("have.value", "%test%"); + cy.get("@awesome_bar").type("anothertest in todo"); + cy.wait(200); // Wait a bit longer before hitting enter. + cy.get("@awesome_bar").type("{enter}"); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.get("@filter").should("have.value", "%anothertest%"); + }); + + it("navigates to another doctype, filter not bleeding", () => { + cy.get("@awesome_bar").type("blog post"); + cy.wait(150); // Wait a bit before hitting enter. + cy.get("@awesome_bar").type("{enter}"); + cy.get(".title-text").should("contain", "Blog Post"); + cy.wait(200); // Wait a bit longer before checking the filter. + cy.location("search").should("be.empty"); }); it("navigates to new form", () => { cy.get("@awesome_bar").type("new blog post"); - cy.wait(100); + cy.wait(150); // Wait a bit before hitting enter cy.get("@awesome_bar").type("{enter}"); cy.get(".title-text:visible").should("have.text", "New Blog Post"); }); it("calculates math expressions", () => { cy.get("@awesome_bar").type("55 + 32"); - cy.wait(100); + cy.wait(150); // Wait a bit before hitting enter cy.get("@awesome_bar").type("{downarrow}{enter}"); cy.get(".modal-title").should("contain", "Result"); cy.get(".msgprint").should("contain", "55 + 32 = 87"); diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py index 5d453e3568..cf760cf4f0 100755 --- a/frappe/commands/scheduler.py +++ b/frappe/commands/scheduler.py @@ -202,7 +202,7 @@ def start_scheduler(): def start_worker( queue, quiet=False, rq_username=None, rq_password=None, burst=False, strategy=None ): - """Start a backgrond worker""" + """Start a background worker""" from frappe.utils.background_jobs import start_worker start_worker( @@ -225,7 +225,7 @@ def start_worker( @click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs") @click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.") def start_worker_pool(queue, quiet=False, num_workers=2, burst=False): - """Start a backgrond worker""" + """Start a pool of background workers""" from frappe.utils.background_jobs import start_worker_pool start_worker_pool( diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 54b23094f8..45fa621cec 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -23,38 +23,23 @@ "float_precision", "currency_precision", "rounding_method", - "sec_backup_limit", - "backup_limit", - "encrypt_backup", - "background_workers", - "enable_scheduler", - "dormant_days", "permissions", "apply_strict_user_permissions", "column_break_21", - "allow_guests_to_upload_files", - "force_web_capture_mode_for_uploads", + "allow_older_web_view_links", + "security_tab", "security", "session_expiry", "document_share_key_expiry", - "column_break_13", + "column_break_txqh", "deny_multiple_sessions", + "disable_user_pass_login", + "login_methods_section", "allow_login_using_mobile_number", "allow_login_using_user_name", - "disable_user_pass_login", + "column_break_uhqk", "login_with_email_link", "login_with_email_link_expiry", - "allow_error_traceback", - "strip_exif_metadata_from_uploaded_images", - "allow_older_web_view_links", - "password_settings", - "logout_on_password_reset", - "force_user_to_reset_password", - "reset_password_link_expiry_duration", - "password_reset_limit", - "column_break_31", - "enable_password_policy", - "minimum_password_score", "brute_force_security", "allow_consecutive_login_attempts", "column_break_34", @@ -66,6 +51,16 @@ "two_factor_method", "lifespan_qrcode_image", "otp_issuer_name", + "password_tab", + "password_settings", + "logout_on_password_reset", + "force_user_to_reset_password", + "reset_password_link_expiry_duration", + "password_reset_limit", + "column_break_31", + "enable_password_policy", + "minimum_password_score", + "email_tab", "email", "email_footer_address", "email_retry_limit", @@ -75,17 +70,31 @@ "attach_view_link", "welcome_email_template", "reset_password_template", - "prepared_report_section", - "max_auto_email_report_per_user", + "files_tab", + "files_section", + "max_file_size", + "allow_guests_to_upload_files", + "force_web_capture_mode_for_uploads", + "strip_exif_metadata_from_uploaded_images", + "column_break_uqma", + "allowed_file_extensions", + "updates_tab", "system_updates_section", "disable_system_update_notification", "disable_change_log_notification", + "backups_tab", + "sec_backup_limit", + "backup_limit", + "encrypt_backup", + "advanced_tab", + "prepared_report_section", + "max_auto_email_report_per_user", + "background_workers", + "enable_scheduler", + "dormant_days", "telemetry_section", - "enable_telemetry", - "files_section", - "max_file_size", - "column_break_uqma", - "allowed_file_extensions" + "allow_error_traceback", + "enable_telemetry" ], "fields": [ { @@ -126,7 +135,6 @@ "read_only": 1 }, { - "collapsible": 1, "fieldname": "date_and_number_format", "fieldtype": "Section Break", "label": "Date and Number Format" @@ -171,10 +179,8 @@ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" }, { - "collapsible": 1, "fieldname": "sec_backup_limit", - "fieldtype": "Section Break", - "label": "Backups" + "fieldtype": "Section Break" }, { "default": "3", @@ -184,7 +190,6 @@ "label": "Number of Backups" }, { - "collapsible": 1, "fieldname": "background_workers", "fieldtype": "Section Break", "label": "Background Workers" @@ -198,7 +203,6 @@ "label": "Enable Scheduled Jobs" }, { - "collapsible": 1, "fieldname": "permissions", "fieldtype": "Section Break", "label": "Permissions" @@ -211,10 +215,8 @@ "label": "Apply Strict User Permissions" }, { - "collapsible": 1, "fieldname": "security", - "fieldtype": "Section Break", - "label": "Security" + "fieldtype": "Section Break" }, { "default": "170:00", @@ -223,10 +225,6 @@ "fieldtype": "Data", "label": "Session Expiry (idle timeout)" }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, { "default": "0", "description": "Note: Multiple sessions will be allowed in case of mobile device", @@ -255,7 +253,6 @@ "label": "Show Full Error and Allow Reporting of Issues to the Developer" }, { - "collapsible": 1, "fieldname": "password_settings", "fieldtype": "Section Break", "label": "Password" @@ -286,7 +283,6 @@ "options": "2\n3\n4" }, { - "collapsible": 1, "fieldname": "brute_force_security", "fieldtype": "Section Break", "label": "Brute Force Security" @@ -309,7 +305,6 @@ "label": "Allow Login After Fail" }, { - "collapsible": 1, "fieldname": "two_factor_authentication", "fieldtype": "Section Break", "label": "Two Factor Authentication" @@ -338,6 +333,7 @@ }, { "default": "OTP App", + "depends_on": "enable_two_factor_auth", "description": "Choose authentication method to be used by all users", "fieldname": "two_factor_method", "fieldtype": "Select", @@ -345,7 +341,7 @@ "options": "OTP App\nSMS\nEmail" }, { - "depends_on": "eval:doc.two_factor_method == \"OTP App\"", + "depends_on": "eval:doc.enable_two_factor_auth && doc.two_factor_method == \"OTP App\"", "description": "Time in seconds to retain QR code image on server. Min:240", "fieldname": "lifespan_qrcode_image", "fieldtype": "Int", @@ -359,10 +355,8 @@ "label": "OTP Issuer Name" }, { - "collapsible": 1, "fieldname": "email", - "fieldtype": "Section Break", - "label": "Email" + "fieldtype": "Section Break" }, { "description": "Your organization name and address for the email footer.", @@ -430,7 +424,6 @@ "label": "Include Web View Link in Email" }, { - "collapsible": 1, "fieldname": "prepared_report_section", "fieldtype": "Section Break", "label": "Reports" @@ -456,10 +449,8 @@ "label": "Encrypt Backups" }, { - "collapsible": 1, "fieldname": "system_updates_section", - "fieldtype": "Section Break", - "label": "System Updates" + "fieldtype": "Section Break" }, { "default": "0", @@ -547,7 +538,6 @@ "label": "Disable Document Sharing" }, { - "collapsible": 1, "fieldname": "telemetry_section", "fieldtype": "Section Break", "label": "Telemetry" @@ -578,10 +568,8 @@ "label": "Force Web Capture Mode for Uploads" }, { - "collapsible": 1, "fieldname": "files_section", - "fieldtype": "Section Break", - "label": "Files" + "fieldtype": "Section Break" }, { "fieldname": "max_file_size", @@ -598,12 +586,60 @@ "fieldname": "allowed_file_extensions", "fieldtype": "Small Text", "label": "Allowed File Extensions" + }, + { + "fieldname": "security_tab", + "fieldtype": "Tab Break", + "label": "Login" + }, + { + "fieldname": "email_tab", + "fieldtype": "Tab Break", + "label": "Email" + }, + { + "fieldname": "files_tab", + "fieldtype": "Tab Break", + "label": "Files" + }, + { + "fieldname": "updates_tab", + "fieldtype": "Tab Break", + "label": "Updates" + }, + { + "fieldname": "backups_tab", + "fieldtype": "Tab Break", + "label": "Backups" + }, + { + "fieldname": "advanced_tab", + "fieldtype": "Tab Break", + "label": "Advanced" + }, + { + "fieldname": "password_tab", + "fieldtype": "Tab Break", + "label": "Password" + }, + { + "fieldname": "column_break_txqh", + "fieldtype": "Column Break" + }, + { + "fieldname": "login_methods_section", + "fieldtype": "Section Break", + "label": "Login Methods" + }, + { + "fieldname": "column_break_uhqk", + "fieldtype": "Column Break" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-10-17 16:12:28.145496", + "modified": "2023-11-27 14:08:01.927794", "modified_by": "Administrator", "module": "Core", "name": "System Settings", diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py index 1c64a22e54..1a548b580b 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -93,7 +93,6 @@ class SystemSettings(Document): time_zone: DF.Literal two_factor_method: DF.Literal["OTP App", "SMS", "Email"] welcome_email_template: DF.Link | None - # end: auto-generated types def validate(self): from frappe.twofactor import toggle_two_factor_auth diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index d7c333ac44..028af756df 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1227,27 +1227,31 @@ def create_contact(user, ignore_links=False, ignore_mandatory=False): contact_name = get_contact_name(user.email) if not contact_name: - contact = frappe.get_doc( - { - "doctype": "Contact", - "first_name": user.first_name, - "last_name": user.last_name, - "user": user.name, - "gender": user.gender, - } - ) + try: + contact = frappe.get_doc( + { + "doctype": "Contact", + "first_name": user.first_name, + "last_name": user.last_name, + "user": user.name, + "gender": user.gender, + } + ) - if user.email: - contact.add_email(user.email, is_primary=True) + if user.email: + contact.add_email(user.email, is_primary=True) - if user.phone: - contact.add_phone(user.phone, is_primary_phone=True) + if user.phone: + contact.add_phone(user.phone, is_primary_phone=True) - if user.mobile_no: - contact.add_phone(user.mobile_no, is_primary_mobile_no=True) - contact.insert( - ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory - ) + if user.mobile_no: + contact.add_phone(user.mobile_no, is_primary_mobile_no=True) + + contact.insert( + ignore_permissions=True, ignore_links=ignore_links, ignore_mandatory=ignore_mandatory + ) + except frappe.DuplicateEntryError: + pass else: contact = frappe.get_doc("Contact", contact_name) contact.first_name = user.first_name diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py index 526543e825..27ffb4ffb8 100644 --- a/frappe/desk/doctype/bulk_update/bulk_update.py +++ b/frappe/desk/doctype/bulk_update/bulk_update.py @@ -46,8 +46,29 @@ class BulkUpdate(Document): @frappe.whitelist() def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): - docnames = frappe.parse_json(docnames) + if isinstance(docnames, str): + docnames = frappe.parse_json(docnames) + if len(docnames) < 20: + return _bulk_action(doctype, docnames, action, data) + elif len(docnames) <= 500: + frappe.msgprint(_("Bulk operation is enqueued in background."), alert=True) + frappe.enqueue( + _bulk_action, + doctype=doctype, + docnames=docnames, + action=action, + data=data, + queue="short", + timeout=1000, + ) + else: + frappe.throw( + _("Bulk operations only support up to 500 documents."), title=_("Too Many Documents") + ) + + +def _bulk_action(doctype, docnames, action, data): if data: data = frappe.parse_json(data) @@ -85,5 +106,4 @@ def submit_cancel_or_update_docs(doctype, docnames, action="submit", data=None): def show_progress(docnames, message, i, description): n = len(docnames) - if n >= 10: - frappe.publish_progress(float(i) * 100 / n, title=message, description=description) + frappe.publish_progress(float(i) * 100 / n, title=message, description=description) diff --git a/frappe/desk/doctype/bulk_update/test_bulk_update.py b/frappe/desk/doctype/bulk_update/test_bulk_update.py new file mode 100644 index 0000000000..7611141a0a --- /dev/null +++ b/frappe/desk/doctype/bulk_update/test_bulk_update.py @@ -0,0 +1,48 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See LICENSE + +import time + +import frappe +from frappe.core.doctype.doctype.test_doctype import new_doctype +from frappe.desk.doctype.bulk_update.bulk_update import submit_cancel_or_update_docs +from frappe.tests.utils import FrappeTestCase, timeout + + +class TestBulkUpdate(FrappeTestCase): + @classmethod + def setUpClass(cls) -> None: + super().setUpClass() + cls.doctype = new_doctype(is_submittable=1, custom=1).insert().name + frappe.db.commit() + for _ in range(50): + frappe.new_doc(cls.doctype, some_fieldname=frappe.mock("name")).insert() + + @timeout() + def wait_for_assertion(self, assertion): + """Wait till an assertion becomes True""" + while True: + if assertion(): + break + time.sleep(0.2) + + def test_bulk_submit_in_background(self): + unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=5, pluck="name") + failed = submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit") + self.assertEqual(failed, []) + + def check_docstatus(docs, status): + frappe.db.rollback() + matching_docs = frappe.get_all( + self.doctype, {"docstatus": status, "name": ("in", docs)}, pluck="name" + ) + return set(matching_docs) == set(docs) + + unsubmitted = frappe.get_all(self.doctype, {"docstatus": 0}, limit=20, pluck="name") + submit_cancel_or_update_docs(self.doctype, unsubmitted, action="submit") + + self.wait_for_assertion(lambda: check_docstatus(unsubmitted, 1)) + + submitted = frappe.get_all(self.doctype, {"docstatus": 1}, limit=20, pluck="name") + submit_cancel_or_update_docs(self.doctype, submitted, action="cancel") + self.wait_for_assertion(lambda: check_docstatus(submitted, 2)) diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json index 854305ad80..e47487eaaf 100644 --- a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json +++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json @@ -102,8 +102,7 @@ "fieldname": "url", "fieldtype": "Data", "in_list_view": 1, - "label": "URL", - "options": "URL" + "label": "URL" }, { "depends_on": "eval:doc.doc_view == \"Kanban\"", @@ -116,7 +115,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2023-07-18 16:12:53.546430", + "modified": "2023-11-27 14:13:38.489737", "modified_by": "Administrator", "module": "Desk", "name": "Workspace Shortcut", diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 8d42b804cd..c9f7929b28 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -196,7 +196,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { this.abort_setup(r.message.fail); } }, - error: () => this.abort_setup("Error in setup"), + error: () => this.abort_setup(), }); } @@ -213,7 +213,11 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides { abort_setup(fail_msg) { this.$working_state.find(".state-icon-container").html(""); - fail_msg = fail_msg ? fail_msg : __("Failed to complete setup"); + fail_msg = fail_msg + ? fail_msg + : frappe.last_response.setup_wizard_failure_message + ? frappe.last_response.setup_wizard_failure_message + : __("Failed to complete setup"); this.update_setup_message("Could not start up: " + fail_msg); @@ -463,7 +467,7 @@ frappe.setup.slides_settings = [ fieldtype: "Data", options: "Email", }, - { fieldname: "password", label: __("Password"), fieldtype: "Password" }, + { fieldname: "password", label: __("Password"), fieldtype: "Password", length: 512 }, ], onload: function (slide) { diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 3a2b369a23..e3002e89a5 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -83,11 +83,14 @@ def process_setup_stages(stages, user_input, is_background_task=False): task.get("fn")(task.get("args")) except Exception: handle_setup_exception(user_input) + message = current_task.get("fail_msg") if current_task else "Failed to complete setup" + frappe.log_error(title=f"Setup failed: {message}") if not is_background_task: - return {"status": "fail", "fail": current_task.get("fail_msg")} + frappe.response["setup_wizard_failure_message"] = message + raise frappe.publish_realtime( "setup_task", - {"status": "fail", "fail_msg": current_task.get("fail_msg")}, + {"status": "fail", "fail_msg": message}, user=frappe.session.user, ) else: diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py index ade401a83c..91eac003f7 100644 --- a/frappe/model/mapper.py +++ b/frappe/model/mapper.py @@ -163,6 +163,7 @@ def get_mapped_doc( if postprocess: postprocess(source_doc, target_doc) + ret_doc.run_method("after_mapping", source_doc) ret_doc.set_onload("load_after_mapping", True) if ( diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 0d7ce13d95..cc51a55d90 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import json +from collections import defaultdict from typing import TYPE_CHECKING, Union import frappe @@ -233,17 +234,30 @@ def get_workflow_field_value(workflow_name, field): @frappe.whitelist() def bulk_workflow_approval(docnames, doctype, action): - from collections import defaultdict + docnames = json.loads(docnames) + if len(docnames) < 20: + _bulk_workflow_action(docnames, doctype, action) + elif len(docnames) <= 500: + frappe.msgprint(_("Bulk {0} is enqueued in background.").format(action), alert=True) + frappe.enqueue( + _bulk_workflow_action, + docnames=docnames, + doctype=doctype, + action=action, + queue="short", + timeout=1000, + ) + else: + frappe.throw(_("Bulk approval only support up to 500 documents."), title=_("Too Many Documents")) + + +def _bulk_workflow_action(docnames, doctype, action): # dictionaries for logging failed_transactions = defaultdict(list) successful_transactions = defaultdict(list) - # WARN: message log is cleared - print("Clearing frappe.message_log...") frappe.clear_messages() - - docnames = json.loads(docnames) for (idx, docname) in enumerate(docnames, 1): message_dict = {} try: @@ -308,7 +322,9 @@ def print_workflow_log(messages, title, doctype, indicator): html = f"