diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 71589ea1e839..000000000000 --- a/.flake8 +++ /dev/null @@ -1,76 +0,0 @@ -[flake8] -ignore = - B001, - B007, - B009, - B010, - B950, - E101, - E111, - E114, - E116, - E117, - E121, - E122, - E123, - E124, - E125, - E126, - E127, - E128, - E131, - E201, - E202, - E203, - E211, - E221, - E222, - E223, - E224, - E225, - E226, - E228, - E231, - E241, - E242, - E251, - E261, - E262, - E265, - E266, - E271, - E272, - E273, - E274, - E301, - E302, - E303, - E305, - E306, - E402, - E501, - E502, - E701, - E702, - E703, - E741, - F401, - F403, - F405, - W191, - W291, - W292, - W293, - W391, - W503, - W504, - E711, - E129, - F841, - E713, - E712, - B028, - W604, - -max-line-length = 200 -exclude=,test_*.py diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py index 8156137e3f2b..ac7151c51216 100644 --- a/.github/helper/documentation.py +++ b/.github/helper/documentation.py @@ -15,13 +15,14 @@ def uri_validator(x): result = urlparse(x) return all([result.scheme, result.netloc, result.path]) + def docs_link_exists(body): for line in body.splitlines(): for word in line.split(): - if word.startswith('http') and uri_validator(word): + if word.startswith("http") and uri_validator(word): parsed_url = urlparse(word) if parsed_url.netloc == "github.com": - parts = parsed_url.path.split('/') + parts = parsed_url.path.split("/") if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos: return True if parsed_url.netloc in ["docs.erpnext.com", "frappeframework.com"]: diff --git a/.github/helper/roulette.py b/.github/helper/roulette.py index fc9c71a858a6..0b7e4ae9b26b 100644 --- a/.github/helper/roulette.py +++ b/.github/helper/roulette.py @@ -6,11 +6,11 @@ import sys import time import urllib.request -from functools import lru_cache +from functools import cache, lru_cache from urllib.error import HTTPError -@lru_cache(maxsize=None) +@cache def fetch_pr_data(pr_number, repo, endpoint=""): api_url = f"https://api.github.com/repos/{repo}/pulls/{pr_number}" @@ -82,9 +82,7 @@ def is_ci(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): diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 72f661d3e15c..96a319f97edc 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,9 @@ import sys errors_encounter = 0 -pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +pattern = re.compile( + r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)" +) words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") @@ -10,44 +12,50 @@ # skip first argument files = sys.argv[1:] -files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))] +files_to_scan = [_file for _file in files if _file.endswith((".py", ".js"))] for _file in files_to_scan: - with open(_file, 'r') as f: - print(f'Checking: {_file}') + with open(_file) as f: + print(f"Checking: {_file}") file_lines = f.readlines() for line_number, line in enumerate(file_lines, 1): - if 'frappe-lint: disable-translate' in line: + if "frappe-lint: disable-translate" in line: continue if start_matches := start_pattern.search(line): if starts_with_f := starts_with_f_pattern.search(line): if has_f_string := f_string_pattern.search(line): errors_encounter += 1 - print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}') + print( + f"\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}" + ) continue match = pattern.search(line) error_found = False - if not match and line.endswith((',\n', '[\n')): + if not match and line.endswith((",\n", "[\n")): # concat remaining text to validate multiline pattern - line = "".join(file_lines[line_number - 1:]) - line = line[start_matches.start() + 1:] + line = "".join(file_lines[line_number - 1 :]) + line = line[start_matches.start() + 1 :] match = pattern.match(line) if not match: error_found = True - print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}') + print(f"\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}") if not error_found and not words_pattern.search(line): error_found = True - print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}') + print( + f"\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}" + ) if error_found: errors_encounter += 1 if errors_encounter > 0: - print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.') + print( + '\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.' + ) sys.exit(1) else: - print('\nGood To Go!') + print("\nGood To Go!") diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 535e71f2efe8..09db6a251dc4 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -53,7 +53,7 @@ jobs: python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER linter: - name: 'Frappe Linter' + name: 'Semgrep Rules' runs-on: ubuntu-latest if: github.event_name == 'pull_request' @@ -62,7 +62,7 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' - - uses: pre-commit/action@v3.0.0 + cache: pip - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 000000000000..32ececc78aad --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,26 @@ +name: Pre-commit + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: precommit-frappe-${{ github.event_name }}-${{ github.event.number }} + cancel-in-progress: true + +jobs: + linter: + name: 'precommit' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: pip + - uses: pre-commit/action@v3.0.0 diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0911dc7be113..34e745f34807 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -21,8 +21,6 @@ jobs: strategy: fail-fast: false - matrix: - containers: [1, 2] name: UI Tests (Cypress) @@ -155,7 +153,7 @@ jobs: if: ${{ steps.check-build.outputs.build == 'strawberry' }} run: | cd ~/frappe-bench/ - bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID-$GITHUB_RUN_ATTEMPT -- --record + bench --site test_site run-ui-tests frappe --headless -- --record env: CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 067de0f6640c..fc037dda3e79 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,7 @@ -exclude: 'node_modules|.git' +exclude: "node_modules|.git" default_stages: [commit] fail_fast: false - repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.3.0 @@ -12,7 +11,7 @@ repos: exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" - id: check-yaml - id: no-commit-to-branch - args: ['--branch', 'develop'] + args: ["--branch", "develop"] - id: check-merge-conflict - id: check-ast - id: check-json @@ -20,18 +19,12 @@ repos: - id: check-yaml - id: debug-statements - - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 - hooks: - - id: pyupgrade - args: ['--py310-plus'] - - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.2.0 hooks: - id: ruff - name: "Sort Python imports" - args: ["--select", "I", "--fix"] + name: "Run ruff linter and apply fixes" + args: ["--fix"] - id: ruff-format name: "Format Python code" @@ -43,24 +36,17 @@ repos: types_or: [javascript, vue, scss] # Ignore any files that might contain jinja / bundles exclude: | - (?x)^( - frappe/public/dist/.*| - .*node_modules.*| - .*boilerplate.*| - frappe/www/website_script.js| - frappe/templates/includes/.*| - frappe/public/js/lib/.*| - frappe/website/doctype/website_theme/website_theme_template.scss - )$ - - - - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 - hooks: - - id: flake8 - additional_dependencies: ['flake8-bugbear',] + (?x)^( + frappe/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + frappe/www/website_script.js| + frappe/templates/includes/.*| + frappe/public/js/lib/.*| + frappe/website/doctype/website_theme/website_theme_template.scss + )$ ci: - autoupdate_schedule: weekly - skip: [] - submodules: false + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/cypress.config.js b/cypress.config.js index 30eb1458ba9b..26b6f7a45d36 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -8,9 +8,11 @@ module.exports = defineConfig({ pageLoadTimeout: 15000, video: true, videoUploadOnPasses: false, + viewportWidth: 1920, + viewportHeight: 1200, retries: { - runMode: 2, - openMode: 2, + runMode: 1, + openMode: 1, }, e2e: { // We've imported your old cypress plugins here. diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js deleted file mode 100644 index ba65454ef6a3..000000000000 --- a/cypress/integration/folder_navigation.js +++ /dev/null @@ -1,96 +0,0 @@ -context("Folder Navigation", () => { - before(() => { - cy.visit("/login"); - cy.login(); - cy.visit("/app/file"); - }); - - it("Adding Folders", () => { - //Adding filter to go into the home folder - cy.get(".filter-x-button").click(); - cy.click_filter_button(); - cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click(); - cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}"); - cy.get( - ".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback" - ).type("Home{enter}"); - cy.get(".filter-action-buttons > div > .btn-primary").findByText("Apply Filters").click(); - - //Adding folder (Test Folder) - cy.click_menu_button("New Folder"); - cy.fill_field("value", "Test Folder"); - cy.click_modal_primary_button("Create"); - }); - - it("Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct", () => { - //Navigating inside the Attachments folder - cy.wait(500); - cy.get('[title="Attachments"] > span').click(); - - //To check if the URL formed after visiting the attachments folder is correct - cy.location("pathname").should("eq", "/app/file/view/home/Attachments"); - cy.visit("/app/file/view/home/Attachments"); - - //Adding folder inside the attachments folder - cy.click_menu_button("New Folder"); - cy.fill_field("value", "Test Folder"); - cy.click_modal_primary_button("Create"); - - //Navigating inside the added folder in the Attachments folder - cy.wait(500); - cy.get('[title="Test Folder"] > span').click(); - - //To check if the URL is correct after visiting the Test Folder - cy.location("pathname").should("eq", "/app/file/view/home/Attachments/Test%20Folder"); - cy.visit("/app/file/view/home/Attachments/Test%20Folder"); - - //Adding a file inside the Test Folder - cy.findByRole("button", { name: "Add File" }).eq(0).click({ force: true }); - cy.get(".file-uploader").findByText("Link").click(); - cy.get(".input-group > input.form-control:visible").as("upload_input"); - cy.get("@upload_input").type("https://wallpaperplay.com/walls/full/8/2/b/72402.jpg", { - waitForAnimations: false, - parseSpecialCharSequences: false, - force: true, - delay: 100, - }); - cy.click_modal_primary_button("Upload"); - - //To check if the added file is present in the Test Folder - cy.visit("/app/file/view/home/Attachments"); - cy.wait(500); - cy.get("span.level-item > a > span").should("contain", "Test Folder"); - cy.visit("/app/file/view/home/Attachments/Test%20Folder"); - - cy.wait(500); - cy.get(".list-row-container").eq(0).should("contain.text", "72402.jpg"); - cy.get(".list-row-checkbox").eq(0).click(); - - cy.intercept({ - method: "POST", - url: "api/method/frappe.desk.reportview.delete_items", - }).as("file_deleted"); - - //Deleting the added file from the Test folder - cy.click_action_button("Delete"); - cy.click_modal_primary_button("Yes"); - cy.wait("@file_deleted"); - - //Deleting the Test Folder - cy.visit("/app/file/view/home/Attachments"); - cy.get(".list-row-checkbox").eq(0).click(); - cy.click_action_button("Delete"); - cy.click_modal_primary_button("Yes"); - cy.wait("@file_deleted"); - }); - - it("Deleting Test Folder from the home", () => { - //Deleting the Test Folder added in the home directory - cy.visit("/app/file/view/home"); - cy.get(".level-left > .list-subject > .file-select >.list-row-checkbox") - .eq(0) - .click({ force: true, delay: 500 }); - cy.click_action_button("Delete"); - cy.click_modal_primary_button("Yes"); - }); -}); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 6f9460588dd0..9845ee15f8f2 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -15,6 +15,7 @@ context("Form", () => { cy.get_field("description", "Text Editor") .type("this is a test todo", { force: true }) .wait(200); + cy.wait(1000); cy.get(".page-title").should("contain", "Not Saved"); cy.intercept({ method: "POST", diff --git a/cypress/integration/grid.js b/cypress/integration/grid.js index 6cf9e8cdc7b7..ae9b59f86856 100644 --- a/cypress/integration/grid.js +++ b/cypress/integration/grid.js @@ -24,14 +24,14 @@ context("Grid", () => { let field = frm.get_field("phone_nos"); field.grid.update_docfield_property("is_primary_phone", "hidden", true); - cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="1"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get("@table-form") .find('.frappe-control[data-fieldname="is_primary_phone"]') .should("be.hidden"); cy.get("@table-form").find(".grid-footer-toolbar").click(); - cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="2"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get("@table-form") .find('.frappe-control[data-fieldname="is_primary_phone"]') @@ -48,14 +48,14 @@ context("Grid", () => { let field = frm.get_field("phone_nos"); field.grid.toggle_display("is_primary_mobile_no", false); - cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="1"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get("@table-form") .find('.frappe-control[data-fieldname="is_primary_mobile_no"]') .should("be.hidden"); cy.get("@table-form").find(".grid-footer-toolbar").click(); - cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="2"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get("@table-form") .find('.frappe-control[data-fieldname="is_primary_mobile_no"]') @@ -72,14 +72,14 @@ context("Grid", () => { let field = frm.get_field("phone_nos"); field.grid.toggle_enable("phone", false); - cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="1"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get("@table-form") .find('.frappe-control[data-fieldname="phone"] .control-value') .should("have.class", "like-disabled-input"); cy.get("@table-form").find(".grid-footer-toolbar").click(); - cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="2"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get("@table-form") .find('.frappe-control[data-fieldname="phone"] .control-value') @@ -96,14 +96,14 @@ context("Grid", () => { let field = frm.get_field("phone_nos"); field.grid.toggle_reqd("phone", false); - cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="1"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get_field("phone").as("phone-field"); cy.get("@phone-field").focus().clear().wait(500).blur(); cy.get("@phone-field").should("not.have.class", "has-error"); cy.get("@table-form").find(".grid-footer-toolbar").click(); - cy.get("@table").find('[data-idx="2"] .edit-grid-row').click(); + cy.get("@table").find('[data-idx="2"] .btn-open-row').click(); cy.get(".grid-row-open").as("table-form"); cy.get_field("phone").as("phone-field"); cy.get("@phone-field").focus().clear().wait(500).blur(); diff --git a/cypress/integration/grid_configuration.js b/cypress/integration/grid_configuration.js deleted file mode 100644 index 9112d7023e06..000000000000 --- a/cypress/integration/grid_configuration.js +++ /dev/null @@ -1,23 +0,0 @@ -context("Grid Configuration", () => { - beforeEach(() => { - cy.login(); - cy.visit("/app/doctype/User"); - }); - it("Set user wise grid settings", () => { - cy.wait(100); - cy.get('.frappe-control[data-fieldname="fields"]').as("table"); - cy.get("@table").find(".icon-sm").click(); - cy.wait(100); - cy.get('.frappe-control[data-fieldname="fields_html"]').as("modal"); - cy.get("@modal").find(".add-new-fields").click(); - cy.wait(100); - cy.get('[type="checkbox"][data-unit="read_only"]').check(); - cy.findByRole("button", { name: "Add" }).click(); - cy.wait(100); - cy.get('[data-fieldname="options"]').invoke("attr", "value", "1"); - cy.get('.form-control.column-width[data-fieldname="options"]').trigger("change"); - cy.findByRole("button", { name: "Update" }).click(); - cy.wait(200); - cy.get('[title="Read Only"').should("be.visible"); - }); -}); diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js deleted file mode 100644 index f14c991c7c52..000000000000 --- a/cypress/integration/kanban.js +++ /dev/null @@ -1,134 +0,0 @@ -context("Kanban Board", () => { - before(() => { - cy.login("frappe@example.com"); - cy.visit("/app"); - }); - - it("Create ToDo Kanban", () => { - cy.visit("/app/todo"); - - cy.get(".page-actions .custom-btn-group button").click(); - cy.get(".page-actions .custom-btn-group ul.dropdown-menu li").contains("Kanban").click(); - - cy.focused().blur(); - cy.fill_field("board_name", "ToDo Kanban", "Data"); - cy.fill_field("field_name", "Status", "Select"); - cy.click_modal_primary_button("Save"); - - cy.get(".title-text").should("contain", "ToDo Kanban"); - }); - - it("Create ToDo from kanban", () => { - cy.intercept({ - method: "POST", - url: "api/method/frappe.client.save", - }).as("save-todo"); - - cy.click_listview_primary_button("Add ToDo"); - - cy.fill_field("description", "Test Kanban ToDo", "Text Editor").wait(300); - cy.get(".modal-footer .btn-primary").last().click(); - - cy.wait("@save-todo"); - }); - - it("Add and Remove fields", () => { - cy.visit("/app/todo/view/kanban/ToDo Kanban"); - - cy.intercept( - "POST", - "/api/method/frappe.desk.doctype.kanban_board.kanban_board.save_settings" - ).as("save-kanban"); - cy.intercept( - "POST", - "/api/method/frappe.desk.doctype.kanban_board.kanban_board.update_order" - ).as("update-order"); - - cy.get(".page-actions .menu-btn-group > .btn").click(); - cy.get(".page-actions .menu-btn-group .dropdown-menu li") - .contains("Kanban Settings") - .click(); - cy.get(".add-new-fields").click(); - - cy.get(".checkbox-options .checkbox").contains("ID").click(); - cy.get(".checkbox-options .checkbox").contains("Status").first().click(); - cy.get(".checkbox-options .checkbox").contains("Priority").click(); - - cy.get(".modal-footer .btn-primary").last().click(); - - cy.get(".frappe-control .label-area").contains("Show Labels").click(); - cy.click_modal_primary_button("Save"); - - cy.wait("@save-kanban"); - - cy.get('.kanban-column[data-column-value="Open"] .kanban-cards').as("open-cards"); - cy.get("@open-cards") - .find(".kanban-card .kanban-card-doc") - .first() - .should("contain", "ID:"); - cy.get("@open-cards") - .find(".kanban-card .kanban-card-doc") - .first() - .should("contain", "Status:"); - cy.get("@open-cards") - .find(".kanban-card .kanban-card-doc") - .first() - .should("contain", "Priority:"); - - cy.get(".page-actions .menu-btn-group > .btn").click(); - cy.get(".page-actions .menu-btn-group .dropdown-menu li") - .contains("Kanban Settings") - .click(); - cy.get_open_dialog() - .find( - '.frappe-control[data-fieldname="fields_html"] div[data-label="ID"] .remove-field' - ) - .click(); - - cy.wait("@update-order"); - cy.get_open_dialog().find(".frappe-control .label-area").contains("Show Labels").click(); - cy.get(".modal-footer .btn-primary").last().click(); - - cy.wait("@save-kanban"); - - cy.get("@open-cards") - .find(".kanban-card .kanban-card-doc") - .first() - .should("not.contain", "ID:"); - }); - - it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => { - cy.switch_to_user("Administrator"); - - const noSystemManager = "nosysmanager@example.com"; - cy.call("frappe.tests.ui_test_helpers.create_test_user", { - username: noSystemManager, - }); - cy.remove_role(noSystemManager, "System Manager"); - cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" }); - cy.call("frappe.tests.ui_test_helpers.create_admin_kanban"); - - cy.switch_to_user(noSystemManager); - - cy.visit("/app/todo/view/kanban/Admin Kanban"); - - // Menu button should be hidden (dropdown for 'Save Filters' and 'Delete Kanban Board') - cy.get(".no-list-sidebar .menu-btn-group .btn-default[data-original-title='Menu']").should( - "have.length", - 0 - ); - // Kanban Columns should be visible (read-only) - cy.get(".kanban .kanban-column").should("have.length", 2); - // User should be able to add card (has access to ToDo) - cy.get(".kanban .add-card").should("have.length", 2); - // Column actions should be hidden (dropdown for 'Archive' and indicators) - cy.get(".kanban .column-options").should("have.length", 0); - - cy.switch_to_user("Administrator"); - cy.call("frappe.client.delete", { doctype: "User", name: noSystemManager }); - }); - - after(() => { - cy.call("logout"); - }); -}); diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js index 5195d0b3ae4b..e746eb22ede6 100644 --- a/cypress/integration/list_paging.js +++ b/cypress/integration/list_paging.js @@ -37,6 +37,6 @@ context("List Paging", () => { cy.get(".list-paging-area .list-count").should("contain.text", "500 of"); cy.get(".list-paging-area .btn-more").click(); - cy.get(".list-paging-area .list-count").should("contain.text", "1000 of"); + cy.get(".list-paging-area .list-count").should("contain.text", "1,000 of"); }); }); diff --git a/cypress/integration/multi_select_dialog.js b/cypress/integration/multi_select_dialog.js deleted file mode 100644 index 1be56d3b3d01..000000000000 --- a/cypress/integration/multi_select_dialog.js +++ /dev/null @@ -1,102 +0,0 @@ -context("MultiSelectDialog", () => { - before(() => { - cy.login(); - cy.visit("/app"); - const contact_template = { - doctype: "Contact", - first_name: "Test", - status: "Passive", - email_ids: [ - { - doctype: "Contact Email", - email_id: "test@example.com", - is_primary: 0, - }, - ], - }; - const promises = Array.from({ length: 25 }).map(() => - cy.insert_doc("Contact", contact_template, true) - ); - Promise.all(promises); - }); - - function open_multi_select_dialog() { - cy.window() - .its("frappe") - .then((frappe) => { - new frappe.ui.form.MultiSelectDialog({ - doctype: "Contact", - target: {}, - setters: { - status: null, - gender: null, - }, - add_filters_group: 1, - allow_child_item_selection: 1, - child_fieldname: "email_ids", - child_columns: ["email_id", "is_primary"], - }); - }); - } - - it("checks multi select dialog api works", () => { - open_multi_select_dialog(); - cy.get_open_dialog().should("contain", "Select Contacts"); - }); - - it("checks for filters", () => { - ["search_term", "status", "gender"].forEach((fieldname) => { - cy.get_open_dialog() - .get(`.frappe-control[data-fieldname="${fieldname}"]`) - .should("exist"); - }); - - // add_filters_group: 1 should add a filter group - cy.get_open_dialog().get(`.frappe-control[data-fieldname="filter_area"]`).should("exist"); - }); - - it("checks for child item selection", () => { - cy.get_open_dialog().get(`.dt-row-header`).should("not.exist"); - - cy.get_open_dialog() - .get(`.frappe-control[data-fieldname="allow_child_item_selection"]`) - .find('input[data-fieldname="allow_child_item_selection"]') - .should("exist") - .click({ force: true }); - - cy.get_open_dialog() - .get(`.frappe-control[data-fieldname="child_selection_area"]`) - .should("exist"); - - cy.get_open_dialog().get(`.dt-row-header`).should("contain", "Contact"); - - cy.get_open_dialog().get(`.dt-row-header`).should("contain", "Email Id"); - - cy.get_open_dialog().get(`.dt-row-header`).should("contain", "Is Primary"); - }); - - it("tests more button", () => { - cy.get_open_dialog() - .get(`.frappe-control[data-fieldname="more_child_btn"]`) - .should("exist") - .as("more-btn"); - - cy.get_open_dialog() - .get(".datatable .dt-scrollable .dt-row") - .should(($rows) => { - expect($rows).to.have.length(20); - }); - - cy.intercept("POST", "api/method/frappe.client.get_list").as("get-more-records"); - cy.get("@more-btn").find("button").click({ force: true }); - cy.wait("@get-more-records"); - - cy.get_open_dialog() - .get(".datatable .dt-scrollable .dt-row") - .should(($rows) => { - if ($rows.length <= 20) { - throw new Error("More button doesn't work"); - } - }); - }); -}); diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js deleted file mode 100644 index 7582b046f2d7..000000000000 --- a/cypress/integration/sidebar.js +++ /dev/null @@ -1,137 +0,0 @@ -const verify_attachment_visibility = (document, is_private) => { - cy.visit(`/app/${document}`); - - const assertion = is_private ? "be.checked" : "not.be.checked"; - cy.findByRole("button", { name: "Attach File" }).click(); - - cy.get_open_dialog() - .find(".file-upload-area") - .selectFile("cypress/fixtures/sample_image.jpg", { - action: "drag-drop", - }); - - cy.get_open_dialog().findByRole("checkbox", { name: "Private" }).should(assertion); -}; - -const attach_file = (file, no_of_files = 1) => { - let files = []; - if (file) { - files = [file]; - } else if (no_of_files > 1) { - // attach n files - files = [...Array(no_of_files)].map( - (el, idx) => - "cypress/fixtures/sample_attachments/attachment-" + - (idx + 1) + - (idx == 0 ? ".jpg" : ".txt") - ); - } - - cy.findByRole("button", { name: "Attach File" }).click(); - cy.get_open_dialog().find(".file-upload-area").selectFile(files, { - action: "drag-drop", - }); - cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); -}; - -context("Sidebar", () => { - before(() => { - cy.visit("/"); - cy.login(); - cy.visit("/app"); - return cy - .window() - .its("frappe") - .then((frappe) => { - return frappe.call("frappe.tests.ui_test_helpers.create_blog_post"); - }); - }); - - it("Verify attachment visibility config", () => { - verify_attachment_visibility("doctype/Blog Post", true); - verify_attachment_visibility("blog-post/test-blog-attachment-post", false); - }); - - it("Verify attachment accessibility UX", () => { - cy.call("frappe.tests.ui_test_helpers.create_todo_with_attachment_limit", { - description: "Sidebar Attachment Access Test ToDo", - }).then((todo) => { - cy.visit(`/app/todo/${todo.message.name}`); - - // explore icon btn should be hidden as there are no attachments - cy.get(".explore-btn").should("be.hidden"); - - attach_file("cypress/fixtures/sample_image.jpg"); - cy.get(".explore-btn").should("be.visible"); - cy.get(".show-all-btn").should("be.hidden"); - - // attach 10 images - attach_file(null, 10); - cy.get(".show-all-btn").should("be.visible"); - - // attach 1 more image to reach attachment limit - attach_file("cypress/fixtures/sample_attachments/attachment-11.txt"); - cy.get(".explore-full-btn").should("be.visible"); - cy.get(".attachments-actions").should("be.hidden"); - cy.get(".explore-btn").should("be.hidden"); - - // test "Show All" button - cy.get(".attachment-row").should("have.length", 10); - cy.get(".show-all-btn").click(); - cy.get(".attachment-row").should("have.length", 12); - }); - }); - - it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { - cy.visit("/app/doctype"); - cy.click_sidebar_button("Assigned To"); - - //To check if no filter is available in "Assigned To" dropdown - cy.get(".empty-state").should("contain", "No filters found"); - - //Assigning a doctype to a user - cy.visit("/app/doctype/ToDo"); - cy.get(".form-assignments > .flex > .text-muted").click(); - cy.get_field("assign_to_me", "Check").click(); - cy.get(".modal-footer > .standard-actions > .btn-primary").click(); - cy.visit("/app/doctype"); - cy.click_sidebar_button("Assigned To"); - - //To check if filter is added in "Assigned To" dropdown after assignment - cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").should( - "contain", - "1" - ); - - //To check if there is no filter added to the listview - cy.get(".filter-button").should("contain", "Filter"); - - //To add a filter to display data into the listview - cy.get(".group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item").click(); - - //To check if filter is applied - cy.click_filter_button().should("contain", "1 filter"); - cy.get(".fieldname-select-area > .awesomplete > .form-control").should( - "have.value", - "Assigned To" - ); - cy.get(".condition").should("have.value", "like"); - cy.get(".filter-field > .form-group > .input-with-feedback").should( - "have.value", - `%${cy.config("testUser")}%` - ); - cy.click_filter_button(); - - //To remove the applied filter - cy.clear_filters(); - - //To remove the assignment - cy.visit("/app/doctype/ToDo"); - cy.get(".assignments > .avatar-group > .avatar > .avatar-frame").click(); - cy.get(".remove-btn").click({ force: true }); - cy.hide_dialog(); - cy.visit("/app/doctype"); - cy.click_sidebar_button("Assigned To"); - cy.get(".empty-state").should("contain", "No filters found"); - }); -}); diff --git a/cypress/integration/theme_switcher_dialog.js b/cypress/integration/theme_switcher_dialog.js deleted file mode 100644 index 158ff3e244ce..000000000000 --- a/cypress/integration/theme_switcher_dialog.js +++ /dev/null @@ -1,29 +0,0 @@ -context("Theme Switcher Shortcut", () => { - before(() => { - cy.login(); - cy.visit("/app"); - }); - beforeEach(() => { - cy.reload(); - }); - it("Check Toggle", () => { - cy.open_theme_dialog("{ctrl+shift+g}"); - cy.get(".modal-backdrop").should("exist"); - cy.get(".theme-grid > div").first().click(); - cy.close_theme("{ctrl+shift+g}"); - cy.get(".modal-backdrop").should("not.exist"); - }); - it("Check Enter", () => { - cy.open_theme_dialog("{ctrl+shift+g}"); - cy.get(".theme-grid > div").first().click(); - cy.close_theme("{enter}"); - cy.get(".modal-backdrop").should("not.exist"); - }); -}); - -Cypress.Commands.add("open_theme_dialog", (shortcut_keys) => { - cy.get("body").type(shortcut_keys); -}); -Cypress.Commands.add("close_theme", (shortcut_keys) => { - cy.get(".modal-header").type(shortcut_keys); -}); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js deleted file mode 100644 index ce5cae8f5d0e..000000000000 --- a/cypress/integration/timeline.js +++ /dev/null @@ -1,91 +0,0 @@ -import custom_submittable_doctype from "../fixtures/custom_submittable_doctype"; - -context("Timeline", () => { - before(() => { - cy.visit("/login"); - cy.login(); - }); - - it("Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo", () => { - //Adding new ToDo - cy.new_form("ToDo"); - cy.get('[data-fieldname="description"] .ql-editor.ql-blank') - .type("Test ToDo", { force: true }) - .wait(200); - cy.get(".page-head .page-actions").findByRole("button", { name: "Save" }).click(); - - cy.go_to_list("ToDo"); - cy.clear_filters(); - cy.click_listview_row_item(0); - - //To check if the comment box is initially empty and tying some text into it - cy.get('[data-fieldname="comment"] .ql-editor') - .should("contain", "") - .type("Testing Timeline"); - - //Adding new comment - cy.get(".comment-box").findByRole("button", { name: "Comment" }).click(); - - //To check if the commented text is visible in the timeline content - cy.get(".timeline-content").should("contain", "Testing Timeline"); - - //Editing comment - cy.click_timeline_action_btn("Edit"); - cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(" 123"); - cy.click_timeline_action_btn("Save"); - - //To check if the edited comment text is visible in timeline content - cy.get(".timeline-content").should("contain", "Testing Timeline 123"); - - //Discarding comment - cy.click_timeline_action_btn("Edit"); - cy.click_timeline_action_btn("Dismiss"); - - //To check if after discarding the timeline content is same as previous - cy.get(".timeline-content").should("contain", "Testing Timeline 123"); - - //Deleting the added comment - cy.get(".timeline-message-box .more-actions > .action-btn").click(); //Menu button in timeline item - cy.get(".timeline-message-box .more-actions .dropdown-item") - .contains("Delete") - .click({ force: true }); - cy.get_open_dialog().findByRole("button", { name: "Yes" }).click({ force: true }); - - cy.get(".timeline-content").should("not.contain", "Testing Timeline 123"); - }); - - it("Timeline should have submit and cancel activity information", () => { - cy.visit("/app/doctype"); - - //Creating custom doctype - cy.insert_doc("DocType", custom_submittable_doctype, true); - - cy.visit("/app/custom-submittable-doctype"); - cy.click_listview_primary_button("Add Custom Submittable DocType"); - - //Adding a new entry for the created custom doctype - cy.fill_field("title", "Test"); - cy.click_modal_primary_button("Save"); - cy.click_modal_primary_button("Submit"); - - cy.visit("/app/custom-submittable-doctype"); - cy.click_listview_row_item(0); - - //To check if the submission of the documemt is visible in the timeline content - cy.get(".timeline-content").should("contain", "Frappe submitted this document"); - cy.get('[id="page-Custom Submittable DocType"] .page-actions') - .findByRole("button", { name: "Cancel" }) - .click(); - cy.get_open_dialog().findByRole("button", { name: "Yes" }).click(); - - //To check if the cancellation of the documemt is visible in the timeline content - cy.get(".timeline-content").should("contain", "Frappe cancelled this document"); - - //Deleting the document - cy.visit("/app/custom-submittable-doctype"); - cy.select_listview_row_checkbox(0); - cy.get(".page-actions").findByRole("button", { name: "Actions" }).click(); - cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click(); - cy.click_modal_primary_button("Yes"); - }); -}); diff --git a/cypress/integration/web_form.js b/cypress/integration/web_form.js index 2ccb3ed4185e..fcbb805ccf46 100644 --- a/cypress/integration/web_form.js +++ b/cypress/integration/web_form.js @@ -139,29 +139,6 @@ context("Web Form", () => { cy.get(".web-list-table thead th").contains("Content"); }); - it("Breadcrumbs", () => { - cy.visit("/note/Note 1"); - cy.get(".breadcrumb-container .breadcrumb .breadcrumb-item:first a") - .should("contain.text", "Note") - .click(); - cy.url().should("include", "/note/list"); - }); - - it("Custom Breadcrumbs", () => { - cy.visit("/app/web-form/note"); - - cy.findByRole("tab", { name: "Customization" }).click(); - cy.fill_field("breadcrumbs", '[{"label": _("Notes"), "route":"note"}]', "Code"); - cy.get(".form-tabs .nav-item .nav-link").contains("Customization").click(); - cy.save(); - - cy.visit("/note/Note 1"); - cy.get(".breadcrumb-container .breadcrumb .breadcrumb-item:first a").should( - "contain.text", - "Notes" - ); - }); - it("Read Only", () => { cy.login("Administrator"); cy.visit("/note"); diff --git a/frappe/__init__.py b/frappe/__init__.py index 83bf61671db0..151234f6f961 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -10,6 +10,7 @@ Read the documentation: https://frappeframework.com/docs """ +import faulthandler import functools import gc import importlib @@ -17,9 +18,11 @@ import json import os import re +import signal import unicodedata import warnings -from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, overload +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Literal, Optional, overload import click from werkzeug.local import Local, release_local @@ -44,7 +47,7 @@ ) from .utils.lazy_loader import lazy_import -__version__ = "14.66.2" +__version__ = "14.68.2" __title__ = "Frappe Framework" controllers = {} @@ -252,6 +255,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False) if not _qb_patched.get(local.conf.db_type): patch_query_execute() patch_query_aggregation() + _register_fault_handler() local.initialised = True @@ -365,9 +369,9 @@ def cache() -> "RedisWrapper": """Returns redis connection.""" global redis_server if not redis_server: - from frappe.utils.redis_wrapper import RedisWrapper + from frappe.utils.redis_wrapper import setup_cache - redis_server = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311") + redis_server = setup_cache() return redis_server @@ -383,7 +387,7 @@ def errprint(msg: str) -> None: :param msg: Message.""" msg = as_unicode(msg) - if not request or (not "cmd" in local.form_dict) or conf.developer_mode: + if not request or ("cmd" not in local.form_dict) or conf.developer_mode: print(msg) error_log.append({"exc": msg}) @@ -404,6 +408,13 @@ def log(msg: str) -> None: debug_log.append(as_unicode(msg)) +@functools.lru_cache(maxsize=1024) +def _strip_html_tags(message): + from frappe.utils import strip_html_tags + + return strip_html_tags(message) + + def msgprint( msg: str, title: str | None = None, @@ -412,7 +423,7 @@ def msgprint( as_list: bool = False, indicator: Literal["blue", "green", "orange", "red", "yellow"] | None = None, alert: bool = False, - primary_action: str = None, + primary_action: str | None = None, is_minimizable: bool = False, wide: bool = False, *, @@ -435,15 +446,9 @@ def msgprint( import inspect import sys - from frappe.utils import strip_html_tags - msg = safe_decode(msg) out = _dict(message=msg) - @functools.lru_cache(maxsize=1024) - def _strip_html_tags(message): - return strip_html_tags(message) - def _raise_exception(): if raise_exception: if inspect.isclass(raise_exception) and issubclass(raise_exception, Exception): @@ -790,6 +795,7 @@ def is_whitelisted(method): def read_only(): def innfn(fn): + @functools.wraps(fn) def wrapper_fn(*args, **kwargs): # frappe.read_only could be called from nested functions, in such cases don't swap the # connection again. @@ -1161,7 +1167,7 @@ def get_cached_value(doctype: str, name: str, fieldname: str = "name", as_dict: values = [doc.get(f) for f in fieldname] if as_dict: - return _dict(zip(fieldname, values)) + return _dict(zip(fieldname, values, strict=False)) return values @@ -1370,7 +1376,7 @@ def get_pymodule_path(modulename, *joins): :param modulename: Python module name. :param *joins: Join additional path elements using `os.path.join`.""" - if not "public" in joins: + if "public" not in joins: joins = [scrub(part) for part in joins] return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ""), *joins) @@ -1474,7 +1480,7 @@ def _load_app_hooks(app_name: str | None = None): raise def _is_valid_hook(obj): - return not isinstance(obj, (types.ModuleType, types.FunctionType, type)) + return not isinstance(obj, types.ModuleType | types.FunctionType | type) for key, value in inspect.getmembers(app_hooks, predicate=_is_valid_hook): if not key.startswith("_"): @@ -1482,7 +1488,9 @@ def _is_valid_hook(obj): return hooks -def get_hooks(hook: str = None, default: Any | None = "_KEEP_DEFAULT_LIST", app_name: str = None) -> _dict: +def get_hooks( + hook: str | None = None, default: Any | None = "_KEEP_DEFAULT_LIST", app_name: str | None = None +) -> _dict: """Get hooks via `app/hooks.py` :param hook: Name of the hook. Will gather all hooks for this name and return as a list. @@ -1725,13 +1733,13 @@ def remove_no_copy_fields(d): newdoc = get_doc(copy.deepcopy(d)) newdoc.set("__islocal", 1) - for fieldname in fields_to_clear + ["amended_from", "amendment_date"]: + for fieldname in [*fields_to_clear, "amended_from", "amendment_date"]: newdoc.set(fieldname, None) if not ignore_no_copy: remove_no_copy_fields(newdoc) - for i, d in enumerate(newdoc.get_all_children()): + for _i, d in enumerate(newdoc.get_all_children()): d.set("__islocal", 1) for fieldname in fields_to_clear: @@ -1917,7 +1925,7 @@ def get_all(doctype, *args, **kwargs): frappe.get_all("ToDo", fields=["*"], filters = {"description": ("like", "test%")}) """ kwargs["ignore_permissions"] = True - if not "limit_page_length" in kwargs: + if "limit_page_length" not in kwargs: kwargs["limit_page_length"] = 0 return get_list(doctype, *args, **kwargs) @@ -2366,7 +2374,7 @@ def mock(type, size=1, locale="en"): if type not in dir(fake): raise ValueError("Not a valid mock type.") else: - for i in range(size): + for _i in range(size): data = getattr(fake, type)() results.append(data) @@ -2380,7 +2388,7 @@ def validate_and_sanitize_search_inputs(fn): def wrapper(*args, **kwargs): from frappe.desk.search import sanitize_searchfield - kwargs.update(dict(zip(fn.__code__.co_varnames, args))) + kwargs.update(dict(zip(fn.__code__.co_varnames, args, strict=False))) sanitize_searchfield(kwargs["searchfield"]) kwargs["start"] = cint(kwargs["start"]) kwargs["page_len"] = cint(kwargs["page_len"]) @@ -2393,6 +2401,10 @@ def wrapper(*args, **kwargs): return wrapper +def _register_fault_handler(): + faulthandler.register(signal.SIGUSR1) + + if _tune_gc: # generational GC gets triggered after certain allocs (g0) which is 700 by default. # This number is quite small for frappe where a single query can potentially create 700+ diff --git a/frappe/app.py b/frappe/app.py index a7ccf9f6b094..4b10f7798da9 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -125,7 +125,7 @@ def application(request: Request): try: run_after_request_hooks(request, response) - except Exception as e: + except Exception: # We can not handle exceptions safely here. frappe.logger().error("Failed to run after request hook", exc_info=True) @@ -375,11 +375,7 @@ def handle_exception(e): def sync_database(rollback: bool) -> bool: # if HTTP method would change server state, commit if necessary - if ( - frappe.db - and (frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS) - and frappe.db.transaction_writes - ): + if frappe.db and (frappe.local.flags.commit or frappe.local.request.method in UNSAFE_HTTP_METHODS): frappe.db.commit() rollback = False elif frappe.db: diff --git a/frappe/auth.py b/frappe/auth.py index 032e83883a76..355273c61eed 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -223,7 +223,7 @@ def clear_active_sessions(self): clear_sessions(frappe.session.user, keep_current=True) - def authenticate(self, user: str = None, pwd: str = None): + def authenticate(self, user: str | None = None, pwd: str | None = None): from frappe.core.doctype.user.user import User if not (user and pwd): @@ -384,7 +384,7 @@ def set_cookie( } def delete_cookie(self, to_delete): - if not isinstance(to_delete, (list, tuple)): + if not isinstance(to_delete, list | tuple): to_delete = [to_delete] self.to_delete.extend(to_delete) @@ -491,7 +491,7 @@ def __init__( max_consecutive_login_attempts: int = 3, lock_interval: int = 5 * 60, *, - user_name: str = None, + user_name: str | None = None, ): """Initialize the tracker. diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py index ae4a717074e0..0901a6da3469 100644 --- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py @@ -81,7 +81,7 @@ def test_load_balancing(self): self.assignment_rule.save() for _ in range(30): - note = make_note(dict(public=1)) + make_note(dict(public=1)) # check if each user has 10 assignments (?) for user in ("test@example.com", "test1@example.com", "test2@example.com"): @@ -95,7 +95,7 @@ def test_load_balancing(self): frappe.db.delete("ToDo", {"name": d.name}) # add 5 more assignments - for i in range(5): + for _i in range(5): make_note(dict(public=1)) # check if each user still has 10 assignments diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 5440d78ca32e..b8ff9a94ea35 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -42,7 +42,8 @@ def get_doctype_map_key(doctype): "sitemap_routes", "db_tables", "server_script_autocompletion_items", -) + doctype_map_keys + *doctype_map_keys, +) user_cache_keys = ( "bootinfo", @@ -114,7 +115,7 @@ def clear_global_cache(): def clear_defaults_cache(user=None): if user: - for p in [user] + common_default_keys: + for p in [user, *common_default_keys]: frappe.cache().hdel("defaults", p) elif frappe.flags.in_install != "frappe": frappe.cache().delete_key("defaults") diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 69cc73dae136..92e67142a8a2 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -403,7 +403,7 @@ def install_app(context, apps, force=False): print(f"App {app} is Incompatible with Site {site}{err_msg}") exit_code = 1 except Exception as err: - err_msg = f": {str(err)}\n{frappe.get_traceback()}" + err_msg = f": {err!s}\n{frappe.get_traceback()}" print(f"An error occurred while installing {app}{err_msg}") exit_code = 1 @@ -970,9 +970,9 @@ def _drop_site( messages = [ "=" * 80, f"Error: The operation has stopped because backup of {site}'s database failed.", - f"Reason: {str(err)}\n", + f"Reason: {err!s}\n", "Fix the issue and try again.", - "Hint: Use 'bench drop-site {0} --force' to force the removal of {0}".format(site), + f"Hint: Use 'bench drop-site {site} --force' to force the removal of {site}", ] click.echo("\n".join(messages)) sys.exit(1) diff --git a/frappe/commands/translate.py b/frappe/commands/translate.py index d030656e773d..96af8bc154cc 100644 --- a/frappe/commands/translate.py +++ b/frappe/commands/translate.py @@ -37,9 +37,7 @@ def new_language(context, lang_code, app): frappe.connect(site=context["sites"][0]) frappe.translate.write_translations_file(app, lang_code) - print( - "File created at ./apps/{app}/{app}/translations/{lang_code}.csv".format(app=app, lang_code=lang_code) - ) + print(f"File created at ./apps/{app}/{app}/translations/{lang_code}.csv") print("You will need to add the language in frappe/geo/languages.json, if you haven't done it already.") diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index bc16d231e399..d4788df9b162 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -2,6 +2,7 @@ import os import subprocess import sys +import typing from shutil import which import click @@ -17,6 +18,9 @@ "Use `data-import` command instead to import data via 'Data Import'." ) +if typing.TYPE_CHECKING: + from IPython.terminal.embed import InteractiveShellEmbed + @click.command("build") @click.option("--app", help="Build assets for app") @@ -499,10 +503,13 @@ def mariadb(context): """ Enter into mariadb console for a given site. """ + from frappe.utils import get_site_path + site = get_site(context) if not site: raise SiteNotSpecifiedError frappe.init(site=site) + os.environ["MYSQL_HISTFILE"] = os.path.abspath(get_site_path("logs", "mariadb_console.log")) _mariadb() @@ -573,7 +580,7 @@ def jupyter(context): os.mkdir(jupyter_notebooks_path) bin_path = os.path.abspath("../env/bin") print( - """ + f""" Starting Jupyter notebook Run the following in your first cell to connect notebook to frappe ``` @@ -583,7 +590,7 @@ def jupyter(context): frappe.local.lang = frappe.db.get_default('lang') frappe.db.connect() ``` - """.format(site=site, sites_path=sites_path) + """ ) os.execv( f"{bin_path}/jupyter", @@ -601,6 +608,18 @@ def _console_cleanup(): frappe.destroy() +def store_logs(terminal: "InteractiveShellEmbed") -> None: + from contextlib import suppress + + frappe.log_level = 20 # info + with suppress(Exception): + logger = frappe.logger("ipython") + logger.info("=== bench console session ===") + for line in terminal.history_manager.get_range(): + logger.info(line[2]) + logger.info("=== session end ===") + + @click.command("console") @click.option("--autoreload", is_flag=True, help="Reload changes to code automatically") @pass_context @@ -624,6 +643,7 @@ def console(context, autoreload=False): all_apps = frappe.get_installed_apps() failed_to_import = [] + register(store_logs, terminal) # Note: atexit runs in reverse order of registration for app in list(all_apps): try: diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 0f6fbf6bb1ad..ad150b49655a 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -85,11 +85,10 @@ def get_preferred_address(doctype, name, preferred_key="is_primary_address"): FROM `tabAddress` addr, `tabDynamic Link` dl WHERE - dl.parent = addr.name and dl.link_doctype = %s and - dl.link_name = %s and ifnull(addr.disabled, 0) = 0 and - %s = %s - """ - % ("%s", "%s", preferred_key, "%s"), + dl.parent = addr.name and dl.link_doctype = {} and + dl.link_name = {} and ifnull(addr.disabled, 0) = 0 and + {} = {} + """.format("%s", "%s", preferred_key, "%s"), (doctype, name, 1), as_dict=1, ) diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index d3130e0fe724..82713bf76904 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -217,7 +217,7 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): link_name = filters.pop("link_name") return frappe.db.sql( - """select + f"""select `tabContact`.name, `tabContact`.first_name, `tabContact`.last_name from `tabContact`, `tabDynamic Link` @@ -226,12 +226,12 @@ def contact_query(doctype, txt, searchfield, start, page_len, filters): `tabDynamic Link`.parenttype = 'Contact' and `tabDynamic Link`.link_doctype = %(link_doctype)s and `tabDynamic Link`.link_name = %(link_name)s and - `tabContact`.`{key}` like %(txt)s - {mcond} + `tabContact`.`{searchfield}` like %(txt)s + {get_match_cond(doctype)} order by if(locate(%(_txt)s, `tabContact`.name), locate(%(_txt)s, `tabContact`.name), 99999), `tabContact`.idx desc, `tabContact`.name - limit %(start)s, %(page_len)s """.format(mcond=get_match_cond(doctype), key=searchfield), + limit %(start)s, %(page_len)s """, { "txt": "%" + txt + "%", "_txt": txt.replace("%", ""), diff --git a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py index 220d76b9049d..bed976e23427 100644 --- a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py @@ -52,7 +52,6 @@ def get_columns(filters): def get_data(filters): - data = [] reference_doctype = filters.get("reference_doctype") reference_name = filters.get("reference_name") @@ -108,7 +107,7 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_ ["Dynamic Link", "link_doctype", "=", reference_doctype], ["Dynamic Link", "link_name", "in", reference_list], ] - fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) + fields = ["`tabDynamic Link`.link_name", *field_map.get(doctype, [])] records = frappe.get_list(doctype, filters=filters, fields=fields, as_list=True) temp_records = list() diff --git a/frappe/core/doctype/activity_log/activity_log.py b/frappe/core/doctype/activity_log/activity_log.py index 58c5a63369be..a40a9fcf8ee5 100644 --- a/frappe/core/doctype/activity_log/activity_log.py +++ b/frappe/core/doctype/activity_log/activity_log.py @@ -28,7 +28,7 @@ def set_status(self): def set_ip_address(self): if self.operation in ("Login", "Logout"): - self.ip_address = getattr(frappe.local, "request_ip") + self.ip_address = frappe.local.request_ip @staticmethod def clear_old_logs(days=None): diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 25b3277ee409..0e78287c636a 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -153,8 +153,9 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): except Exception as e: if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None): # missing column and in request, add column and update after commit - frappe.local._comments = getattr(frappe.local, "_comments", []) + [ - (reference_doctype, reference_name, _comments) + frappe.local._comments = [ + *getattr(frappe.local, "_comments", []), + (reference_doctype, reference_name, _comments), ] elif frappe.db.is_data_too_long(e): diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 42063986291e..f73955f7a87d 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -45,6 +45,7 @@ def make( print_letterhead=True, email_template=None, communication_type=None, + now=False, **kwargs, ) -> dict[str, str]: """Make a new communication. Checks for email permissions for specified Document. @@ -98,6 +99,7 @@ def make( email_template=email_template, communication_type=communication_type, add_signature=False, + now=now, ) @@ -123,6 +125,7 @@ def _make( email_template=None, communication_type=None, add_signature=True, + now=False, ) -> dict[str, str]: """Internal method to make a new communication that ignores Permission checks.""" @@ -175,6 +178,7 @@ def _make( print_format=print_format, send_me_a_copy=send_me_a_copy, print_letterhead=print_letterhead, + now=now, ) emails_not_sent_to = comm.exclude_emails_list(include_sender=send_me_a_copy) @@ -256,7 +260,7 @@ def add_attachments(name: str, attachments: Iterable[str | dict]) -> None: @frappe.whitelist(allow_guest=True, methods=("GET",)) -def mark_email_as_seen(name: str = None): +def mark_email_as_seen(name: str | None = None): try: update_communication_as_read(name) frappe.db.commit() # nosemgrep: this will be called in a GET request diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 89202794335e..34da01681cef 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) @@ -304,6 +305,7 @@ def send_email( send_me_a_copy=None, print_letterhead=None, is_inbound_mail_communcation=None, + now=False, ): if input_dict := self.sendmail_input_dict( print_html=print_html, @@ -312,4 +314,4 @@ def send_email( print_letterhead=print_letterhead, is_inbound_mail_communcation=is_inbound_mail_communcation, ): - frappe.sendmail(**input_dict) + frappe.sendmail(now=now, **input_dict) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index bab2d2e0192a..3d42c823c51c 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -4,7 +4,7 @@ import frappe from frappe.core.doctype.communication.communication import Communication, get_emails, parse_email -from frappe.core.doctype.communication.email import add_attachments +from frappe.core.doctype.communication.email import add_attachments, make from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.tests.utils import FrappeTestCase diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index cfcf22ee8ed2..407673924581 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -240,10 +240,10 @@ def post_process(out): for key in del_keys: if key in doc: del doc[key] - for k, v in doc.items(): + for _k, v in doc.items(): if isinstance(v, list): for child in v: - for key in del_keys + ("docstatus", "doctype", "modified", "name"): + for key in (*del_keys, "docstatus", "doctype", "modified", "name"): if key in child: del child[key] diff --git a/frappe/core/doctype/data_import/exporter.py b/frappe/core/doctype/data_import/exporter.py index 1702040f6efb..b3a79e467617 100644 --- a/frappe/core/doctype/data_import/exporter.py +++ b/frappe/core/doctype/data_import/exporter.py @@ -107,7 +107,7 @@ def is_exportable(df): fields = [df for df in fields if is_exportable(df)] if "name" in fieldnames: - fields = [name_field] + fields + fields = [name_field, *fields] return fields or [] @@ -165,7 +165,7 @@ def format_column_name(df): parent_data = frappe.db.get_list( self.doctype, filters=filters, - fields=["name"] + parent_fields, + fields=["name", *parent_fields], limit_page_length=self.export_page_length, order_by=order_by, as_list=0, @@ -178,9 +178,13 @@ def format_column_name(df): continue child_table_df = self.meta.get_field(key) child_table_doctype = child_table_df.options - child_fields = ["name", "idx", "parent", "parentfield"] + list( - {format_column_name(df) for df in self.fields if df.parent == child_table_doctype} - ) + child_fields = [ + "name", + "idx", + "parent", + "parentfield", + *list({format_column_name(df) for df in self.fields if df.parent == child_table_doctype}), + ] data = frappe.get_all( child_table_doctype, filters={ diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 64748ebd69f8..8b21a3eda755 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -6,6 +6,7 @@ import os import re import timeit +import typing from datetime import date, datetime, time import frappe @@ -482,7 +483,7 @@ def get_data_for_import_preview(self): "read_only": col.df.read_only, } - data = [[row.row_number] + row.as_list() for row in self.data] + data = [[row.row_number, *row.as_list()] for row in self.data] warnings = self.get_warnings() @@ -523,7 +524,7 @@ def parse_next_row_for_import(self, data): # subsequent rows that have blank values in parent columns # are considered as child rows parent_column_indexes = self.header.get_column_indexes(self.doctype) - parent_row_values = first_row.get_values(parent_column_indexes) + first_row.get_values(parent_column_indexes) data_without_first_row = data[1:] for row in data_without_first_row: @@ -599,7 +600,7 @@ def read_content(self, content, extension): class Row: - link_values_exist_map = {} + link_values_exist_map: typing.ClassVar[dict] = {} def __init__(self, index, row, doctype, header, import_type): self.index = index @@ -653,7 +654,7 @@ def _parse_doc(self, doctype, columns, values, parent_doc=None, table_df=None): for key in frappe.model.default_fields + frappe.model.child_table_fields + ("__islocal",): doc.pop(key, None) - for col, value in zip(columns, values): + for col, value in zip(columns, values, strict=False): df = col.df if value in INVALID_VALUES: value = None @@ -751,7 +752,7 @@ def link_exists(self, value, df): def parse_value(self, value, col): df = col.df - if isinstance(value, (datetime, date)) and df.fieldtype in ["Date", "Datetime"]: + if isinstance(value, datetime | date) and df.fieldtype in ["Date", "Datetime"]: return value value = cstr(value) @@ -774,7 +775,7 @@ def parse_value(self, value, col): return value def get_date(self, value, column): - if isinstance(value, (datetime, date)): + if isinstance(value, datetime | date): return value date_format = column.date_format @@ -843,8 +844,7 @@ def get_columns(self, indexes): class Column: - seen = [] - fields_column_map = {} + seen: typing.ClassVar[list] = [] def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=None): if seen is None: @@ -941,7 +941,7 @@ def guess_date_format_for_column(self): """ def guess_date_format(d): - if isinstance(d, (datetime, date, time)): + if isinstance(d, datetime | date | time): if self.df.fieldtype == "Date": return "%Y-%m-%d" if self.df.fieldtype == "Datetime": @@ -1140,7 +1140,6 @@ def get_standard_fields(doctype): label = (df.label or "").strip() translated_label = _(label) - parent = df.parent or parent_doctype if parent_doctype == doctype: # for parent doctypes keys will be diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index d94dd80f986d..7b22044f650b 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -198,7 +198,7 @@ def set_default_in_list_view(self): if not [d.fieldname for d in self.fields if d.in_list_view]: cnt = 0 for d in self.fields: - if d.reqd and not d.hidden and not d.fieldtype in not_allowed_in_list_view: + if d.reqd and not d.hidden and d.fieldtype not in not_allowed_in_list_view: d.in_list_view = 1 cnt += 1 if cnt == 4: @@ -294,7 +294,7 @@ def validate_website(self): if self.has_web_view: # route field must be present - if not "route" in [d.fieldname for d in self.fields]: + if "route" not in [d.fieldname for d in self.fields]: frappe.throw(_('Field "route" is mandatory for Web Views'), title="Missing Field") # clear website cache @@ -1107,7 +1107,7 @@ def check_link_table_options(docname, d): if frappe.flags.in_patch or frappe.flags.in_fixtures: return - if d.fieldtype in ("Link",) + table_fields: + if d.fieldtype in ("Link", *table_fields): if not d.options: frappe.throw( _("{0}: Options required for Link or Table type field {1} in row {2}").format( @@ -1225,11 +1225,9 @@ def check_unique_and_text(docname, d): if not d.get("__islocal") and frappe.db.has_column(d.parent, d.fieldname): has_non_unique_values = frappe.db.sql( - """select `{fieldname}`, count(*) - from `tab{doctype}` where ifnull(`{fieldname}`, '') != '' - group by `{fieldname}` having count(*) > 1 limit 1""".format( - doctype=d.parent, fieldname=d.fieldname - ) + f"""select `{d.fieldname}`, count(*) + from `tab{d.parent}` where ifnull(`{d.fieldname}`, '') != '' + group by `{d.fieldname}` having count(*) > 1 limit 1""" ) if has_non_unique_values and has_non_unique_values[0][0]: @@ -1403,7 +1401,7 @@ def scrub_options_in_select(field): field.options = "\n".join(options_list) def scrub_fetch_from(field): - if hasattr(field, "fetch_from") and getattr(field, "fetch_from"): + if hasattr(field, "fetch_from") and field.fetch_from: field.fetch_from = field.fetch_from.strip("\n").strip() def validate_data_field_type(docfield): @@ -1685,7 +1683,7 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): r.desk_access = 1 r.flags.ignore_mandatory = r.flags.ignore_permissions = True r.insert() - except frappe.DoesNotExistError as e: + except frappe.DoesNotExistError: pass except frappe.db.ProgrammingError as e: if frappe.db.is_table_missing(e): diff --git a/frappe/core/doctype/document_naming_settings/document_naming_settings.py b/frappe/core/doctype/document_naming_settings/document_naming_settings.py index 9d6d5e2427e3..30d2bd6e2f4c 100644 --- a/frappe/core/doctype/document_naming_settings/document_naming_settings.py +++ b/frappe/core/doctype/document_naming_settings/document_naming_settings.py @@ -105,7 +105,7 @@ def set_series_options_in_meta(self, doctype: str, options: str) -> None: self.validate_series_name(series) if options and self.user_must_always_select: - options = [""] + options + options = ["", *options] default = options[0] if options else "" @@ -208,7 +208,7 @@ def preview_series(self) -> str: except Exception as e: if frappe.message_log: frappe.message_log.pop() - return _("Failed to generate names from the series") + f"\n{str(e)}" + return _("Failed to generate names from the series") + f"\n{e!s}" def _fetch_last_doc_if_available(self): """Fetch last doc for evaluating naming series with fields.""" diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py index 85b26f53dd3d..6b4fd3136e85 100644 --- a/frappe/core/doctype/domain_settings/domain_settings.py +++ b/frappe/core/doctype/domain_settings/domain_settings.py @@ -10,7 +10,7 @@ def set_active_domains(self, domains): active_domains = [d.domain for d in self.active_domains] added = False for d in domains: - if not d in active_domains: + if d not in active_domains: self.append("active_domains", dict(domain=d)) added = True diff --git a/frappe/core/doctype/dynamic_link/dynamic_link.py b/frappe/core/doctype/dynamic_link/dynamic_link.py index 53eb750b5c7e..9922db7f06db 100644 --- a/frappe/core/doctype/dynamic_link/dynamic_link.py +++ b/frappe/core/doctype/dynamic_link/dynamic_link.py @@ -17,7 +17,7 @@ def deduplicate_dynamic_links(doc): links, duplicate = [], False for l in doc.links or []: t = (l.link_doctype, l.link_name) - if not t in links: + if t not in links: links.append(t) else: duplicate = True diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 87a906856ea8..639e15c1de2c 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -104,7 +104,7 @@ def validate_attachment_references(self): if not self.attached_to_doctype: return - if not self.attached_to_name or not isinstance(self.attached_to_name, (str, int)): + if not self.attached_to_name or not isinstance(self.attached_to_name, str | int): frappe.throw(_("Attached To Name must be a string or an integer"), frappe.ValidationError) if self.attached_to_field and SPECIAL_CHAR_PATTERN.search(self.attached_to_field): @@ -730,10 +730,16 @@ def on_doctype_update(): def has_permission(doc, ptype=None, user=None): user = user or frappe.session.user + if user == "Administrator": + return True + if ptype == "create": return frappe.has_permission("File", "create", user=user) - if not doc.is_private or (user != "Guest" and doc.owner == user) or user == "Administrator": + if not doc.is_private and ptype in ("read", "select"): + return True + + if user != "Guest" and doc.owner == user: return True if doc.attached_to_doctype and doc.attached_to_name: @@ -754,7 +760,7 @@ def has_permission(doc, ptype=None, user=None): return False -def get_permission_query_conditions(user: str = None) -> str: +def get_permission_query_conditions(user: str | None = None) -> str: user = user or frappe.session.user if user == "Administrator": return "" diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 8ffc1012cf1e..5ceb050fc061 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -37,13 +37,18 @@ def make_test_doc(ignore_permissions=False): @contextmanager -def make_test_image_file(): +def make_test_image_file(private=False): file_path = frappe.get_app_path("frappe", "tests/data/sample_image_for_optimization.jpg") with open(file_path, "rb") as f: file_content = f.read() test_file = frappe.get_doc( - {"doctype": "File", "file_name": "sample_image_for_optimization.jpg", "content": file_content} + { + "doctype": "File", + "file_name": "sample_image_for_optimization.jpg", + "content": file_content, + "is_private": private, + } ).insert() # remove those flags _test_file: "File" = frappe.get_doc("File", test_file.name) diff --git a/frappe/core/doctype/file/utils.py b/frappe/core/doctype/file/utils.py index fbd4c6bfc362..5f245707936b 100644 --- a/frappe/core/doctype/file/utils.py +++ b/frappe/core/doctype/file/utils.py @@ -165,7 +165,7 @@ def delete_file(path: str) -> None: os.remove(path) -def remove_file_by_url(file_url: str, doctype: str = None, name: str = None) -> "Document": +def remove_file_by_url(file_url: str, doctype: str | None = None, name: str | None = None) -> "Document": if doctype and name: fid = frappe.db.get_value( "File", {"file_url": file_url, "attached_to_doctype": doctype, "attached_to_name": name} @@ -272,7 +272,7 @@ def _save_file(match): return content -def get_random_filename(content_type: str = None) -> str: +def get_random_filename(content_type: str | None = None) -> str: extn = None if content_type: extn = mimetypes.guess_extension(content_type) @@ -407,3 +407,19 @@ def decode_file_content(content: bytes) -> bytes: if b"," in content: content = content.split(b",")[1] return safe_b64decode(content) + + +def find_file_by_url(path: str, name: str | None = None) -> Optional["File"]: + filters = {"file_url": str(path)} + if name: + filters["name"] = str(name) + + files = frappe.get_all("File", filters=filters, fields="*") + + # this file might be attached to multiple documents + # if the file is accessible from any one of those documents + # then it should be downloadable + for file_data in files: + file: "File" = frappe.get_doc(doctype="File", **file_data) + if file.is_downloadable(): + return file diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index b8a103524b6e..7751daa98a40 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -32,7 +32,7 @@ def add_to_modules_txt(self): if not frappe.local.module_app.get(frappe.scrub(self.name)): with open(frappe.get_app_path(self.app_name, "modules.txt")) as f: content = f.read() - if not self.name in content.splitlines(): + if self.name not in content.splitlines(): modules = list(filter(None, content.splitlines())) modules.append(self.name) diff --git a/frappe/core/doctype/module_def/module_def_list.js b/frappe/core/doctype/module_def/module_def_list.js new file mode 100644 index 000000000000..4ff0c203e075 --- /dev/null +++ b/frappe/core/doctype/module_def/module_def_list.js @@ -0,0 +1,16 @@ +frappe.listview_settings["Module Def"] = { + onload: function (list_view) { + frappe.call({ + method: "frappe.core.doctype.module_def.module_def.get_installed_apps", + callback: (r) => { + const field = list_view.page.fields_dict.app_name; + if (!field) return; + + const options = JSON.parse(r.message); + options.unshift(""); // Add empty option + field.df.options = options; + field.set_options(); + }, + }); + }, +}; diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 09f7cb5f430b..89aa0e7a08c0 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -68,14 +68,13 @@ def on_update(self): if not os.path.exists(path + ".js"): with open(path + ".js", "w") as f: f.write( - """frappe.pages['%s'].on_page_load = function(wrapper) { - var page = frappe.ui.make_app_page({ + f"""frappe.pages['{self.name}'].on_page_load = function(wrapper) {{ + var page = frappe.ui.make_app_page({{ parent: wrapper, - title: '%s', + title: '{self.title}', single_column: true - }); -}""" - % (self.name, self.title) + }}); +}}""" ) def as_dict(self, no_nulls=False): diff --git a/frappe/core/doctype/patch_log/patch_log.json b/frappe/core/doctype/patch_log/patch_log.json index b586aeabfcf3..53e85b99d3c9 100644 --- a/frappe/core/doctype/patch_log/patch_log.json +++ b/frappe/core/doctype/patch_log/patch_log.json @@ -7,7 +7,9 @@ "document_type": "System", "engine": "InnoDB", "field_order": [ - "patch" + "patch", + "skipped", + "traceback" ], "fields": [ { @@ -15,12 +17,26 @@ "fieldtype": "Code", "label": "Patch", "read_only": 1 + }, + { + "default": "0", + "fieldname": "skipped", + "fieldtype": "Check", + "label": "Skipped", + "read_only": 1 + }, + { + "depends_on": "eval:doc.skipped == 1", + "fieldname": "traceback", + "fieldtype": "Code", + "label": "Traceback", + "read_only": 1 } ], "icon": "fa fa-cog", "idx": 1, "links": [], - "modified": "2023-01-17 15:35:11.688615", + "modified": "2023-05-10 19:27:10.883330", "modified_by": "Administrator", "module": "Core", "name": "Patch Log", @@ -33,6 +49,14 @@ "read": 1, "report": 1, "role": "Administrator" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager" } ], "quick_entry": 1, diff --git a/frappe/core/doctype/patch_log/patch_log.py b/frappe/core/doctype/patch_log/patch_log.py index eee57af4c24d..c7d619017efe 100644 --- a/frappe/core/doctype/patch_log/patch_log.py +++ b/frappe/core/doctype/patch_log/patch_log.py @@ -3,8 +3,13 @@ # License: MIT. See LICENSE +import frappe from frappe.model.document import Document class PatchLog(Document): pass + + +def before_migrate(): + frappe.reload_doc("core", "doctype", "patch_log") diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index b54249bbc6d9..619dadbb428e 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -163,7 +163,7 @@ def create_json_gz_file(data, dt, dn): # Storing data in CSV file causes information loss # Reports like P&L Statement were completely unsuable because of this json_filename = "{}.json.gz".format(frappe.utils.data.format_datetime(frappe.utils.now(), "Y-m-d-H:M")) - encoded_content = frappe.safe_encode(frappe.as_json(data)) + encoded_content = frappe.safe_encode(frappe.as_json(data, indent=None, separators=(",", ":"))) compressed_content = gzip_compress(encoded_content) # Call save() file function to upload and attach the file diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 0f5fa484f609..257f682098d9 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -266,7 +266,7 @@ def get_standard_report_filters(self, params, filters): if filters: for key, value in filters.items(): condition, _value = "=", value - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): condition, _value = value _filters.append([key, condition, _value]) @@ -325,7 +325,7 @@ def build_standard_report_columns(self, columns, group_by_args): def build_data_dict(self, result, columns): data = [] for row in result: - if isinstance(row, (list, tuple)): + if isinstance(row, list | tuple): _row = frappe._dict() for i, val in enumerate(row): _row[columns[i].get("fieldname")] = val diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 0af55e3c71d9..62586594b3b9 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -162,9 +162,7 @@ def test_report_permissions(self): frappe.db.delete("Has Role", {"parent": frappe.session.user, "role": "Test Has Role"}) frappe.db.commit() if not frappe.db.exists("Role", "Test Has Role"): - role = frappe.get_doc({"doctype": "Role", "role_name": "Test Has Role"}).insert( - ignore_permissions=True - ) + frappe.get_doc({"doctype": "Role", "role_name": "Test Has Role"}).insert(ignore_permissions=True) if not frappe.db.exists("Report", "Test Report"): report = frappe.get_doc( diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json index 2039d3889d26..e86a861bfce0 100644 --- a/frappe/core/doctype/role/role.json +++ b/frappe/core/doctype/role/role.json @@ -64,7 +64,7 @@ "options": "Domain" }, { - "description": "Route: Example \"/desk\"", + "description": "Route: Example \"/app\"", "fieldname": "home_page", "fieldtype": "Data", "label": "Home Page" @@ -148,7 +148,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-05 18:33:27.694065", + "modified": "2024-03-13 20:59:37.875253", "modified_by": "Administrator", "module": "Core", "name": "Role", @@ -173,4 +173,4 @@ "states": [], "track_changes": 1, "translated_doctype": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index be4ca5dd6712..c50bab3e6903 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -3,6 +3,7 @@ import frappe from frappe.model.document import Document +from frappe.website.path_resolver import validate_path desk_properties = ( "search_bar", @@ -31,6 +32,7 @@ def validate(self): self.disable_role() else: self.set_desk_properties() + self.validate_homepage() def disable_role(self): if self.name in STANDARD_ROLES: @@ -38,6 +40,10 @@ def disable_role(self): else: self.remove_roles() + def validate_homepage(self): + if frappe.request and self.home_page: + validate_path(self.home_page) + def set_desk_properties(self): # set if desk_access is not allowed, unset all desk properties if self.name == "Guest": diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index 0aa020cc47b5..0bc7b3391d88 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -18,7 +18,7 @@ class TestRQJob(FrappeTestCase): BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func" - @timeout(seconds=20) + @timeout(seconds=60) def check_status(self, job: Job, status, wait=True): while wait: if not (job.is_queued or job.is_started): @@ -55,6 +55,7 @@ def test_func_obj_serialization(self): rq_job = frappe.get_doc("RQ Job", job.id) self.assertEqual(rq_job.job_name, "test_func") + @timeout def test_get_list_filtering(self): # Check failed job clearning and filtering remove_failed_jobs() @@ -95,7 +96,6 @@ def test_multi_queue_burst_consumption(self): _, stderr = execute_in_shell("bench worker --queue short,default --burst", check_exit_code=True) self.assertIn("quitting", cstr(stderr)) - @timeout(20) def test_job_id_dedup(self): job_id = "test_dedup" job = frappe.enqueue(self.BG_JOB, sleep=5, job_id=job_id) @@ -103,7 +103,7 @@ def test_job_id_dedup(self): self.check_status(job, "finished") self.assertFalse(is_job_enqueued(job_id)) - @timeout(20) + @timeout(60) def test_clear_failed_jobs(self): limit = 10 update_site_config("rq_failed_jobs_limit", limit) diff --git a/frappe/core/doctype/rq_worker/rq_worker.json b/frappe/core/doctype/rq_worker/rq_worker.json index 841c01ddece3..cc301be5b31d 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 8cdf63a5fad8..f93eed2df108 100644 --- a/frappe/core/doctype/rq_worker/rq_worker.py +++ b/frappe/core/doctype/rq_worker/rq_worker.py @@ -15,7 +15,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]) @@ -58,7 +58,7 @@ def serialize_worker(worker: Worker) -> frappe._dict: queue_types = ",".join(q.rsplit(":", 1)[1] for q in queue_names) 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 f07338d6303f..5803b1a87591 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/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 51acba84a8c4..5cc3bad227f2 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -161,7 +161,7 @@ def run_scheduled_job(job_type: str): print(frappe.get_traceback()) -def sync_jobs(hooks: dict = None): +def sync_jobs(hooks: dict | None = None): frappe.reload_doc("core", "doctype", "scheduled_job_type") scheduler_events = hooks or frappe.get_hooks("scheduler_events") all_events = insert_events(scheduler_events) @@ -198,7 +198,7 @@ def insert_event_jobs(events: list, event_type: str) -> list: return event_jobs -def insert_single_event(frequency: str, event: str, cron_format: str = None): +def insert_single_event(frequency: str, event: str, cron_format: str | None = None): cron_expr = {"cron_format": cron_format} if cron_format else {} try: diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index cc050f8512d0..455084a0b381 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -147,7 +147,7 @@ def get_keys(obj): if key.startswith("_"): continue value = obj[key] - if isinstance(value, (NamespaceDict, dict)) and value: + if isinstance(value, NamespaceDict | dict) and value: if key == "form_dict": out.append(["form_dict", 7]) continue @@ -159,7 +159,7 @@ def get_keys(obj): score = 0 elif isinstance(value, ModuleType): score = 10 - elif isinstance(value, (FunctionType, MethodType)): + elif isinstance(value, FunctionType | MethodType): score = 9 elif isinstance(value, type): score = 8 diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index b807b43d10ae..91e614ceb2b7 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -23,7 +23,7 @@ def run_server_script_for_doc_event(doc, event): # run document event method - if not event in EVENT_MAP: + if event not in EVENT_MAP: return if frappe.flags.in_install: diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 455d700acc7f..7a85ff936b37 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -216,7 +216,7 @@ def test_scripts_all_the_way_down(self): name="test_nested_scripts_1", script_type="API", api_method="test_nested_scripts_1", - script=f"""log("nothing")""", + script="""log("nothing")""", ) script.insert() script.execute_method() @@ -226,7 +226,7 @@ def test_scripts_all_the_way_down(self): name="test_nested_scripts_2", script_type="API", api_method="test_nested_scripts_2", - script=f"""frappe.call("test_nested_scripts_1")""", + script="""frappe.call("test_nested_scripts_1")""", ) script.insert() script.execute_method() diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index dc6c467c6655..48c1c9f4e254 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -75,6 +75,7 @@ "disable_standard_email_footer", "hide_footer_in_auto_email_reports", "attach_view_link", + "store_attached_pdf_document", "prepared_report_section", "max_auto_email_report_per_user", "system_updates_section", @@ -592,12 +593,19 @@ { "fieldname": "column_break_uqma", "fieldtype": "Column Break" + }, + { + "default": "1", + "description": "When sending document using email, store the PDF on Communication. Warning: This can increase your storage usage.", + "fieldname": "store_attached_pdf_document", + "fieldtype": "Check", + "label": "Store Attached PDF Document" } ], "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2023-10-17 16:12:28.145496", + "modified": "2024-03-14 15:18:01.465057", "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 48a5bfb87a5c..f8d207594ea0 100644 --- a/frappe/core/doctype/system_settings/system_settings.py +++ b/frappe/core/doctype/system_settings/system_settings.py @@ -99,7 +99,7 @@ def update_last_reset_password_date(): def load(): from frappe.utils.momentjs import get_all_timezones - if not "System Manager" in frappe.get_roles(): + if "System Manager" not in frappe.get_roles(): frappe.throw(_("Not permitted"), frappe.PermissionError) all_defaults = frappe.db.get_defaults() diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index e5bf13dbf0ec..8a5b35fa0c06 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -14,6 +14,7 @@ toggle_notifications, ) from frappe.desk.notifications import clear_notifications +from frappe.model.delete_doc import check_if_doc_is_linked from frappe.model.document import Document from frappe.query_builder import DocType from frappe.rate_limiter import rate_limit @@ -455,6 +456,19 @@ def on_trash(self): # delete user permissions frappe.db.delete("User Permission", {"user": self.name}) + # Delete OAuth data + frappe.db.delete("OAuth Authorization Code", {"user": self.name}) + frappe.db.delete("Token Cache", {"user": self.name}) + + # Delete EPS data + frappe.db.delete("Energy Point Log", {"user": self.name}) + + # Ask user to disable instead if document is still linked + try: + check_if_doc_is_linked(self) + except frappe.LinkExistsError: + frappe.throw(_("You can disable the user instead of deleting it."), frappe.LinkExistsError) + def before_rename(self, old_name, new_name, merge=False): frappe.clear_cache(user=old_name) self.validate_rename(old_name, new_name) @@ -481,10 +495,9 @@ def after_rename(self, old_name, new_name, merge=False): has_fields.append(d.get("name")) for field in has_fields: frappe.db.sql( - """UPDATE `%s` - SET `%s` = %s - WHERE `%s` = %s""" - % (tab, field, "%s", field, "%s"), + """UPDATE `{}` + SET `{}` = {} + WHERE `{}` = {}""".format(tab, field, "%s", field, "%s"), (new_name, old_name), ) @@ -527,7 +540,7 @@ def remove_disabled_roles(self): def ensure_unique_roles(self): exists = [] - for i, d in enumerate(self.get("roles")): + for _i, d in enumerate(self.get("roles")): if (not d.role) or (d.role in exists): self.get("roles").remove(d) else: @@ -708,7 +721,7 @@ def get_perm_info(role): return get_all_perms(role) -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=["POST"]) def update_password(new_password, logout_all_sessions=0, key=None, old_password=None): # validate key to avoid key input like ['like', '%'], '', ['in', ['']] if key and not isinstance(key, str): @@ -854,7 +867,7 @@ def reset_user_data(user): return user_doc, redirect_url -@frappe.whitelist() +@frappe.whitelist(methods=["POST"]) def verify_password(password): frappe.local.login_manager.check_password(frappe.session.user, password) @@ -910,7 +923,7 @@ def sign_up(email, full_name, redirect_to): return 2, _("Please ask your administrator to verify your sign-up") -@frappe.whitelist(allow_guest=True) +@frappe.whitelist(allow_guest=True, methods=["POST"]) @rate_limit(limit=get_password_reset_limit, seconds=60 * 60) def reset_password(user: str) -> str: try: @@ -991,7 +1004,7 @@ def get_total_users(): def get_system_users(exclude_users=None, limit=None): if not exclude_users: exclude_users = [] - elif not isinstance(exclude_users, (list, tuple)): + elif not isinstance(exclude_users, list | tuple): exclude_users = [exclude_users] limit_cond = "" @@ -1081,7 +1094,7 @@ def handle_password_test_fail(feedback: dict): suggestions = feedback.get("suggestions", []) warning = feedback.get("warning", "") - frappe.throw(msg=" ".join([warning] + suggestions), title=_("Invalid Password")) + frappe.throw(msg=" ".join([warning, *suggestions]), title=_("Invalid Password")) def update_gravatar(name): @@ -1175,7 +1188,7 @@ def get_restricted_ip_list(user): return [i.strip() for i in user.restrict_ip.split(",")] -@frappe.whitelist() +@frappe.whitelist(methods=["POST"]) def generate_keys(user): """ generate api key and api secret diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index ab7ca43fc557..b89dc5e61608 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -36,11 +36,7 @@ def get_columns_and_fields(doctype): if df.in_list_view and df.fieldtype in data_fieldtypes: fields.append(f"`{df.fieldname}`") fieldtype = f"Link/{df.options}" if df.fieldtype == "Link" else df.fieldtype - columns.append( - "{label}:{fieldtype}:{width}".format( - label=df.label, fieldtype=fieldtype, width=df.width or 100 - ) - ) + columns.append(f"{df.label}:{fieldtype}:{df.width or 100}") return columns, fields diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index 9bfaf52e7903..abdb3c3e32fc 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.json b/frappe/custom/doctype/custom_field/custom_field.json index 53a003c88ea8..ed7b1967f42c 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -360,7 +360,7 @@ "label": "Ignore XSS Filter" }, { - "default": "1", + "default": "0", "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", "fieldname": "translatable", "fieldtype": "Check", @@ -450,7 +450,7 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2023-10-25 06:55:10.713382", + "modified": "2024-03-07 17:34:47.167183", "modified_by": "Administrator", "module": "Custom", "name": "Custom Field", @@ -484,4 +484,4 @@ "sort_order": "ASC", "states": [], "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index ff62a77b049b..ea377bc23d27 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -265,6 +265,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() diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 4c3e55c0fa0b..a2793e8b80ca 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -521,11 +521,11 @@ def validate_fieldtype_length(self): max_length = cint(frappe.db.type_map.get(df.fieldtype)[1]) fieldname = df.fieldname docs = frappe.db.sql( - """ + f""" SELECT name, {fieldname}, LENGTH({fieldname}) AS len - FROM `tab{doctype}` + FROM `tab{self.doc_type}` WHERE LENGTH({fieldname}) > {max_length} - """.format(fieldname=fieldname, doctype=self.doc_type, max_length=max_length), + """, as_dict=True, ) links = [] @@ -700,7 +700,7 @@ def is_standard_or_system_generated_field(df): ("Text", "Data"), ("Text", "Text Editor", "Code", "Signature", "HTML Editor"), ("Data", "Select"), - ("Text", "Small Text"), + ("Text", "Small Text", "Long Text"), ("Text", "Data", "Barcode"), ("Code", "Geolocation"), ("Table", "Table MultiSelect"), diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py index 0c3b49527190..07b8e18b4d3b 100644 --- a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py @@ -35,7 +35,7 @@ def fmt_hook_values(v): v = delist(v) - if isinstance(v, (dict, list)): + if isinstance(v, dict | list): try: return frappe.as_json(v) except Exception: diff --git a/frappe/database/database.py b/frappe/database/database.py index a7a7423024e3..799148aa25dc 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -27,6 +27,7 @@ ) from frappe.exceptions import DoesNotExistError, ImplicitCommitError from frappe.model.utils.link_count import flush_local_link_count +from frappe.monitor import get_trace_id from frappe.query_builder.functions import Count from frappe.utils import cast as cast_fieldtype from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool @@ -57,10 +58,10 @@ class Database: VARCHAR_LEN = 140 MAX_COLUMN_LENGTH = 64 - OPTIONAL_COLUMNS = ["_user_tags", "_comments", "_assign", "_liked_by"] - DEFAULT_SHORTCUTS = ["_Login", "__user", "_Full Name", "Today", "__today", "now", "Now"] + OPTIONAL_COLUMNS = ("_user_tags", "_comments", "_assign", "_liked_by") + DEFAULT_SHORTCUTS = ("_Login", "__user", "_Full Name", "Today", "__today", "now", "Now") STANDARD_VARCHAR_COLUMNS = ("name", "owner", "modified_by") - DEFAULT_COLUMNS = ["name", "creation", "modified", "modified_by", "owner", "docstatus", "idx"] + DEFAULT_COLUMNS = ("name", "creation", "modified", "modified_by", "owner", "docstatus", "idx") CHILD_TABLE_COLUMNS = ("parent", "parenttype", "parentfield") MAX_WRITES_PER_TRANSACTION = 200_000 @@ -122,8 +123,8 @@ def setup_type_map(self): def connect(self): """Connects to a database as set in `site_config.json`.""" self.cur_db_name = self.user - self._conn: Union["MariadbConnection", "PostgresConnection"] = self.get_connection() - self._cursor: Union["MariadbCursor", "PostgresCursor"] = self._conn.cursor() + self._conn: "MariadbConnection" | "PostgresConnection" = self.get_connection() + self._cursor: "MariadbCursor" | "PostgresCursor" = self._conn.cursor() frappe.local.rollback_observers = [] try: @@ -231,10 +232,14 @@ def sql( if values == EmptyQueryValues: values = None - elif not isinstance(values, (tuple, dict, list)): + elif not isinstance(values, tuple | dict | list): values = (values,) + query, values = self._transform_query(query, values) + if trace_id := get_trace_id(): + query += f" /* FRAPPE_TRACE_ID: {trace_id} */" + try: self._cursor.execute(query, values) except Exception as e: @@ -321,7 +326,7 @@ def _return_as_iterator(self, *, pluck, as_dict, as_list, update): elif as_dict: keys = [column[0] for column in self._cursor.description] for row in result: - row = frappe._dict(zip(keys, row)) + row = frappe._dict(zip(keys, row, strict=False)) if update: row.update(update) yield row @@ -390,7 +395,7 @@ def mogrify(self, query: Query, values: QueryValues): return query % { k: frappe.db.escape(v) if isinstance(v, str) else v for k, v in values.items() } - elif isinstance(values, (list, tuple)): + elif isinstance(values, list | tuple): return query % tuple(frappe.db.escape(v) if isinstance(v, str) else v for v in values) return query, values @@ -458,7 +463,7 @@ def fetch_as_dict(self, result, as_utf8=False) -> list[frappe._dict]: keys = [column[0] for column in self._cursor.description] if not as_utf8: - return [frappe._dict(zip(keys, row)) for row in result] + return [frappe._dict(zip(keys, row, strict=False)) for row in result] ret = [] for r in result: @@ -468,7 +473,7 @@ def fetch_as_dict(self, result, as_utf8=False) -> list[frappe._dict]: value = value.encode("utf-8") values.append(value) - ret.append(frappe._dict(zip(keys, values))) + ret.append(frappe._dict(zip(keys, values, strict=False))) return ret @staticmethod @@ -481,9 +486,9 @@ def needs_formatting(result, formatted): """Returns true if the first row in the result has a Date, Datetime, Long Int.""" if result and result[0]: for v in result[0]: - if isinstance(v, (datetime.date, datetime.timedelta, datetime.datetime, int)): + if isinstance(v, datetime.date | datetime.timedelta | datetime.datetime | int): return True - if formatted and isinstance(v, (int, float)): + if formatted and isinstance(v, int | float): return True return False @@ -527,6 +532,8 @@ def get_value( run=True, pluck=False, distinct=False, + skip_locked=False, + wait=True, ): """Returns a document property or list of properties. @@ -537,6 +544,11 @@ def get_value( :param as_dict: Return values as dict. :param debug: Print query in error log. :param order_by: Column to order by + :param cache: Use cached results fetched during current job/request + :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: @@ -567,6 +579,8 @@ def get_value( pluck=pluck, distinct=distinct, limit=1, + skip_locked=skip_locked, + wait=wait, ) if not run: @@ -599,6 +613,8 @@ def get_values( pluck=False, distinct=False, limit=None, + skip_locked=False, + wait=True, ): """Returns multiple document properties. @@ -638,6 +654,9 @@ def get_values( distinct=distinct, limit=limit, as_dict=as_dict, + skip_locked=skip_locked, + wait=True, + for_update=for_update, ) else: @@ -658,11 +677,13 @@ def get_values( debug=debug, order_by=order_by, update=update, - for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=limit, + for_update=for_update, + skip_locked=skip_locked, + wait=wait, ) except Exception as e: if ignore and ( @@ -864,6 +885,8 @@ def _get_values_from_table( order_by=None, update=None, for_update=False, + skip_locked=False, + wait=True, run=True, pluck=False, distinct=False, @@ -874,12 +897,14 @@ def _get_values_from_table( filters=filters, order_by=order_by, for_update=for_update, + skip_locked=skip_locked, + wait=wait, fields=fields, distinct=distinct, limit=limit, validate_filters=True, ) - if fields == "*" and not isinstance(fields, (list, tuple)) and not isinstance(fields, Criterion): + if fields == "*" and not isinstance(fields, list | tuple) and not isinstance(fields, Criterion): as_dict = True return query.run(as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck) @@ -897,6 +922,9 @@ def _get_value_for_many_names( distinct=False, limit=None, as_dict=False, + for_update=False, + skip_locked=False, + wait=True, ): if names := list(filter(None, names)): return frappe.qb.get_query( @@ -907,6 +935,9 @@ def _get_value_for_many_names( distinct=distinct, limit=limit, 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 {} @@ -1300,7 +1331,7 @@ def multisql(self, sql_dict, values=(), **kwargs): query = sql_dict.get(current_dialect) return self.sql(query, values, **kwargs) - def delete(self, doctype: str, filters: dict | list = None, debug=False, **kwargs): + def delete(self, doctype: str, filters: dict | list | None = None, debug=False, **kwargs): """Delete rows from a table in site which match the passed filters. This does trigger DocType hooks. Simply runs a DELETE query in the database. diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 412c2cd149dc..645964e72c19 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -284,20 +284,20 @@ def create_auth_table(self): ) def create_global_search_table(self): - if not "__global_search" in self.get_tables(): + if "__global_search" not in self.get_tables(): self.sql( - """create table __global_search( + f"""create table __global_search( doctype varchar(100), - name varchar({0}), - title varchar({0}), + name varchar({self.VARCHAR_LEN}), + title varchar({self.VARCHAR_LEN}), content text, fulltext(content), - route varchar({0}), + route varchar({self.VARCHAR_LEN}), published int(1) not null default 0, unique `doctype_name` (doctype, name)) COLLATE=utf8mb4_unicode_ci ENGINE=MyISAM - CHARACTER SET=utf8mb4""".format(self.VARCHAR_LEN) + CHARACTER SET=utf8mb4""" ) def create_user_settings_table(self): @@ -317,7 +317,7 @@ def get_on_duplicate_update(key=None): def get_table_columns_description(self, table_name): """Returns list of column and its description""" return self.sql( - """select + f"""select column_name as 'name', column_type as 'type', column_default as 'default', @@ -332,14 +332,14 @@ def get_table_columns_description(self, table_name): ), 0) as 'index', column_key = 'UNI' as 'unique' from information_schema.columns as columns - where table_name = '{table_name}' """.format(table_name=table_name), + where table_name = '{table_name}' """, as_dict=1, ) def has_index(self, table_name, index_name): return self.sql( - """SHOW INDEX FROM `{table_name}` - WHERE Key_name='{index_name}'""".format(table_name=table_name, index_name=index_name) + f"""SHOW INDEX FROM `{table_name}` + WHERE Key_name='{index_name}'""" ) def get_column_index(self, table_name: str, fieldname: str, unique: bool = False) -> frappe._dict | None: @@ -372,7 +372,7 @@ def get_column_index(self, table_name: str, fieldname: str, unique: bool = False if not clustered_index: return index - def add_index(self, doctype: str, fields: list, index_name: str = None): + def add_index(self, doctype: str, fields: list, index_name: str | None = None): """Creates an index with given fields if not already created. Index name will be `fieldname1_fieldname2_index`""" index_name = index_name or self.get_index_name(fields) @@ -380,9 +380,8 @@ def add_index(self, doctype: str, fields: list, index_name: str = None): if not self.has_index(table_name, index_name): self.commit() self.sql( - """ALTER TABLE `%s` - ADD INDEX `%s`(%s)""" - % (table_name, index_name, ", ".join(fields)) + """ALTER TABLE `{}` + ADD INDEX `{}`({})""".format(table_name, index_name, ", ".join(fields)) ) def add_unique(self, doctype, fields, constraint_name=None): @@ -398,9 +397,8 @@ def add_unique(self, doctype, fields, constraint_name=None): ): self.commit() self.sql( - """alter table `tab%s` - add unique `%s`(%s)""" - % (doctype, constraint_name, ", ".join(fields)) + """alter table `tab{}` + add unique `{}`({})""".format(doctype, constraint_name, ", ".join(fields)) ) def updatedb(self, doctype, meta=None): @@ -418,9 +416,8 @@ def updatedb(self, doctype, meta=None): db_table = MariaDBTable(doctype, meta) db_table.validate() - self.commit() db_table.sync() - self.begin() + self.commit() def get_database_list(self): return self.sql("SHOW DATABASES", pluck=True) diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql index eff0d740cdd7..6b82dd207649 100644 --- a/frappe/database/mariadb/framework_mariadb.sql +++ b/frappe/database/mariadb/framework_mariadb.sql @@ -176,6 +176,7 @@ CREATE TABLE `tabDocType` ( `idx` int(8) NOT NULL DEFAULT 0, `search_fields` varchar(255) DEFAULT NULL, `issingle` int(1) NOT NULL DEFAULT 0, + `is_virtual` int(1) NOT NULL DEFAULT 0, `is_tree` int(1) NOT NULL DEFAULT 0, `istable` int(1) NOT NULL DEFAULT 0, `editable_grid` int(1) NOT NULL DEFAULT 1, diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 7b55157d367a..2831e51c5d5f 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -62,7 +62,7 @@ def create(self): CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci""" - frappe.db.sql(query) + frappe.db.sql_ddl(query) def alter(self): for col in self.columns.values(): @@ -110,7 +110,7 @@ def alter(self): if query_parts: query_body = ", ".join(query_parts) query = f"ALTER TABLE `{self.table_name}` {query_body}" - frappe.db.sql(query) + frappe.db.sql_ddl(query) except Exception as e: if query := locals().get("query"): # this weirdness is to avoid potentially unbounded vars diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py index b15c6e0873f6..95cc4467bc7e 100644 --- a/frappe/database/mariadb/setup_db.py +++ b/frappe/database/mariadb/setup_db.py @@ -63,7 +63,7 @@ def setup_help_database(help_db_name): dbman.drop_database(help_db_name) # make database - if not help_db_name in dbman.get_database_list(): + if help_db_name not in dbman.get_database_list(): try: dbman.create_user(help_db_name, help_db_name) except Exception as e: @@ -127,10 +127,7 @@ def check_database_settings(): result = True for key, expected_value in REQUIRED_MARIADB_CONFIG.items(): if mariadb_variables.get(key) != expected_value: - print( - "For key %s. Expected value %s, found value %s" - % (key, expected_value, mariadb_variables.get(key)) - ) + print(f"For key {key}. Expected value {expected_value}, found value {mariadb_variables.get(key)}") result = False if not result: diff --git a/frappe/database/operator_map.py b/frappe/database/operator_map.py index 2c8b53dae3a7..d98f46d758e2 100644 --- a/frappe/database/operator_map.py +++ b/frappe/database/operator_map.py @@ -2,7 +2,7 @@ # MIT License. See license.txt import operator -from typing import Callable +from collections.abc import Callable import frappe from frappe.database.utils import NestedSetHierarchy diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 05899790d67d..2bf869983824 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: @@ -277,16 +282,16 @@ def create_auth_table(self): ) def create_global_search_table(self): - if not "__global_search" in self.get_tables(): + if "__global_search" not in self.get_tables(): self.sql( - """create table "__global_search"( + f"""create table "__global_search"( doctype varchar(100), - name varchar({0}), - title varchar({0}), + name varchar({self.VARCHAR_LEN}), + title varchar({self.VARCHAR_LEN}), content text, - route varchar({0}), + route varchar({self.VARCHAR_LEN}), published int not null default 0, - unique (doctype, name))""".format(self.VARCHAR_LEN) + unique (doctype, name))""" ) def create_user_settings_table(self): @@ -314,7 +319,6 @@ def updatedb(self, doctype, meta=None): db_table = PostgresTable(doctype, meta) db_table.validate() - self.commit() db_table.sync() self.begin() @@ -329,11 +333,11 @@ def check_implicit_commit(self, query): def has_index(self, table_name, index_name): return self.sql( - """SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' - and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name) + f"""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' + and indexname='{index_name}' limit 1""" ) - def add_index(self, doctype: str, fields: list, index_name: str = None): + def add_index(self, doctype: str, fields: list, index_name: str | None = None): """Creates an index with given fields if not already created. Index name will be `fieldname1_fieldname2_index`""" table_name = get_table_name(doctype) @@ -359,16 +363,15 @@ def add_unique(self, doctype, fields, constraint_name=None): ): self.commit() self.sql( - """ALTER TABLE `tab%s` - ADD CONSTRAINT %s UNIQUE (%s)""" - % (doctype, constraint_name, ", ".join(fields)) + """ALTER TABLE `tab{}` + ADD CONSTRAINT {} UNIQUE ({})""".format(doctype, constraint_name, ", ".join(fields)) ) def get_table_columns_description(self, table_name): """Returns list of column and its description""" # pylint: disable=W1401 return self.sql( - """ + f""" SELECT a.column_name AS name, CASE LOWER(a.data_type) WHEN 'character varying' THEN CONCAT('varchar(', a.character_maximum_length ,')') @@ -388,7 +391,7 @@ def get_table_columns_description(self, table_name): ON SUBSTRING(b.indexdef, '(.*)') LIKE CONCAT('%', a.column_name, '%') WHERE a.table_name = '{table_name}' GROUP BY a.column_name, a.data_type, a.column_default, a.character_maximum_length; - """.format(table_name=table_name), + """, as_dict=1, ) @@ -416,7 +419,7 @@ def modify_query(query): def modify_values(values): def modify_value(value): - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): value = tuple(modify_values(value)) elif isinstance(value, int): @@ -430,7 +433,7 @@ def modify_value(value): if isinstance(values, dict): for k, v in values.items(): values[k] = modify_value(v) - elif isinstance(values, (tuple, list)): + elif isinstance(values, tuple | list): new_values = [] for val in values: new_values.append(modify_value(val)) diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql index bc394491130f..e6e0ecbd1f47 100644 --- a/frappe/database/postgres/framework_postgres.sql +++ b/frappe/database/postgres/framework_postgres.sql @@ -179,6 +179,7 @@ CREATE TABLE "tabDocType" ( "idx" bigint NOT NULL DEFAULT 0, "search_fields" varchar(255) DEFAULT NULL, "issingle" smallint NOT NULL DEFAULT 0, + "is_virtual" smallint NOT NULL DEFAULT 0, "is_tree" smallint NOT NULL DEFAULT 0, "istable" smallint NOT NULL DEFAULT 0, "editable_grid" smallint NOT NULL DEFAULT 1, diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index c586940caff7..24237276c09d 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -54,16 +54,14 @@ def create(self): def create_indexes(self): create_index_query = "" - for key, col in self.columns.items(): + for _key, col in self.columns.items(): if ( col.set_index and col.fieldtype in frappe.db.type_map and frappe.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext") ): create_index_query += ( - 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( - index_name=col.fieldname, table_name=self.table_name, field=col.fieldname - ) + f'CREATE INDEX IF NOT EXISTS "{col.fieldname}" ON `{self.table_name}`(`{col.fieldname}`);' ) if create_index_query: # nosemgrep @@ -118,9 +116,7 @@ def alter(self): for col in self.add_index: # if index key not exists create_contraint_query += ( - 'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}`(`{field}`);'.format( - index_name=col.fieldname, table_name=self.table_name, field=col.fieldname - ) + f'CREATE INDEX IF NOT EXISTS "{col.fieldname}" ON `{self.table_name}`(`{col.fieldname}`);' ) for col in self.add_unique: diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 267e26d4ede4..c997f094b04b 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -11,7 +11,7 @@ def setup_database(force, source_sql=None, verbose=False): root_conn.sql(f"DROP USER IF EXISTS {frappe.conf.db_name}") root_conn.sql(f"CREATE DATABASE `{frappe.conf.db_name}`") root_conn.sql(f"CREATE user {frappe.conf.db_name} password '{frappe.conf.db_password}'") - root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name)) + root_conn.sql(f"GRANT ALL PRIVILEGES ON DATABASE `{frappe.conf.db_name}` TO {frappe.conf.db_name}") root_conn.close() bootstrap_database(frappe.conf.db_name, verbose, source_sql=source_sql) @@ -54,7 +54,7 @@ def import_db_from_sql(source_sql=None, verbose=False): _command = ( f"psql {frappe.conf.db_name} " - f"-h {frappe.conf.db_host or 'localhost'} -p {str(frappe.conf.db_port or '5432')} " + f"-h {frappe.conf.db_host or 'localhost'} -p {frappe.conf.db_port or '5432'!s} " f"-U {frappe.conf.db_name}" ) @@ -79,7 +79,7 @@ def setup_help_database(help_db_name): root_conn.sql(f"DROP USER IF EXISTS {help_db_name}") root_conn.sql(f"CREATE DATABASE `{help_db_name}`") root_conn.sql(f"CREATE user {help_db_name} password '{help_db_name}'") - root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(help_db_name)) + root_conn.sql(f"GRANT ALL PRIVILEGES ON DATABASE `{help_db_name}` TO {help_db_name}") def get_root_connection(root_login=None, root_password=None): diff --git a/frappe/database/query.py b/frappe/database/query.py index b42054dd6d9a..7321063f7678 100644 --- a/frappe/database/query.py +++ b/frappe/database/query.py @@ -47,6 +47,8 @@ def get_query( delete: bool = False, *, 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" @@ -85,7 +87,7 @@ def get_query( self.query = self.query.distinct() if for_update: - self.query = self.query.for_update() + self.query = self.query.for_update(skip_locked=skip_locked, nowait=not wait) if group_by: self.query = self.query.groupby(group_by) @@ -100,7 +102,7 @@ def apply_fields(self, fields): # add fields self.fields = self.parse_fields(fields) if not self.fields: - self.fields = [getattr(self.table, "name")] + self.fields = [self.table.name] self.query._child_queries = [] for field in self.fields: @@ -118,7 +120,7 @@ def apply_filters( if filters is None: return - if isinstance(filters, (str, int)): + if isinstance(filters, str | int): filters = {"name": str(filters)} if isinstance(filters, Criterion): @@ -127,14 +129,14 @@ def apply_filters( elif isinstance(filters, dict): self.apply_dict_filters(filters) - elif isinstance(filters, (list, tuple)): - if all(isinstance(d, (str, int)) for d in filters) and len(filters) > 0: + elif isinstance(filters, list | tuple): + if all(isinstance(d, str | int) for d in filters) and len(filters) > 0: self.apply_dict_filters({"name": ("in", filters)}) else: for filter in filters: - if isinstance(filter, (str, int, Criterion, dict)): + if isinstance(filter, str | int | Criterion | dict): self.apply_filters(filter) - elif isinstance(filter, (list, tuple)): + elif isinstance(filter, list | tuple): self.apply_list_filters(filter) def apply_list_filters(self, filter: list): @@ -151,7 +153,7 @@ def apply_list_filters(self, filter: list): def apply_dict_filters(self, filters: dict[str, str | int | list]): for field, value in filters.items(): operator = "=" - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): operator, value = value self._apply_filter(field, value, operator) @@ -188,7 +190,7 @@ def _apply_filter( if isinstance(_value, bool): _value = int(_value) - elif not _value and isinstance(_value, (list, tuple)): + elif not _value and isinstance(_value, list | tuple): _value = ("",) # Nested set @@ -280,7 +282,7 @@ def _sanitize_field(field: str): return MARIADB_SPECIFIC_COMMENT.sub("", stripped_field) return stripped_field - if isinstance(fields, (list, tuple)): + if isinstance(fields, list | tuple): return [_sanitize_field(field) for field in fields] elif isinstance(fields, str): return _sanitize_field(fields) @@ -305,10 +307,10 @@ def parse_fields(self, fields: str | list | tuple | None) -> list: if not fields: return [] fields = self.sanitize_fields(fields) - if isinstance(fields, (list, tuple, set)) and None in fields and Field not in fields: + if isinstance(fields, list | tuple | set) and None in fields and Field not in fields: return [] - if not isinstance(fields, (list, tuple)): + if not isinstance(fields, list | tuple): fields = [fields] def parse_field(field: str): @@ -505,7 +507,7 @@ def get_query(self, parent_names=None) -> QueryBuilder: } return frappe.qb.get_query( self.doctype, - fields=self.fields + ["parent", "parentfield"], + fields=[*self.fields, "parent", "parentfield"], filters=filters, order_by="idx asc", ) diff --git a/frappe/database/schema.py b/frappe/database/schema.py index cfaaaadfb1b1..fe7b2c93c51a 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -47,7 +47,7 @@ def create(self): pass def get_column_definitions(self): - column_list = [] + frappe.db.DEFAULT_COLUMNS + column_list = [*frappe.db.DEFAULT_COLUMNS] ret = [] for k in list(self.columns): if k not in column_list: @@ -141,9 +141,7 @@ def validate(self): try: # check for truncation max_length = frappe.db.sql( - """SELECT MAX(CHAR_LENGTH(`{fieldname}`)) FROM `tab{doctype}`""".format( - fieldname=col.fieldname, doctype=self.doctype - ) + f"""SELECT MAX(CHAR_LENGTH(`{col.fieldname}`)) FROM `tab{self.doctype}`""" ) except frappe.db.InternalError as e: @@ -251,7 +249,7 @@ def build_for_alter_table(self, current_def): if (current_def["index"] and not self.set_index) and column_type not in ("text", "longtext"): self.table.drop_index.append(self) - elif (not current_def["index"] and self.set_index) and not (column_type in ("text", "longtext")): + elif (not current_def["index"] and self.set_index) and column_type not in ("text", "longtext"): self.table.add_index.append(self) def default_changed(self, current_def): diff --git a/frappe/defaults.py b/frappe/defaults.py index 2a19cfce5caf..2619a269163c 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -21,7 +21,7 @@ def get_user_default(key, user=None): d = user_defaults.get(key, None) if is_a_user_permission_key(key): - if d and isinstance(d, (list, tuple)) and len(d) == 1: + if d and isinstance(d, list | tuple) and len(d) == 1: # Use User Permission value when only when it has a single value d = d[0] else: @@ -31,7 +31,7 @@ def get_user_default(key, user=None): # If no default value is found, use the User Permission value d = user_permission_default - value = isinstance(d, (list, tuple)) and d[0] or d + value = isinstance(d, list | tuple) and d[0] or d if not_in_user_permission(key, value, user): return @@ -61,14 +61,14 @@ def get_user_default_as_list(key, user=None): d = user_defaults.get(key, None) if is_a_user_permission_key(key): - if d and isinstance(d, (list, tuple)) and len(d) == 1: + if d and isinstance(d, list | tuple) and len(d) == 1: # Use User Permission value when only when it has a single value d = [d[0]] else: d = user_defaults.get(frappe.scrub(key), None) - d = list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d)) + d = list(filter(None, (not isinstance(d, list | tuple)) and [d] or d)) # filter default values if not found in user permission values = [value for value in d if not not_in_user_permission(key, value)] @@ -137,7 +137,7 @@ def add_global_default(key, value): def get_global_default(key): d = get_defaults().get(key, None) - value = isinstance(d, (list, tuple)) and d[0] or d + value = isinstance(d, list | tuple) and d[0] or d if not_in_user_permission(key, value): return diff --git a/frappe/deferred_insert.py b/frappe/deferred_insert.py index 328d8dd55588..4795ab21af73 100644 --- a/frappe/deferred_insert.py +++ b/frappe/deferred_insert.py @@ -13,7 +13,7 @@ def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str): - if isinstance(records, (dict, list)): + if isinstance(records, dict | list): _records = json.dumps(records) else: _records = records diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index a0aa41ae0cab..a5f56e1c8325 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -420,7 +420,7 @@ def get_workspace_sidebar_items(): blocked_modules.append("Dummy Module") # adding None to allowed_domains to include pages without domain restriction - allowed_domains = [None] + frappe.get_active_domains() + allowed_domains = [None, *frappe.get_active_domains()] filters = { "restrict_to_domain": ["in", allowed_domains], @@ -558,11 +558,11 @@ def save_new_widget(doc, page, blocks, new_widgets): json_config = widgets and dumps(widgets, sort_keys=True, indent=4) # Error log body - log = """ - page: {} - config: {} - exception: {} - """.format(page, json_config, e) + log = f""" + page: {page} + config: {json_config} + exception: {e} + """ doc.log_error("Could not save customization", log) return False diff --git a/frappe/desk/doctype/console_log/console_log.json b/frappe/desk/doctype/console_log/console_log.json index b8ccf8c9b56d..a2374fe2a69e 100644 --- a/frappe/desk/doctype/console_log/console_log.json +++ b/frappe/desk/doctype/console_log/console_log.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-05 22:16:02.823955", + "modified": "2024-03-12 20:35:43.921009", "modified_by": "Administrator", "module": "Desk", "name": "Console Log", @@ -28,7 +28,6 @@ "permissions": [ { "create": 1, - "delete": 1, "email": 1, "export": 1, "print": 1, diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py index 7e20afb22f2e..5ccc246d74ce 100644 --- a/frappe/desk/doctype/console_log/console_log.py +++ b/frappe/desk/doctype/console_log/console_log.py @@ -1,9 +1,11 @@ # Copyright (c) 2020, Frappe Technologies and contributors # License: MIT. See LICENSE -# import frappe +import frappe from frappe.model.document import Document class ConsoleLog(Document): - pass + def after_delete(self): + # because on_trash can be bypassed + frappe.throw(frappe._("Console Logs can not be deleted")) diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 7f3e7c944d3d..a6080ce69169 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -113,9 +113,7 @@ def get_non_standard_warning_message(non_standard_docs_map): def get_html(docs, doctype): html = f"

