diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py index 554f4ae5f5..f5b7176e9c 100644 --- a/.github/helper/roulette.py +++ b/.github/helper/roulette.py @@ -17,39 +17,57 @@ def fetch_pr_data(pr_number, repo, endpoint=""): req = urllib.request.Request(api_url) res = urllib.request.urlopen(req) - return json.loads(res.read().decode('utf8')) + return json.loads(res.read().decode("utf8")) + def get_files_list(pr_number, repo="frappe/frappe"): return [change["filename"] for change in fetch_pr_data(pr_number, repo, "files")] + def get_output(command, shell=True): print(command) command = shlex.split(command) return subprocess.check_output(command, shell=shell, encoding="utf8").strip() + def has_skip_ci_label(pr_number, repo="frappe/frappe"): return has_label(pr_number, "Skip CI", repo) + def has_run_server_tests_label(pr_number, repo="frappe/frappe"): return has_label(pr_number, "Run Server Tests", repo) + def has_run_ui_tests_label(pr_number, repo="frappe/frappe"): return has_label(pr_number, "Run UI Tests", repo) + def has_label(pr_number, label, repo="frappe/frappe"): - return any([fetched_label["name"] for fetched_label in fetch_pr_data(pr_number, repo)["labels"] if fetched_label["name"] == label]) + return any( + [ + fetched_label["name"] + for fetched_label in fetch_pr_data(pr_number, repo)["labels"] + if fetched_label["name"] == label + ] + ) + def is_py(file): return file.endswith("py") + def is_ci(file): return ".github" in file + def is_frontend_code(file): - return file.lower().endswith((".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue", ".html")) + return file.lower().endswith( + (".css", ".scss", ".less", ".sass", ".styl", ".js", ".ts", ".vue", ".html") + ) + def is_docs(file): - regex = re.compile(r'\.(md|png|jpg|jpeg|csv|svg)$|^.github|LICENSE') + regex = re.compile(r"\.(md|png|jpg|jpeg|csv|svg)$|^.github|LICENSE") return bool(regex.search(file)) @@ -78,8 +96,13 @@ if __name__ == "__main__": only_py_changed = updated_py_file_count == len(files_list) if has_skip_ci_label(pr_number, repo): - print("Found `Skip CI` label on pr, stopping build process.") - sys.exit(0) + if build_type == "ui" and has_run_ui_tests_label(pr_number, repo): + print("Running UI tests only.") + elif build_type == "server" and has_run_server_tests_label(pr_number, repo): + print("Running server tests only.") + else: + print("Found `Skip CI` label on pr, stopping build process.") + sys.exit(0) elif ci_files_changed: print("CI related files were updated, running all build processes.") @@ -88,7 +111,11 @@ if __name__ == "__main__": print("Only docs were updated, stopping build process.") sys.exit(0) - elif only_frontend_code_changed and build_type == "server" and not has_run_server_tests_label(pr_number, repo): + elif ( + only_frontend_code_changed + and build_type == "server" + and not has_run_server_tests_label(pr_number, repo) + ): print("Only Frontend code was updated; Stopping Python build process.") sys.exit(0) diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml index 6f2d9dc7ef..bf547f1f03 100644 --- a/.github/workflows/server-mariadb-tests.yml +++ b/.github/workflows/server-mariadb-tests.yml @@ -22,8 +22,6 @@ jobs: strategy: fail-fast: false - matrix: - container: [1, 2] services: mariadb: @@ -122,7 +120,4 @@ jobs: - name: Run Tests if: ${{ steps.check-build.outputs.build == 'strawberry' }} - run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator - env: - CI_BUILD_ID: ${{ github.run_id }} - ORCHESTRATOR_URL: http://test-orchestrator.frappe.io + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml index 659fd00472..4ec193d8ba 100644 --- a/.github/workflows/server-postgres-tests.yml +++ b/.github/workflows/server-postgres-tests.yml @@ -21,8 +21,6 @@ jobs: strategy: fail-fast: false - matrix: - container: [1, 2] services: postgres: @@ -125,7 +123,4 @@ jobs: - name: Run Tests if: ${{ steps.check-build.outputs.build == 'strawberry' }} - run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --use-orchestrator - env: - CI_BUILD_ID: ${{ github.run_id }} - ORCHESTRATOR_URL: http://test-orchestrator.frappe.io + run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index f6293db18f..db215cbc7b 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - containers: [1, 2, 3] + containers: [1, 2] name: UI Tests (Cypress) @@ -105,19 +105,16 @@ jobs: id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} + key: ${{ runner.os }}-yarn-ui-${{ hashFiles('**/yarn.lock') }} restore-keys: | - ${{ runner.os }}-yarn- + ${{ runner.os }}-yarn-ui- - name: Cache cypress binary if: ${{ steps.check-build.outputs.build == 'strawberry' }} uses: actions/cache@v3 with: - path: ~/.cache - key: ${{ runner.os }}-cypress- - restore-keys: | - ${{ runner.os }}-cypress- - ${{ runner.os }}- + path: ~/.cache/Cypress + key: ${{ runner.os }}-cypress - name: Install Dependencies if: ${{ steps.check-build.outputs.build == 'strawberry' }} diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000000..f86354a06d --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,23 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + projectId: "92odwv", + adminPassword: "admin", + defaultCommandTimeout: 20000, + pageLoadTimeout: 15000, + video: true, + videoUploadOnPasses: false, + retries: { + runMode: 2, + openMode: 2, + }, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require("./cypress/plugins/index.js")(on, config); + }, + baseUrl: "http://test_site_ui:8000", + specPattern: ["./cypress/integration/*.js", "**/ui_test_*.js"], + }, +}); diff --git a/cypress.json b/cypress.json deleted file mode 100644 index 15f8f230fa..0000000000 --- a/cypress.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "baseUrl": "http://test_site_ui:8000", - "projectId": "92odwv", - "adminPassword": "admin", - "defaultCommandTimeout": 20000, - "pageLoadTimeout": 15000, - "video": true, - "videoUploadOnPasses": false, - "retries": { - "runMode": 2, - "openMode": 2 - }, - "integrationFolder": ".", - "testFiles": ["cypress/integration/*.js", "**/ui_test_*.js"] -} diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js index c8261ad043..65aa21ed69 100644 --- a/cypress/integration/control_float.js +++ b/cypress/integration/control_float.js @@ -29,6 +29,7 @@ context("Control Float", () => { }); x.values.forEach((d) => { cy.get_field("float_number", "Float").clear(); + cy.wait(200); cy.fill_field("float_number", d.input, "Float").blur(); cy.get_field("float_number", "Float").should("have.value", d.blur_expected); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 43ab5350b7..6f9460588d 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -132,9 +132,6 @@ context("Form", () => { jump_to_field("Username"); type_value("admin42"); - jump_to_field("Birth Date"); - type_value("12-31-01"); - jump_to_field("Send Welcome Email"); cy.focused().uncheck(); @@ -155,16 +152,15 @@ context("Form", () => { undo(); undo(); undo(); - undo(); - redo(); redo(); redo(); redo(); redo(); - cy.get_field("username").should("have.value", "admin24"); - cy.get_field("email").should("have.value", "admin@example.com"); - cy.get_field("birth_date").should("have.value", "12-31-2001"); // parsed value - cy.get_field("send_welcome_email").should("not.be.checked"); + cy.compare_document({ + username: "admin24", + email: "admin@example.com", + send_welcome_email: 0, + }); }); }); diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js index 7296a12666..1b7e45ac55 100644 --- a/cypress/integration/kanban.js +++ b/cypress/integration/kanban.js @@ -96,17 +96,4 @@ context("Kanban Board", () => { .first() .should("not.contain", "ID:"); }); - - // it('Drag todo', () => { - // cy.intercept({ - // method: 'POST', - // url: 'api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order_for_single_card' - // }).as('drag-completed'); - - // cy.get('.kanban-card-body') - // .contains('Test Kanban ToDo').first() - // .drag('[data-column-value="Closed"] .kanban-cards', { force: true }); - - // cy.wait('@drag-completed'); - // }); }); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 7fb0ef445c..1fed62d678 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -10,10 +10,13 @@ context("List View", () => { }); }); - it("Keep checkbox checked after Refresh", () => { + it("Keep checkbox checked after Refresh", { scrollBehavior: false }, () => { cy.go_to_list("ToDo"); cy.clear_filters(); - cy.get(".list-row-container .list-row-checkbox").click({ multiple: true, force: true }); + cy.get(".list-row-container .list-row-checkbox").click({ + multiple: true, + force: true, + }); cy.get(".actions-btn-group button").contains("Actions").should("be.visible"); cy.intercept("/api/method/frappe.desk.reportview.get").as("list-refresh"); cy.wait(3000); // wait before you hit another refresh @@ -22,7 +25,7 @@ context("List View", () => { cy.get(".list-row-container .list-row-checkbox:checked").should("be.visible"); }); - it('enables "Actions" button', () => { + it('enables "Actions" button', { scrollBehavior: false }, () => { const actions = [ "Approve", "Reject", diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index 5e66ee43f5..898fe1dec4 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -5,12 +5,14 @@ context("List View Settings", () => { }); it("Default settings", () => { cy.visit("/app/List/DocType/List"); + cy.clear_filters(); cy.get(".list-count").should("contain", "20 of"); cy.get(".list-stats").should("contain", "Tags"); }); it("disable count and sidebar stats then verify", () => { cy.wait(300); cy.visit("/app/List/DocType/List"); + cy.clear_filters(); cy.wait(300); cy.get(".list-count").should("contain", "20 of"); cy.get(".menu-btn-group button").click(); diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js deleted file mode 100644 index 3a22f49bfa..0000000000 --- a/cypress/integration/timeline_email.js +++ /dev/null @@ -1,93 +0,0 @@ -context("Timeline Email", () => { - before(() => { - cy.visit("/login"); - cy.login(); - cy.visit("/app/todo"); - }); - - it("Adding new ToDo", () => { - cy.click_listview_primary_button("Add ToDo"); - cy.get(".custom-actions:visible > .btn").contains("Edit Full Form").click({ delay: 500 }); - cy.fill_field("description", "Test ToDo", "Text Editor"); - cy.wait(500); - cy.get(".primary-action").contains("Save").click({ force: true }); - cy.wait(700); - }); - - it("Adding email and verifying timeline content for email attachment", () => { - cy.visit("/app/todo"); - cy.click_listview_row_item_with_text("Test ToDo"); - - //Creating a new email - cy.get(".timeline-actions > .timeline-item > .action-buttons > .action-btn").click(); - cy.fill_field("recipients", "test@example.com", "MultiSelect"); - cy.get( - '.modal.show > .modal-dialog > .modal-content > .modal-body > :nth-child(1) > .form-layout > .form-page > :nth-child(3) > .section-body > .form-column > form > [data-fieldtype="Text Editor"] > .form-group > .control-input-wrapper > .control-input > .ql-container > .ql-editor' - ).type("Test Mail"); - - //Adding attachment to the email - cy.get(".add-more-attachments > .btn").click(); - cy.get(".mt-2 > .btn > .mt-1").eq(2).click(); - cy.get(".input-group > .form-control").type( - "https://wallpaperplay.com/walls/full/8/2/b/72402.jpg" - ); - cy.get(".btn-primary").contains("Upload").click(); - - //Sending the email - cy.click_modal_primary_button("Send", { delay: 500 }); - - //To check if the sent mail content is shown in the timeline content - cy.get('[data-doctype="Communication"] > .timeline-content').should( - "contain", - "Test Mail" - ); - - //To check if the attachment of email is shown in the timeline content - cy.get(".timeline-content").should("contain", "Added 72402.jpg"); - - //Deleting the sent email - cy.get('[title="Open Communication"] > .icon').first().click({ force: true }); - cy.get( - "#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn" - ).click(); - cy.get( - "#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link" - ) - .eq(9) - .click(); - cy.get( - ".modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary" - ).click(); - }); - - it("Deleting attachment and ToDo", () => { - cy.visit("/app/todo"); - cy.click_listview_row_item_with_text("Test ToDo"); - - //Removing the added attachment - cy.get(".attachment-row > .data-pill > .remove-btn > .icon").click(); - cy.wait(500); - cy.get(".modal-footer:visible > .standard-actions > .btn-primary").contains("Yes").click(); - - //To check if the removed attachment is shown in the timeline content - cy.get(".timeline-content").should("contain", "Removed 72402.jpg"); - cy.wait(500); - - //To check if the discard button functionality in email is working correctly - cy.get(".timeline-actions > .timeline-item > .action-buttons > .action-btn").click(); - cy.fill_field("recipients", "test@example.com", "MultiSelect"); - cy.get(".modal-footer > .standard-actions > .btn-secondary").contains("Discard").click(); - cy.wait(500); - cy.get(".timeline-actions > .timeline-item > .action-buttons > .action-btn").click(); - cy.wait(500); - cy.get_field("recipients", "MultiSelect").should("have.text", ""); - cy.get(".modal-header:visible > .modal-actions > .btn-modal-close > .icon").click(); - - //Deleting the added ToDo - cy.get(".menu-btn-group:visible > .btn").click(); - cy.get(".menu-btn-group:visible > .dropdown-menu > li > .dropdown-item") - .contains("Delete") - .click(); - cy.get(".modal-footer:visible > .standard-actions > .btn-primary").click(); - }); -}); diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js index 5b3167b3ac..47c5424bce 100644 --- a/cypress/integration/workspace_blocks.js +++ b/cypress/integration/workspace_blocks.js @@ -68,9 +68,10 @@ context("Workspace Blocks", () => { cy.intercept({ method: "GET", - url: "api/method/frappe.desk.form.load.getdoctype", + url: "api/method/frappe.desk.form.load.getdoctype?**", }).as("get_doctype"); + cy.visit("/app/tools"); cy.get(".codex-editor__redactor .ce-block"); cy.get(".standard-actions .btn-secondary[data-label=Edit]").click(); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index cbb88cb8cb..fb1ff99678 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -281,12 +281,12 @@ Cypress.Commands.add("get_open_dialog", () => { }); Cypress.Commands.add("save", () => { - cy.intercept("/api").as("api"); + cy.intercept("/api/method/frappe.desk.form.save.savedocs").as("save_call"); cy.get(`button[data-label="Save"]:visible`).click({ scrollBehavior: false, force: true }); - cy.wait("@api"); + cy.wait("@save_call"); }); Cypress.Commands.add("hide_dialog", () => { - cy.wait(300); + cy.wait(500); cy.get_open_dialog().focus().find(".btn-modal-close").click(); cy.get(".modal:visible").should("not.exist"); }); @@ -459,3 +459,26 @@ Cypress.Commands.add("select_listview_row_checkbox", (row_no) => { Cypress.Commands.add("click_form_section", (section_name) => { cy.get(".section-head").contains(section_name).click(); }); + +const compare_document = (expected, actual) => { + for (const prop in expected) { + if (expected[prop] instanceof Array) { + // recursively compare child documents. + expected[prop].forEach((item, idx) => { + compare_document(item, actual[prop][idx]); + }); + } else { + assert.equal(expected[prop], actual[prop], `${prop} should be equal.`); + } + } +}; + +Cypress.Commands.add("compare_document", (expected_document) => { + cy.window() + .its("cur_frm") + .then((frm) => { + // Don't remove this, cypress can't magically wait for events it has no control over. + cy.wait(1000); + compare_document(expected_document, frm.doc); + }); +}); diff --git a/cypress/support/index.js b/cypress/support/e2e.js similarity index 100% rename from cypress/support/index.js rename to cypress/support/e2e.js diff --git a/esbuild/utils.js b/esbuild/utils.js index db58b89e8b..3edccfd024 100644 --- a/esbuild/utils.js +++ b/esbuild/utils.js @@ -111,15 +111,15 @@ function get_redis_subscriber(kind) { let retry_strategy; let { get_redis_subscriber: get_redis, get_conf } = require("../node_utils"); - if (process.env.CI == 1 || get_conf().developer_mode == 1) { + if (process.env.CI == 1 || get_conf().developer_mode == 0) { retry_strategy = () => {}; } else { retry_strategy = function (options) { - // abort after 10 connection attempts - if (options.attempt > 10) { + // abort after 5 x 3 connection attempts ~= 3 seconds + if (options.attempt > 4) { return undefined; } - return Math.min(options.attempt * 100, 2000); + return options.attempt * 100; }; } return get_redis(kind, { retry_strategy }); diff --git a/frappe/app.py b/frappe/app.py index 298d94b06c..3cf1bf555a 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -46,7 +46,7 @@ class RequestContext: @local_manager.middleware @Request.application -def application(request): +def application(request: Request): response = None try: diff --git a/frappe/auth.py b/frappe/auth.py index fec7ade839..7ce2b17680 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -140,6 +140,9 @@ class LoginManager: self.set_user_info() def login(self): + if frappe.get_system_settings("disable_user_pass_login"): + frappe.throw(_("Login with username and password is not allowed."), frappe.AuthenticationError) + # clear cache frappe.clear_cache(user=frappe.form_dict.get("usr")) user, pwd = get_cached_user_pass() @@ -226,14 +229,16 @@ class LoginManager: def clear_active_sessions(self): """Clear other sessions of the current user if `deny_multiple_sessions` is not set""" + if frappe.session.user == "Guest": + return + if not ( cint(frappe.conf.get("deny_multiple_sessions")) or cint(frappe.db.get_system_setting("deny_multiple_sessions")) ): return - if frappe.session.user != "Guest": - clear_sessions(frappe.session.user, keep_current=True) + clear_sessions(frappe.session.user, keep_current=True) def authenticate(self, user: str = None, pwd: str = None): from frappe.core.doctype.user.user import User diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 3658a35992..c875667c82 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -879,7 +879,7 @@ def run_ui_tests( click.secho("Installing Cypress...", fg="yellow") packages = " ".join( [ - "cypress@^6", + "cypress@^10", "cypress-file-upload@^5", "@4tw/cypress-drag-drop@^2", "cypress-real-events", diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 02a5d2d58e..985dde8ce2 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -161,12 +161,13 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): else: raise ImplicitCommitError - else: - if not frappe.flags.in_patch: - reference_doc = frappe.get_doc(reference_doctype, reference_name) - if getattr(reference_doc, "route", None): - clear_cache(reference_doc.route) + if frappe.flags.in_patch: + return + + # Clear route cache + if route := frappe.get_cached_value(reference_doctype, reference_name, "route"): + clear_cache(route) def update_comments_in_parent_after_request(): diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index 8009324e70..4a519dcaf4 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -16,6 +16,7 @@ DEFAULT_LOGTYPES_RETENTION = { "Email Queue": 30, "Error Snapshot": 30, "Scheduled Job Log": 90, + "Route History": 90, } diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index a444062b5a..0d612149a6 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -39,6 +39,7 @@ "deny_multiple_sessions", "allow_login_using_mobile_number", "allow_login_using_user_name", + "disable_user_pass_login", "allow_error_traceback", "strip_exif_metadata_from_uploaded_images", "allow_older_web_view_links", @@ -525,12 +526,19 @@ "fieldname": "email_retry_limit", "fieldtype": "Int", "label": "Email Retry Limit" + }, + { + "default": "0", + "description": "Make sure to configure a Social Login Key before disabling to prevent lockout", + "fieldname": "disable_user_pass_login", + "fieldtype": "Check", + "label": "Disable Username/Password Login" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2022-06-21 13:55:04.796152", + "modified": "2022-09-06 03:16:59.090906", "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 4bd41be974..1fc27ca114 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -43,6 +43,22 @@ class SystemSettings(Document): ): frappe.flags.update_last_reset_password_date = True + self.validate_user_pass_login() + + def validate_user_pass_login(self): + if not self.disable_user_pass_login: + return + + social_login_enabled = frappe.db.exists("Social Login Key", {"enable_social_login": 1}) + ldap_enabled = frappe.db.get_single_value("LDAP Settings", "enabled") + + if not (social_login_enabled or ldap_enabled): + frappe.throw( + _( + "Please enable atleast one Social Login Key or LDAP before disabling username/password based login." + ) + ) + def on_update(self): self.set_defaults() diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json index c1c506ae3a..9282c50e67 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]", "creation": "2021-01-02 10:51:16.579957", "docstatus": 0, "doctype": "Workspace", @@ -13,7 +13,7 @@ { "hidden": 0, "is_query_report": 0, - "label": "Modules", + "label": "Models", "link_count": 0, "link_type": "DocType", "onboard": 0, @@ -23,9 +23,9 @@ { "hidden": 0, "is_query_report": 0, - "label": "Module Def", + "label": "DocType", "link_count": 0, - "link_to": "Module Def", + "link_to": "DocType", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -34,9 +34,9 @@ { "hidden": 0, "is_query_report": 0, - "label": "Workspace", + "label": "Workflow", "link_count": 0, - "link_to": "Workspace", + "link_to": "Workflow", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -45,9 +45,19 @@ { "hidden": 0, "is_query_report": 0, - "label": "Module Onboarding", + "label": "Scripting", "link_count": 0, - "link_to": "Module Onboarding", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Server Script", + "link_count": 0, + "link_to": "Server Script", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -56,9 +66,9 @@ { "hidden": 0, "is_query_report": 0, - "label": "Block Module", + "label": "Client Script", "link_count": 0, - "link_to": "Block Module", + "link_to": "Client Script", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -67,30 +77,56 @@ { "hidden": 0, "is_query_report": 0, - "label": "Models", + "label": "Scheduled Job Type", "link_count": 0, + "link_to": "Scheduled Job Type", "link_type": "DocType", "onboard": 0, "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Packages", + "link_count": 2, + "onboard": 0, "type": "Card Break" }, { "hidden": 0, "is_query_report": 0, - "label": "DocType", + "label": "Package", "link_count": 0, - "link_to": "DocType", + "link_to": "Package", "link_type": "DocType", "onboard": 0, - "only_for": "", "type": "Link" }, { "hidden": 0, "is_query_report": 0, - "label": "Workflow", + "label": "Package Import", "link_count": 0, - "link_to": "Workflow", + "link_to": "Package Import", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Modules", + "link_count": 3, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Def", + "link_count": 0, + "link_to": "Module Def", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -99,11 +135,30 @@ { "hidden": 0, "is_query_report": 0, - "label": "Views", + "label": "Module Onboarding", "link_count": 0, + "link_to": "Module Onboarding", "link_type": "DocType", "onboard": 0, "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Profile", + "link_count": 0, + "link_to": "Module Profile", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Views", + "link_count": 4, + "onboard": 0, "type": "Card Break" }, { @@ -131,9 +186,9 @@ { "hidden": 0, "is_query_report": 0, - "label": "Workspace", + "label": "Dashboard", "link_count": 0, - "link_to": "Workspace", + "link_to": "Dashboard", "link_type": "DocType", "onboard": 0, "only_for": "", @@ -142,71 +197,67 @@ { "hidden": 0, "is_query_report": 0, - "label": "Dashboard", + "label": "Workspace", "link_count": 0, - "link_to": "Dashboard", + "link_to": "Workspace", "link_type": "DocType", "onboard": 0, - "only_for": "", "type": "Link" }, { "hidden": 0, "is_query_report": 0, - "label": "Scripting", - "link_count": 0, - "link_type": "DocType", + "label": "System Logs", + "link_count": 6, "onboard": 0, - "only_for": "", "type": "Card Break" }, { "hidden": 0, "is_query_report": 0, - "label": "Server Script", + "label": "Background Jobs", "link_count": 0, - "link_to": "Server Script", - "link_type": "DocType", + "link_to": "background_jobs", + "link_type": "Page", "onboard": 0, - "only_for": "", "type": "Link" }, { "hidden": 0, "is_query_report": 0, - "label": "Client Script", + "label": "Scheduled Jobs Logs", "link_count": 0, - "link_to": "Client Script", + "link_to": "Scheduled Job Log", "link_type": "DocType", "onboard": 0, - "only_for": "", "type": "Link" }, { "hidden": 0, "is_query_report": 0, - "label": "Scheduled Job Type", + "label": "Error Logs", "link_count": 0, - "link_to": "Scheduled Job Type", + "link_to": "Error Log", "link_type": "DocType", "onboard": 0, - "only_for": "", "type": "Link" }, { "hidden": 0, "is_query_report": 0, - "label": "Packages", - "link_count": 2, + "label": "Error Snapshot", + "link_count": 0, + "link_to": "Error Snapshot", + "link_type": "DocType", "onboard": 0, - "type": "Card Break" + "type": "Link" }, { "hidden": 0, "is_query_report": 0, - "label": "Package", + "label": "Communication Logs", "link_count": 0, - "link_to": "Package", + "link_to": "Communication", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -214,21 +265,22 @@ { "hidden": 0, "is_query_report": 0, - "label": "Package Import", + "label": "Activity Log", "link_count": 0, - "link_to": "Package Import", + "link_to": "Activity Log", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2022-01-13 17:26:02.736366", + "modified": "2022-09-02 01:48:28.029135", "modified_by": "Administrator", "module": "Core", "name": "Build", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 5.0, diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json index 5aadbc42d5..1469892bd8 100644 --- a/frappe/core/workspace/settings/settings.json +++ b/frappe/core/workspace/settings/settings.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", + "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", "creation": "2020-03-02 15:09:40.527211", "docstatus": 0, "doctype": "Workspace", @@ -224,7 +224,7 @@ { "hidden": 0, "is_query_report": 0, - "label": "Core", + "label": "Printing", "link_count": 0, "onboard": 0, "type": "Card Break" @@ -233,10 +233,10 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "System Settings", + "label": "Print Format Builder", "link_count": 0, - "link_to": "System Settings", - "link_type": "DocType", + "link_to": "print-format-builder", + "link_type": "Page", "onboard": 0, "type": "Link" }, @@ -244,9 +244,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Error Log", + "label": "Print Settings", "link_count": 0, - "link_to": "Error Log", + "link_to": "Print Settings", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -255,9 +255,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Error Snapshot", + "label": "Print Format", "link_count": 0, - "link_to": "Error Snapshot", + "link_to": "Print Format", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -266,9 +266,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Domain Settings", + "label": "Print Style", "link_count": 0, - "link_to": "Domain Settings", + "link_to": "Print Style", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -276,7 +276,7 @@ { "hidden": 0, "is_query_report": 0, - "label": "Printing", + "label": "Workflow", "link_count": 0, "onboard": 0, "type": "Card Break" @@ -285,20 +285,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Print Format Builder", - "link_count": 0, - "link_to": "print-format-builder", - "link_type": "Page", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Settings", + "label": "Workflow", "link_count": 0, - "link_to": "Print Settings", + "link_to": "Workflow", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -307,9 +296,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Print Format", + "label": "Workflow State", "link_count": 0, - "link_to": "Print Format", + "link_to": "Workflow State", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -318,9 +307,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Print Style", + "label": "Workflow Action", "link_count": 0, - "link_to": "Print Style", + "link_to": "Workflow Action", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -328,8 +317,8 @@ { "hidden": 0, "is_query_report": 0, - "label": "Workflow", - "link_count": 0, + "label": "Core", + "link_count": 2, "onboard": 0, "type": "Card Break" }, @@ -337,20 +326,9 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Workflow", - "link_count": 0, - "link_to": "Workflow", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow State", + "label": "System Settings", "link_count": 0, - "link_to": "Workflow State", + "link_to": "System Settings", "link_type": "DocType", "onboard": 0, "type": "Link" @@ -359,21 +337,22 @@ "dependencies": "", "hidden": 0, "is_query_report": 0, - "label": "Workflow Action", + "label": "Domain Settings", "link_count": 0, - "link_to": "Workflow Action", + "link_to": "Domain Settings", "link_type": "DocType", "onboard": 0, "type": "Link" } ], - "modified": "2022-01-13 17:49:59.586909", + "modified": "2022-08-28 21:41:28.065190", "modified_by": "Administrator", "module": "Core", "name": "Settings", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 29.0, diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json index 1756abcb1d..8985bf54ed 100644 --- a/frappe/custom/workspace/customization/customization.json +++ b/frappe/custom/workspace/customization/customization.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]", + "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]", "creation": "2020-03-02 15:15:03.839594", "docstatus": 0, "doctype": "Workspace", @@ -107,7 +107,7 @@ "hidden": 0, "is_query_report": 0, "label": "Other", - "link_count": 0, + "link_count": 2, "onboard": 0, "type": "Card Break" }, @@ -121,15 +121,26 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Navbar Settings", + "link_count": 0, + "link_to": "Navbar Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2022-01-13 17:28:08.345794", + "modified": "2022-08-28 20:56:24.980719", "modified_by": "Administrator", "module": "Custom", "name": "Customization", "owner": "Administrator", "parent_page": "", "public": 1, + "quick_lists": [], "restrict_to_domain": "", "roles": [], "sequence_id": 8.0, diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index a3c4fe33cc..82077cc59c 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -138,6 +138,7 @@ class Workspace(Document): def disable_saving_as_public(): return ( frappe.flags.in_install + or frappe.flags.in_uninstall or frappe.flags.in_patch or frappe.flags.in_test or frappe.flags.in_fixtures diff --git a/frappe/patches.txt b/frappe/patches.txt index e4facb7e3d..3a902cb35e 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -196,6 +196,7 @@ frappe.patches.v14_0.log_settings_migration frappe.patches.v14_0.setup_likes_from_feedback frappe.patches.v14_0.update_webforms frappe.patches.v14_0.delete_payment_gateways +frappe.patches.v14_0.event_streaming_deprecation_warning [post_model_sync] frappe.patches.v14_0.drop_data_import_legacy diff --git a/frappe/patches/v14_0/event_streaming_deprecation_warning.py b/frappe/patches/v14_0/event_streaming_deprecation_warning.py new file mode 100644 index 0000000000..b709c9c1d3 --- /dev/null +++ b/frappe/patches/v14_0/event_streaming_deprecation_warning.py @@ -0,0 +1,9 @@ +import click + + +def execute(): + click.secho( + "Event Streaming is moved to a separate app in version 15.\n" + "When upgrading to Frappe version-15, Please install the 'Event Streaming' app to continue using them: https://github.com/frappe/event_streaming", + fg="yellow", + ) diff --git a/frappe/patches/v14_0/update_multistep_webforms.py b/frappe/patches/v14_0/update_multistep_webforms.py index a4a2885c4a..9919ef6e15 100644 --- a/frappe/patches/v14_0/update_multistep_webforms.py +++ b/frappe/patches/v14_0/update_multistep_webforms.py @@ -2,11 +2,11 @@ import frappe def execute(): - frappe.reload_doctype("Web Form") + if not frappe.db.has_column("Web Form", "is_multi_step_form"): + return for web_form in frappe.get_all("Web Form", filters={"is_multi_step_form": 1}): web_form_fields = frappe.get_doc("Web Form", web_form.name).web_form_fields for web_form_field in web_form_fields: if web_form_field.fieldtype == "Section Break" and web_form_field.idx != 1: frappe.db.set_value("Web Form Field", web_form_field.name, "fieldtype", "Page Break") - frappe.db.commit() diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index 2c8c5749d8..96e3bf7227 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -94,6 +94,10 @@ frappe.views.TreeView = class TreeView { this.page.main.addClass("frappe-card"); if (this.opts.show_expand_all) { + this.page.add_inner_button(__("Collapse All"), function () { + me.tree.load_children(me.tree.root_node, false); + }); + this.page.add_inner_button(__("Expand All"), function () { me.tree.load_children(me.tree.root_node, true); }); diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js index 88ff21caa7..f5fdab24ab 100644 --- a/frappe/public/js/frappe/views/workspace/workspace.js +++ b/frappe/public/js/frappe/views/workspace/workspace.js @@ -117,6 +117,7 @@ frappe.views.Workspace = class Workspace { (page) => page.parent_page == "" || page.parent_page == null ); } + root_pages = root_pages.uniqBy((d) => d.title); this.build_sidebar_section(category, root_pages); }); diff --git a/frappe/public/scss/desk/report.scss b/frappe/public/scss/desk/report.scss index 8ed0fb740c..a2c940dc58 100644 --- a/frappe/public/scss/desk/report.scss +++ b/frappe/public/scss/desk/report.scss @@ -196,8 +196,8 @@ &.blue { color: var(--blue-500); } // SIZE & SPACING - margin-top: 12px; - margin-bottom: 5px; + padding-top: 12px; + padding-bottom: 5px; // LAYOUT text-align: center; diff --git a/frappe/tests/test_auth.py b/frappe/tests/test_auth.py index 4378d75484..403d0cf34c 100644 --- a/frappe/tests/test_auth.py +++ b/frappe/tests/test_auth.py @@ -98,6 +98,7 @@ class TestAuth(FrappeTestCase): def test_deny_multiple_login(self): self.set_system_settings("deny_multiple_sessions", 1) + self.addCleanup(self.set_system_settings, "deny_multiple_sessions", 0) first_login = FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password) first_login.get_list("ToDo") @@ -114,6 +115,14 @@ class TestAuth(FrappeTestCase): second_login.get_list("ToDo") third_login.get_list("ToDo") + def test_disable_user_pass_login(self): + FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password).get_list("ToDo") + self.set_system_settings("disable_user_pass_login", 1) + self.addCleanup(self.set_system_settings, "disable_user_pass_login", 0) + + with self.assertRaises(Exception): + FrappeClient(self.HOST_NAME, self.test_user_email, self.test_user_password).get_list("ToDo") + class TestLoginAttemptTracker(FrappeTestCase): def test_account_lock(self): diff --git a/frappe/tests/test_perf.py b/frappe/tests/test_perf.py new file mode 100644 index 0000000000..6e23fb9856 --- /dev/null +++ b/frappe/tests/test_perf.py @@ -0,0 +1,64 @@ +""" +This file contains multiple primitive tests for avoiding performance regressions. + +- Time bound tests: Benchmarks are done on GHA before adding numbers +- Query count tests: More than expected # of queries for any action is frequent source of + performance issues. This guards against such problems. + + +E.g. We know get_controller is supposed to be cached and hence shouldn't make query post first +query. This test can be written like this. + +>>> def test_controller_caching(self): +>>> +>>> get_controller("User") # <- "warm up code" +>>> with self.assertQueryCount(0): +>>> get_controller("User") + +""" +import unittest + +import frappe +from frappe.model.base_document import get_controller +from frappe.tests.utils import FrappeTestCase +from frappe.website.path_resolver import PathResolver + + +class TestPerformance(FrappeTestCase): + def reset_request_specific_caches(self): + # To simulate close to request level of handling + frappe.destroy() # releases everything on frappe.local + frappe.init(site=self.TEST_SITE) + frappe.connect() + frappe.clear_cache() + + def setUp(self) -> None: + self.reset_request_specific_caches() + + def test_meta_caching(self): + frappe.get_meta("User") + + with self.assertQueryCount(0): + frappe.get_meta("User") + + def test_controller_caching(self): + + get_controller("User") + with self.assertQueryCount(0): + get_controller("User") + + def test_db_value_cache(self): + """Link validation if repeated should just use db.value_cache, hence no extra queries""" + doc = frappe.get_last_doc("User") + doc.get_invalid_links() + + with self.assertQueryCount(0): + doc.get_invalid_links() + + @unittest.skip("Not implemented") + def test_homepage_resolver(self): + paths = ["/", "/app"] + for path in paths: + PathResolver(path).resolve() + with self.assertQueryCount(1): + PathResolver(path).resolve() diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index f9be638385..39a23f0468 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -38,8 +38,7 @@ def create_if_not_exists(doc): @frappe.whitelist() def create_todo_records(): - if frappe.get_all("ToDo", {"description": "this is first todo"}): - return + frappe.db.truncate("ToDo") frappe.get_doc( {"doctype": "ToDo", "date": add_to_date(now(), days=7), "description": "this is first todo"} diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 79dfd76238..aa27a5eb01 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -19,10 +19,13 @@ class FrappeTestCase(unittest.TestCase): otherwise this class will become ineffective. """ + TEST_SITE = "test_site" + SHOW_TRANSACTION_COMMIT_WARNINGS = False @classmethod def setUpClass(cls) -> None: + cls.TEST_SITE = getattr(frappe.local, "site", None) or cls.TEST_SITE # flush changes done so far to avoid flake frappe.db.commit() frappe.db.begin() @@ -64,6 +67,21 @@ class FrappeTestCase(unittest.TestCase): else: self.assertEqual(expected, actual, msg=msg) + @contextmanager + def assertQueryCount(self, count): + def _sql_with_count(*args, **kwargs): + frappe.db.sql_query_count += 1 + return orig_sql(*args, **kwargs) + + try: + orig_sql = frappe.db.sql + frappe.db.sql_query_count = 0 + frappe.db.sql = _sql_with_count + yield + self.assertLessEqual(frappe.db.sql_query_count, count) + finally: + frappe.db.sql = orig_sql + def _commit_watcher(): import traceback diff --git a/frappe/utils/install.py b/frappe/utils/install.py index fcf8f9d436..a5f46dc555 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -289,12 +289,10 @@ def add_standard_navbar_items(): "is_standard": 1, }, { - "item_label": "Background Jobs", - "item_type": "Route", - "route": "/app/background_jobs", + "item_type": "Separator", "is_standard": 1, + "item_label": "", }, - {"item_type": "Separator", "is_standard": 1}, { "item_label": "Log out", "item_type": "Action", diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 6fdfba19e3..65967a47ba 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -167,6 +167,7 @@ def get_safe_globals(): rollback=frappe.db.rollback, add_index=frappe.db.add_index, ), + lang=getattr(frappe.local, "lang", "en"), ), FrappeClient=FrappeClient, style=frappe._dict(border_color="#d1d8dd"), diff --git a/frappe/www/login.html b/frappe/www/login.html index 1aaaf85656..3e16088e90 100644 --- a/frappe/www/login.html +++ b/frappe/www/login.html @@ -1,6 +1,7 @@ {% extends "templates/web.html" %} {% macro email_login_body() -%} +{% if not disable_user_pass_login or (ldap_settings and ldap_settings.enabled) %}
@@ -38,13 +39,15 @@

- {{ _("Forgot Password?") }}

+ {{ _("Forgot Password?") }} +

- +{% endif %}
+ {% if not disable_user_pass_login %} - + {% endif %} {% if ldap_settings and ldap_settings.enabled %} @@ -83,7 +86,9 @@ {{ email_login_body() }}
+ {% if not disable_user_pass_login or (ldap_settings and ldap_settings.enabled) %} + {% endif %}