{frappe.bold(doctype)}

" for doc in docs: - html += ''.format( - doctype=doctype, doc=doc - ) + html += f'' html += "
" return html diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index e1ab1d795948..83a16fe82dc5 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -55,7 +55,7 @@ def get_permission_query_conditions(user): module_condition = """`tabDashboard Chart`.`module` in ({allowed_modules}) or `tabDashboard Chart`.`module` is NULL""".format(allowed_modules=",".join(allowed_modules)) - return """ + return f""" ((`tabDashboard Chart`.`chart_type` in ('Count', 'Sum', 'Average') and {doctype_condition}) or @@ -63,11 +63,7 @@ def get_permission_query_conditions(user): and {report_condition})) and ({module_condition}) - """.format( - doctype_condition=doctype_condition, - report_condition=report_condition, - module_condition=module_condition, - ) + """ def has_permission(doc, ptype, user): @@ -248,9 +244,7 @@ def get_heatmap_chart_config(chart, filters, heatmap_year): doctype, fields=[ timestamp_field, - "{aggregate_function}({value_field})".format( - aggregate_function=aggregate_function, value_field=value_field - ), + f"{aggregate_function}({value_field})", ], filters=filters, group_by=f"date({datefield})", @@ -310,7 +304,7 @@ def get_result(data, timegrain, from_date, to_date, chart_type): result = [[date, 0] for date in dates] data_index = 0 if data: - for i, d in enumerate(result): + for _i, d in enumerate(result): count = 0 while data_index < len(data) and getdate(data[data_index][0]) <= d[0]: d[1] += data[data_index][1] diff --git a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py index 61dc8b6af439..3f2a757891fd 100644 --- a/frappe/desk/doctype/dashboard_settings/dashboard_settings.py +++ b/frappe/desk/doctype/dashboard_settings/dashboard_settings.py @@ -40,7 +40,7 @@ def save_chart_config(reset, config, chart_name): chart_config[chart_name] = {} else: config = frappe.parse_json(config) - if not chart_name in chart_config: + if chart_name not in chart_config: chart_config[chart_name] = {} chart_config[chart_name].update(config) diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py index 2b4d050c1f7a..3aba14baadf5 100644 --- a/frappe/desk/doctype/desktop_icon/desktop_icon.py +++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py @@ -193,7 +193,7 @@ def add_user_icon(_doctype, _report=None, label=None, link=None, type="link", st icon_name = new_icon.name - except frappe.UniqueValidationError as e: + except frappe.UniqueValidationError: frappe.throw(_("Desktop Icon already exists")) except Exception as e: raise e diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 782202bc5bb7..e521ca38c853 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -180,9 +180,7 @@ def delete_communication(event, reference_doctype, reference_docname): def get_permission_query_conditions(user): if not user: user = frappe.session.user - return """(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`={user})""".format( - user=frappe.db.escape(user), - ) + return f"""(`tabEvent`.`event_type`='Public' or `tabEvent`.`owner`={frappe.db.escape(user)})""" def has_permission(doc, user): diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py index ed86ff3fbb24..a7db76da0cbe 100644 --- a/frappe/desk/doctype/kanban_board/kanban_board.py +++ b/frappe/desk/doctype/kanban_board/kanban_board.py @@ -33,9 +33,7 @@ def get_permission_query_conditions(user): if user == "Administrator": return "" - return """(`tabKanban Board`.private=0 or `tabKanban Board`.owner={user})""".format( - user=frappe.db.escape(user) - ) + return f"""(`tabKanban Board`.private=0 or `tabKanban Board`.owner={frappe.db.escape(user)})""" def has_permission(doc, ptype, user): diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py index 4a1787a33fc1..7a9b1f71c6fe 100644 --- a/frappe/desk/doctype/list_view_settings/list_view_settings.py +++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py @@ -79,7 +79,7 @@ def get_default_listview_fields(doctype): fields = [f.get("fieldname") for f in doctype_json.get("fields") if f.get("in_list_view")] if meta.title_field: - if not meta.title_field.strip() in fields: + if meta.title_field.strip() not in fields: fields.append(meta.title_field.strip()) return fields diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 7c066c9bdb6c..e901a347ce82 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -72,11 +72,11 @@ def get_permission_query_conditions(user=None): module_condition = """`tabNumber Card`.`module` in ({allowed_modules}) or `tabNumber Card`.`module` is NULL""".format(allowed_modules=",".join(allowed_modules)) - return """ + return f""" {doctype_condition} and {module_condition} - """.format(doctype_condition=doctype_condition, module_condition=module_condition) + """ def has_permission(doc, ptype, user): @@ -112,11 +112,7 @@ def get_result(doc, filters, to_date=None): if function == "count": fields = [f"{function}(*) as result"] else: - fields = [ - "{function}({based_on}) as result".format( - function=function, based_on=doc.aggregate_function_based_on - ) - ] + fields = [f"{function}({doc.aggregate_function_based_on}) as result"] if not filters: filters = [] diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index fb88b1222981..cb979b552d71 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -77,7 +77,7 @@ def get_tags(self, dn): def add(self, dn, tag): """add a new user tag""" tl = self.get_tags(dn).split(",") - if not tag in tl: + if tag not in tl: tl.append(tag) if not frappe.db.exists("Tag", tag): frappe.get_doc({"doctype": "Tag", "name": tag}).insert(ignore_permissions=True) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index e3528c6d69ec..a73fcdb3aac5 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -247,6 +247,7 @@ def update_page(name, title, icon, parent, public): ) if doc: + child_docs = frappe.get_all("Workspace", filters={"parent_page": doc.title, "public": doc.public}) doc.title = title doc.icon = icon doc.parent_page = parent @@ -261,7 +262,6 @@ def update_page(name, title, icon, parent, public): rename_doc("Workspace", name, new_name, force=True, ignore_permissions=True) # update new name and public in child pages - child_docs = frappe.get_all("Workspace", filters={"parent_page": doc.title, "public": doc.public}) if child_docs: for child in child_docs: child_doc = frappe.get_doc("Workspace", child.name) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 84c30abb264c..0e489ee548db 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -134,7 +134,7 @@ def get_doctype_references(self, doctype): def get_document_sources(self): """Returns list of doctypes from where we access submittable documents.""" - return list(set(self.get_link_sources() + [self.root_doctype])) + return list(set([*self.get_link_sources(), self.root_doctype])) def get_link_sources(self): """limit doctype links to these doctypes.""" @@ -149,15 +149,15 @@ def get_submittable_doctypes(self) -> list[str]: return self._submittable_doctypes -def get_child_tables_of_doctypes(doctypes: list[str] = None): +def get_child_tables_of_doctypes(doctypes: list[str] | None = None): """Returns child tables by doctype.""" filters = [["fieldtype", "=", "Table"]] filters_for_docfield = filters filters_for_customfield = filters if doctypes: - filters_for_docfield = filters + [["parent", "in", tuple(doctypes)]] - filters_for_customfield = filters + [["dt", "in", tuple(doctypes)]] + filters_for_docfield = [*filters, ["parent", "in", tuple(doctypes)]] + filters_for_customfield = [*filters, ["dt", "in", tuple(doctypes)]] links = frappe.get_all( "DocField", @@ -184,7 +184,7 @@ def get_child_tables_of_doctypes(doctypes: list[str] = None): def get_references_across_doctypes( - to_doctypes: list[str] = None, limit_link_doctypes: list[str] = None + to_doctypes: list[str] | None = None, limit_link_doctypes: list[str] | None = None ) -> list: """Find doctype wise foreign key references. @@ -214,14 +214,14 @@ def get_references_across_doctypes( for k, v in references_by_dlink_fields.items(): references.setdefault(k, []).extend(v) - for doctype, links in references.items(): + for _doctype, links in references.items(): for link in links: link["is_child"] = link["doctype"] in all_child_tables return references def get_references_across_doctypes_by_link_field( - to_doctypes: list[str] = None, limit_link_doctypes: list[str] = None + to_doctypes: list[str] | None = None, limit_link_doctypes: list[str] | None = None ): """Find doctype wise foreign key references based on link fields. @@ -261,7 +261,7 @@ def get_references_across_doctypes_by_link_field( def get_references_across_doctypes_by_dynamic_link_field( - to_doctypes: list[str] = None, limit_link_doctypes: list[str] = None + to_doctypes: list[str] | None = None, limit_link_doctypes: list[str] | None = None ): """Find doctype wise foreign key references based on dynamic link fields. @@ -315,7 +315,7 @@ def get_referencing_documents( reference_names: list[str], link_info: dict, get_parent_if_child_table_doc: bool = True, - parent_filters: list[list] = None, + parent_filters: list[list] | None = None, child_filters=None, allowed_parents=None, ): @@ -443,7 +443,7 @@ def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> di "fields", { "in_list_view": 1, - "fieldtype": ["not in", ("Image", "HTML", "Button") + frappe.model.table_fields], + "fieldtype": ["not in", ("Image", "HTML", "Button", *frappe.model.table_fields)], }, ) ] + ["name", "modified", "docstatus"] @@ -618,6 +618,10 @@ def get_linked_fields(doctype, without_ignore_user_permissions_enabled=False): if options in ret: del ret[options] + virtual_doctypes = frappe.get_all("DocType", {"is_virtual": 1}, pluck="name") + for dt in virtual_doctypes: + ret.pop(dt, None) + return ret @@ -644,7 +648,11 @@ def get_dynamic_linked_fields(doctype, without_ignore_user_permissions_enabled=F if is_single(df.doctype): continue - is_child = frappe.get_meta(df.doctype).istable + meta = frappe.get_meta(df.doctype) + if meta.is_virtual: + continue + + is_child = meta.istable possible_link = frappe.get_all( df.doctype, filters={df.doctype_fieldname: doctype}, diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 7ca1132e1809..ae7dce91ee83 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -261,7 +261,7 @@ def get_point_logs(doctype, docname): def _get_communications(doctype, name, start=0, limit=20): communications = get_communication_data(doctype, name, start, limit) for c in communications: - if c.communication_type == "Communication": + if c.communication_type in ("Communication", "Automated Message"): c.attachments = json.dumps( frappe.get_all( "File", @@ -290,9 +290,9 @@ def get_communication_data( conditions = "" if after: # find after a particular date - conditions += """ - AND C.communication_date > {} - """.format(after) + conditions += f""" + AND C.communication_date > {after} + """ if doctype == "User": conditions += """ @@ -300,23 +300,23 @@ def get_communication_data( """ # communications linked to reference_doctype - part1 = """ + part1 = f""" SELECT {fields} FROM `tabCommunication` as C WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message') AND (C.reference_doctype = %(doctype)s AND C.reference_name = %(name)s) {conditions} - """.format(fields=fields, conditions=conditions) + """ # communications linked in Timeline Links - part2 = """ + part2 = f""" SELECT {fields} FROM `tabCommunication` as C INNER JOIN `tabCommunication Link` ON C.name=`tabCommunication Link`.parent WHERE C.communication_type IN ('Communication', 'Feedback', 'Automated Message') AND `tabCommunication Link`.link_doctype = %(doctype)s AND `tabCommunication Link`.link_name = %(name)s {conditions} - """.format(fields=fields, conditions=conditions) + """ communications = frappe.db.sql( """ diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index 353dea305f4a..b93ebdb5bcf3 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -105,6 +105,4 @@ def get_next(doctype, value, prev, filters=None, sort_order="desc", sort_field=" def get_pdf_link(doctype, docname, print_format="Standard", no_letterhead=0): - return "/api/method/frappe.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}".format( - doctype=doctype, docname=docname, print_format=print_format, no_letterhead=no_letterhead - ) + return f"/api/method/frappe.utils.print_format.download_pdf?doctype={doctype}&name={docname}&format={print_format}&no_letterhead={no_letterhead}" diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index e74e01189e4a..7e238b454ea8 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -46,8 +46,6 @@ def get_energy_point_leaderboard(date_range, company=None, field=None, limit=Non for user in energy_point_users: user_id = user["name"] user["name"] = get_fullname(user["name"]) - user["formatted_name"] = '{}'.format( - user_id, get_fullname(user_id) - ) + user["formatted_name"] = f'{get_fullname(user_id)}' return energy_point_users diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index b688ce42942c..9bb765ab0a08 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -246,7 +246,7 @@ def get_filters_for(doctype): @frappe.whitelist() @frappe.read_only() -def get_open_count(doctype, name, items=None): +def get_open_count(doctype: str, name: str, items=None): """Get count for internal and external links for given transactions :param doctype: Reference DocType @@ -287,7 +287,7 @@ def get_open_count(doctype, name, items=None): try: external_links_data_for_d = get_external_links(d, name, links) out["external_links_found"].append(external_links_data_for_d) - except Exception as e: + except Exception: out["external_links_found"].append({"doctype": d, "open_count": 0, "count": 0}) else: external_links_data_for_d = get_external_links(d, name, links) diff --git a/frappe/desk/page/activity/activity.py b/frappe/desk/page/activity/activity.py index d22fa006a442..02adfd6cc36c 100644 --- a/frappe/desk/page/activity/activity.py +++ b/frappe/desk/page/activity/activity.py @@ -13,7 +13,7 @@ def get_feed(start, page_length): match_conditions_comment = get_feed_match_conditions(frappe.session.user, "Comment") result = frappe.db.sql( - """select X.* + f"""select X.* from (select name, owner, modified, creation, seen, comment_type, reference_doctype, reference_name, '' as link_doctype, '' as link_name, subject, communication_type, communication_medium, content @@ -40,10 +40,7 @@ def get_feed(start, page_length): ) X order by X.creation DESC LIMIT %(page_length)s - OFFSET %(start)s""".format( - match_conditions_comment=match_conditions_comment, - match_conditions_communication=match_conditions_communication, - ), + OFFSET %(start)s""", {"user": frappe.session.user, "start": cint(start), "page_length": cint(page_length)}, as_dict=True, ) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index ff4fe68c1714..3fa27c7b3eea 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -132,7 +132,7 @@ def normalize_result(result, columns): # Converts to list of dicts from list of lists/tuples data = [] column_names = [column["fieldname"] for column in columns] - if result and isinstance(result[0], (list, tuple)): + if result and isinstance(result[0], list | tuple): for row in result: row_obj = {} for idx, column_name in enumerate(column_names): @@ -309,7 +309,7 @@ def get_report_data(doc, data): report_data = get_report_data(doc, data) except Exception as e: doc.log_error("Prepared report render failed") - frappe.msgprint(_("Prepared report render failed") + f": {str(e)}") + frappe.msgprint(_("Prepared report render failed") + f": {e!s}") doc = None return report_data | {"prepared_report": True, "doc": doc} @@ -385,7 +385,7 @@ def build_xlsx_data(data, visible_idx, include_indentation, ignore_visible_idx=F datetime.timedelta, ) - if len(visible_idx) == len(data.result): + if len(visible_idx) == len(data.result) or not visible_idx: # It's not possible to have same length and different content. ignore_visible_idx = True else: @@ -501,7 +501,7 @@ def get_data_for_custom_field(doctype, field, names=None): filters = {} if names: - if isinstance(names, (str, bytearray)): + if isinstance(names, str | bytearray): names = frappe.json.loads(names) filters.update({"name": ["in", names]}) @@ -654,7 +654,7 @@ def has_match( cell_value = None if isinstance(row, dict): cell_value = row.get(idx) - elif isinstance(row, (list, tuple)): + elif isinstance(row, list | tuple): cell_value = row[idx] if ( @@ -686,10 +686,10 @@ def get_linked_doctypes(columns, data): columns_dict = get_columns_dict(columns) - for idx, col in enumerate(columns): + for idx, _ in enumerate(columns): # noqa: F402 df = columns_dict[idx] if df.get("fieldtype") == "Link": - if data and isinstance(data[0], (list, tuple)): + if data and isinstance(data[0], list | tuple): linked_doctypes[df["options"]] = idx else: # dict @@ -700,7 +700,7 @@ def get_linked_doctypes(columns, data): for row in data: if row: if len(row) != len(columns_with_value): - if isinstance(row, (list, tuple)): + if isinstance(row, list | tuple): row = enumerate(row) elif isinstance(row, dict): row = row.items() diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d193570112a7..c0a2eb12aa78 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -14,7 +14,8 @@ from frappe.model.base_document import get_controller from frappe.model.db_query import DatabaseQuery from frappe.model.utils import is_virtual_doctype -from frappe.utils import add_user_info, cstr, format_duration +from frappe.utils import add_user_info, cint, cstr, format_duration +from frappe.utils.data import sbool @frappe.whitelist() @@ -52,13 +53,23 @@ def get_count(): if is_virtual_doctype(args.doctype): controller = get_controller(args.doctype) - data = controller.get_count(args) + count = controller.get_count(args) else: - distinct = "distinct " if args.distinct == "true" else "" - args.fields = [f"count({distinct}`tab{args.doctype}`.name) as total_count"] - data = execute(**args)[0].get("total_count") + args.distinct = sbool(args.distinct) + distinct = "distinct " if args.distinct else "" + args.limit = cint(args.limit) + fieldname = f"{distinct}`tab{args.doctype}`.name" + args.order_by = None + + if args.limit: + args.fields = [fieldname] + partial_query = execute(**args, run=0) + count = frappe.db.sql(f"""select count(*) from ( {partial_query} ) p""")[0][0] + else: + args.fields = [f"count({fieldname}) as total_count"] + count = execute(**args)[0].get("total_count") - return data + return count def execute(doctype, *args, **kwargs): @@ -200,7 +211,7 @@ def get_meta_and_docfield(fieldname, data): def update_wildcard_field_param(data): if (isinstance(data.fields, str) and data.fields == "*") or ( - isinstance(data.fields, (list, tuple)) and len(data.fields) == 1 and data.fields[0] == "*" + isinstance(data.fields, list | tuple) and len(data.fields) == 1 and data.fields[0] == "*" ): if frappe.get_system_settings("apply_perm_level_on_api_calls"): data.fields = get_permitted_fields(data.doctype, parenttype=data.parenttype) @@ -374,9 +385,9 @@ def export_query(): if add_totals_row: ret = append_totals_row(ret) - data = [[_("Sr")] + get_labels(db_query.fields, doctype)] + data = [[_("Sr"), *get_labels(db_query.fields, doctype)]] for i, row in enumerate(ret): - data.append([i + 1] + list(row)) + data.append([i + 1, *list(row)]) data = handle_duration_fieldtype_values(doctype, data, db_query.fields) @@ -416,10 +427,10 @@ def append_totals_row(data): for row in data: for i in range(len(row)): - if isinstance(row[i], (float, int)): + if isinstance(row[i], float | int): totals[i] = (totals[i] or 0) + row[i] - if not isinstance(totals[0], (int, float)): + if not isinstance(totals[0], int | float): totals[0] = "Total" data.append(totals) @@ -559,7 +570,7 @@ def get_stats(stats, doctype, filters=None): tag_count = frappe.get_list( doctype, fields=[column, "count(*)"], - filters=filters + [[column, "!=", ""]], + filters=[*filters, [column, "!=", ""]], group_by=column, as_list=True, distinct=1, @@ -570,7 +581,7 @@ def get_stats(stats, doctype, filters=None): no_tag_count = frappe.get_list( doctype, fields=[column, "count(*)"], - filters=filters + [[column, "in", ("", ",")]], + filters=[*filters, [column, "in", ("", ",")]], as_list=True, group_by=column, order_by=column, @@ -584,7 +595,7 @@ def get_stats(stats, doctype, filters=None): except frappe.db.SQLError: pass - except frappe.db.InternalError as e: + except frappe.db.InternalError: # raised when _user_tags column is added on the fly pass @@ -602,14 +613,14 @@ def get_filter_dashboard_data(stats, doctype, filters=None): columns = frappe.db.get_table_columns(doctype) for tag in tags: - if not tag["name"] in columns: + if tag["name"] not in columns: continue tagcount = [] if tag["type"] not in ["Date", "Datetime"]: tagcount = frappe.get_list( doctype, fields=[tag["name"], "count(*)"], - filters=filters + ["ifnull(`%s`,'')!=''" % tag["name"]], + filters=[*filters, "ifnull(`%s`,'')!=''" % tag["name"]], group_by=tag["name"], as_list=True, ) @@ -631,7 +642,7 @@ def get_filter_dashboard_data(stats, doctype, filters=None): frappe.get_list( doctype, fields=[tag["name"], "count(*)"], - filters=filters + ["({0} = '' or {0} is null)".format(tag["name"])], + filters=[*filters, "({0} = '' or {0} is null)".format(tag["name"])], as_list=True, )[0][1], ] @@ -693,7 +704,7 @@ def get_filters_cond(doctype, filters, conditions, ignore_permissions=None, with for f in filters: if isinstance(f[1], str) and f[1][0] == "!": flt.append([doctype, f[0], "!=", f[1][1:]]) - elif isinstance(f[1], (list, tuple)) and f[1][0].lower() in ( + elif isinstance(f[1], list | tuple) and f[1][0].lower() in ( "=", ">", "<", diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 8d298e289829..c4bb5e1fb39b 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -132,7 +132,7 @@ def search_widget( filters_items = filters.items() filters = [] for f in filters_items: - if isinstance(f[1], (list, tuple)): + if isinstance(f[1], list | tuple): filters.append([doctype, f[0], f[1][0], f[1][1]]) else: filters.append([doctype, f[0], "=", f[1]]) @@ -283,6 +283,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: 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 63cd72a7bc6b..8eed8beca7b8 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -258,7 +258,7 @@ def send_daily(): continue try: auto_email_report.send() - except Exception as e: + except Exception: auto_email_report.log_error(f"Failed to send {auto_email_report.name} Auto Email Report") @@ -282,7 +282,9 @@ def make_links(columns, data): if col.options and row.get(col.options): row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) elif col.fieldtype == "Currency": - doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.get("parent") else None + doc = None + if doc_name and col.get("parent") and not frappe.get_meta(col.parent).istable: + doc = frappe.get_doc(col.parent, doc_name) # Pass the Document to get the currency based on docfield option row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc) return columns, data diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 833c88b3e322..24a755b30ab0 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -35,14 +35,14 @@ def wrapper_cache_email_account(*args, **kwargs): setattr(frappe.local, cache_name, {}) cached_accounts = getattr(frappe.local, cache_name) - match_by = list(kwargs.values()) + ["default"] + match_by = [*list(kwargs.values()), "default"] matched_accounts = list(filter(None, [cached_accounts.get(key) for key in match_by])) if matched_accounts: return matched_accounts[0] matched_accounts = func(*args, **kwargs) cached_accounts.update(matched_accounts or {}) - return matched_accounts and list(matched_accounts.values())[0] + return matched_accounts and next(iter(matched_accounts.values())) return wrapper_cache_email_account diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py index aa732c0f96ef..ef46ab19292d 100644 --- a/frappe/email/doctype/email_account/test_email_account.py +++ b/frappe/email/doctype/email_account/test_email_account.py @@ -413,10 +413,13 @@ def test_append_to_with_imap_folders(self): @patch("frappe.email.receive.EmailServer.select_imap_folder", return_value=True) @patch("frappe.email.receive.EmailServer.logout", side_effect=lambda: None) def mocked_get_inbound_mails( - email_account, messages={}, mocked_logout=None, mocked_select_imap_folder=None + email_account, messages=None, mocked_logout=None, mocked_select_imap_folder=None ): from frappe.email.receive import EmailServer + if messages is None: + messages = {} + def get_mocked_messages(**kwargs): return messages.get(kwargs["folder"], {}) @@ -427,7 +430,12 @@ def get_mocked_messages(**kwargs): @patch("frappe.email.receive.EmailServer.select_imap_folder", return_value=True) @patch("frappe.email.receive.EmailServer.logout", side_effect=lambda: None) - def mocked_email_receive(email_account, messages={}, mocked_logout=None, mocked_select_imap_folder=None): + def mocked_email_receive( + email_account, messages=None, mocked_logout=None, mocked_select_imap_folder=None + ): + if messages is None: + messages = {} + def get_mocked_messages(**kwargs): return messages.get(kwargs["folder"], {}) diff --git a/frappe/email/doctype/email_group/email_group.py b/frappe/email/doctype/email_group/email_group.py index a8b766638689..5aed818afe00 100755 --- a/frappe/email/doctype/email_group/email_group.py +++ b/frappe/email/doctype/email_group/email_group.py @@ -19,11 +19,11 @@ def onload(self): def import_from(self, doctype): """Extract Email Addresses from given doctype and add them to the current list""" meta = frappe.get_meta(doctype) - email_field = [ + email_field = next( d.fieldname for d in meta.fields if d.fieldtype in ("Data", "Small Text", "Text", "Code") and d.options == "Email" - ][0] + ) unsubscribed_field = "unsubscribed" if meta.get_field("unsubscribed") else None added = 0 @@ -90,7 +90,7 @@ def import_from(name, doctype): @frappe.whitelist() def add_subscribers(name, email_list): - if not isinstance(email_list, (list, tuple)): + if not isinstance(email_list, list | tuple): email_list = email_list.replace(",", "\n").split("\n") template = frappe.db.get_value("Email Group", name, "welcome_email_template") diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py index 11faf417309e..ddb01192d9d6 100644 --- a/frappe/email/doctype/email_queue/email_queue.py +++ b/frappe/email/doctype/email_queue/email_queue.py @@ -347,11 +347,34 @@ def include_attachments(self, message): elif attachment.get("print_format_attachment") == 1: attachment.pop("print_format_attachment", None) print_format_file = frappe.attach_print(**attachment) + self._store_file(print_format_file["fname"], print_format_file["fcontent"]) print_format_file.update({"parent": message_obj}) add_attachment(**print_format_file) return safe_encode(message_obj.as_string()) + def _store_file(self, file_name, content): + if not frappe.get_system_settings("store_attached_pdf_document"): + return + + file_data = frappe._dict(file_name=file_name, is_private=1) + + # Store on communication if available, else email queue doc + if self.queue_doc.communication: + file_data.attached_to_doctype = "Communication" + file_data.attached_to_name = self.queue_doc.communication + else: + file_data.attached_to_doctype = self.queue_doc.doctype + file_data.attached_to_name = self.queue_doc.name + + if frappe.db.exists("File", file_data): + return + + file = frappe.new_doc("File") + file.update(file_data) + file.content = content + file.insert() + @frappe.whitelist() def bulk_retry(queues): @@ -683,7 +706,7 @@ def send_emails(self, queue_data, final_recipients): # This re-uses smtp server instance to minimize the cost of new session creation smtp_server_instance = None for r in final_recipients: - recipients = list(set([r] + self.final_cc() + self.bcc)) + recipients = list(set([r, *self.final_cc(), *self.bcc])) q = EmailQueue.new({**queue_data, **{"recipients": recipients}}, ignore_permissions=True) if not smtp_server_instance: email_account = q.get_email_account(raise_error=True) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index f11536cbd107..7b0fa3c412cb 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -229,9 +229,7 @@ def get_messages(self, folder="INBOX"): self.pop.dele(m) except Exception as e: - if self.has_login_limit_exceeded(e): - pass - else: + if not self.has_login_limit_exceeded(e): raise out = {"latest_messages": self.latest_messages} @@ -373,7 +371,7 @@ def get_email_seen_status(self, uid, flag_string): self.seen_status.update({uid: "UNSEEN"}) def has_login_limit_exceeded(self, e): - return "-ERR Exceeded the login limit" in strip(cstr(e.message)) + return "-ERR Exceeded the login limit" in strip(cstr(e)) def is_temporary_system_problem(self, e): messages = ( @@ -741,7 +739,7 @@ def replace_inline_images(self, attachments): # replace inline images content = self.content for file in attachments: - if file.name in self.cid_map and self.cid_map[file.name]: + if self.cid_map.get(file.name): content = content.replace(f"cid:{self.cid_map[file.name]}", file.unique_url) return content @@ -927,7 +925,7 @@ def clean_subject(subject): """Remove Prefixes like 'fw', FWD', 're' etc from subject.""" # Match strings like "fw:", "re :" etc. regex = r"(^\s*(fw|fwd|wg)[^:]*:|\s*(re|aw)[^:]*:\s*)*" - return frappe.as_unicode(strip(re.sub(regex, "", subject, 0, flags=re.IGNORECASE))) + return frappe.as_unicode(strip(re.sub(regex, "", subject, count=0, flags=re.IGNORECASE))) @staticmethod def get_email_fields(doctype): diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 82a2fa6f1c78..0e5a268177b9 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import smtplib +from contextlib import suppress import frappe from frappe import _ @@ -69,7 +70,7 @@ def session(self): SMTP = smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP try: - _session = SMTP(self.server, self.port) + _session = SMTP(self.server, self.port, timeout=2 * 60) if not _session: frappe.msgprint( _("Could not connect to outgoing email server"), raise_exception=frappe.OutgoingEmailError @@ -108,8 +109,9 @@ def is_session_active(self): return False def quit(self): - if self.is_session_active(): - self._session.quit() + with suppress(TimeoutError): + if self.is_session_active(): + self._session.quit() @classmethod def throw_invalid_credentials_exception(cls): diff --git a/frappe/event_streaming/doctype/event_consumer/event_consumer.py b/frappe/event_streaming/doctype/event_consumer/event_consumer.py index c2379a381581..596ae560c43a 100644 --- a/frappe/event_streaming/doctype/event_consumer/event_consumer.py +++ b/frappe/event_streaming/doctype/event_consumer/event_consumer.py @@ -214,6 +214,6 @@ def has_consumer_access(consumer, update_log): return frappe.call(cmd, **args) else: return frappe.safe_eval(condition, frappe._dict(doc=doc)) - except Exception as e: + except Exception: consumer.log_error("has_consumer_access error") return False diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index 0dd64be1ce1f..44a7526083a7 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -354,7 +354,7 @@ def set_update(update, producer_site): def update_row_removed(local_doc, removed): """Sync child table row deletion type update""" for tablename, rownames in removed.items(): - table = local_doc.get_table_field_doctype(tablename) + local_doc.get_table_field_doctype(tablename) for row in rownames: table_rows = local_doc.get(tablename) child_table_row = get_child_table_row(table_rows, row) diff --git a/frappe/event_streaming/doctype/event_producer/test_event_producer.py b/frappe/event_streaming/doctype/event_producer/test_event_producer.py index d015147f9a39..15fd48305246 100644 --- a/frappe/event_streaming/doctype/event_producer/test_event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/test_event_producer.py @@ -124,7 +124,7 @@ def test_conditional_events(self): # Add Condition event_producer = frappe.get_doc("Event Producer", producer_url) - note_producer_entry = [x for x in event_producer.producer_doctypes if x.ref_doctype == "Note"][0] + note_producer_entry = next(x for x in event_producer.producer_doctypes if x.ref_doctype == "Note") note_producer_entry.condition = "doc.public == 1" event_producer.save() @@ -160,7 +160,7 @@ def test_conditional_events_with_cmd(self): # Add Condition event_producer = frappe.get_doc("Event Producer", producer_url) - note_producer_entry = [x for x in event_producer.producer_doctypes if x.ref_doctype == "Note"][0] + note_producer_entry = next(x for x in event_producer.producer_doctypes if x.ref_doctype == "Note") note_producer_entry.condition = ( "cmd: frappe.event_streaming.doctype.event_producer.test_event_producer.can_sync_note" ) diff --git a/frappe/event_streaming/doctype/event_update_log/event_update_log.py b/frappe/event_streaming/doctype/event_update_log/event_update_log.py index 71285578d4ed..6eeeadecc015 100644 --- a/frappe/event_streaming/doctype/event_update_log/event_update_log.py +++ b/frappe/event_streaming/doctype/event_update_log/event_update_log.py @@ -25,7 +25,7 @@ def notify_consumers(doc, event): if frappe.flags.in_install or frappe.flags.in_migrate: return - if consumers := check_doctype_has_consumers(doc.doctype): + if check_doctype_has_consumers(doc.doctype): if event == "after_insert": doc.flags.event_update_log = make_event_update_log(doc, update_type="Create") elif event == "on_trash": diff --git a/frappe/frappeclient.py b/frappe/frappeclient.py index feec802fc1cd..7470c487b2d5 100644 --- a/frappe/frappeclient.py +++ b/frappe/frappeclient.py @@ -356,7 +356,7 @@ def post_request(self, data): def preprocess(self, params): """convert dicts, lists to json""" for key, value in params.items(): - if isinstance(value, (dict, list)): + if isinstance(value, dict | list): params[key] = json.dumps(value) return params diff --git a/frappe/handler.py b/frappe/handler.py index 2253c161e89a..c2f1342ed3bf 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -191,7 +191,8 @@ def upload_file(): optimize = frappe.form_dict.optimize content = None - if frappe.form_dict.get("library_file_name", False): + if library_file := frappe.form_dict.get("library_file_name"): + frappe.has_permission("File", doc=library_file, throw=True) doc = frappe.get_value( "File", frappe.form_dict.library_file_name, diff --git a/frappe/hooks.py b/frappe/hooks.py index bcd0d8848d48..98d4d8381c09 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -161,6 +161,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", @@ -278,7 +279,7 @@ "frappe.desk.page.setup_wizard.setup_wizard.log_setup_wizard_exception", ] -before_migrate = [] +before_migrate = ["frappe.core.doctype.patch_log.patch_log.before_migrate"] after_migrate = ["frappe.website.doctype.website_theme.website_theme.after_migrate"] otp_methods = ["OTP App", "Email", "SMS"] @@ -407,6 +408,8 @@ "Unhandled Email", "Webhook Request Log", "Workspace", + "Route History", + "Access Log", ] # Request Hooks diff --git a/frappe/installer.py b/frappe/installer.py index 89b4ebfb1978..004ca5b02b04 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -210,7 +210,7 @@ def fetch_details_from_tag(_tag: str) -> tuple[str, str, str]: try: repo, tag = app_tag except ValueError: - repo, tag = app_tag + [None] + repo, tag = [*app_tag, None] try: org, repo = org_repo @@ -264,7 +264,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False): if app_hooks.required_apps: for app in app_hooks.required_apps: required_app = parse_app_name(app) - install_app(required_app, verbose=verbose, force=force) + install_app(required_app, verbose=verbose) frappe.flags.in_install = name frappe.clear_cache() @@ -353,6 +353,14 @@ def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False) click.secho(f"App {app_name} not installed on Site {site}", fg="yellow") return + # Don't allow uninstalling if we have dependent apps installed + for app in frappe.get_installed_apps(): + if app != app_name: + hooks = frappe.get_hooks(app_name=app) + if hooks.required_apps and any(app_name in required_app for required_app in hooks.required_apps): + click.secho(f"App {app_name} is a dependency of {app}. Uninstall {app} first.", fg="yellow") + return + print(f"Uninstalling App {app_name} from Site {site}...") if not dry_run and not yes: @@ -770,8 +778,6 @@ def is_downgrade(sql_file_path, verbose=False): from semantic_version import Version - head = "INSERT INTO `tabInstalled Application` VALUES" - with open(sql_file_path) as f: header = f.readline() # Example first line: @@ -812,7 +818,7 @@ def partial_restore(sql_file_path, verbose=False): " partial restore operation for PostreSQL databases", fg="yellow", ) - warnings.warn(warn) + warnings.warn(warn, stacklevel=1) import_db_from_sql(source_sql=sql_file, verbose=verbose) diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index cdfb585acfe0..2c0494f5cdba 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -85,7 +85,7 @@ def take_backup_to_dropbox(retry_count=0, upload_db_backup=True): if isinstance(error_log, str): error_message = error_log + "\n" + frappe.get_traceback() else: - file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log)] + file_and_error = [" - ".join(f) for f in zip(did_not_upload, error_log, strict=False)] error_message = "\n".join(file_and_error) + "\n" + frappe.get_traceback() send_email(False, "Dropbox", "Dropbox Settings", "send_notifications_to", error_message) diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 892657c61c16..499c9c58e9bc 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -144,7 +144,7 @@ def update_user_fields(cls, user: "User", user_data: dict): setattr(user, key, value) user.save(ignore_permissions=True) - def sync_roles(self, user: "User", additional_groups: list = None): + def sync_roles(self, user: "User", additional_groups: list | None = None): current_roles = {d.role for d in user.get("roles")} if self.default_user_type == "System User": needed_roles = {self.default_role} @@ -164,7 +164,7 @@ def sync_roles(self, user: "User", additional_groups: list = None): user.remove_roles(*roles_to_remove) - def create_or_update_user(self, user_data: dict, groups: list = None): + def create_or_update_user(self, user_data: dict, groups: list | None = None): user: "User" = None role: str = None diff --git a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py index 5893c8a404a4..b46a36d96e9e 100644 --- a/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/test_ldap_settings.py @@ -4,6 +4,7 @@ import functools import os import ssl +import typing from unittest import TestCase, mock import ldap3 @@ -18,7 +19,7 @@ class LDAP_TestCase: TEST_LDAP_SERVER = None # must match the 'LDAP Settings' field option TEST_LDAP_SEARCH_STRING = None LDAP_USERNAME_FIELD = None - DOCUMENT_GROUP_MAPPINGS = [] + DOCUMENT_GROUP_MAPPINGS: typing.ClassVar[list] = [] LDAP_SCHEMA = None LDAP_LDIF_JSON = None TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = None @@ -484,7 +485,7 @@ def test_create_or_update_user(self): @mock_ldap_connection def test_get_ldap_attributes(self): method_return = self.test_class.get_ldap_attributes() - self.assertTrue(type(method_return) is list) + self.assertTrue(isinstance(method_return, list)) @mock_ldap_connection def test_fetch_ldap_groups(self): @@ -594,14 +595,14 @@ def test_convert_ldap_entry_to_dict(self): test_ldap_entry = self.connection.entries[0] method_return = self.test_class.convert_ldap_entry_to_dict(test_ldap_entry) - self.assertTrue(type(method_return) is dict) # must be dict + self.assertTrue(isinstance(method_return, dict)) # must be dict self.assertTrue(len(method_return) == 6) # there are 6 fields in mock_ldap for use class Test_OpenLDAP(LDAP_TestCase, TestCase): TEST_LDAP_SERVER = "OpenLDAP" TEST_LDAP_SEARCH_STRING = "(uid={0})" - DOCUMENT_GROUP_MAPPINGS = [ + DOCUMENT_GROUP_MAPPINGS: typing.ClassVar[list] = [ { "doctype": "LDAP Group Mapping", "ldap_group": "Administrators", @@ -614,7 +615,7 @@ class Test_OpenLDAP(LDAP_TestCase, TestCase): LDAP_SCHEMA = OFFLINE_SLAPD_2_4 LDAP_LDIF_JSON = "test_data_ldif_openldap.json" - TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [ + TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING: typing.ClassVar[list] = [ "(uid={0})", "(&(objectclass=posixaccount)(uid={0}))", "(&(description=*ACCESS:test1*)(uid={0}))", # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf' @@ -625,7 +626,7 @@ class Test_OpenLDAP(LDAP_TestCase, TestCase): class Test_ActiveDirectory(LDAP_TestCase, TestCase): TEST_LDAP_SERVER = "Active Directory" TEST_LDAP_SEARCH_STRING = "(samaccountname={0})" - DOCUMENT_GROUP_MAPPINGS = [ + DOCUMENT_GROUP_MAPPINGS: typing.ClassVar[list] = [ { "doctype": "LDAP Group Mapping", "ldap_group": "Domain Administrators", @@ -642,7 +643,7 @@ class Test_ActiveDirectory(LDAP_TestCase, TestCase): LDAP_SCHEMA = OFFLINE_AD_2012_R2 LDAP_LDIF_JSON = "test_data_ldif_activedirectory.json" - TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING = [ + TEST_VALUES_LDAP_COMPLEX_SEARCH_STRING: typing.ClassVar[dict] = [ "(samaccountname={0})", "(&(objectclass=user)(samaccountname={0}))", "(&(description=*ACCESS:test1*)(samaccountname={0}))", # OpenLDAP has no member of group, use description to filter posix.user has equivilent of AD 'memberOf' diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index c8eedf23dad3..68520888df2a 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -55,7 +55,7 @@ def test_normal_signup_and_github_login(self): def make_social_login_key(**kwargs): kwargs["doctype"] = "Social Login Key" - if not "provider_name" in kwargs: + if "provider_name" not in kwargs: kwargs["provider_name"] = "Test OAuth2 Provider" doc = frappe.get_doc(kwargs) return doc diff --git a/frappe/integrations/frappe_providers/frappecloud.py b/frappe/integrations/frappe_providers/frappecloud.py index bae811d41d6d..b3b905bc0e37 100644 --- a/frappe/integrations/frappe_providers/frappecloud.py +++ b/frappe/integrations/frappe_providers/frappecloud.py @@ -12,11 +12,7 @@ def frappecloud_migrator(local_site): request = requests.get(request_url) if request.status_code / 100 != 2: - print( - "Request exitted with Status Code: {}\nPayload: {}".format( - request.status_code, html2text(request.text) - ) - ) + print(f"Request exitted with Status Code: {request.status_code}\nPayload: {html2text(request.text)}") click.secho( "Some errors occurred while recovering the migration script. Please contact us @ Frappe Cloud if this issue persists", fg="yellow", diff --git a/frappe/integrations/google_oauth.py b/frappe/integrations/google_oauth.py index 2070f292271a..92943b172769 100644 --- a/frappe/integrations/google_oauth.py +++ b/frappe/integrations/google_oauth.py @@ -39,7 +39,7 @@ def __init__(self, domain: str, validate: bool = True): self.domain = domain.lower() self.scopes = ( " ".join(_SCOPES[self.domain]) - if isinstance(_SCOPES[self.domain], (list, tuple)) + if isinstance(_SCOPES[self.domain], list | tuple) else _SCOPES[self.domain] ) @@ -165,7 +165,7 @@ def is_valid_access_token(access_token: str) -> bool: @frappe.whitelist(methods=["GET"]) -def callback(state: str, code: str = None, error: str = None) -> None: +def callback(state: str, code: str | None = None, error: str | None = None) -> None: """Common callback for google integrations. Invokes functions using `frappe.get_attr` and also adds required (keyworded) arguments along with committing and redirecting us back to frappe site.""" diff --git a/frappe/integrations/offsite_backup_utils.py b/frappe/integrations/offsite_backup_utils.py index 714b4a590c37..de413f5080c1 100644 --- a/frappe/integrations/offsite_backup_utils.py +++ b/frappe/integrations/offsite_backup_utils.py @@ -29,11 +29,11 @@ def send_email(success, service_name, doctype, email_field, error_status=None): ) else: subject = "[Warning] Backup Upload Failed" - message = """ + message = f"""

Backup Upload Failed!

-

Oops, your automated backup to {} failed.

-

Error message: {}

-

Please contact your system manager for more information.

""".format(service_name, error_status) +

Oops, your automated backup to {service_name} failed.

+

Error message: {error_status}

+

Please contact your system manager for more information.

""" frappe.sendmail(recipients=recipients, subject=subject, message=message) diff --git a/frappe/integrations/utils.py b/frappe/integrations/utils.py index 3365ba9b6238..dd25b92d913b 100644 --- a/frappe/integrations/utils.py +++ b/frappe/integrations/utils.py @@ -105,5 +105,5 @@ def get_json(obj): def json_handler(obj): - if isinstance(obj, (datetime.date, datetime.timedelta, datetime.datetime)): + if isinstance(obj, datetime.date | datetime.timedelta | datetime.datetime): return str(obj) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index cdefeb66cfa4..840d5b737b00 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import datetime import json +import types from functools import cached_property import frappe @@ -96,19 +97,21 @@ def _get_controller(): class BaseDocument: - _reserved_keywords = { - "doctype", - "meta", - "flags", - "parent_doc", - "_table_fields", - "_valid_columns", - "_doc_before_save", - "_table_fieldnames", - "_reserved_keywords", - "permitted_fieldnames", - "dont_update_if_missing", - } + _reserved_keywords = frozenset( + { + "doctype", + "meta", + "flags", + "parent_doc", + "_table_fields", + "_valid_columns", + "_doc_before_save", + "_table_fieldnames", + "_reserved_keywords", + "permitted_fieldnames", + "dont_update_if_missing", + } + ) def __init__(self, d): if d.get("doctype"): @@ -199,7 +202,7 @@ def get(self, key, filters=None, limit=None, default=None): value = self.__dict__.get(key, default) - if limit and isinstance(value, (list, tuple)) and len(value) > limit: + if limit and isinstance(value, list | tuple) and len(value) > limit: value = value[:limit] return value @@ -361,7 +364,7 @@ def get_valid_dict( value = None if convert_dates_to_str and isinstance( - value, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) + value, datetime.datetime | datetime.date | datetime.time | datetime.timedelta ): value = str(value) @@ -502,7 +505,7 @@ def db_insert(self, ignore_if_duplicate=False): if not self.creation: self.creation = self.modified = now() - self.created_by = self.modified_by = frappe.session.user + self.owner = self.modified_by = frappe.session.user # if doctype is "DocType", don't insert null values as we don't know who is valid yet d = self.get_valid_dict( @@ -574,7 +577,7 @@ def db_update(self): SET {values} WHERE `name`=%s""".format( doctype=self.doctype, values=", ".join("`" + c + "`=%s" for c in columns) ), - list(d.values()) + [name], + [*list(d.values()), name], ) except Exception as e: if frappe.db.is_unique_key_violation(e): @@ -1138,7 +1141,7 @@ def get_formatted( if not doc: doc = getattr(self, "parent_doc", None) or self - if (absolute_value or doc.get("absolute_value")) and isinstance(val, (int, float)): + if (absolute_value or doc.get("absolute_value")) and isinstance(val, int | float): val = abs(self.get(fieldname)) return format_value(val, df=df, doc=doc, currency=currency, format=format) @@ -1245,7 +1248,7 @@ def _filter(data, filters, limit=None): for f in filters: fval = filters[f] - if not isinstance(fval, (tuple, list)): + if not isinstance(fval, tuple | list): if fval is True: fval = ("not None", fval) elif fval is False: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index bde0e778e551..173314c2d6f1 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -221,15 +221,12 @@ def build_and_run(self): if frappe.db.db_type == "postgres" and args.order_by and args.group_by: args = self.prepare_select_args(args) - query = ( - """select %(fields)s - from %(tables)s - %(conditions)s - %(group_by)s - %(order_by)s - %(limit)s""" - % args - ) + query = """select {fields} + from {tables} + {conditions} + {group_by} + {order_by} + {limit}""".format(**args) return frappe.db.sql( query, @@ -475,7 +472,9 @@ def extract_tables(self): if table_name.lower().startswith("group_concat("): table_name = table_name[13:] - if not table_name[0] == "`": + if table_name.lower().startswith("distinct"): + table_name = table_name[8:].strip() + if table_name[0] != "`": table_name = f"`{table_name}`" if ( table_name not in self.query_tables @@ -558,7 +557,7 @@ def set_optional_columns(self): to_remove = [] for fld in self.fields: for f in optional_fields: - if f in fld and not f in self.columns: + if f in fld and f not in self.columns: to_remove.append(fld) for fld in to_remove: @@ -1261,7 +1260,7 @@ def get_between_date_filter(value, df=None): from_date = frappe.utils.nowdate() to_date = frappe.utils.nowdate() - if value and isinstance(value, (list, tuple)): + if value and isinstance(value, list | tuple): if len(value) >= 1: from_date = value[0] if len(value) >= 2: diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index f5f773a8f888..cb7b7a617091 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -102,6 +102,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: @@ -345,8 +355,8 @@ def check_if_doc_is_dynamically_linked(doc, method="Delete"): def raise_link_exists_exception(doc, reference_doctype, reference_docname, row=""): - doc_link = '{1}'.format(doc.doctype, doc.name) - reference_link = '{1}'.format(reference_doctype, reference_docname) + doc_link = f'{doc.name}' + reference_link = f'{reference_docname}' # hack to display Single doctype only once in message if reference_doctype == reference_docname: @@ -398,12 +408,12 @@ def clear_references( reference_name_field="reference_name", ): frappe.db.sql( - """update - `tab{0}` + f"""update + `tab{doctype}` set - {1}=NULL, {2}=NULL + {reference_doctype_field}=NULL, {reference_name_field}=NULL where - {1}=%s and {2}=%s""".format(doctype, reference_doctype_field, reference_name_field), # nosec + {reference_doctype_field}=%s and {reference_name_field}=%s""", # nosec (reference_doctype, reference_name), ) diff --git a/frappe/model/document.py b/frappe/model/document.py index 82fe8d550873..cae1ccd63f7a 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -540,6 +540,7 @@ def _validate(self): self._validate_selects() self._validate_non_negative() self._validate_length() + self._fix_rating_value() self._validate_code_fields() self._sync_autoname_field() self._extract_images_from_text_editor() @@ -552,6 +553,7 @@ def _validate(self): d._validate_selects() d._validate_non_negative() d._validate_length() + d._fix_rating_value() d._validate_code_fields() d._sync_autoname_field() d._extract_images_from_text_editor() @@ -586,6 +588,15 @@ def get_msg(df): msg = get_msg(df) frappe.throw(msg, frappe.NonNegativeError, title=_("Negative Value")) + def _fix_rating_value(self): + for field in self.meta.get("fields", {"fieldtype": "Rating"}): + value = self.get(field.fieldname) + if not isinstance(value, float): + value = flt(value) + + # Make sure rating is between 0 and 1 + self.set(field.fieldname, max(0, min(value, 1))) + def validate_workflow(self): """Validate if the workflow transition is valid""" if frappe.flags.in_install == "frappe": @@ -855,7 +866,7 @@ def _validate_mandatory(self): if not missing: return - for fieldname, msg in missing: + for _fieldname, msg in missing: msgprint(msg) if frappe.flags.print_messages: @@ -954,7 +965,7 @@ def _get_notifications(): return def _evaluate_alert(alert): - if not alert.name in self.flags.notifications_executed: + if alert.name not in self.flags.notifications_executed: evaluate_alert(self, alert.name, alert.event) self.flags.notifications_executed.append(alert.name) @@ -1216,7 +1227,7 @@ def save_version(self): doc_to_compare = frappe.get_doc(self.doctype, amended_from) version = frappe.new_doc("Version") - if is_useful_diff := version.update_version_info(doc_to_compare, self): + if version.update_version_info(doc_to_compare, self): version.insert(ignore_permissions=True) if not frappe.flags.in_migrate: @@ -1462,11 +1473,16 @@ def queue_action(self, action, **kwargs): title=_("Document Queued"), ) + enqueue_after_commit = kwargs.pop("enqueue_after_commit", None) + if enqueue_after_commit is None: + enqueue_after_commit = True + return enqueue( "frappe.model.document.execute_action", __doctype=self.doctype, __name=self.name, __action=action, + enqueue_after_commit=enqueue_after_commit, **kwargs, ) @@ -1479,7 +1495,7 @@ def lock(self, timeout=None): if file_lock.lock_exists(signature): lock_exists = True if timeout: - for i in range(timeout): + for _i in range(timeout): time.sleep(1) if not file_lock.lock_exists(signature): lock_exists = False diff --git a/frappe/model/dynamic_links.py b/frappe/model/dynamic_links.py index 96545f9f7547..e4b86ac14551 100644 --- a/frappe/model/dynamic_links.py +++ b/frappe/model/dynamic_links.py @@ -47,7 +47,7 @@ def get_dynamic_link_map(for_delete=False): ) for doctype in links: dynamic_link_map.setdefault(doctype, []).append(df) - except frappe.db.TableMissingError: # noqa: E722 + except frappe.db.TableMissingError: pass frappe.local.dynamic_link_map = dynamic_link_map diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 4877ee8e1a74..3ce9194edcc1 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -96,19 +96,21 @@ def load_doctype_from_file(doctype): class Meta(Document): _metaclass = True default_fields = list(default_fields)[1:] - special_doctypes = { - "DocField", - "DocPerm", - "DocType", - "Module Def", - "DocType Action", - "DocType Link", - "DocType State", - } - standard_set_once_fields = [ + special_doctypes = frozenset( + { + "DocField", + "DocPerm", + "DocType", + "Module Def", + "DocType Action", + "DocType Link", + "DocType State", + } + ) + standard_set_once_fields = ( frappe._dict(fieldname="creation", fieldtype="Datetime"), frappe._dict(fieldname="owner", fieldtype="Data"), - ] + ) def __init__(self, doctype): if isinstance(doctype, Document): @@ -146,7 +148,7 @@ def as_dict(self, no_nulls=False): def serialize(doc): out = {} for key, value in doc.__dict__.items(): - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): if not value or not isinstance(value[0], BaseDocument): # non standard list object, skip continue @@ -154,7 +156,7 @@ def serialize(doc): value = [serialize(d) for d in value] if (not no_nulls and value is None) or isinstance( - value, (str, int, float, datetime, list, tuple) + value, str | int | float | datetime | list | tuple ): out[key] = value @@ -716,9 +718,7 @@ def get_web_template(self, suffix=""): module_name, "doctype", doctype, "templates", doctype + suffix + ".html" ) if os.path.exists(template_path): - return "{module_name}/doctype/{doctype_name}/templates/{doctype_name}{suffix}.html".format( - module_name=module_name, doctype_name=doctype, suffix=suffix - ) + return f"{module_name}/doctype/{doctype}/templates/{doctype}{suffix}.html" return None def is_nested_set(self): diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 33d0d65f4821..b473d3c2a4ba 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -4,7 +4,8 @@ import datetime import re from collections import defaultdict -from typing import TYPE_CHECKING, Callable, Optional +from collections.abc import Callable +from typing import TYPE_CHECKING, Optional import frappe from frappe import _ @@ -102,7 +103,7 @@ def fake_counter(_prefix, digits): # ignore B023: binding `count` is not necessary because # function is evaluated immediately and it can not be done # because of function signature requirement - return str(count).zfill(digits) # noqa: B023 + return str(count).zfill(digits) generated_names.append(parse_naming_series(self.series, doc=doc, number_generator=fake_counter)) return generated_names @@ -482,12 +483,10 @@ def append_number_if_name_exists(doctype, value, fieldname="name", separator="-" if exists: last = frappe.db.sql( - """SELECT `{fieldname}` FROM `tab{doctype}` - WHERE `{fieldname}` {regex_character} %s + f"""SELECT `{fieldname}` FROM `tab{doctype}` + WHERE `{fieldname}` {frappe.db.REGEX_CHARACTER} %s ORDER BY length({fieldname}) DESC, - `{fieldname}` DESC LIMIT 1""".format( - doctype=doctype, fieldname=fieldname, regex_character=frappe.db.REGEX_CHARACTER - ), + `{fieldname}` DESC LIMIT 1""", regex, ) diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 5509771b6f42..938557c8d1cb 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -47,7 +47,7 @@ def update_document_title( # TODO: omit this after runtime type checking (ref: https://github.com/frappe/frappe/pull/14927) for obj in [docname, updated_title, updated_name]: - if not isinstance(obj, (str, NoneType)): + if not isinstance(obj, str | NoneType): frappe.throw(f"{obj=} must be of type str or None") # handle bad API usages @@ -109,7 +109,7 @@ def update_document_title( def rename_doc( doctype: str | None = None, old: str | None = None, - new: str = None, + new: str | None = None, force: bool = False, merge: bool = False, ignore_permissions: bool = False, @@ -379,7 +379,7 @@ def validate_rename( def rename_doctype(doctype: str, old: str, new: str) -> None: # change options for fieldtype Table, Table MultiSelect and Link - fields_with_options = ("Link",) + frappe.model.table_fields + fields_with_options = ("Link", *frappe.model.table_fields) for fieldtype in fields_with_options: update_options_for_fieldtype(fieldtype, old, new) @@ -440,6 +440,8 @@ def get_link_fields(doctype: str) -> list[dict]: frappe.flags.link_fields = {} if doctype not in frappe.flags.link_fields: + virtual_doctypes = frappe.get_all("DocType", {"is_virtual": 1}, pluck="name") + dt = frappe.qb.DocType("DocType") df = frappe.qb.DocType("DocField") cf = frappe.qb.DocType("Custom Field") @@ -458,7 +460,7 @@ def get_link_fields(doctype: str) -> list[dict]: custom_fields = ( frappe.qb.from_(cf) .select(cf.dt.as_("parent"), cf.fieldname, cf_issingle) - .where((cf.options == doctype) & (cf.fieldtype == "Link")) + .where((cf.options == doctype) & (cf.fieldtype == "Link") & (cf.dt.notin(virtual_doctypes))) .run(as_dict=True) ) @@ -466,7 +468,12 @@ def get_link_fields(doctype: str) -> list[dict]: property_setter_fields = ( frappe.qb.from_(ps) .select(ps.doc_type.as_("parent"), ps.field_name.as_("fieldname"), ps_issingle) - .where((ps.property == "options") & (ps.value == doctype) & (ps.field_name.notnull())) + .where( + (ps.property == "options") + & (ps.value == doctype) + & (ps.field_name.notnull()) + & (ps.doc_type.notin(virtual_doctypes)) + ) .run(as_dict=True) ) @@ -617,7 +624,10 @@ def rename_dynamic_links(doctype: str, old: str, new: str): Singles = frappe.qb.DocType("Singles") for df in get_dynamic_link_map().get(doctype, []): # dynamic link in single, just one value to check - if frappe.get_meta(df.parent).issingle: + meta = frappe.get_meta(df.parent) + if meta.is_virtual: + continue + if meta.issingle: refdoc = frappe.db.get_singles_dict(df.parent) if refdoc.get(df.options) == doctype and refdoc.get(df.fieldname) == old: frappe.qb.update(Singles).set(Singles.value, new).where( diff --git a/frappe/model/utils/__init__.py b/frappe/model/utils/__init__.py index 2935872fc7e9..f17494854f42 100644 --- a/frappe/model/utils/__init__.py +++ b/frappe/model/utils/__init__.py @@ -30,9 +30,8 @@ def set_default(doc, key): frappe.db.set(doc, "is_default", 1) frappe.db.sql( - """update `tab%s` set `is_default`=0 - where `%s`=%s and name!=%s""" - % (doc.doctype, key, "%s", "%s"), + """update `tab{}` set `is_default`=0 + where `{}`={} and name!={}""".format(doc.doctype, key, "%s", "%s"), (doc.get(key), doc.name), ) @@ -62,7 +61,7 @@ def render_include(content): content = cstr(content) # try 5 levels of includes - for i in range(5): + for _i in range(5): if "{% include" in content: paths = INCLUDE_DIRECTIVE_PATTERN.findall(content) if not paths: diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py index 9a7694b9f859..3029c0434b6d 100644 --- a/frappe/model/utils/link_count.py +++ b/frappe/model/utils/link_count.py @@ -24,7 +24,7 @@ def flush_local_link_count(): if not link_count: link_count = {} - for key, value in frappe.local.link_count.items(): + for key, _value in frappe.local.link_count.items(): if key in link_count: link_count[key] += frappe.local.link_count[key] else: diff --git a/frappe/model/utils/rename_field.py b/frappe/model/utils/rename_field.py index c17d01183bb6..262d5d4a2119 100644 --- a/frappe/model/utils/rename_field.py +++ b/frappe/model/utils/rename_field.py @@ -25,9 +25,8 @@ def rename_field(doctype, old_fieldname, new_fieldname): if new_field.fieldtype in table_fields: # change parentfield of table mentioned in options frappe.db.sql( - """update `tab%s` set parentfield=%s - where parentfield=%s""" - % (new_field.options.split("\n", 1)[0], "%s", "%s"), + """update `tab{}` set parentfield={} + where parentfield={}""".format(new_field.options.split("\n", 1)[0], "%s", "%s"), (new_fieldname, old_fieldname), ) @@ -140,9 +139,8 @@ def update_users_report_view_settings(doctype, ref_fieldname, new_fieldname): if columns_modified: frappe.db.sql( - """update `tabDefaultValue` set defvalue=%s - where defkey=%s""" - % ("%s", "%s"), + """update `tabDefaultValue` set defvalue={} + where defkey={}""".format("%s", "%s"), (json.dumps(new_columns), key), ) diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 4dab73be0e9f..e07c6f963b18 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -120,7 +120,7 @@ def apply_workflow(doc, action): doc.set(workflow.workflow_state_field, transition.next_state) # find settings for the next state - next_state = [d for d in workflow.states if d.state == transition.next_state][0] + next_state = next(d for d in workflow.states if d.state == transition.next_state) # update any additional field if next_state.update_field: diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py index 8ccff725cb41..4da11bcff324 100644 --- a/frappe/modules/import_file.py +++ b/frappe/modules/import_file.py @@ -40,7 +40,7 @@ def calculate_hash(path: str) -> str: def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False): - if type(module) is list: + if isinstance(module, list): out = [] for m in module: out.append( @@ -80,7 +80,7 @@ def import_file_by_path( force: bool = False, data_import: bool = False, pre_process=None, - ignore_version: bool = None, + ignore_version: bool | None = None, reset_permissions: bool = False, ): """Import file from the given path @@ -215,11 +215,7 @@ def import_doc( docdict["__islocal"] = 1 controller = get_controller(docdict["doctype"]) - if ( - controller - and hasattr(controller, "prepare_for_import") - and callable(getattr(controller, "prepare_for_import")) - ): + if controller and hasattr(controller, "prepare_for_import") and callable(controller.prepare_for_import): controller.prepare_for_import(docdict) doc = frappe.get_doc(docdict) diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index c2d5b381b8ab..444c4a170e76 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -53,7 +53,7 @@ class PatchType(Enum): def run_all(skip_failing: bool = False, patch_type: PatchType | None = None) -> None: """run all pending patches""" - executed = set(frappe.get_all("Patch Log", fields="patch", pluck="patch")) + executed = set(frappe.get_all("Patch Log", filters={"skipped": 0}, fields="patch", pluck="patch")) frappe.flags.final_patches = [] @@ -65,8 +65,9 @@ def run_patch(patch): except Exception: if not skip_failing: raise - else: - print("Failed to execute patch") + + print("Failed to execute patch") + update_patch_log(patch, skipped=True) patches = get_all_patches(patch_type=patch_type) @@ -203,9 +204,18 @@ def execute_patch(patchmodule: str, method=None, methodargs=None): return True -def update_patch_log(patchmodule): +def update_patch_log(patchmodule, skipped=False): """update patch_file in patch log""" - frappe.get_doc({"doctype": "Patch Log", "patch": patchmodule}).insert(ignore_permissions=True) + + patch = frappe.get_doc({"doctype": "Patch Log", "patch": patchmodule}) + + if skipped: + traceback = frappe.get_traceback(with_context=True) + patch.skipped = 1 + patch.traceback = traceback + print(traceback, end="\n\n") + + patch.insert(ignore_permissions=True) def executed(patchmodule): @@ -213,7 +223,7 @@ def executed(patchmodule): if patchmodule.startswith("finally:"): # patches are saved without the finally: tag patchmodule = patchmodule.replace("finally:", "") - return frappe.db.get_value("Patch Log", {"patch": patchmodule}) + return frappe.db.get_value("Patch Log", {"patch": patchmodule, "skipped": 0}) def _patch_mode(enable): diff --git a/frappe/monitor.py b/frappe/monitor.py index b93ba1d3bbb0..dcac0a7048f7 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -32,6 +32,12 @@ def add_data_to_monitor(**kwargs) -> None: frappe.local.monitor.add_custom_data(**kwargs) +def get_trace_id() -> str | None: + """Get unique ID for current transaction.""" + if monitor := getattr(frappe.local, "monitor", None): + return monitor.data.uuid + + def log_file(): return os.path.join(frappe.utils.get_bench_path(), "logs", "monitor.json.log") diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index ed3c6b358e71..d7d64feac886 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -143,7 +143,7 @@ def split_by_weight(work, weights, chunk_count): chunk_no = 0 chunk_weight = 0 - for task, weight in zip(work, weights): + for task, weight in zip(work, weights, strict=False): if chunk_weight > expected_weight: chunk_weight = 0 chunk_no += 1 diff --git a/frappe/patches/v11_0/delete_duplicate_user_permissions.py b/frappe/patches/v11_0/delete_duplicate_user_permissions.py index a8bb291769a9..27af1e98c702 100644 --- a/frappe/patches/v11_0/delete_duplicate_user_permissions.py +++ b/frappe/patches/v11_0/delete_duplicate_user_permissions.py @@ -12,7 +12,7 @@ def execute(): for record in duplicateRecords: frappe.db.sql( - """delete from `tabUser Permission` - where allow=%s and user=%s and for_value=%s limit {}""".format(record.count - 1), + f"""delete from `tabUser Permission` + where allow=%s and user=%s and for_value=%s limit {record.count - 1}""", (record.allow, record.user, record.for_value), ) diff --git a/frappe/patches/v11_0/update_list_user_settings.py b/frappe/patches/v11_0/update_list_user_settings.py index 5cbcd3bc0ab8..f90871af274e 100644 --- a/frappe/patches/v11_0/update_list_user_settings.py +++ b/frappe/patches/v11_0/update_list_user_settings.py @@ -12,8 +12,8 @@ def execute(): for user in users: # get user_settings for each user settings = frappe.db.sql( - "select * from `__UserSettings` \ - where user={}".format(frappe.db.escape(user.user)), + f"select * from `__UserSettings` \ + where user={frappe.db.escape(user.user)}", as_dict=True, ) diff --git a/frappe/patches/v12_0/delete_duplicate_indexes.py b/frappe/patches/v12_0/delete_duplicate_indexes.py index 45f495fe695f..6724f28e2611 100644 --- a/frappe/patches/v12_0/delete_duplicate_indexes.py +++ b/frappe/patches/v12_0/delete_duplicate_indexes.py @@ -38,7 +38,7 @@ def execute(): frappe.db.sql_ddl(f"ALTER TABLE `{table_name}` DROP INDEX `{index}`") except Exception as e: frappe.log_error("Failed to drop index") - print(f"x Failed to drop index {index} from {table_name}\n {str(e)}") + print(f"x Failed to drop index {index} from {table_name}\n {e!s}") else: print(f"✓ dropped {index} index from {table}") diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index 7511a2ee1d8d..e1fcb72e212b 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -22,7 +22,6 @@ def execute(): phone_values = [] for count, contact_detail in enumerate(contact_details): phone_counter = 1 - is_primary = 1 if contact_detail.email_id: email_values.append( ( diff --git a/frappe/patches/v12_0/replace_null_values_in_tables.py b/frappe/patches/v12_0/replace_null_values_in_tables.py index 1dc8d964a105..617e9886f694 100644 --- a/frappe/patches/v12_0/replace_null_values_in_tables.py +++ b/frappe/patches/v12_0/replace_null_values_in_tables.py @@ -18,7 +18,7 @@ def execute(): update_column_table_map.setdefault(field.TABLE_NAME, []) update_column_table_map[field.TABLE_NAME].append( - "`{fieldname}`=COALESCE(`{fieldname}`, 0)".format(fieldname=field.COLUMN_NAME) + f"`{field.COLUMN_NAME}`=COALESCE(`{field.COLUMN_NAME}`, 0)" ) for table in frappe.db.get_tables(): diff --git a/frappe/patches/v12_0/setup_tags.py b/frappe/patches/v12_0/setup_tags.py index cb0d46a45d7a..fb33043745e7 100644 --- a/frappe/patches/v12_0/setup_tags.py +++ b/frappe/patches/v12_0/setup_tags.py @@ -12,7 +12,7 @@ def execute(): tag_links = [] time = frappe.utils.get_datetime() - for doctype in frappe.get_list("DocType", filters={"istable": 0, "issingle": 0}): + for doctype in frappe.get_list("DocType", filters={"istable": 0, "issingle": 0, "is_virtual": 0}): if not frappe.db.count(doctype.name) or not frappe.db.has_column(doctype.name, "_user_tags"): continue diff --git a/frappe/patches/v13_0/update_date_filters_in_user_settings.py b/frappe/patches/v13_0/update_date_filters_in_user_settings.py index 6565641d9b86..030ea3936dc5 100644 --- a/frappe/patches/v13_0/update_date_filters_in_user_settings.py +++ b/frappe/patches/v13_0/update_date_filters_in_user_settings.py @@ -9,12 +9,12 @@ def execute(): for user in users: user_settings = frappe.db.sql( - """ + f""" select * from `__UserSettings` where - user='{user}' - """.format(user=user.user), + user='{user.user}' + """, as_dict=True, ) diff --git a/frappe/patches/v14_0/drop_unused_indexes.py b/frappe/patches/v14_0/drop_unused_indexes.py index 83fe1a4c372a..7f9f960e42b0 100644 --- a/frappe/patches/v14_0/drop_unused_indexes.py +++ b/frappe/patches/v14_0/drop_unused_indexes.py @@ -50,7 +50,7 @@ def _drop_index_if_exists(table: str, index: str): frappe.db.sql_ddl(f"ALTER TABLE `{table}` DROP INDEX `{index}`") except Exception as e: frappe.log_error("Failed to drop index") - click.secho(f"x Failed to drop index {index} from {table}\n {str(e)}", fg="red") + click.secho(f"x Failed to drop index {index} from {table}\n {e!s}", fg="red") return click.echo(f"✓ dropped {index} index from {table}") diff --git a/frappe/permissions.py b/frappe/permissions.py index f828bdaacf85..b6370ce51198 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -135,7 +135,7 @@ def has_permission( if not perm: push_perm_check_log( _("User {0} does not have doctype access via role permission for document {1}").format( - frappe.bold(user), frappe.bold(doctype) + frappe.bold(user), frappe.bold(_(doctype)) ) ) @@ -311,7 +311,7 @@ def has_user_permission(doc, user=None): # if allowed_docs is empty it states that there is no applicable permission under the current doctype # only check if allowed_docs is not empty - if allowed_docs and docname not in allowed_docs: + if allowed_docs and str(docname) not in allowed_docs: # no user permissions for this doc specified push_perm_check_log(_("Not allowed for {0}: {1}").format(_(doctype), docname)) return False @@ -405,7 +405,7 @@ def get_valid_perms(doctype=None, user=None): doctypes_with_custom_perms = get_doctypes_with_custom_docperms() for p in perms: - if not p.parent in doctypes_with_custom_perms: + if p.parent not in doctypes_with_custom_perms: custom_perms.append(p) if doctype: @@ -449,7 +449,7 @@ def get(): .select(table.role) .run(pluck=True) ) - return roles + ["All", "Guest"] + return [*roles, "All", "Guest"] roles = frappe.cache().hget("roles", user, get) diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 661772fffbb2..36fe83364226 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -96,6 +96,7 @@ frappe.db = { }, count: function (doctype, args = {}) { let filters = args.filters || {}; + let limit = args.limit; // has a filter with childtable? const distinct = @@ -111,6 +112,7 @@ frappe.db = { filters, fields, distinct, + limit, }); }, get_link_options(doctype, txt = "", filters = {}) { diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index 65940820eb2a..ec1add67e94c 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -201,7 +201,7 @@ />
- `; diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js index 09bf21c827e6..1e20b1c67b83 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 { diff --git a/frappe/public/js/frappe/form/controls/time.js b/frappe/public/js/frappe/form/controls/time.js index 93a2a73e8a0b..b6a66f83000d 100644 --- a/frappe/public/js/frappe/form/controls/time.js +++ b/frappe/public/js/frappe/form/controls/time.js @@ -44,6 +44,9 @@ frappe.ui.form.ControlTime = class ControlTime extends frappe.ui.form.ControlDat } set_input(value) { super.set_input(value); + if (!this.datepicker) { + return; + } if ( value && ((this.last_value && this.last_value !== this.value) || diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index c028f0e52bdf..9236b0330dfe 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -552,8 +552,8 @@ class FormTimeline extends BaseTimeline { } if (reply_all) { // if reply_all then add cc and bcc as well. - args.cc += communication_doc.cc; - args.bcc = communication_doc.bcc; + args.cc += cstr(communication_doc.cc); + args.bcc = cstr(communication_doc.bcc); } } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index d44c132e19f8..0f593379aa6f 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -797,7 +797,11 @@ export default class Grid { if (!this.df.data) { this.df.data = this.get_data() || []; } - this.df.data.push({ idx: this.df.data.length + 1, __islocal: true }); + const defaults = this.docfields.reduce((acc, d) => { + acc[d.fieldname] = d.default; + return acc; + }, {}); + this.df.data.push({ idx: this.df.data.length + 1, __islocal: true, ...defaults }); this.refresh(); } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 8105c6c106d9..f59e0b46b2c4 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -247,8 +247,9 @@ export default class GridRow { // index (1, 2, 3 etc) if (!this.row_index && !this.show_search) { - // REDESIGN-TODO: Make translation contextual, this No is Number - var txt = this.doc ? this.doc.idx : __("No."); + const txt = this.doc + ? this.doc.idx + : __("No.", null, "Title of the 'row number' column"); this.row_check = $( `
@@ -303,8 +304,6 @@ export default class GridRow { }, 500) ); frappe.utils.only_allow_num_decimal(this.row_index.find("input")); - } else { - this.row_index.find("span").html(txt); } this.setup_columns(); @@ -335,10 +334,10 @@ export default class GridRow { this.open_form_button = $('
').appendTo(this.row); if (!this.configure_columns) { + const edit_msg = __("Edit", "", "Edit grid row"); this.open_form_button = $(` -
+
${frappe.utils.icon("edit", "xs")} -
${__("Edit", "", "Edit grid row")}
`) .appendTo(this.open_form_button) @@ -346,6 +345,8 @@ export default class GridRow { me.toggle_view(); return false; }); + + this.open_form_button.tooltip({ delay: { show: 600, hide: 100 } }); } if (this.is_too_small()) { diff --git a/frappe/public/js/frappe/form/templates/timeline_message_box.html b/frappe/public/js/frappe/form/templates/timeline_message_box.html index 7392a6a97f27..26d1ef284e85 100644 --- a/frappe/public/js/frappe/form/templates/timeline_message_box.html +++ b/frappe/public/js/frappe/form/templates/timeline_message_box.html @@ -23,7 +23,7 @@ {% } %}
- {{ comment_when(doc.communication_date) }} + {{ comment_when(doc.communication_date || doc.creation) }}
{% } else if (doc.comment_type && doc.comment_type == "Comment") { %} @@ -33,7 +33,7 @@ {{ __("commented") }} . - {{ comment_when(doc.communication_date) }} + {{ comment_when(doc.communication_date || doc.creation) }} {% } else { %} @@ -44,7 +44,7 @@ {{ doc.user_full_name || frappe.user.full_name(doc.owner) }} . - {{ comment_when(doc.communication_date) }} + {{ comment_when(doc.communication_date || doc.creation) }} {% if (doc.subject) { %}
{{doc.subject}}
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 3a4d6c13304b..a4265ff45897 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -384,20 +384,22 @@ frappe.views.BaseList = class BaseList { .find(`.btn-paging[data-value="${this.page_length}"]`) .addClass("btn-info"); - this.$paging_area.on("click", ".btn-paging, .btn-more", (e) => { + this.$paging_area.on("click", ".btn-paging", (e) => { const $this = $(e.currentTarget); - if ($this.is(".btn-paging")) { - // set active button - this.$paging_area.find(".btn-paging").removeClass("btn-info"); - $this.addClass("btn-info"); + // set active button + this.$paging_area.find(".btn-paging").removeClass("btn-info"); + $this.addClass("btn-info"); + + this.start = 0; + this.page_length = this.selected_page_count = $this.data().value; - this.start = 0; - this.page_length = this.selected_page_count = $this.data().value; - } else if ($this.is(".btn-more")) { - this.start = this.start + this.page_length; - this.page_length = this.selected_page_count || 20; - } + this.refresh(); + }); + + this.$paging_area.on("click", ".btn-more", (e) => { + this.start += this.page_length; + this.page_length = this.selected_page_count || 20; this.refresh(); }); } @@ -579,6 +581,11 @@ class FilterArea { this.$filter_list_wrapper = this.list_view.$filter_section; this.trigger_refresh = true; + + this.debounced_refresh_list_view = frappe.utils.debounce( + this.refresh_list_view.bind(this), + 300 + ); this.setup(); } @@ -741,13 +748,13 @@ class FilterArea { label: "ID", condition: "like", fieldname: "name", - onchange: () => this.refresh_list_view(), + onchange: () => this.debounced_refresh_list_view(), }); } if (this.list_view.custom_filter_configs) { this.list_view.custom_filter_configs.forEach((config) => { - config.onchange = () => this.refresh_list_view(); + config.onchange = () => this.debounced_refresh_list_view(); }); fields = fields.concat(this.list_view.custom_filter_configs); @@ -797,7 +804,7 @@ class FilterArea { options: options, fieldname: df.fieldname, condition: condition, - onchange: () => this.refresh_list_view(), + onchange: () => this.debounced_refresh_list_view(), ignore_link_validation: fieldtype === "Dynamic Link", is_filter: 1, }; @@ -859,7 +866,7 @@ class FilterArea { filter_button: this.filter_button, filter_x_button: this.filter_x_button, default_filters: [], - on_change: () => this.refresh_list_view(), + on_change: () => this.debounced_refresh_list_view(), }); } diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index a0271967b4ec..6e33f81a51a5 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: [ diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index a71ba353b1df..02e6cddc9af7 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -28,6 +28,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.process_document_refreshes.bind(this), 2000 ); + this.count_upper_bound = 1001; } has_permissions() { @@ -498,9 +499,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { freeze() { if (this.list_view_settings && !this.list_view_settings.disable_count) { - this.$result - .find(".list-count") - .html(`${__("Refreshing", null, "Document count in list view")}...`); + this.get_count_element().html( + `${__("Refreshing", null, "Document count in list view")}...` + ); } } @@ -598,25 +599,45 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { render_list() { // clear rows this.$result.find(".list-row-container").remove(); + if (this.data.length > 0) { // append rows - this.$result.append( - this.data - .map((doc, i) => { - doc._idx = i; - return this.get_list_row_html(doc); - }) - .join("") - ); + let idx = 0; + for (let doc of this.data) { + doc._idx = idx++; + this.$result.append(this.get_list_row_html(doc)); + } } } render_count() { - if (!this.list_view_settings.disable_count) { - this.get_count_str().then((str) => { - this.$result.find(".list-count").html(`${str}`); - }); - } + if (this.list_view_settings.disable_count) return; + + let me = this; + let $count = this.get_count_element(); + this.get_count_str().then((count) => { + $count.html(`${count}`); + if (this.count_upper_bound && this.count_upper_bound == this.total_count) { + $count.attr( + "title", + __( + "The count shown is an estimated count. Click here to see the accurate count." + ) + ); + $count.tooltip({ delay: { show: 600, hide: 100 }, trigger: "hover" }); + $count.on("click", () => { + me.count_upper_bound = 0; + $count.off("click"); + $count.tooltip("disable"); + me.freeze(); + me.render_count(); + }); + } + }); + } + + get_count_element() { + return this.$result.find(".list-count"); } get_header_html() { @@ -891,7 +912,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
`; - let assigned_users = JSON.parse(doc._assign || "[]"); + let assigned_users = doc._assign ? JSON.parse(doc._assign) : []; if (assigned_users.length) { assigned_to = `
${frappe.avatar_group(assigned_users, 3, { filterable: true })[0].outerHTML} @@ -926,16 +947,25 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { return frappe.db .count(this.doctype, { filters: this.get_filters_for_args(), + limit: this.count_upper_bound, }) .then((total_count) => { this.total_count = total_count || current_count; this.count_without_children = count_without_children !== current_count ? count_without_children : undefined; - let str = __("{0} of {1}", [current_count, this.total_count]); + + let count_str; + if (this.total_count === this.count_upper_bound) { + count_str = `${format_number(this.total_count - 1, null, 0)}+`; + } else { + count_str = format_number(this.total_count, null, 0); + } + + let str = __("{0} of {1}", [format_number(current_count, null, 0), count_str]); if (this.count_without_children) { str = __("{0} of {1} ({2} rows with children)", [ count_without_children, - this.total_count, + count_str, current_count, ]); } @@ -969,9 +999,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { - + `; const like = div.querySelector(".like-action"); @@ -1437,8 +1465,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { // this doc was changed and should not be visible // in the listview according to filters applied // let's remove it manually - this.data = this.data.filter((d) => names.indexOf(d.name) === -1); - this.render_list(); + this.data = this.data.filter((d) => !names.includes(d.name)); + for (let name of names) { + this.$result + .find(`.list-row-checkbox[data-name='${name.replace(/'/g, "\\'")}']`) + .closest(".list-row-container") + .remove(); + } return; } diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js index 7c07e8c34721..d59676ea9666 100644 --- a/frappe/public/js/frappe/model/perm.js +++ b/frappe/public/js/frappe/model/perm.js @@ -289,10 +289,9 @@ $.extend(frappe.perm, { const allowed_docs = filtered_perms.map((perm) => perm.doc); if (with_default_doc) { - const default_doc = - allowed_docs.length === 1 - ? allowed_docs - : filtered_perms.filter((perm) => perm.is_default).map((record) => record.doc); + const default_doc = filtered_perms + .filter((perm) => perm.is_default) + .map((record) => record.doc); return { allowed_records: allowed_docs, diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 691acb035d88..c53c7976ea57 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) => diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 58e307ec71bc..356b6e5f060c 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -418,7 +418,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); @@ -474,6 +474,7 @@ frappe.ui.filter_utils = { df.description = ""; df.reqd = 0; + df.length = 1000; // this won't be saved, no need to apply 140 character limit here df.ignore_link_validation = true; // given diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 9d2dba32e17a..cbc6cb308536 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -67,7 +67,9 @@ frappe.ui.FilterGroup = class { const in_datepicker = $(e.target).is(".datepicker--cell") || $(e.target).closest(".datepicker--nav-title").length !== 0 || - $(e.target).parents(".datepicker--nav-action").length !== 0; + $(e.target).parents(".datepicker--nav-action").length !== 0 || + $(e.target).parents(".datepicker").length !== 0 || + $(e.target).is(".datepicker--button"); if ( $(e.target).parents(".filter-popover").length === 0 && diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js index a992fe4f7aeb..7f21fcdb77a7 100644 --- a/frappe/public/js/frappe/ui/keyboard.js +++ b/frappe/public/js/frappe/ui/keyboard.js @@ -98,8 +98,11 @@ frappe.ui.keys.show_keyboard_shortcut_dialog = () => { .map(frappe.utils.to_title_case) .join("+"); if (frappe.utils.is_mac()) { - shortcut_label = shortcut_label.replace("Ctrl", "⌘"); + shortcut_label = shortcut_label.replace("Ctrl", "⌘").replace("Alt", "⌥"); } + + shortcut_label = shortcut_label.replace("Shift", "⇧"); + return ` ${shortcut_label} ${shortcut.description || ""} diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 8f09405f0966..1c5d745f4a2d 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -499,10 +499,15 @@ frappe.ui.Page = class Page { } // label if (frappe.utils.is_mac()) { - shortcut_obj.shortcut_label = shortcut_obj.shortcut.replace("Ctrl", "⌘"); + shortcut_obj.shortcut_label = shortcut_obj.shortcut + .replace("Ctrl", "⌘") + .replace("Alt", "⌥"); } else { shortcut_obj.shortcut_label = shortcut_obj.shortcut; } + + shortcut_obj.shortcut_label = shortcut_obj.shortcut_label.replace("Shift", "⇧"); + // actual shortcut string shortcut_obj.shortcut = shortcut_obj.shortcut.toLowerCase(); // action is button click diff --git a/frappe/public/js/frappe/ui/toolbar/navbar.html b/frappe/public/js/frappe/ui/toolbar/navbar.html index ee070d437866..21012498aae4 100644 --- a/frappe/public/js/frappe/ui/toolbar/navbar.html +++ b/frappe/public/js/frappe/ui/toolbar/navbar.html @@ -16,7 +16,7 @@ id="navbar-search" type="text" class="form-control" - placeholder="{%= __("Search or type a command (Ctrl + G)") %}" + placeholder="{%= __('Search or type a command ({0})', [frappe.utils.is_mac() ? '⌘ + G' : 'Ctrl + G']) %}" aria-haspopup="true" > diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index e340ac315371..10ea745f71e8 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -72,7 +72,11 @@ function convert_old_to_new_number_format(v, old_number_format, new_number_forma let old_group_regex = new RegExp(old_info.group_sep === "." ? "\\." : old_info.group_sep, "g"); v_before_decimal = v_before_decimal.replace(old_group_regex, new_info.group_sep); - v = v_before_decimal + new_info.decimal_str + v_after_decimal; + v = v_before_decimal; + if (v_after_decimal) { + v = v + new_info.decimal_str + v_after_decimal; + } + return v; } diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js index a4bd8214f163..361daf9b2b3a 100644 --- a/frappe/public/js/frappe/views/file/file_view.js +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -233,6 +233,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { } else { super.render(); this.render_header(); + this.render_count(); } } diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index efb59839964d..d5ee8c05441e 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -745,15 +745,19 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { add_prepared_report_buttons(doc) { if (doc) { - this.page.add_inner_button(__("Download Report"), function () { - window.open( - frappe.urllib.get_full_url( - "/api/method/frappe.core.doctype.prepared_report.prepared_report.download_attachment?" + - "dn=" + - encodeURIComponent(doc.name) - ) - ); - }); + this.page.add_inner_button( + __("Download Report"), + function () { + window.open( + frappe.urllib.get_full_url( + "/api/method/frappe.core.doctype.prepared_report.prepared_report.download_attachment?" + + "dn=" + + encodeURIComponent(doc.name) + ) + ); + }, + __("Actions") + ); let pretty_diff = frappe.datetime.comment_when(doc.report_end_time); const days_old = frappe.datetime.get_day_diff( @@ -937,6 +941,16 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { let data = this.data; let columns = this.columns.filter((col) => !col.hidden); + if (data.length > 100000) { + let msg = __( + "This report contains {0} rows and is too big to display in browser, you can {1} this report instead.", + [cstr(format_number(data.length, null, 0)).bold(), __("export").bold()] + ); + + this.toggle_message(true, `${frappe.utils.icon("solid-warning")} ${msg}`); + return; + } + if (this.raw_data.add_total_row && !this.report_settings.tree) { data = data.slice(); data.splice(-1, 1); @@ -1453,12 +1467,17 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return; } + let export_options = ["Excel"]; + if (this.datatable) { + export_options.push("CSV"); + } + let export_dialog_fields = [ { label: __("Select File Format"), fieldname: "file_format", fieldtype: "Select", - options: ["Excel", "CSV"], + options: export_options, default: "Excel", reqd: 1, }, @@ -1496,15 +1515,15 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ); } - const visible_idx = this.datatable.bodyRenderer.visibleRowIndices; - if (visible_idx.length + 1 === this.data.length) { + const visible_idx = this.datatable?.bodyRenderer.visibleRowIndices || []; + if (visible_idx.length + 1 === this.data?.length) { visible_idx.push(visible_idx.length); } const args = { cmd: "frappe.desk.query_report.export_query", report_name: this.report_name, - custom_columns: this.custom_columns.length ? this.custom_columns : [], + custom_columns: this.custom_columns?.length ? this.custom_columns : [], file_format_type: file_format, filters: filters, visible_idx, diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index d203b575b89f..f19237a5386e 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -221,19 +221,14 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { this.setup_datatable(this.data); } - render_count() { - if (this.list_view_settings?.disable_count) { - return; - } - let $list_count = this.$paging_area.find(".list-count"); - if (!$list_count.length) { - $list_count = $("") + get_count_element() { + let $count = this.$paging_area.find(".list-count"); + if (!$count.length) { + $count = $("") .addClass("text-muted list-count") .prependTo(this.$paging_area.find(".level-right")); } - this.get_count_str().then((str) => { - $list_count.text(str); - }); + return $count; } on_update(data) { @@ -897,7 +892,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { .filter(standard_fields_filter); // filter out docstatus field from picker - let std_fields = frappe.model.std_fields.filter((df) => df.fieldname !== "docstatus"); + let std_fields = frappe.model.std_fields.filter( + (df) => !["docstatus", "_comments"].includes(df.fieldname) + ); // add status field derived from docstatus, if status is not a standard field let has_status_values = false; diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index 14c688f1b9b4..b8ca95064195 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -50,12 +50,6 @@ .row-index { display: none; } - - .btn-open-row { - .edit-grid-row { - display: none; - } - } } } diff --git a/frappe/query_builder/builder.py b/frappe/query_builder/builder.py index 535284f34d0c..45042c0820dd 100644 --- a/frappe/query_builder/builder.py +++ b/frappe/query_builder/builder.py @@ -1,3 +1,4 @@ +import types import typing from pypika import MySQLQuery, Order, PostgreSQLQuery, terms @@ -62,8 +63,8 @@ def from_(cls, table, *args, **kwargs): class Postgres(Base, PostgreSQLQuery): - field_translation = {"table_name": "relname", "table_rows": "n_tup_ins"} - schema_translation = {"tables": "pg_stat_all_tables"} + field_translation = types.MappingProxyType({"table_name": "relname", "table_rows": "n_tup_ins"}) + schema_translation = types.MappingProxyType({"tables": "pg_stat_all_tables"}) # TODO: Find a better way to do this # These are interdependent query changes that need fixing. These # translations happen in the same query. But there is no check to see if diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py index 76b084d60afe..42d0aa59ad17 100644 --- a/frappe/rate_limiter.py +++ b/frappe/rate_limiter.py @@ -1,9 +1,9 @@ # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +from collections.abc import Callable from datetime import datetime from functools import wraps -from typing import Callable from werkzeug.wrappers import Response @@ -83,7 +83,7 @@ def respond(self): def rate_limit( - key: str = None, + key: str | None = None, limit: int | Callable = 5, seconds: int = 24 * 60 * 60, methods: str | list = "ALL", diff --git a/frappe/realtime.py b/frappe/realtime.py index 96c7391e58d2..864c72482c28 100644 --- a/frappe/realtime.py +++ b/frappe/realtime.py @@ -23,13 +23,13 @@ def publish_progress(percent, title=None, doctype=None, docname=None, descriptio def publish_realtime( - event: str = None, - message: dict = None, - room: str = None, - user: str = None, - doctype: str = None, - docname: str = None, - task_id: str = None, + event: str | None = None, + message: dict | None = None, + room: str | None = None, + user: str | None = None, + doctype: str | None = None, + docname: str | None = None, + task_id: str | None = None, after_commit: bool = False, ): """Publish real-time updates diff --git a/frappe/recorder.py b/frappe/recorder.py index 6143e1a658d2..5b25537ce310 100644 --- a/frappe/recorder.py +++ b/frappe/recorder.py @@ -44,7 +44,7 @@ def get_current_stack_frames(): try: current = inspect.currentframe() frames = inspect.getouterframes(current, context=10) - for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]: + for _frame, filename, lineno, function, _context, _index in list(reversed(frames))[:-2]: if "/apps/" in filename or "" in filename: yield { "filename": TRACEBACK_PATH_PATTERN.sub("", filename), @@ -71,7 +71,9 @@ def post_process(): for request in result: for call in request["calls"]: - formatted_query = sqlparse.format(call["query"].strip(), keyword_case="upper", reindent=True) + formatted_query = sqlparse.format( + call["query"].strip(), keyword_case="upper", reindent=True, strip_comments=True + ) call["query"] = formatted_query # Collect EXPLAIN for executed query diff --git a/frappe/search/website_search.py b/frappe/search/website_search.py index 4270d5c7c0ff..1dd030e21cda 100644 --- a/frappe/search/website_search.py +++ b/frappe/search/website_search.py @@ -98,7 +98,7 @@ def slugs_with_web_view(_items_to_index): fields = ["route", doctype.website_search_field] filters = ({doctype.is_published_field: 1},) if doctype.website_search_field: - docs = frappe.get_all(doctype.name, filters=filters, fields=fields + ["title"]) + docs = frappe.get_all(doctype.name, filters=filters, fields=[*fields, "title"]) for doc in docs: content = frappe.utils.md_to_html(getattr(doc, doctype.website_search_field)) soup = BeautifulSoup(content, "html.parser") diff --git a/frappe/sessions.py b/frappe/sessions.py index eaa0092656a7..65bc6d79bd7c 100644 --- a/frappe/sessions.py +++ b/frappe/sessions.py @@ -61,7 +61,7 @@ def get_sessions_to_clear(user=None, keep_current=False, device=None): if not device: device = ("desktop", "mobile") - if not isinstance(device, (tuple, list)): + if not isinstance(device, tuple | list): device = (device,) offset = 0 diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 14e1f2a5b9e9..e835cbe7cc00 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -242,7 +242,7 @@ def get_user_energy_and_review_points(user=None, from_date=None, as_dict=True): values.from_date = from_date points_list = frappe.db.sql( - """ + f""" SELECT SUM(CASE WHEN `type` != 'Review' THEN `points` ELSE 0 END) AS energy_points, SUM(CASE WHEN `type` = 'Review' THEN `points` ELSE 0 END) AS review_points, @@ -256,7 +256,7 @@ def get_user_energy_and_review_points(user=None, from_date=None, as_dict=True): {conditions} GROUP BY `user` ORDER BY `energy_points` DESC - """.format(conditions=conditions, given_points_condition=given_points_condition), + """, values=values, as_dict=1, ) 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 1057ac274940..d9d5c1fa9809 100644 --- a/frappe/social/doctype/energy_point_rule/energy_point_rule.py +++ b/frappe/social/doctype/energy_point_rule/energy_point_rule.py @@ -55,7 +55,7 @@ def apply(self, doc): {"points": points, "user": user, "rule": rule}, self.apply_only_once, ) - except Exception as e: + except Exception: self.log_error("Energy points failed") def rule_condition_satisfied(self, doc): diff --git a/frappe/test_runner.py b/frappe/test_runner.py index b17535e4b253..94b75e33f613 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -206,7 +206,7 @@ def run_tests_for_doctype( junit_xml_output=False, ): modules = [] - if not isinstance(doctypes, (list, tuple)): + if not isinstance(doctypes, list | tuple): doctypes = [doctypes] for doctype in doctypes: @@ -259,7 +259,7 @@ def _run_unittest( test_suite = unittest.TestSuite() - if not isinstance(modules, (list, tuple)): + if not isinstance(modules, list | tuple): modules = [modules] for module in modules: diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py index b3483d888f5e..834fe0cac8db 100644 --- a/frappe/tests/test_api.py +++ b/frappe/tests/test_api.py @@ -1,5 +1,6 @@ import json import sys +import typing from contextlib import contextmanager from random import choice from threading import Thread @@ -48,7 +49,9 @@ def patch_request_header(key, *args, **kwargs): class ThreadWithReturnValue(Thread): - def __init__(self, group=None, target=None, name=None, args=(), kwargs={}): + def __init__(self, group=None, target=None, name=None, args=(), kwargs=None): + if kwargs is None: + kwargs = {} Thread.__init__(self, group, target, name, args, kwargs) self._return = None @@ -102,7 +105,7 @@ def delete(self, path, **kwargs) -> TestResponse: class TestResourceAPI(FrappeAPITestCase): DOCTYPE = "ToDo" - GENERATED_DOCUMENTS = [] + GENERATED_DOCUMENTS: typing.ClassVar[list] = [] @classmethod def setUpClass(cls): @@ -270,7 +273,7 @@ def test_auth_cycle(self): response = self.get(f"{self.METHOD_PATH}/frappe.auth.get_logged_user") self.assertEqual(response.status_code, 401) - authorization_token = f"NonExistentKey:INCORRECT" + authorization_token = "NonExistentKey:INCORRECT" response = self.get(f"{self.METHOD_PATH}/frappe.auth.get_logged_user") self.assertEqual(response.status_code, 401) @@ -334,7 +337,7 @@ def after_request(*args, **kwargs): class TestResponse(FrappeAPITestCase): def test_generate_pdf(self): response = self.get( - f"/api/method/frappe.utils.print_format.download_pdf", + "/api/method/frappe.utils.print_format.download_pdf", {"sid": self.sid, "doctype": "User", "name": "Guest"}, ) self.assertEqual(response.status_code, 200) diff --git a/frappe/tests/test_assign.py b/frappe/tests/test_assign.py index f3b579c02875..9260f71b7aee 100644 --- a/frappe/tests/test_assign.py +++ b/frappe/tests/test_assign.py @@ -18,7 +18,7 @@ def test_assign(self): self.assertTrue("test@example.com" in [d.owner for d in added]) - removed = frappe.desk.form.assign_to.remove(todo.doctype, todo.name, "test@example.com") + frappe.desk.form.assign_to.remove(todo.doctype, todo.name, "test@example.com") # assignment is cleared assignments = frappe.desk.form.assign_to.get(dict(doctype=todo.doctype, name=todo.name)) diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py index 4faade331c1d..34d2a92f28b9 100644 --- a/frappe/tests/test_caching.py +++ b/frappe/tests/test_caching.py @@ -15,7 +15,7 @@ def request_specific_api(a: list | tuple | dict | int, b: int) -> int: # API that takes very long to return a result todays_value = external_service() - if not isinstance(a, (int, float)): + if not isinstance(a, int | float): a = 1 return a**b * todays_value @@ -44,7 +44,9 @@ def test_request_cache(self): frappe.get_last_doc("DocType"), frappe._dict(), ] - same_output_received = lambda: all([x for x in set(retval) if x == retval[0]]) + + def same_output_received(): + return all([x for x in set(retval) if x == retval[0]]) # ensure that external service was called only once # thereby return value of request_specific_api is cached diff --git a/frappe/tests/test_child_table.py b/frappe/tests/test_child_table.py index 5dce5f54c869..920a800bf0b1 100644 --- a/frappe/tests/test_child_table.py +++ b/frappe/tests/test_child_table.py @@ -1,4 +1,4 @@ -from typing import Callable +from collections.abc import Callable import frappe from frappe.model import child_table_fields diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py index da2fd83b0d1b..abc5fd10398e 100644 --- a/frappe/tests/test_commands.py +++ b/frappe/tests/test_commands.py @@ -8,6 +8,7 @@ import os import shlex import subprocess +import types import unittest from contextlib import contextmanager from functools import wraps @@ -489,15 +490,17 @@ def test_set_global_conf(self): class TestBackups(BaseTestCommands): - backup_map = { - "includes": { - "includes": [ - "ToDo", - "Note", - ] - }, - "excludes": {"excludes": ["Activity Log", "Access Log", "Error Log"]}, - } + backup_map = types.MappingProxyType( + { + "includes": { + "includes": [ + "ToDo", + "Note", + ] + }, + "excludes": {"excludes": ["Activity Log", "Access Log", "Error Log"]}, + } + ) home = os.path.expanduser("~") site_backup_path = frappe.utils.get_site_path("private", "backups") diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index a94c52e32805..290890013788 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -16,7 +16,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, cint, now, random_string, set_request from frappe.utils.testutils import clear_custom_fields @@ -336,36 +336,40 @@ def add_custom_field(field): random_value = random_string(20) # Testing read - self.assertEqual(list(frappe.get_all("ToDo", fields=[random_field], limit=1)[0])[0], random_field) + self.assertEqual(next(iter(frappe.get_all("ToDo", fields=[random_field], limit=1)[0])), random_field) self.assertEqual( - list(frappe.get_all("ToDo", fields=[f"`{random_field}` as total"], limit=1)[0])[0], "total" + next(iter(frappe.get_all("ToDo", fields=[f"`{random_field}` as total"], limit=1)[0])), "total" ) # Testing read for distinct and sql functions self.assertEqual( - list( - frappe.get_all( - "ToDo", - fields=[f"`{random_field}` as total"], - distinct=True, - limit=1, - )[0] - )[0], + next( + iter( + frappe.get_all( + "ToDo", + fields=[f"`{random_field}` as total"], + distinct=True, + limit=1, + )[0] + ) + ), "total", ) self.assertEqual( - list( - frappe.get_all( - "ToDo", - fields=[f"`{random_field}`"], - distinct=True, - limit=1, - )[0] - )[0], + next( + iter( + frappe.get_all( + "ToDo", + fields=[f"`{random_field}`"], + distinct=True, + limit=1, + )[0] + ) + ), random_field, ) self.assertEqual( - list(frappe.get_all("ToDo", fields=[f"count(`{random_field}`)"], limit=1)[0])[0], + next(iter(frappe.get_all("ToDo", fields=[f"count(`{random_field}`)"], limit=1)[0])), "count" if frappe.conf.db_type == "postgres" else f"count(`{random_field}`)", ) @@ -433,7 +437,7 @@ def test_transaction_writes_error(self): frappe.db.MAX_WRITES_PER_TRANSACTION = 1 note = frappe.get_last_doc("ToDo") note.description = "changed" - with self.assertRaises(frappe.TooManyWritesError) as tmw: + with self.assertRaises(frappe.TooManyWritesError): note.save() frappe.db.MAX_WRITES_PER_TRANSACTION = Database.MAX_WRITES_PER_TRANSACTION @@ -924,6 +928,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_db_query.py b/frappe/tests/test_db_query.py index 75b0ac6a6b5d..af2093a11ae8 100644 --- a/frappe/tests/test_db_query.py +++ b/frappe/tests/test_db_query.py @@ -14,6 +14,7 @@ from frappe.model.db_query import DatabaseQuery, get_between_date_filter from frappe.permissions import add_user_permission, clear_user_permissions_for_doctype from frappe.query_builder import Column +from frappe.tests.test_query_builder import db_type_is, run_only_if from frappe.tests.utils import FrappeTestCase from frappe.utils.testutils import add_custom_field, clear_custom_fields @@ -743,7 +744,7 @@ def test_is_set_is_not_set(self): def test_set_field_tables(self): # Tests _in_standard_sql_methods method in test_set_field_tables # The following query will break if the above method is broken - data = frappe.db.get_list( + frappe.db.get_list( "Web Form", filters=[["Web Form Field", "reqd", "=", 1]], fields=["count(*) as count"], @@ -755,7 +756,7 @@ def test_virtual_field_get_list(self): try: frappe.get_list("Prepared Report", ["*"]) frappe.get_list("Scheduled Job Type", ["*"]) - except Exception as e: + except Exception: print(frappe.get_traceback()) self.fail("get_list not working with virtual field") @@ -1022,11 +1023,11 @@ def test_permission_query_condition(self): create_dashboard_settings(self.user) dashboard_settings = frappe.db.sql( - """ + f""" SELECT name FROM `tabDashboard Settings` - WHERE {condition} - """.format(condition=permission_query_conditions), + WHERE {permission_query_conditions} + """, as_dict=1, )[0] @@ -1071,6 +1072,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 @@ -1147,6 +1150,7 @@ def setUp(self) -> None: frappe.set_user("Administrator") return super().setUp() + @run_only_if(db_type_is.MARIADB) # TODO: postgres name casting is messed up def test_get_count(self): frappe.local.request = frappe._dict() frappe.local.request.method = "GET" @@ -1160,13 +1164,13 @@ def test_get_count(self): "distinct": "false", } ) - list_filter_response = execute_cmd("frappe.desk.reportview.get_count") + count = execute_cmd("frappe.desk.reportview.get_count") frappe.local.form_dict = frappe._dict( {"doctype": "DocType", "filters": {"show_title_field_in_link": 1}, "distinct": "true"} ) dict_filter_response = execute_cmd("frappe.desk.reportview.get_count") - self.assertIsInstance(list_filter_response, int) - self.assertEqual(list_filter_response, dict_filter_response) + self.assertIsInstance(count, int) + self.assertEqual(count, dict_filter_response) # test with child table filter frappe.local.form_dict = frappe._dict( @@ -1187,6 +1191,35 @@ def test_get_count(self): )[0][0] self.assertEqual(child_filter_response, current_value) + # test with limit + limit = 2 + frappe.local.form_dict = frappe._dict( + { + "doctype": "DocType", + "filters": [["DocType", "is_virtual", "=", 1]], + "fields": [], + "distinct": "false", + "limit": limit, + } + ) + count = execute_cmd("frappe.desk.reportview.get_count") + self.assertIsInstance(count, int) + self.assertLessEqual(count, limit) + + # test with distinct + limit = 2 + frappe.local.form_dict = frappe._dict( + { + "doctype": "DocType", + "fields": [], + "distinct": "true", + "limit": limit, + } + ) + count = execute_cmd("frappe.desk.reportview.get_count") + self.assertIsInstance(count, int) + self.assertLessEqual(count, limit) + def test_reportview_get(self): user = frappe.get_doc("User", "test@example.com") add_child_table_to_blog_post() diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 3ebbabee1da9..8918e64ad9b0 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/test_email.py b/frappe/tests/test_email.py index ea68e0b0b6d2..5e6e28e6895f 100644 --- a/frappe/tests/test_email.py +++ b/frappe/tests/test_email.py @@ -8,9 +8,13 @@ import requests import frappe +from frappe.core.doctype.communication.email import make +from frappe.desk.form.load import get_attachments from frappe.email.doctype.email_account.test_email_account import TestEmailAccount from frappe.email.doctype.email_queue.email_queue import QueueBuilder -from frappe.tests.utils import FrappeTestCase +from frappe.query_builder.utils import db_type_is +from frappe.tests.test_query_builder import run_only_if +from frappe.tests.utils import FrappeTestCase, change_settings test_dependencies = ["Email Account"] @@ -362,11 +366,16 @@ def tearDown(self) -> None: frappe.flags.testing_email = False return super().tearDown() - def get_last_sent_emails(self): + @classmethod + def get_last_sent_emails(cls): return requests.get( - f"{self.SMTP4DEV_WEB}/api/Messages?sortColumn=receivedDate&sortIsDescending=true" + f"{cls.SMTP4DEV_WEB}/api/Messages?sortColumn=receivedDate&sortIsDescending=true" ).json() + @classmethod + def get_message(cls, message_id): + return requests.get(f"{cls.SMTP4DEV_WEB}/api/Messages/{message_id}").json() + def test_send_email(self): sender = "a@example.io" recipients = "b@example.io,c@example.io" @@ -386,3 +395,46 @@ def test_send_email(self): self.assertEqual(sent_mail["from"], sender) self.assertEqual(sent_mail["subject"], subject) self.assertSetEqual(set(recipients.split(",")), {m["to"] for m in sent_mails}) + + @run_only_if(db_type_is.MARIADB) + @change_settings("System Settings", store_attached_pdf_document=1) + def test_store_attachments(self): + """ "attach print" feature just tells email queue which document to attach, this is not + actually stored unless system setting says so.""" + + name = make( + sender="test_sender@example.com", + recipients="test_recipient@example.com,test_recipient2@example.com", + content="test mail 001", + subject="test-mail-002", + doctype="Email Account", + name="_Test Email Account 1", + print_format="Standard", + send_email=True, + now=True, + ).get("name") + + communication = frappe.get_doc("Communication", name) + + attachments = get_attachments(communication.doctype, communication.name) + self.assertEqual(len(attachments), 1) + + file = frappe.get_doc("File", attachments[0].name) + self.assertGreater(file.file_size, 1000) + self.assertIn("pdf", file.file_name.lower()) + sent_mails = self.get_last_sent_emails() + self.assertEqual(len(sent_mails), 2) + + for mail in sent_mails: + email_content = self.get_message(mail["id"]) + + attachment_found = False + for part in email_content["parts"]: + for child_part in part["childParts"]: + if child_part["isAttachment"]: + attachment_found = True + self.assertIn("pdf", child_part["name"]) + break + + if not attachment_found: + self.fail("Attachment not found", email_content) diff --git a/frappe/tests/test_form_load.py b/frappe/tests/test_form_load.py index e7a2851b5fba..3cec82b4ba40 100644 --- a/frappe/tests/test_form_load.py +++ b/frappe/tests/test_form_load.py @@ -13,13 +13,13 @@ class TestFormLoad(FrappeTestCase): def test_load(self): getdoctype("DocType") - meta = list(filter(lambda d: d.name == "DocType", frappe.response.docs))[0] + meta = next(filter(lambda d: d.name == "DocType", frappe.response.docs)) self.assertEqual(meta.name, "DocType") self.assertTrue(meta.get("__js")) frappe.response.docs = [] getdoctype("Event") - meta = list(filter(lambda d: d.name == "Event", frappe.response.docs))[0] + meta = next(filter(lambda d: d.name == "Event", frappe.response.docs)) self.assertTrue(meta.get("__calendar_js")) def test_fieldlevel_permissions_in_load(self): diff --git a/frappe/tests/test_monitor.py b/frappe/tests/test_monitor.py index e59ebcde31fe..ab47e7f9880c 100644 --- a/frappe/tests/test_monitor.py +++ b/frappe/tests/test_monitor.py @@ -3,7 +3,7 @@ import frappe import frappe.monitor -from frappe.monitor import MONITOR_REDIS_KEY +from frappe.monitor import MONITOR_REDIS_KEY, get_trace_id from frappe.tests.utils import FrappeTestCase from frappe.utils import set_request from frappe.utils.response import build_response @@ -14,6 +14,10 @@ def setUp(self): frappe.conf.monitor = 1 frappe.cache().delete_value(MONITOR_REDIS_KEY) + def tearDown(self): + frappe.conf.monitor = 0 + frappe.cache().delete_value(MONITOR_REDIS_KEY) + def test_enable_monitor(self): set_request(method="GET", path="/api/method/frappe.ping") response = build_response("json") @@ -77,6 +81,10 @@ def test_flush(self): log = frappe.parse_json(logs[0]) self.assertEqual(log.transaction_type, "request") - def tearDown(self): - frappe.conf.monitor = 0 - frappe.cache().delete_value(MONITOR_REDIS_KEY) + def test_trace_ids(self): + set_request(method="GET", path="/api/method/frappe.ping") + response = build_response("json") + frappe.monitor.start() + frappe.db.sql("select 1") + self.assertIn(get_trace_id(), str(frappe.db.last_query)) + frappe.monitor.stop(response) diff --git a/frappe/tests/test_naming.py b/frappe/tests/test_naming.py index 020f91f7f05f..af232f7c1e02 100644 --- a/frappe/tests/test_naming.py +++ b/frappe/tests/test_naming.py @@ -322,7 +322,7 @@ def test_naming_series_prefix(self): def test_naming_series_validation(self): dns = frappe.get_doc("Document Naming Settings") exisiting_series = dns.get_transactions_and_prefixes()["prefixes"] - valid = ["SINV-", "SI-.{field}.", "SI-#.###", ""] + exisiting_series + valid = ["SINV-", "SI-.{field}.", "SI-#.###", "", *exisiting_series] invalid = ["$INV-", r"WINDOWS\NAMING"] for series in valid: diff --git a/frappe/tests/test_oauth20.py b/frappe/tests/test_oauth20.py index ee98f1a935ee..e667f035febb 100644 --- a/frappe/tests/test_oauth20.py +++ b/frappe/tests/test_oauth20.py @@ -275,7 +275,7 @@ def test_login_using_implicit_token(self): self.assertTrue(check_valid_openid_response(response_dict.get("access_token")[0])) def test_openid_code_id_token(self): - client = update_client_for_auth_code_grant(self.client_id) + update_client_for_auth_code_grant(self.client_id) session = requests.Session() login(session) diff --git a/frappe/tests/test_patches.py b/frappe/tests/test_patches.py index 99fe76ce8422..135f24662a32 100644 --- a/frappe/tests/test_patches.py +++ b/frappe/tests/test_patches.py @@ -165,7 +165,7 @@ def check_patch_files(app): missing_patches.append(module) if missing_patches: - raise Exception(f"Patches missing in patch.txt: \n" + "\n".join(missing_patches)) + raise Exception("Patches missing in patch.txt: \n" + "\n".join(missing_patches)) def _get_dotted_path(file: Path, app) -> str: @@ -174,4 +174,4 @@ def _get_dotted_path(file: Path, app) -> str: *path, filename = file.relative_to(app_path).parts base_filename = Path(filename).stem - return ".".join(path + [base_filename]) + return ".".join([*path, base_filename]) diff --git a/frappe/tests/test_pdf.py b/frappe/tests/test_pdf.py index 4a8fef253cd6..335bd2c8ee5f 100644 --- a/frappe/tests/test_pdf.py +++ b/frappe/tests/test_pdf.py @@ -6,6 +6,7 @@ import frappe import frappe.utils.pdf as pdfgen +from frappe.core.doctype.file.test_file import make_test_image_file from frappe.tests.utils import FrappeTestCase @@ -50,3 +51,16 @@ def test_pdf_generation_as_a_user(self): frappe.set_user("Administrator") pdf = pdfgen.get_pdf(self.html) self.assertTrue(pdf) + + def test_private_images_in_pdf(self): + with make_test_image_file(private=True) as file: + html = f"""
+ + +
+ """ + + pdf = pdfgen.get_pdf(html) + + # If image was actually retrieved then size will be in few kbs, else bytes. + self.assertGreaterEqual(len(pdf), 10_000) diff --git a/frappe/tests/test_perf.py b/frappe/tests/test_perf.py index 11f796b98141..54369328544c 100644 --- a/frappe/tests/test_perf.py +++ b/frappe/tests/test_perf.py @@ -142,7 +142,7 @@ def test_req_per_seconds_basic(self): self.assertGreaterEqual( rps, EXPECTED_RPS * (1 - FAILURE_THREASHOLD), - f"Possible performance regression in basic /api/Resource list requests", + "Possible performance regression in basic /api/Resource list requests", ) @unittest.skip("Not implemented") diff --git a/frappe/tests/test_query_report.py b/frappe/tests/test_query_report.py index 99cf50b2348e..6f77a3380c99 100644 --- a/frappe/tests/test_query_report.py +++ b/frappe/tests/test_query_report.py @@ -80,7 +80,7 @@ def test_report_for_duplicate_column_names(self): {"label": "First Name", "fieldname": "first_name", "fieldtype": "Data"}, {"label": "Last Name", "fieldname": "last_name", "fieldtype": "Data"}, ] - docA = frappe.get_doc( + frappe.get_doc( { "doctype": "DocType", "name": "Doc A", @@ -92,7 +92,7 @@ def test_report_for_duplicate_column_names(self): } ).insert(ignore_if_duplicate=True) - docB = frappe.get_doc( + frappe.get_doc( { "doctype": "DocType", "name": "Doc B", diff --git a/frappe/tests/test_rating.py b/frappe/tests/test_rating.py new file mode 100644 index 000000000000..ca7fabf4cffe --- /dev/null +++ b/frappe/tests/test_rating.py @@ -0,0 +1,29 @@ +import frappe +from frappe.core.doctype.doctype.test_doctype import new_doctype +from frappe.tests.utils import FrappeTestCase + + +class TestRating(FrappeTestCase): + def setUp(self): + doc = new_doctype( + fields=[ + { + "fieldname": "rating", + "fieldtype": "Rating", + "label": "rating", + "reqd": 1, # mandatory + }, + ], + ) + doc.insert() + self.doctype_name = doc.name + + def test_negative_rating(self): + doc = frappe.get_doc(doctype=self.doctype_name, rating=-1) + doc.insert() + self.assertEqual(doc.rating, 0) + + def test_positive_rating(self): + doc = frappe.get_doc(doctype=self.doctype_name, rating=5) + doc.insert() + self.assertEqual(doc.rating, 1) diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py index 8d5f5d27275b..f743a8305f9b 100644 --- a/frappe/tests/test_recorder.py +++ b/frappe/tests/test_recorder.py @@ -101,10 +101,12 @@ def test_multiple_queries(self): self.assertEqual(len(request["calls"]), len(queries)) - for query, call in zip(queries, request["calls"]): + for query, call in zip(queries, request["calls"], strict=False): self.assertEqual( call["query"], - sqlparse.format(query[sql_dialect].strip(), keyword_case="upper", reindent=True), + sqlparse.format( + query[sql_dialect].strip(), keyword_case="upper", reindent=True, strip_comments=True + ), ) def test_duplicate_queries(self): @@ -124,7 +126,7 @@ def test_duplicate_queries(self): requests = frappe.recorder.get() request = frappe.recorder.get(requests[0]["uuid"]) - for query, call in zip(queries, request["calls"]): + for query, call in zip(queries, request["calls"], strict=False): self.assertEqual(call["exact_copies"], query[1]) def test_error_page_rendering(self): diff --git a/frappe/tests/test_rename_doc.py b/frappe/tests/test_rename_doc.py index 6488df678f56..6b630cb3f358 100644 --- a/frappe/tests/test_rename_doc.py +++ b/frappe/tests/test_rename_doc.py @@ -23,7 +23,7 @@ @contextmanager -def patch_db(endpoints: list[str] = None): +def patch_db(endpoints: list[str] | None = None): patched_endpoints = [] for point in endpoints: diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py index f700d995512c..fb59783e64bf 100644 --- a/frappe/tests/test_safe_exec.py +++ b/frappe/tests/test_safe_exec.py @@ -88,7 +88,7 @@ def test_enqueue(self): def test_ensure_getattrable_globals(self): def check_safe(objects): for obj in objects: - if isinstance(obj, (types.ModuleType, types.CodeType, types.TracebackType, types.FrameType)): + if isinstance(obj, types.ModuleType | types.CodeType | types.TracebackType | types.FrameType): self.fail(f"{obj} wont work in safe exec.") elif isinstance(obj, dict): check_safe(obj.values()) diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index bf8ec2b72265..a0378d4973aa 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -29,10 +29,10 @@ class TestTranslate(FrappeTestCase): - guest_sessions_required = [ + guest_sessions_required = ( "test_guest_request_language_resolution_with_cookie", "test_guest_request_language_resolution_with_request_header", - ] + ) def setUp(self): if self._testMethodName in self.guest_sessions_required: @@ -53,7 +53,7 @@ def test_extract_message_from_file(self): msg=f"Mismatched output:\nExpected: {expected_output}\nFound: {data}", ) - for extracted, expected in zip(data, expected_output): + for extracted, expected in zip(data, expected_output, strict=False): ext_filename, ext_message, ext_context, ext_line = extracted exp_message, exp_context, exp_line = expected self.assertEqual(ext_filename, exp_filename) @@ -168,7 +168,7 @@ def test_python_extractor(self): output = extract_messages_from_python_code(code) self.assertEqual(len(expected_output), len(output)) - for expected, actual in zip(expected_output, output): + for expected, actual in zip(expected_output, output, strict=False): with self.subTest(): self.assertEqual(expected, actual) @@ -205,7 +205,7 @@ def test_js_extractor(self): output = extract_messages_from_javascript_code(code) self.assertEqual(len(expected_output), len(output)) - for expected, actual in zip(expected_output, output): + for expected, actual in zip(expected_output, output, strict=False): with self.subTest(): self.assertEqual(expected, actual) diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py index 053b755b654b..ff782fb96373 100644 --- a/frappe/tests/test_twofactor.py +++ b/frappe/tests/test_twofactor.py @@ -233,7 +233,7 @@ def toggle_2fa_all_role(state=None): """Enable or disable 2fa for 'all' role on the system.""" all_role = frappe.get_doc("Role", "All") state = state if state is not None else False - if type(state) != bool: + if not isinstance(state, bool): return all_role.two_factor_auth = cint(state) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 247bd8c70f81..bf2f83db6c29 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -384,7 +384,7 @@ def test_validation_for_good_python_expression(self): try: validate_python_code(expr) except Exception as e: - self.fail(f"Invalid error thrown for valid expression: {expr}: {str(e)}") + self.fail(f"Invalid error thrown for valid expression: {expr}: {e!s}") def test_validation_for_bad_python_expression(self): invalid_expressions = [ @@ -546,7 +546,7 @@ class TEST(Enum): self.assertTrue(all([isinstance(x, str) for x in processed_object["time_types"]])) self.assertTrue(all([isinstance(x, float) for x in processed_object["float"]])) - self.assertTrue(all([isinstance(x, (list, str)) for x in processed_object["iter"]])) + self.assertTrue(all([isinstance(x, list | str) for x in processed_object["iter"]])) self.assertIsInstance(processed_object["string"], str) with self.assertRaises(TypeError): json.dumps(BAD_OBJECT, default=json_handler) @@ -757,9 +757,9 @@ def test_url_expansion(self): class TestTBSanitization(FrappeTestCase): def test_traceback_sanitzation(self): try: - password = "42" + password = "42" # noqa: F841 args = {"password": "42", "pwd": "42", "safe": "safe_value"} - args = frappe._dict({"password": "42", "pwd": "42", "safe": "safe_value"}) + args = frappe._dict({"password": "42", "pwd": "42", "safe": "safe_value"}) # noqa: F841 raise Exception except Exception: traceback = frappe.get_traceback(with_context=True) diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 34a45b874395..c2e985f95f5f 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -2,8 +2,8 @@ import datetime import signal import unittest +from collections.abc import Sequence from contextlib import contextmanager -from typing import Sequence from unittest.mock import patch import pytz @@ -32,6 +32,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: @@ -58,7 +60,7 @@ def assertDocumentEqual(self, expected, actual): if isinstance(value, list): actual_child_docs = actual.get(field) self.assertEqual(len(value), len(actual_child_docs), msg=f"{field} length should be same") - for exp_child, actual_child in zip(value, actual_child_docs): + for exp_child, actual_child in zip(value, actual_child_docs, strict=False): self.assertDocumentEqual(exp_child, actual_child) else: self._compare_field(value, actual.get(field), actual, field) @@ -71,7 +73,7 @@ def _compare_field(self, expected, actual, doc: BaseDocument, field: str): self.assertAlmostEqual( expected, actual, places=precision, msg=f"{field} should be same to {precision} digits" ) - elif isinstance(expected, (bool, int)): + elif isinstance(expected, bool | int): self.assertEqual(expected, cint(actual), msg=msg) elif isinstance(expected, datetime_like_types): self.assertEqual(str(expected), str(actual), msg=msg) @@ -90,6 +92,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)) @@ -239,13 +272,18 @@ def timeout(seconds=30, error_message="Test timed out."): adapted from https://stackoverflow.com/a/2282656""" + # Support @timeout (without function call) + no_args = bool(callable(seconds)) + actual_timeout = 30 if no_args else seconds + actual_error_message = "Test timed out" if no_args else error_message + def decorator(func): def _handle_timeout(signum, frame): - raise Exception(error_message) + raise Exception(actual_error_message) def wrapper(*args, **kwargs): signal.signal(signal.SIGALRM, _handle_timeout) - signal.alarm(seconds) + signal.alarm(actual_timeout) try: result = func(*args, **kwargs) finally: @@ -254,6 +292,9 @@ def wrapper(*args, **kwargs): return wrapper + if no_args: + return decorator(seconds) + return decorator diff --git a/frappe/translate.py b/frappe/translate.py index e8874e6dfaf0..1909504d00d3 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -59,7 +59,7 @@ USER_TRANSLATION_KEY = "lang_user_translations" -def get_language(lang_list: list = None) -> str: +def get_language(lang_list: list | None = None) -> str: """Set `frappe.local.lang` from HTTP headers at beginning of request Order of priority for setting language: @@ -122,7 +122,7 @@ def get_parent_language(lang: str) -> str: return lang[: lang.index("-")] -def get_user_lang(user: str = None) -> str: +def get_user_lang(user: str | None = None) -> str: """Set frappe.local.lang from user preferences on session beginning or resumption""" user = user or frappe.session.user lang = frappe.cache().hget("lang", user) @@ -333,9 +333,7 @@ def get_translation_dict_from_file(path, lang, app, throw=False) -> dict[str, st elif len(item) in [2, 3]: translation_map[item[0]] = strip(item[1]) elif item: - msg = "Bad translation in '{app}' for language '{lang}': {values}".format( - app=app, lang=lang, values=cstr(item) - ) + msg = f"Bad translation in '{app}' for language '{lang}': {cstr(item)}" frappe.log_error(message=msg, title="Error in translation file") if throw: frappe.throw(msg, title="Error in translation file") @@ -460,7 +458,7 @@ def get_messages_from_doctype(name): if d.fieldtype == "Select" and d.options: options = d.options.split("\n") - if not "icon" in options[0]: + if "icon" not in options[0]: messages.extend(options) if d.fieldtype == "HTML" and d.options: messages.append(d.options) @@ -684,7 +682,7 @@ def get_all_messages_from_js_files(app_name=None): messages = [] for app in [app_name] if app_name else frappe.get_installed_apps(_ensure_on_bench=True): if os.path.exists(frappe.get_app_path(app, "public")): - for basepath, folders, files in os.walk(frappe.get_app_path(app, "public")): + for basepath, _folders, files in os.walk(frappe.get_app_path(app, "public")): if "frappe/public/js/lib" in basepath: continue @@ -1091,6 +1089,7 @@ def restore_newlines(s): for key, value in zip( frappe.get_file_items(untranslated_file, ignore_empty_lines=False), frappe.get_file_items(translated_file, ignore_empty_lines=False), + strict=False, ): # undo hack in get_untranslated translation_dict[restore_newlines(key)] = restore_newlines(value) @@ -1200,7 +1199,7 @@ def deduplicate_messages(messages): ret = [] op = operator.itemgetter(1) messages = sorted(messages, key=op) - for k, g in itertools.groupby(messages, op): + for _k, g in itertools.groupby(messages, op): ret.append(next(g)) return ret diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 8019f4b71e12..54da9a309d2f 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -4790,6 +4790,7 @@ No Data...,Keine Daten..., Don't have an account?,Sie haben noch kein Benutzerkonto?, {0} changed value of {1},{0} hat den Wert von {1} geändert, Basic Info,Grundlegende Informationen, +No.,Pos.,Title of the 'row number' column No.,Nr.,number No.,Nein.,opposite of yes There are no upcoming events for you.,Es sind keine Termine für Sie geplant., diff --git a/frappe/twofactor.py b/frappe/twofactor.py index 27c75771e2f0..3b4e912d27c2 100644 --- a/frappe/twofactor.py +++ b/frappe/twofactor.py @@ -222,7 +222,7 @@ def process_2fa_for_sms(user, token, otp_secret): def process_2fa_for_otp_app(user, otp_secret, otp_issuer): """Process OTP App method for 2fa.""" - totp_uri = pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) + pyotp.TOTP(otp_secret).provisioning_uri(user, issuer_name=otp_issuer) if get_default(user + "_otplogin"): otp_setup_completed = True else: diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 90e9d1b06782..72b7f798e897 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -233,7 +233,7 @@ def validate_url(txt, throw=False, valid_schemes=None): # Handle scheme validation if isinstance(valid_schemes, str): is_valid = is_valid and (url.scheme == valid_schemes) - elif isinstance(valid_schemes, (list, tuple, set)): + elif isinstance(valid_schemes, list | tuple | set): is_valid = is_valid and (url.scheme in valid_schemes) if not is_valid and throw: @@ -625,7 +625,7 @@ def update_progress_bar(txt, i, l, absolute=False): complete = int(float(i + 1) / l * col) completion_bar = ("=" * complete).ljust(col, " ") - percent_complete = f"{str(int(float(i + 1) / l * 100))}%" + percent_complete = f"{int(float(i + 1) / l * 100)!s}%" status = f"{i} of {l}" if absolute else percent_complete sys.stdout.write(f"\r{txt}: [{completion_bar}] {status}") sys.stdout.flush() @@ -659,7 +659,7 @@ def is_markdown(text): def is_a_property(x) -> bool: """Get properties (@property, @cached_property) in a controller class""" - return isinstance(x, (property, functools.cached_property)) + return isinstance(x, property | functools.cached_property) def get_sites(sites_path=None): @@ -902,7 +902,7 @@ def get_safe_filters(filters): try: filters = json.loads(filters) - if isinstance(filters, (int, float)): + if isinstance(filters, int | float): filters = frappe.as_unicode(filters) except (TypeError, ValueError): diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index ba4892c31296..a5c64d797185 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -236,7 +236,7 @@ def start_worker( rq_password: str | None = None, burst: bool = False, strategy: Literal["round_robin", "random"] | None = None, -) -> NoReturn | None: +) -> None: """Wrapper to start rq worker. Connects to redis and monitors these queues.""" DEQUEUE_STRATEGIES = {"round_robin": RoundRobinWorker, "random": RandomWorker} @@ -251,7 +251,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") @@ -262,7 +261,7 @@ def start_worker( logging_level = "INFO" if quiet: logging_level = "WARNING" - worker = WorkerKlass(queues, name=get_worker_name(queue_name), connection=redis_connection) + worker = WorkerKlass(queues, connection=redis_connection) worker.work( logging_level=logging_level, burst=burst, @@ -277,9 +276,7 @@ def get_worker_name(queue): if queue: # hostname.pid is the default worker name - name = "{uuid}.{hostname}.{pid}.{queue}".format( - uuid=uuid4().hex, hostname=socket.gethostname(), pid=os.getpid(), queue=queue - ) + name = f"{uuid4().hex}.{socket.gethostname()}.{os.getpid()}.{queue}" return name @@ -400,7 +397,7 @@ def get_redis_conn(username=None, password=None): raise except Exception as e: log( - f"Please make sure that Redis Queue runs @ {frappe.get_conf().redis_queue}. Redis reported error: {str(e)}", + f"Please make sure that Redis Queue runs @ {frappe.get_conf().redis_queue}. Redis reported error: {e!s}", colour="red", ) raise @@ -480,7 +477,7 @@ def truncate_failed_registry(job, connection, type, value, traceback): """Ensures that number of failed jobs don't exceed specified limits.""" from frappe.utils import create_batch - conf = frappe.get_conf(site=job.kwargs.get("site")) + conf = frappe.conf if frappe.conf else frappe.get_conf(site=job.kwargs.get("site")) limit = (conf.get("rq_failed_jobs_limit") or RQ_FAILED_JOBS_LIMIT) - 1 for queue in get_queues(connection=connection): diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 35de261dfd6c..eb9a4bf44581 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -470,7 +470,7 @@ def send_email(self): db_backup_url = get_url(os.path.join("backups", os.path.basename(self.backup_path_db))) files_backup_url = get_url(os.path.join("backups", os.path.basename(self.backup_path_files))) - msg = """Hello, + msg = f"""Hello, Your backups are ready to be downloaded. @@ -478,10 +478,7 @@ def send_email(self): 2. [Click here to download the files backup]({files_backup_url}) This link will be valid for 24 hours. A new backup will be available for -download only after 24 hours.""".format( - db_backup_url=db_backup_url, - files_backup_url=files_backup_url, - ) +download only after 24 hours.""" datetime_str = datetime.fromtimestamp(os.stat(self.backup_path_db).st_ctime) subject = datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 4dd37a4489d7..4ba679038c44 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -110,9 +110,7 @@ def _create_app_boilerplate(dest, hooks, no_git=False): with open(os.path.join(dest, hooks.app_name, "README.md"), "w") as f: f.write( frappe.as_unicode( - "## {}\n\n{}\n\n#### License\n\n{}".format( - hooks.app_title, hooks.app_description, hooks.app_license - ) + f"## {hooks.app_title}\n\n{hooks.app_description}\n\n#### License\n\n{hooks.app_license}" ) ) @@ -242,7 +240,7 @@ def create_patch_file(self): raise Exception(f"Patch {self.patch_file} already exists") *path, _filename = self.patch_file.relative_to(self.app_dir.parents[0]).parts - dotted_path = ".".join(path + [self.patch_file.stem]) + dotted_path = ".".join([*path, self.patch_file.stem]) patches_txt = self.app_dir / "patches.txt" existing_patches = patches_txt.read_text() diff --git a/frappe/utils/caching.py b/frappe/utils/caching.py index 6f590b8b8ff4..8bfc08c401fd 100644 --- a/frappe/utils/caching.py +++ b/frappe/utils/caching.py @@ -3,9 +3,9 @@ import json from collections import defaultdict +from collections.abc import Callable from datetime import datetime, timedelta from functools import wraps -from typing import Callable import frappe @@ -85,7 +85,7 @@ def calculate_pi(): calculate_pi(10) # will calculate value """ - def time_cache_wrapper(func: Callable = None) -> Callable: + def time_cache_wrapper(func: Callable | None = None) -> Callable: func_key = f"{func.__module__}.{func.__name__}" def clear_cache(): @@ -138,7 +138,7 @@ def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Calla user: `true` should cache be specific to session user. """ - def wrapper(func: Callable = None) -> Callable: + def wrapper(func: Callable | None = None) -> Callable: func_key = f"{func.__module__}.{func.__qualname__}" def clear_cache(): diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py index a11bb3da8eed..ab9521ae354d 100644 --- a/frappe/utils/change_log.py +++ b/frappe/utils/change_log.py @@ -118,9 +118,7 @@ def get_versions(): if versions[app]["branch"] != "master": branch_version = app_hooks.get("{}_version".format(versions[app]["branch"])) if branch_version: - versions[app]["branch_version"] = branch_version[0] + " ({})".format( - get_app_last_commit_ref(app) - ) + versions[app]["branch_version"] = branch_version[0] + f" ({get_app_last_commit_ref(app)})" try: versions[app]["version"] = frappe.get_attr(app + ".__version__") diff --git a/frappe/utils/csvutils.py b/frappe/utils/csvutils.py index 413890ee46fa..2aa43f96e265 100644 --- a/frappe/utils/csvutils.py +++ b/frappe/utils/csvutils.py @@ -176,7 +176,7 @@ def import_doc(d, doctype, overwrite, row_idx, submit=False, ignore_links=False) def getlink(doctype, name): - return '%(name)s' % locals() + return '{name}'.format(**locals()) def get_csv_content_from_google_sheets(url): diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py index 980107ce2b03..5ed0d1f33f41 100644 --- a/frappe/utils/dashboard.py +++ b/frappe/utils/dashboard.py @@ -112,4 +112,4 @@ def make_records(path, filters=None): if os.path.isdir(join(path, fname)): if fname == "__pycache__": continue - import_file_by_path("{path}/{fname}/{fname}.json".format(path=path, fname=fname)) + import_file_by_path(f"{path}/{fname}/{fname}.json") diff --git a/frappe/utils/data.py b/frappe/utils/data.py index 2447c0d2b009..ac270a2a38e7 100644 --- a/frappe/utils/data.py +++ b/frappe/utils/data.py @@ -24,8 +24,8 @@ from frappe.desk.utils import slug from frappe.utils.deprecations import deprecation_warning -DateTimeLikeObject = Union[str, datetime.date, datetime.datetime] -NumericType = Union[int, float] +DateTimeLikeObject = str | datetime.date | datetime.datetime +NumericType = int | float if typing.TYPE_CHECKING: @@ -110,10 +110,10 @@ def get_datetime( if datetime_str is None: return now_datetime() - if isinstance(datetime_str, (datetime.datetime, datetime.timedelta)): + if isinstance(datetime_str, datetime.datetime | datetime.timedelta): return datetime_str - elif isinstance(datetime_str, (list, tuple)): + elif isinstance(datetime_str, list | tuple): return datetime.datetime(datetime_str) elif isinstance(datetime_str, datetime.date): @@ -1172,7 +1172,7 @@ def encode(obj, encoding="utf-8"): def parse_val(v): """Converts to simple datatypes from SQL query results""" - if isinstance(v, (datetime.date, datetime.datetime)): + if isinstance(v, datetime.date | datetime.datetime): v = str(v) elif isinstance(v, datetime.timedelta): v = ":".join(str(v).split(":")[:2]) @@ -1545,7 +1545,7 @@ def comma_and(some_list, add_quotes=True): def comma_sep(some_list, pattern, add_quotes=True): - if isinstance(some_list, (list, tuple)): + if isinstance(some_list, list | tuple): # list(some_list) is done to preserve the existing list some_list = [str(s) for s in list(some_list)] if not some_list: @@ -1560,7 +1560,7 @@ def comma_sep(some_list, pattern, add_quotes=True): def new_line_sep(some_list): - if isinstance(some_list, (list, tuple)): + if isinstance(some_list, list | tuple): # list(some_list) is done to preserve the existing list some_list = [str(s) for s in list(some_list)] if not some_list: @@ -1740,7 +1740,7 @@ def evaluate_filters(doc, filters: dict | list | tuple): if not compare(doc.get(f.fieldname), f.operator, f.value, f.fieldtype): return False - elif isinstance(filters, (list, tuple)): + elif isinstance(filters, list | tuple): for d in filters: f = get_filter(None, d) if not compare(doc.get(f.fieldname), f.operator, f.value, f.fieldtype): @@ -1777,7 +1777,7 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr key, value = next(iter(f.items())) f = make_filter_tuple(doctype, key, value) - if not isinstance(f, (list, tuple)): + if not isinstance(f, list | tuple): frappe.throw(frappe._("Filter must be a tuple or list (in a list)")) if len(f) == 3: @@ -1850,7 +1850,7 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr def make_filter_tuple(doctype, key, value): """return a filter tuple like [doctype, key, operator, value]""" - if isinstance(value, (list, tuple)): + if isinstance(value, list | tuple): return [doctype, key, value[0], value[1]] else: return [doctype, key, "=", value] @@ -2202,7 +2202,7 @@ def parse_timedelta(s: str) -> datetime.timedelta: return datetime.timedelta(**{key: float(val) for key, val in m.groupdict().items()}) -def get_job_name(key: str, doctype: str = None, doc_name: str = None) -> str: +def get_job_name(key: str, doctype: str | None = None, doc_name: str | None = None) -> str: job_name = key if doctype: job_name += f"_{doctype}" diff --git a/frappe/utils/dateutils.py b/frappe/utils/dateutils.py index 73880d290745..26607aa861ef 100644 --- a/frappe/utils/dateutils.py +++ b/frappe/utils/dateutils.py @@ -54,7 +54,7 @@ def parse_date(date): date = date.split(" ", 1)[0] # why the sorting? checking should be done in a predictable order - check_formats = [None] + sorted(list(dateformats), reverse=not get_user_date_format().startswith("dd")) + check_formats = [None, *sorted(list(dateformats), reverse=not get_user_date_format().startswith("dd"))] for f in check_formats: try: @@ -66,9 +66,8 @@ def parse_date(date): if not parsed_date: raise Exception( - """Cannot understand date - '%s'. - Try formatting it like your default format - '%s'""" - % (date, get_user_date_format()) + f"""Cannot understand date - '{date}'. + Try formatting it like your default format - '{get_user_date_format()}'""" ) return parsed_date diff --git a/frappe/utils/error.py b/frappe/utils/error.py index e58dc6a77a25..d1af15d13aba 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -82,9 +82,7 @@ def get_snapshot(exception, context=10): # creates a snapshot dict with some basic information s = { - "pyver": "Python {version:s}: {executable:s} (prefix: {prefix:s})".format( - version=sys.version.split(maxsplit=1)[0], executable=sys.executable, prefix=sys.prefix - ), + "pyver": f"Python {sys.version.split(maxsplit=1)[0]:s}: {sys.executable:s} (prefix: {sys.prefix:s})", "timestamp": cstr(datetime.datetime.now()), "traceback": traceback.format_exc(), "frames": [], @@ -113,7 +111,7 @@ def get_snapshot(exception, context=10): def reader(lnum=[lnum]): # noqa try: # B023: function is evaluated immediately, binding not necessary - return linecache.getline(file, lnum[0]) # noqa: B023 + return linecache.getline(file, lnum[0]) finally: lnum[0] += 1 diff --git a/frappe/utils/formatters.py b/frappe/utils/formatters.py index 12b3ee11be94..289a001cdec1 100644 --- a/frappe/utils/formatters.py +++ b/frappe/utils/formatters.py @@ -108,7 +108,7 @@ def format_value(value, df=None, doc=None, currency=None, translated=False, form elif df.get("fieldtype") == "Table MultiSelect": values = [] meta = frappe.get_meta(df.options) - link_field = [df for df in meta.fields if df.fieldtype == "Link"][0] + link_field = next(df for df in meta.fields if df.fieldtype == "Link") for v in value: v.update({"__link_titles": doc.get("__link_titles")}) formatted_value = frappe.format_value(v.get(link_field.fieldname, ""), link_field, v) diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py index d32f70659215..c8f5505a0577 100644 --- a/frappe/utils/global_search.py +++ b/frappe/utils/global_search.py @@ -469,7 +469,7 @@ def search(text, start=0, limit=20, doctype=""): # sort results based on allowed_doctype's priority for doctype in allowed_doctypes: - for index, r in enumerate(results): + for _index, r in enumerate(results): if r.doctype == doctype and r.rank > 0.0: try: meta = frappe.get_meta(r.doctype) diff --git a/frappe/utils/goal.py b/frappe/utils/goal.py index 381b70118298..a81f38c71b33 100644 --- a/frappe/utils/goal.py +++ b/frappe/utils/goal.py @@ -50,7 +50,7 @@ def get_monthly_goal_graph_data( goal_doctype_link: str, goal_field: str, date_field: str, - filter_str: str = None, + filter_str: str | None = None, aggregation: str = "sum", filters: dict | None = None, ) -> dict: diff --git a/frappe/utils/image.py b/frappe/utils/image.py index fa0953cf529c..3d7d7f66c57f 100644 --- a/frappe/utils/image.py +++ b/frappe/utils/image.py @@ -10,7 +10,7 @@ def resize_images(path, maxdim=700): size = (maxdim, maxdim) - for basepath, folders, files in os.walk(path): + for basepath, _folders, files in os.walk(path): for fname in files: extn = fname.rsplit(".", 1)[1] if extn in ("jpg", "jpeg", "png", "gif"): diff --git a/frappe/utils/install.py b/frappe/utils/install.py index 59db537c265d..6c247d0f2fd3 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -15,6 +15,7 @@ def before_install(): frappe.reload_doc("desk", "doctype", "form_tour_step") frappe.reload_doc("desk", "doctype", "form_tour") frappe.reload_doc("core", "doctype", "doctype") + frappe.clear_cache() def after_install(): diff --git a/frappe/utils/jinja_globals.py b/frappe/utils/jinja_globals.py index e1efb6d1dd6c..848f41c52761 100644 --- a/frappe/utils/jinja_globals.py +++ b/frappe/utils/jinja_globals.py @@ -11,7 +11,7 @@ def resolve_class(*classes): if classes is False: return "" - if isinstance(classes, (list, tuple)): + if isinstance(classes, list | tuple): return " ".join(resolve_class(c) for c in classes).strip() if isinstance(classes, dict): diff --git a/frappe/utils/make_random.py b/frappe/utils/make_random.py index 1915cbb620b2..da78621fde43 100644 --- a/frappe/utils/make_random.py +++ b/frappe/utils/make_random.py @@ -14,7 +14,7 @@ def add_random_children(doc, fieldname, rows, randomize, unique=None): if rows > 1: nrows = random.randrange(1, rows) - for i in range(nrows): + for _i in range(nrows): d = {} for key, val in randomize.items(): if isinstance(val[0], str): @@ -41,12 +41,10 @@ def get_random(doctype, filters=None, doc=False): out = frappe.db.multisql( { - "mariadb": """select name from `tab%s` %s - order by RAND() limit 1 offset 0""" - % (doctype, condition), - "postgres": """select name from `tab%s` %s - order by RANDOM() limit 1 offset 0""" - % (doctype, condition), + "mariadb": f"""select name from `tab{doctype}` {condition} + order by RAND() limit 1 offset 0""", + "postgres": f"""select name from `tab{doctype}` {condition} + order by RANDOM() limit 1 offset 0""", } ) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index e9d1a237d1ea..af8254219479 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -3,7 +3,8 @@ import base64 import json -from typing import TYPE_CHECKING, Callable +from collections.abc import Callable +from typing import TYPE_CHECKING import frappe import frappe.utils @@ -150,7 +151,7 @@ def get_info_via_oauth(provider: str, code: str, decoder: Callable | None = None if provider == "github" and not info.get("email"): emails = session.get("/user/emails", params=api_endpoint_args).json() - email_dict = list(filter(lambda x: x.get("primary"), emails))[0] + email_dict = next(filter(lambda x: x.get("primary"), emails)) info["email"] = email_dict.get("email") if not (info.get("email_verified") or info.get("email")): diff --git a/frappe/utils/password.py b/frappe/utils/password.py index eeba59f70360..592e0fd31c7a 100644 --- a/frappe/utils/password.py +++ b/frappe/utils/password.py @@ -223,6 +223,10 @@ def decrypt(txt, encryption_key=None): + _( "If you have recently restored the site you may need to copy the site config contaning original Encryption Key." ) + + "
" + + _( + "Please visit https://frappecloud.com/docs/sites/migrate-an-existing-site#encryption-key for more information." + ), ) diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py index 1a79582b6a10..6af688ec0644 100644 --- a/frappe/utils/pdf.py +++ b/frappe/utils/pdf.py @@ -1,10 +1,14 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import base64 +import contextlib import io +import mimetypes import os import re import subprocess from distutils.version import LooseVersion +from urllib.parse import parse_qs, urlparse import pdfkit from bs4 import BeautifulSoup @@ -12,6 +16,7 @@ import frappe from frappe import _ +from frappe.core.doctype.file.utils import find_file_by_url from frappe.utils import scrub_urls from frappe.utils.jinja_globals import bundled_asset, is_rtl @@ -113,6 +118,7 @@ def prepare_options(html, options): # cookies options.update(get_cookie_options()) + html = inline_private_images(html) # page size pdf_page_size = ( @@ -189,6 +195,34 @@ def read_options_from_html(html): return soup.prettify(), options +def inline_private_images(html) -> str: + soup = BeautifulSoup(html, "html.parser") + for img in soup.find_all("img"): + if b64 := _get_base64_image(img["src"]): + img["src"] = b64 + return str(soup) + + +def _get_base64_image(src): + """Return base64 version of image if user has permission to view it""" + try: + parsed_url = urlparse(src) + path = parsed_url.path + query = parse_qs(parsed_url.query) + mime_type = mimetypes.guess_type(path)[0] + if not mime_type.startswith("image/"): + return + filename = query.get("fid") and query["fid"][0] or None + file = find_file_by_url(path, name=filename) + if not file or not file.is_private: + return + + b64_encoded_image = base64.b64encode(file.get_content()).decode() + return f"data:{mime_type};base64,{b64_encoded_image}" + except Exception: + frappe.logger("pdf").error("Failed to convert inline images to base64", exc_info=True) + + def prepare_header_footer(soup: BeautifulSoup): options = {} diff --git a/frappe/utils/print_format.py b/frappe/utils/print_format.py index ae2b4379772a..a7b06f8ebd2d 100644 --- a/frappe/utils/print_format.py +++ b/frappe/utils/print_format.py @@ -70,7 +70,7 @@ def download_multi_pdf(doctype, name, format=None, no_letterhead=False, letterhe result = json.loads(name) # Concatenating pdf files - for i, ss in enumerate(result): + for _i, ss in enumerate(result): pdf_writer = frappe.get_print( doctype, ss, diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index bd2f68118673..041e3e4e2a6b 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -112,7 +112,7 @@ def delete_key(self, *args, **kwargs): def delete_value(self, keys, user=None, make_keys=True, shared=False): """Delete value, list of values.""" - if not isinstance(keys, (list, tuple)): + if not isinstance(keys, list | tuple): keys = (keys,) for key in keys: @@ -252,3 +252,45 @@ def srandmember(self, name, count=None): def smembers(self, name): """Return all members of the set""" return super().smembers(self.make_key(name)) + + +def setup_cache(): + if frappe.conf.redis_cache_sentinel_enabled: + sentinels = [tuple(node.split(":")) for node in frappe.conf.get("redis_cache_sentinels", [])] + sentinel = get_sentinel_connection( + sentinels=sentinels, + sentinel_username=frappe.conf.get("redis_cache_sentinel_username"), + sentinel_password=frappe.conf.get("redis_cache_sentinel_password"), + master_username=frappe.conf.get("redis_cache_master_username"), + master_password=frappe.conf.get("redis_cache_master_password"), + ) + return sentinel.master_for( + frappe.conf.get("redis_cache_master_service"), + redis_class=RedisWrapper, + ) + + return RedisWrapper.from_url(frappe.conf.get("redis_cache")) + + +def get_sentinel_connection( + sentinels: list[tuple[str, int]], + sentinel_username=None, + sentinel_password=None, + master_username=None, + master_password=None, +): + from redis.sentinel import Sentinel + + sentinel_kwargs = {} + if sentinel_username: + sentinel_kwargs["username"] = sentinel_username + + if sentinel_password: + sentinel_kwargs["password"] = sentinel_password + + return Sentinel( + sentinels=sentinels, + sentinel_kwargs=sentinel_kwargs, + username=master_username, + password=master_password, + ) diff --git a/frappe/utils/response.py b/frappe/utils/response.py index a8d439fcad6f..d29997cfe1a5 100644 --- a/frappe/utils/response.py +++ b/frappe/utils/response.py @@ -156,7 +156,7 @@ def json_handler(obj): from collections.abc import Iterable from re import Match - if isinstance(obj, (datetime.date, datetime.datetime, datetime.time)): + if isinstance(obj, datetime.date | datetime.datetime | datetime.time): return str(obj) elif isinstance(obj, datetime.timedelta): @@ -185,7 +185,7 @@ def json_handler(obj): return repr(obj) else: - raise TypeError(f"""Object of type {type(obj)} with value of {repr(obj)} is not JSON serializable""") + raise TypeError(f"""Object of type {type(obj)} with value of {obj!r} is not JSON serializable""") def as_page(): @@ -213,25 +213,13 @@ def download_backup(path): def download_private_file(path: str) -> Response: """Checks permissions and sends back private file""" + from frappe.core.doctype.file.utils import find_file_by_url if frappe.session.user == "Guest": raise Forbidden(_("You don't have permission to access this file")) - filters = {"file_url": path} - if frappe.form_dict.fid: - filters["name"] = str(frappe.form_dict.fid) - - files = frappe.get_all("File", filters=filters, fields="*") - - # this file might be attached to multiple documents - # if the file is accessible from any one of those documents - # then it should be downloadable - for file_data in files: - file: "File" = frappe.get_doc(doctype="File", **file_data) - if file.is_downloadable(): - break - - else: + file = find_file_by_url(path, name=frappe.form_dict.fid) + if not file: raise Forbidden(_("You don't have permission to access this file")) make_access_log(doctype="File", document=file.name, file_type=os.path.splitext(path)[-1][1:]) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 412a073d7a93..41aecc97d66e 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -460,7 +460,7 @@ def _validate_attribute_read(object, name): if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES): raise SyntaxError(f"{name} is an unsafe attribute") - if isinstance(object, (types.ModuleType, types.CodeType, types.TracebackType, types.FrameType)): + if isinstance(object, types.ModuleType | types.CodeType | types.TracebackType | types.FrameType): raise SyntaxError(f"Reading {object} attributes is not allowed") if name.startswith("_"): @@ -471,16 +471,14 @@ def _write(obj): # guard function for RestrictedPython if isinstance( obj, - ( - types.ModuleType, - types.CodeType, - types.TracebackType, - types.FrameType, - type, - types.FunctionType, # covers lambda - types.MethodType, - types.BuiltinFunctionType, # covers methods - ), + types.ModuleType + | types.CodeType + | types.TracebackType + | types.FrameType + | type + | types.FunctionType + | types.MethodType + | types.BuiltinFunctionType, ): raise SyntaxError(f"Not allowed to write to object {obj} of type {type(obj)}") return obj diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py index be5b2e58d2cf..c2b977e8e872 100755 --- a/frappe/utils/scheduler.py +++ b/frappe/utils/scheduler.py @@ -93,7 +93,7 @@ def enqueue_events(site): frappe.flags.enqueued_jobs = [] queued_jobs = get_jobs(site=site, key="job_type").get(site) or [] for job_type in frappe.get_all("Scheduled Job Type", ("name", "method"), dict(stopped=0)): - if not job_type.method in queued_jobs: + if job_type.method not in queued_jobs: # don't add it to queue if still pending frappe.get_doc("Scheduled Job Type", job_type.name).enqueue() diff --git a/frappe/utils/user.py b/frappe/utils/user.py index 333c911e0db2..3b27bb0944b0 100644 --- a/frappe/utils/user.py +++ b/frappe/utils/user.py @@ -332,7 +332,7 @@ def add_system_manager( first_name: str | None = None, last_name: str | None = None, send_welcome_email: bool = False, - password: str = None, + password: str | None = None, ) -> "User": # add user user = frappe.new_doc("User") diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 79ed173ce724..04f48c0e0c3d 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -344,7 +344,7 @@ def get_blog_list(doctype, txt=None, filters=None, limit_start=0, limit_page_len if ( post.avatar - and (not "http:" in post.avatar and not "https:" in post.avatar) + and ("http:" not in post.avatar and "https:" not in post.avatar) and not post.avatar.startswith("/") ): post.avatar = "/" + post.avatar diff --git a/frappe/website/doctype/blog_post/test_blog_post.py b/frappe/website/doctype/blog_post/test_blog_post.py index f0a7fc6fe1e5..5a19ba91b038 100644 --- a/frappe/website/doctype/blog_post/test_blog_post.py +++ b/frappe/website/doctype/blog_post/test_blog_post.py @@ -58,7 +58,7 @@ def test_category_link(self): # On blog post page find link to the category page soup = BeautifulSoup(blog_page_html, "html.parser") - category_page_link = list(soup.find_all("a", href=re.compile(blog.blog_category)))[0] + category_page_link = next(iter(soup.find_all("a", href=re.compile(blog.blog_category)))) category_page_url = category_page_link["href"] cached_value = frappe.db.value_cache.get(("DocType", "Blog Post", "name")) @@ -80,7 +80,7 @@ def test_blog_pagination(self): # Create some Blog Posts for a Blog Category category_title, blogs, BLOG_COUNT = "List Category", [], 4 - for index in range(BLOG_COUNT): + for _index in range(BLOG_COUNT): blog = make_test_blog(category_title) blogs.append(blog) diff --git a/frappe/website/doctype/portal_settings/portal_settings.py b/frappe/website/doctype/portal_settings/portal_settings.py index b8c0e9c0c41b..6c746a3e61bf 100644 --- a/frappe/website/doctype/portal_settings/portal_settings.py +++ b/frappe/website/doctype/portal_settings/portal_settings.py @@ -3,6 +3,7 @@ import frappe from frappe.model.document import Document +from frappe.website.path_resolver import validate_path class PortalSettings(Document): @@ -57,3 +58,7 @@ def remove_deleted_doctype_items(self): for menu_item in list(self.get("menu")): if menu_item.reference_doctype not in existing_doctypes: self.remove(menu_item) + + def validate(self): + if frappe.request and self.default_portal_home: + validate_path(self.default_portal_home) diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 187ee745da50..6523501c731f 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -24,12 +24,11 @@ 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.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); diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index 038ed2570c14..d08318989155 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -93,7 +93,7 @@ def render_dynamic(self, context): frappe.flags.web_block_styles = {} try: context["main_section"] = render_template(context.main_section, context) - if not "" in context.main_section: + if "" not in context.main_section: context["no_cache"] = 1 except TemplateSyntaxError: raise @@ -105,13 +105,13 @@ def set_breadcrumbs(self, context): """Build breadcrumbs template""" if self.breadcrumbs: context.parents = frappe.safe_eval(self.breadcrumbs, {"_": _}) - if not "no_breadcrumbs" in context: + if "no_breadcrumbs" not in context: if "" in context.main_section: context.no_breadcrumbs = 1 def set_title_and_header(self, context): """Extract and set title and header from content or context.""" - if not "no_header" in context: + if "no_header" not in context: if "" in context.main_section: context.no_header = 1 diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py index 0295ac03e4ef..fdc8679e2598 100644 --- a/frappe/website/doctype/website_settings/website_settings.py +++ b/frappe/website/doctype/website_settings/website_settings.py @@ -181,7 +181,7 @@ def get_website_settings(context=None): for key in via_hooks: context[key] = via_hooks[key] if key not in ("top_bar_items", "footer_items", "post_login") and isinstance( - context[key], (list, tuple) + context[key], list | tuple ): context[key] = context[key][-1] diff --git a/frappe/website/page_renderers/template_page.py b/frappe/website/page_renderers/template_page.py index 6a4752b9b7b8..43f2e9108709 100644 --- a/frappe/website/page_renderers/template_page.py +++ b/frappe/website/page_renderers/template_page.py @@ -201,8 +201,8 @@ def set_properties_from_source(self): and "{% extends" not in self.source and "" not in self.source ): - self.source = """{{% extends "{0}" %}} - {{% block page_content %}}{1}{{% endblock %}}""".format(context.base_template, self.source) + self.source = f"""{{% extends "{context.base_template}" %}} + {{% block page_content %}}{self.source}{{% endblock %}}""" self.set_properties_via_comments() diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py index c6ff159e86dd..caa383043105 100644 --- a/frappe/website/path_resolver.py +++ b/frappe/website/path_resolver.py @@ -44,7 +44,8 @@ def resolve(self): return endpoint, TemplatePage(endpoint, 200) custom_renderers = self.get_custom_page_renderers() - renderers = custom_renderers + [ + renderers = [ + *custom_renderers, StaticPage, WebFormPage, DocumentPage, @@ -172,3 +173,8 @@ def _get(): return _get() return frappe.cache().get_value("website_route_rules", _get) + + +def validate_path(path: str): + if not PathResolver(path).is_valid_path(): + frappe.throw(frappe._("Path {0} it not a valid path").format(frappe.bold(path))) diff --git a/frappe/website/report/website_analytics/website_analytics.py b/frappe/website/report/website_analytics/website_analytics.py index e30a5bc85dcf..695b5aff821e 100644 --- a/frappe/website/report/website_analytics/website_analytics.py +++ b/frappe/website/report/website_analytics/website_analytics.py @@ -71,16 +71,16 @@ def _get_query_for_mariadb(self): elif filters_range == "Monthly": date_format = "%Y-%m-01" - query = """ + query = f""" SELECT - DATE_FORMAT({0}, %s) as date, + DATE_FORMAT({field}, %s) as date, COUNT(*) as count, COUNT(CASE WHEN is_unique = 1 THEN 1 END) as unique_count FROM `tabWeb Page View` WHERE creation BETWEEN %s AND %s - GROUP BY DATE_FORMAT({0}, %s) + GROUP BY DATE_FORMAT({field}, %s) ORDER BY creation - """.format(field) + """ values = (date_format, self.filters.from_date, self.filters.to_date, date_format) @@ -97,16 +97,16 @@ def _get_query_for_postgres(self): elif filters_range == "Monthly": granularity = "day" - query = """ + query = f""" SELECT - DATE_TRUNC(%s, {0}) as date, + DATE_TRUNC(%s, {field}) as date, COUNT(*) as count, COUNT(CASE WHEN CAST(is_unique as Integer) = 1 THEN 1 END) as unique_count FROM "tabWeb Page View" - WHERE coalesce("tabWeb Page View".{0}, '0001-01-01') BETWEEN %s AND %s - GROUP BY date_trunc(%s, {0}) + WHERE coalesce("tabWeb Page View".{field}, '0001-01-01') BETWEEN %s AND %s + GROUP BY date_trunc(%s, {field}) ORDER BY date - """.format(field) + """ values = (granularity, self.filters.from_date, self.filters.to_date, granularity) diff --git a/frappe/website/router.py b/frappe/website/router.py index 655fcc135735..39bc79933cd0 100644 --- a/frappe/website/router.py +++ b/frappe/website/router.py @@ -107,14 +107,14 @@ def get_pages_from_path(start, app, app_path): pages = {} start_path = os.path.join(app_path, start) if os.path.exists(start_path): - for basepath, folders, files in os.walk(start_path): + for basepath, _folders, files in os.walk(start_path): # add missing __init__.py - if not "__init__.py" in files and frappe.conf.get("developer_mode"): + if "__init__.py" not in files and frappe.conf.get("developer_mode"): open(os.path.join(basepath, "__init__.py"), "a").close() for fname in files: fname = frappe.utils.cstr(fname) - if not "." in fname: + if "." not in fname: continue page_name, extn = fname.rsplit(".", 1) if extn in ("js", "css") and os.path.exists(os.path.join(basepath, page_name + ".html")): diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py index 5e6c5600c356..8b068659fb08 100644 --- a/frappe/workflow/doctype/workflow/workflow.py +++ b/frappe/workflow/doctype/workflow/workflow.py @@ -46,14 +46,14 @@ def update_default_workflow_status(self): docstatus_map = {} states = self.get("states") for d in states: - if not d.doc_status in docstatus_map: + if d.doc_status not in docstatus_map: frappe.db.sql( - """ - UPDATE `tab{doctype}` - SET `{field}` = %s - WHERE ifnull(`{field}`, '') = '' + f""" + UPDATE `tab{self.document_type}` + SET `{self.workflow_state_field}` = %s + WHERE ifnull(`{self.workflow_state_field}`, '') = '' AND `docstatus` = %s - """.format(doctype=self.document_type, field=self.workflow_state_field), + """, (d.state, d.doc_status), ) diff --git a/frappe/workflow/doctype/workflow_action/workflow_action.py b/frappe/workflow/doctype/workflow_action/workflow_action.py index 5a3ea5083976..3f61772e236b 100644 --- a/frappe/workflow/doctype/workflow_action/workflow_action.py +++ b/frappe/workflow/doctype/workflow_action/workflow_action.py @@ -53,10 +53,10 @@ def get_permission_query_conditions(user): .where(WorkflowActionPermittedRole.role.isin(roles)) ).get_sql() - return """(`tabWorkflow Action`.`name` in ({permitted_workflow_actions}) - or `tabWorkflow Action`.`user`={user}) + return f"""(`tabWorkflow Action`.`name` in ({permitted_workflow_actions}) + or `tabWorkflow Action`.`user`={frappe.db.escape(user)}) and `tabWorkflow Action`.`status`='Open' - """.format(permitted_workflow_actions=permitted_workflow_actions, user=frappe.db.escape(user)) + """ def has_permission(doc, user): diff --git a/frappe/www/list.py b/frappe/www/list.py index 296635468adc..97f1ac82d927 100644 --- a/frappe/www/list.py +++ b/frappe/www/list.py @@ -129,9 +129,7 @@ def set_route(context): elif context.doc and getattr(context.doc, "route", None): context.route = context.doc.route else: - context.route = "{}/{}".format( - context.pathname or quoted(context.doc.doctype), quoted(context.doc.name) - ) + context.route = f"{context.pathname or quoted(context.doc.doctype)}/{quoted(context.doc.name)}" def prepare_filters(doctype, controller, kwargs): @@ -154,7 +152,7 @@ def prepare_filters(doctype, controller, kwargs): filters[key] = val # filter the filters to include valid fields only - for fieldname, val in list(filters.items()): + for fieldname in list(filters.keys()): if not meta.has_field(fieldname): del filters[fieldname] diff --git a/frappe/www/printview.py b/frappe/www/printview.py index d52557660e73..56349cc3b137 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -471,7 +471,7 @@ def append_empty_field_dict_to_page_column(page): if df.fieldtype == "Section Break" or page == []: if len(page) > 1: - if page[-1]["has_data"] == False: + if page[-1]["has_data"] is False: # truncate last section if empty del page[-1] diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py index e76dc655408b..1a3ad036d51c 100644 --- a/frappe/www/qrcode.py +++ b/frappe/www/qrcode.py @@ -18,7 +18,7 @@ def get_query_key(): query_string = frappe.local.request.query_string query = dict(parse_qsl(query_string)) query = {key.decode(): val.decode() for key, val in query.items()} - if not "k" in list(query): + if "k" not in list(query): frappe.throw(_("Not Permitted"), frappe.PermissionError) query = (query["k"]).strip() if False in [i.isalpha() or i.isdigit() for i in query]: diff --git a/frappe/www/sitemap.py b/frappe/www/sitemap.py index 57e9d27049df..905255b69946 100644 --- a/frappe/www/sitemap.py +++ b/frappe/www/sitemap.py @@ -16,7 +16,7 @@ def get_context(context): """generate the sitemap XML""" links = [] - for route, page in get_pages().items(): + for _, page in get_pages().items(): if page.sitemap: links.append({"loc": get_url(quote(page.name.encode("utf-8"))), "lastmod": nowdate()}) diff --git a/pyproject.toml b/pyproject.toml index 0412858bc2e5..5b2fc6f3bc43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -92,13 +92,6 @@ dependencies = [ requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" -[tool.ruff] -line-length = 110 - -[tool.ruff.format] -indent-style = "tab" -docstring-code-format = true - [tool.bench.dev-dependencies] coverage = "~=6.5.0" Faker = "~=13.12.1" @@ -108,3 +101,41 @@ watchdog = "~=3.0.0" hypothesis = "~=6.68.2" freezegun = "~=1.2.2" responses = "==0.23.1" + +[tool.ruff] +line-length = 110 +target-version = "py310" + +[tool.ruff.lint] +select = [ + "F", + "E", + "W", + "I", + "UP", + "B", + "RUF", +] +ignore = [ + "B017", # assertRaises(Exception) - should be more specific + "B018", # useless expression, not assigned to anything + "B023", # function doesn't bind loop variable - will have last iteration's value + "B904", # raise inside except without from + "E101", # indentation contains mixed spaces and tabs + "E402", # module level import not at top of file + "E501", # line too long + "E741", # ambiguous variable name + "F401", # "unused" imports + "F403", # can't detect undefined names from * import + "F405", # can't detect undefined names from * import + "F722", # syntax error in forward type annotation + "F821", # undefined name + "W191", # indentation contains tabs + "RUF001", # string contains ambiguous unicode character +] + +[tool.ruff.format] +quote-style = "double" +indent-style = "tab" +docstring-code-format = true +