diff --git a/.github/workflows/initiate_release.yml b/.github/workflows/initiate_release.yml index 20bf9967ad..567ee6ba80 100644 --- a/.github/workflows/initiate_release.yml +++ b/.github/workflows/initiate_release.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - version: ["13", "14", "15"] + version: ["14", "15"] steps: - uses: octokit/request-action@v2.x diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 572253bdfa..a8af938ec3 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -11,7 +11,6 @@ concurrency: group: server-develop-${{ github.event_name }}-${{ github.event.number }} cancel-in-progress: true - permissions: # Do not change this as GITHUB_TOKEN is being used by roulette contents: read @@ -48,8 +47,8 @@ jobs: strategy: fail-fast: false matrix: - db: ["mariadb", "postgres"] - container: [1, 2] + db: ["mariadb", "postgres"] + container: [1, 2] services: mariadb: @@ -85,7 +84,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - name: Check for valid Python & Merge Conflicts run: | @@ -149,7 +148,14 @@ jobs: - name: Show bench output if: ${{ always() }} - run: cat ~/frappe-bench/bench_start.log || true + run: | + cd ~/frappe-bench + cat bench_start.log || true + cd logs + for f in ./*.log*; do + echo "Printing log: $f"; + cat $f + done - name: Upload coverage data uses: actions/upload-artifact@v3 @@ -166,8 +172,8 @@ jobs: strategy: matrix: - db: ["mariadb", "postgres"] - container: [1, 2] + db: ["mariadb", "postgres"] + container: [1, 2] steps: - name: Pass skipped tests unconditionally diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0bfde0fc25..1f8ee5e575 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -46,8 +46,8 @@ jobs: strategy: fail-fast: false matrix: - # Make sure you modify coverage submission file list if changing this - container: [1, 2, 3] + # Make sure you modify coverage submission file list if changing this + container: [1, 2, 3] name: UI Tests (Cypress) @@ -67,7 +67,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v5 with: - python-version: '3.12' + python-version: "3.12" - name: Check for valid Python & Merge Conflicts run: | @@ -166,7 +166,14 @@ jobs: - name: Show bench output if: ${{ always() }} - run: cat ~/frappe-bench/bench_start.log || true + run: | + cd ~/frappe-bench + cat bench_start.log || true + cd logs + for f in ./*.log*; do + echo "Printing log: $f"; + cat $f + done faux-test: runs-on: ubuntu-latest @@ -175,7 +182,7 @@ jobs: name: UI Tests (Cypress) strategy: matrix: - container: [1, 2, 3] + container: [1, 2, 3] steps: - name: Pass skipped tests unconditionally diff --git a/cypress/integration/control_date.js b/cypress/integration/control_date.js index 442538661e..0744961147 100644 --- a/cypress/integration/control_date.js +++ b/cypress/integration/control_date.js @@ -7,6 +7,7 @@ context("Date Control", () => { function get_dialog(date_field_options) { return cy.dialog({ title: "Date", + animate: false, fields: [ { label: "Date", @@ -75,6 +76,8 @@ context("Date Control", () => { //Verifying if clicking on "Today" button matches today's date cy.window().then((win) => { + // `expect` can not wait like `should` + cy.wait(500); expect(win.cur_dialog.fields_dict.date.value).to.be.equal( win.frappe.datetime.get_today() ); diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js index ec7003aa15..d93adc1538 100644 --- a/cypress/integration/navigation.js +++ b/cypress/integration/navigation.js @@ -6,16 +6,17 @@ context("Navigation", () => { }); it("Navigate to route with hash in document name", () => { cy.insert_doc( - "ToDo", + "Client Script", { __newname: "ABC#123", - description: "Test this", + dt: "User", + script: "console.log('ran')", + enabled: 0, }, true ); - cy.visit(`/app/todo/${encodeURIComponent("ABC#123")}`); - cy.title().should("eq", "Test this - ABC#123"); - cy.get_field("description", "Text Editor").contains("Test this"); + cy.visit(`/app/client-script/${encodeURIComponent("ABC#123")}`); + cy.title().should("eq", "ABC#123"); cy.go("back"); cy.title().should("eq", "Website"); }); diff --git a/cypress/integration/routing.js b/cypress/integration/routing.js index 0822dd9b7d..79c0cea9dc 100644 --- a/cypress/integration/routing.js +++ b/cypress/integration/routing.js @@ -8,6 +8,7 @@ const test_queries = [ `?date=%5B">"%2C"2022-06-01"%5D`, `?name=%5B"like"%2C"%2542%25"%5D`, `?status=%5B"not%20in"%2C%5B"Open"%2C"Closed"%5D%5D`, + `?status=%5B%22%21%3D%22%2C%22Closed%22%5D&status=%5B%22%21%3D%22%2C%22Cancelled%22%5D`, ]; describe("SPA Routing", { scrollBehavior: false }, () => { diff --git a/cypress/integration/view_routing.js b/cypress/integration/view_routing.js index 72fb6836ec..5942d4f005 100644 --- a/cypress/integration/view_routing.js +++ b/cypress/integration/view_routing.js @@ -224,8 +224,8 @@ context("View", () => { }); }); - it("Route to Settings Workspace", () => { - cy.visit("/app/settings"); - cy.get(".title-text").should("contain", "Settings"); + it("Route to Website Workspace", () => { + cy.visit("/app/website"); + cy.get(".title-text").should("contain", "Website"); }); }); diff --git a/cypress/integration/workspace.js b/cypress/integration/workspace.js index 1499c8772e..4079877c9c 100644 --- a/cypress/integration/workspace.js +++ b/cypress/integration/workspace.js @@ -7,8 +7,8 @@ context("Workspace 2.0", () => { it("Navigate to page from sidebar", () => { cy.visit("/app/build"); cy.get(".codex-editor__redactor .ce-block"); - cy.get('.sidebar-item-container[item-name="Settings"]').first().click(); - cy.location("pathname").should("eq", "/app/settings"); + cy.get('.sidebar-item-container[item-name="Website"]').first().click(); + cy.location("pathname").should("eq", "/app/website"); }); it("Create Private Page", () => { diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 50d86d42b1..66defa88f7 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -449,27 +449,8 @@ Cypress.Commands.add("click_menu_button", (name) => { }); Cypress.Commands.add("clear_filters", () => { - let has_filter = false; - cy.intercept({ - method: "POST", - url: "api/method/frappe.model.utils.user_settings.save", - }).as("filter-saved"); - cy.get(".filter-section .filter-button").click({ force: true }); - cy.wait(300); - cy.get(".filter-popover").should("exist"); - cy.get(".filter-popover").then((popover) => { - if (popover.find("input.input-with-feedback")[0].value != "") { - has_filter = true; - } - }); - cy.get(".filter-popover").find(".clear-filters").click(); - cy.get(".filter-section .filter-button").click(); - cy.window() - .its("cur_list") - .then((cur_list) => { - cur_list && cur_list.filter_area && cur_list.filter_area.clear(); - has_filter && cy.wait("@filter-saved"); - }); + cy.get(".filter-x-button").click({ force: true }); + cy.wait(500); }); Cypress.Commands.add("click_modal_primary_button", (btn_name) => { diff --git a/frappe/__init__.py b/frappe/__init__.py index 158d5c9fa4..1568aa9f20 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -59,32 +59,6 @@ if _dev_server: warnings.simplefilter("always", DeprecationWarning) warnings.simplefilter("always", PendingDeprecationWarning) -# Always initialize sentry SDK if the DSN is sent -if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"): - import sentry_sdk - from sentry_sdk.integrations.argv import ArgvIntegration - from sentry_sdk.integrations.atexit import AtexitIntegration - from sentry_sdk.integrations.dedupe import DedupeIntegration - from sentry_sdk.integrations.excepthook import ExcepthookIntegration - from sentry_sdk.integrations.modules import ModulesIntegration - - from frappe.utils.sentry import before_send - - sentry_sdk.init( - dsn=sentry_dsn, - before_send=before_send, - release=__version__, - auto_enabling_integrations=False, - default_integrations=False, - integrations=[ - AtexitIntegration(), - ExcepthookIntegration(), - DedupeIntegration(), - ModulesIntegration(), - ArgvIntegration(), - ], - ) - class _dict(dict): """dict like object that exposes keys as attributes""" @@ -1001,6 +975,7 @@ def has_permission( throw=False, *, parent_doctype=None, + debug=False, ): """ Return True if the user has permission `ptype` for given `doctype` or `doc`. @@ -1025,22 +1000,15 @@ def has_permission( user=user, raise_exception=throw, parent_doctype=parent_doctype, + debug=debug, ) if throw and not out: - # mimics frappe.throw document_label = ( f"{_(doctype)} {doc if isinstance(doc, str) else doc.name}" if doc else _(doctype) ) - msgprint( - _("No permission for {0}").format(document_label), - raise_exception=ValidationError, - title=None, - indicator="red", - is_minimizable=None, - wide=None, - as_list=False, - ) + frappe.flags.error_message = _("No permission for {0}").format(document_label) + raise frappe.PermissionError return out @@ -1720,17 +1688,14 @@ def get_newargs(fn: Callable, kwargs: dict[str, Any]) -> dict[str, Any]: # Ref: https://docs.python.org/3/library/inspect.html#inspect.Parameter.kind varkw_exist = False - if hasattr(fn, "fnargs"): - fnargs = fn.fnargs - else: - signature = inspect.signature(fn) - fnargs = list(signature.parameters) + signature = inspect.signature(fn) + fnargs = list(signature.parameters) - for param_name, parameter in signature.parameters.items(): - if parameter.kind == inspect.Parameter.VAR_KEYWORD: - varkw_exist = True - fnargs.remove(param_name) - break + for param_name, parameter in signature.parameters.items(): + if parameter.kind == inspect.Parameter.VAR_KEYWORD: + varkw_exist = True + fnargs.remove(param_name) + break newargs = {} for a in kwargs: diff --git a/frappe/app.py b/frappe/app.py index 5ddabfbfc9..6d6a747119 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -140,7 +140,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) @@ -420,6 +420,50 @@ def sync_database(rollback: bool) -> bool: return rollback +# Always initialize sentry SDK if the DSN is sent +if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"): + import sentry_sdk + from sentry_sdk.integrations.argv import ArgvIntegration + from sentry_sdk.integrations.atexit import AtexitIntegration + from sentry_sdk.integrations.dedupe import DedupeIntegration + from sentry_sdk.integrations.excepthook import ExcepthookIntegration + from sentry_sdk.integrations.modules import ModulesIntegration + from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware + + from frappe.utils.sentry import FrappeIntegration, before_send + + integrations = [ + AtexitIntegration(), + ExcepthookIntegration(), + DedupeIntegration(), + ModulesIntegration(), + ArgvIntegration(), + ] + + experiments = {} + kwargs = {} + + if os.getenv("ENABLE_SENTRY_DB_MONITORING"): + integrations.append(FrappeIntegration()) + experiments["record_sql_params"] = True + + if tracing_sample_rate := os.getenv("SENTRY_TRACING_SAMPLE_RATE"): + kwargs["traces_sample_rate"] = float(tracing_sample_rate) + application = SentryWsgiMiddleware(application) + + sentry_sdk.init( + dsn=sentry_dsn, + before_send=before_send, + attach_stacktrace=True, + release=frappe.__version__, + auto_enabling_integrations=False, + default_integrations=False, + integrations=integrations, + _experiments=experiments, + **kwargs, + ) + + def serve( port=8000, profile=False, diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json index c3de151282..2c46414e59 100644 --- a/frappe/automation/workspace/tools/tools.json +++ b/frappe/automation/workspace/tools/tools.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"-P-RG1wVHg\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"sR-UFcO7II\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Import Data\",\"col\":3}},{\"id\":\"IkcVmgWb3z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"id\":\"6wir-jZFRE\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"id\":\"45a1jzQkTm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"id\":\"LdZrgvxxo7\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yNSSTIaDWZ\",\"type\":\"header\",\"data\":{\"text\":\"Documents\",\"col\":12}},{\"id\":\"0yceBIfhHM\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"42WbBA9rpj\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"7_U7_xCOos\",\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"id\":\"SlYKJZj5r3\",\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]", + "content": "[{\"id\":\"-P-RG1wVHg\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"sR-UFcO7II\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Import Data\",\"col\":3}},{\"id\":\"IkcVmgWb3z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"id\":\"6wir-jZFRE\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"id\":\"45a1jzQkTm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"id\":\"LdZrgvxxo7\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yNSSTIaDWZ\",\"type\":\"header\",\"data\":{\"text\":\"Documents\",\"col\":12}},{\"id\":\"0yceBIfhHM\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"42WbBA9rpj\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"wE9n7TIrAc\",\"type\":\"card\",\"data\":{\"card_name\":\"Alerts and Notifications\",\"col\":4}},{\"id\":\"7_U7_xCOos\",\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"id\":\"3imoh2oqsJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"SlYKJZj5r3\",\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}},{\"id\":\"O7jrc2YQTN\",\"type\":\"card\",\"data\":{\"card_name\":\"Newsletter\",\"col\":4}}]", "creation": "2020-03-02 14:53:24.980279", "custom_blocks": [], "docstatus": 0, @@ -12,36 +12,6 @@ "is_hidden": 0, "label": "Tools", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Email", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Newsletter", - "link_count": 0, - "link_to": "Newsletter", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Group", - "link_count": 0, - "link_to": "Email Group", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -192,9 +162,165 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email", + "link_count": 3, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email Account", + "link_count": 0, + "link_to": "Email Account", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email Domain", + "link_count": 0, + "link_to": "Email Domain", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email Template", + "link_count": 0, + "link_to": "Email Template", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 2, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Newsletter", + "link_count": 0, + "link_to": "Newsletter", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Email Group", + "link_count": 0, + "link_to": "Email Group", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Printing", + "link_count": 4, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Print Format Builder", + "link_count": 0, + "link_to": "print-format-builder", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Print Format Builder (New)", + "link_count": 0, + "link_to": "print-format-builder-beta", + "link_type": "Page", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Print Settings", + "link_count": 0, + "link_to": "Print Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Print Heading", + "link_count": 0, + "link_to": "Print Heading", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Alerts and Notifications", + "link_count": 3, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Notification", + "link_count": 0, + "link_to": "Notification", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Auto Email Report", + "link_count": 0, + "link_to": "Auto Email Report", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Notification Settings", + "link_count": 0, + "link_to": "Notification Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2023-05-24 14:47:24.740856", + "modified": "2024-01-02 15:47:12.939478", "modified_by": "Administrator", "module": "Automation", "name": "Tools", diff --git a/frappe/boot.py b/frappe/boot.py index 2ce950a55a..d0e6204e78 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -270,9 +270,6 @@ def get_user_info(): user_info = frappe._dict() add_user_info(frappe.session.user, user_info) - if frappe.session.user == "Administrator" and user_info.Administrator.email: - user_info[user_info.Administrator.email] = user_info.Administrator - return user_info diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 48a4feea57..f016724f87 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -260,8 +260,28 @@ def restore_backup( admin_password, force, ): + from pathlib import Path + from frappe.installer import _new_site, is_downgrade, is_partial, validate_database_sql + # Check for the backup file in the backup directory, as well as the main bench directory + dirs = (f"{site}/private/backups", "..") + + # Try to resolve path to the file if we can't find it directly + if not Path(sql_file_path).exists(): + click.secho( + f"File {sql_file_path} not found. Trying to check in alternative directories.", fg="yellow" + ) + for dir in dirs: + potential_path = Path(dir) / Path(sql_file_path) + if potential_path.exists(): + sql_file_path = str(potential_path.resolve()) + click.secho(f"File {sql_file_path} found.", fg="green") + break + else: + click.secho(f"File {sql_file_path} not found.", fg="red") + sys.exit(1) + if is_partial(sql_file_path): click.secho( "Partial Backup file detected. You cannot use a partial file to restore a Frappe site.", @@ -448,7 +468,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": {str(err)}\n{frappe.get_traceback(with_context=True)}" print(f"An error occurred while installing {app}{err_msg}") exit_code = 1 @@ -570,7 +590,7 @@ def describe_database_table(context, doctype, column): def _extract_table_stats(doctype: str, columns: list[str]) -> dict: - from frappe.utils import cstr, get_table_name + from frappe.utils import cint, cstr, get_table_name def sql_bool(val): return cstr(val).lower() in ("yes", "1", "true") @@ -610,7 +630,13 @@ def _extract_table_stats(doctype: str, columns: list[str]) -> dict: if idx["Seq_in_index"] == 1: update_cardinality(idx["Column_name"], idx["Cardinality"]) - total_rows = frappe.db.count(doctype) + total_rows = cint( + frappe.db.sql( + f"""select table_rows + from information_schema.tables + where table_name = 'tab{doctype}'""" + )[0][0] + ) # fetch accurate cardinality for columns by query. WARN: This can take a lot of time. for column in columns: @@ -893,7 +919,7 @@ def backup( fg="red", ) if verbose: - print(frappe.get_traceback()) + print(frappe.get_traceback(with_context=True)) exit_code = 1 continue if frappe.get_system_settings("encrypt_backup") and frappe.get_site_config().encryption_key: diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index a36af705a7..86ef59f994 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -267,6 +267,7 @@ frappe.ui.form.on("Communication", { $.extend(args, { subject: __("Re: {0}", [frm.doc.subject]), recipients: frm.doc.sender, + is_a_reply: true, }); new frappe.views.CommunicationComposer(args); @@ -278,6 +279,7 @@ frappe.ui.form.on("Communication", { subject: __("Res: {0}", [frm.doc.subject]), recipients: frm.doc.sender, cc: frm.doc.cc, + is_a_reply: true, }); new frappe.views.CommunicationComposer(args); }, @@ -287,6 +289,7 @@ frappe.ui.form.on("Communication", { $.extend(args, { forward: true, subject: __("Fw: {0}", [frm.doc.subject]), + is_a_reply: true, }); new frappe.views.CommunicationComposer(args); diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index de2dfb7702..516356308e 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -306,7 +306,7 @@ class Communication(Document, CommunicationEmailMixin): emails = split_emails(emails) if isinstance(emails, str) else (emails or []) if exclude_displayname: return [email.lower() for email in {parse_addr(email)[1] for email in emails} if email] - return [email.lower() for email in set(emails) if email] + return [email for email in set(emails) if email] def to_list(self, exclude_displayname=True): """Return `to` list.""" @@ -501,14 +501,15 @@ def on_doctype_update(): frappe.db.add_index("Communication", ["message_id(140)"]) -def has_permission(doc, ptype, user): +def has_permission(doc, ptype, user=None, debug=False): if ptype == "read": if doc.reference_doctype == "Communication" and doc.reference_name == doc.name: return if doc.reference_doctype and doc.reference_name: - if frappe.has_permission(doc.reference_doctype, ptype="read", doc=doc.reference_name): - return True + return frappe.has_permission( + doc.reference_doctype, ptype="read", doc=doc.reference_name, user=user, debug=debug + ) def get_permission_query_conditions_for_communication(user): diff --git a/frappe/core/doctype/data_export/data_export.js b/frappe/core/doctype/data_export/data_export.js index 54677b98a6..afac4dd3a6 100644 --- a/frappe/core/doctype/data_export/data_export.js +++ b/frappe/core/doctype/data_export/data_export.js @@ -145,6 +145,12 @@ const get_doctypes = (parentdt) => { const add_doctype_field_multicheck_control = (doctype, parent_wrapper) => { const fields = get_fields(doctype); + frappe.model.std_fields + .filter((df) => ["owner", "creation"].includes(df.fieldname)) + .forEach((df) => { + fields.push(df); + }); + const options = fields.map((df) => { return { label: df.label, diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index 6190034308..1e8df913b7 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -212,8 +212,23 @@ class DataExporter: # build list of valid docfields tablecolumns = [] table_name = "tab" + dt + for f in frappe.db.get_table_columns_description(table_name): field = meta.get_field(f.name) + if f.name in ["owner", "creation"]: + std_field = next((x for x in frappe.model.std_fields if x["fieldname"] == f.name), None) + if std_field: + field = frappe._dict( + { + "fieldname": std_field.get("fieldname"), + "label": std_field.get("label"), + "fieldtype": std_field.get("fieldtype"), + "options": std_field.get("options"), + "idx": 0, + "parent": dt, + } + ) + if field and ( (self.select_columns and f.name in self.select_columns[dt]) or not self.select_columns ): @@ -404,7 +419,6 @@ class DataExporter: ) for ci, child in enumerate(data_row.run(as_dict=True)): self.add_data_row(rows, c["doctype"], c["parentfield"], child, ci) - for row in rows: self.writer.writerow(row) diff --git a/frappe/core/doctype/data_export/test_data_exporter.py b/frappe/core/doctype/data_export/test_data_exporter.py index eb3ebaa80d..2f580e4a63 100644 --- a/frappe/core/doctype/data_export/test_data_exporter.py +++ b/frappe/core/doctype/data_export/test_data_exporter.py @@ -88,8 +88,8 @@ class TestDataExporter(FrappeTestCase): self.assertEqual(frappe.response["type"], "csv") self.assertEqual(frappe.response["doctype"], self.doctype_name) self.assertTrue(frappe.response["result"]) - self.assertIn('Child Title 1",50', frappe.response["result"]) - self.assertIn('Child Title 2",51', frappe.response["result"]) + self.assertRegex(frappe.response["result"], r"Child Title 1.*?,50") + self.assertRegex(frappe.response["result"], r"Child Title 2.*?,51") def test_export_type(self): for type in ["csv", "Excel"]: diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index b3fa136eb4..9f0e11a7b7 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -449,7 +449,6 @@ frappe.ui.form.on("Data Import", { } } else { let messages = JSON.parse(log.messages || "[]") - .map(JSON.parse) .map((m) => { let title = m.title ? `${m.title}` : ""; let message = m.message ? `
filters. result = [result], or for old style data = [columns], [result]",
"fieldname": "report_script",
"fieldtype": "Code",
- "label": "Script"
+ "label": "Script",
+ "options": "Python"
},
{
"collapsible": 1,
@@ -188,7 +191,7 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-04-07 18:18:11.782178",
+ "modified": "2024-01-03 15:29:01.460404",
"modified_by": "Administrator",
"module": "Core",
"name": "Report",
@@ -241,4 +244,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 5fb0feeca2..bb39142327 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -45,8 +45,8 @@ class Report(Document):
report_script: DF.Code | None
report_type: DF.Literal["Report Builder", "Query Report", "Script Report", "Custom Report"]
roles: DF.Table[HasRole]
-
# end: auto-generated types
+
def validate(self):
"""only administrator can save standard report"""
if not self.module:
diff --git a/frappe/core/doctype/role_profile/role_profile.py b/frappe/core/doctype/role_profile/role_profile.py
index 74c34e3993..22a6d0a9a7 100644
--- a/frappe/core/doctype/role_profile/role_profile.py
+++ b/frappe/core/doctype/role_profile/role_profile.py
@@ -25,6 +25,9 @@ class RoleProfile(Document):
self.name = self.role_profile
def on_update(self):
+ self.queue_action("update_all_users", now=frappe.flags.in_test)
+
+ def update_all_users(self):
"""Changes in role_profile reflected across all its user"""
has_role = frappe.qb.DocType("Has Role")
user = frappe.qb.DocType("User")
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 59f615d9de..f95c06fdbe 100644
--- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py
@@ -154,7 +154,7 @@ class ScheduledJobType(Document):
if frappe.debug_log:
self.scheduler_log.db_set("debug_log", "\n".join(frappe.debug_log))
if status == "Failed":
- self.scheduler_log.db_set("details", frappe.get_traceback())
+ self.scheduler_log.db_set("details", frappe.get_traceback(with_context=True))
if status == "Start":
self.db_set("last_execution", now_datetime(), update_modified=False)
frappe.db.commit()
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index a9e047d9b2..fbd3ca6f50 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -8,7 +8,13 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.rate_limiter import rate_limit
-from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, is_safe_exec_enabled, safe_exec
+from frappe.utils.safe_exec import (
+ FrappeTransformer,
+ NamespaceDict,
+ get_safe_globals,
+ is_safe_exec_enabled,
+ safe_exec,
+)
class ServerScript(Document):
@@ -123,7 +129,7 @@ class ServerScript(Document):
from RestrictedPython import compile_restricted
try:
- compile_restricted(self.script)
+ compile_restricted(self.script, policy=FrappeTransformer)
except Exception as e:
frappe.msgprint(str(e), title=_("Compilation warning"))
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 24bc610df3..d9467397c3 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -75,7 +75,7 @@ frappe.ui.form.on("User", {
if (
frm.can_edit_roles &&
!frm.is_new() &&
- in_list(["System User", "Website User"], frm.doc.user_type)
+ ["System User", "Website User"].includes(frm.doc.user_type)
) {
if (!frm.roles_editor) {
const role_area = $('*Order Overdue*
@@ -166,7 +166,7 @@ frappe.ui.form.on("Notification", {
frappe.set_route("Form", "Customize Form");
},
event: function (frm) {
- if (in_list(["Days Before", "Days After"], frm.doc.event)) {
+ if (["Days Before", "Days After"].includes(frm.doc.event)) {
frm.add_custom_button(__("Get Alerts for Today"), function () {
frappe.call({
method: "frappe.email.doctype.notification.notification.get_documents_for_today",
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index d89a2bbfbd..28a45ce35e 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -366,7 +366,9 @@ def get_context(context):
# For sending messages to specified role
if recipient.receiver_by_role:
- receiver_list += get_info_based_on_role(recipient.receiver_by_role, "mobile_no")
+ receiver_list += get_info_based_on_role(
+ recipient.receiver_by_role, "mobile_no", ignore_permissions=True
+ )
return receiver_list
@@ -505,8 +507,7 @@ def evaluate_alert(doc: Document, alert, event):
frappe.throw(message, title=_("Error in Notification"))
except Exception as e:
title = str(e)
- message = frappe.get_traceback()
- frappe.log_error(message=message, title=title)
+ frappe.log_error(title=title)
msg = f"{title}
{message}"
frappe.throw(msg, title=_("Error in Notification"))
diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json
index eba166d8fc..8ff39c37d7 100644
--- a/frappe/geo/country_info.json
+++ b/frappe/geo/country_info.json
@@ -2089,8 +2089,17 @@
},
"Palestinian Territory, Occupied": {
"code": "ps",
+ "currency": "ILS",
+ "currency_fraction": "Agora",
+ "currency_fraction_units": 100,
+ "currency_name": "New Israeli Sheqel",
+ "currency_symbol": "\u20aa",
"number_format": "#,###.##",
- "isd": "+970"
+ "isd": "+970",
+ "timezones": [
+ "Asia/Hebron",
+ "Asia/Jerusalem"
+ ]
},
"Panama": {
"code": "pa",
@@ -2541,15 +2550,17 @@
},
"Sudan": {
"code": "sd",
+ "currency": "SDG",
"currency_fraction": "Piastre",
"currency_fraction_units": 100,
- "currency_symbol": "\u00a3",
+ "currency_name": "Sudanese Pound",
+ "currency_symbol": "\u062c.\u0633.",
"number_format": "#,###.##",
"timezones": [
- "Africa/Khartoum"
+ "Africa/Khartoum"
],
"isd": "+249"
- },
+},
"Suriname": {
"code": "sr",
"currency": "SRD",
diff --git a/frappe/hooks.py b/frappe/hooks.py
index d889b5f7c2..36a7748588 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -1,3 +1,5 @@
+import os
+
from . import __version__ as app_version
app_name = "frappe"
@@ -426,9 +428,18 @@ before_request = [
# Background Job Hooks
before_job = [
+ "frappe.recorder.record",
"frappe.monitor.start",
]
+
+if os.getenv("FRAPPE_SENTRY_DSN") and (
+ os.getenv("ENABLE_SENTRY_DB_MONITORING") or os.getenv("SENTRY_TRACING_SAMPLE_RATE")
+):
+ before_request.append("frappe.utils.sentry.set_sentry_context")
+ before_job.append("frappe.utils.sentry.set_sentry_context")
+
after_job = [
+ "frappe.recorder.dump",
"frappe.monitor.stop",
"frappe.utils.file_lock.release_document_locks",
"frappe.utils.telemetry.flush",
diff --git a/frappe/installer.py b/frappe/installer.py
index d96f1167f1..1215aa8e0e 100644
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -757,8 +757,8 @@ def is_downgrade(sql_file_path, verbose=False):
if backup_version is None:
# This is likely an older backup, so try to extract another way
header = get_db_dump_header(sql_file_path).split("\n")
- if "Version" in header[0]:
- backup_version = header[0].split(":")[-1].strip()
+ if match := re.search(r"Frappe (\d+\.\d+\.\d+)", header[0]):
+ backup_version = match.group(1)
# Assume it's not a downgrade if we can't determine backup version
if backup_version is None:
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 3ecfe6cd61..64d38a2ae7 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -153,15 +153,14 @@ def get_context(doc):
def enqueue_webhook(doc, webhook) -> None:
+ request_url = headers = data = None
try:
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
- headers = get_webhook_headers(doc, webhook)
- data = get_webhook_data(doc, webhook)
-
+ request_url = webhook.request_url
if webhook.is_dynamic_url:
request_url = frappe.render_template(webhook.request_url, get_context(doc))
- else:
- request_url = webhook.request_url
+ headers = get_webhook_headers(doc, webhook)
+ data = get_webhook_data(doc, webhook)
except Exception as e:
frappe.logger().debug({"enqueue_webhook_error": e})
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index ad29e31ee4..d63579dff6 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -3,6 +3,7 @@
# model __init__.py
import frappe
+from frappe import _
data_fieldtypes = (
"Currency",
@@ -132,6 +133,25 @@ log_types = (
"Console Log",
)
+std_fields = [
+ {"fieldname": "name", "fieldtype": "Link", "label": _("ID")},
+ {"fieldname": "owner", "fieldtype": "Link", "label": _("Created By"), "options": "User"},
+ {"fieldname": "idx", "fieldtype": "Int", "label": _("Index")},
+ {"fieldname": "creation", "fieldtype": "Datetime", "label": _("Created On")},
+ {"fieldname": "modified", "fieldtype": "Datetime", "label": _("Last Updated On")},
+ {
+ "fieldname": "modified_by",
+ "fieldtype": "Link",
+ "label": _("Last Updated By"),
+ "options": "User",
+ },
+ {"fieldname": "_user_tags", "fieldtype": "Data", "label": _("Tags")},
+ {"fieldname": "_liked_by", "fieldtype": "Data", "label": _("Liked By")},
+ {"fieldname": "_comments", "fieldtype": "Text", "label": _("Comments")},
+ {"fieldname": "_assign", "fieldtype": "Text", "label": _("Assigned To")},
+ {"fieldname": "docstatus", "fieldtype": "Int", "label": _("Document Status")},
+]
+
def delete_fields(args_dict, delete=0):
"""
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 80e78a3f6d..62424ca0aa 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -741,7 +741,8 @@ class DatabaseQuery:
df = meta.get("fields", {"fieldname": f.fieldname})
df = df[0] if df else None
- can_be_null = f.fieldname != "name" # primary key is never nullable
+ # primary key is never nullable, modified is usually indexed by default and always present
+ can_be_null = f.fieldname not in ("name", "modified")
value = None
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 1201d3755b..8ba9b0efd4 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -214,7 +214,7 @@ class Document(BaseDocument):
if not self.has_permission(permtype):
self.raise_no_permission_to(permtype)
- def has_permission(self, permtype="read") -> bool:
+ def has_permission(self, permtype="read", *, debug=False, user=None) -> bool:
"""
Call `frappe.permissions.has_permission` if `ignore_permissions` flag isn't truthy
@@ -226,7 +226,7 @@ class Document(BaseDocument):
import frappe.permissions
- return frappe.permissions.has_permission(self.doctype, permtype, self)
+ return frappe.permissions.has_permission(self.doctype, permtype, self, debug=debug, user=user)
def raise_no_permission_to(self, perm_type):
"""Raise `frappe.PermissionError`."""
@@ -420,36 +420,35 @@ class Document(BaseDocument):
def update_child_table(self, fieldname: str, df: Optional["DocField"] = None):
"""sync child table for given fieldname"""
- rows = []
df: "DocField" = df or self.meta.get_field(fieldname)
-
- for d in self.get(df.fieldname):
- d: Document
- d.db_update()
- rows.append(d.name)
-
- if (
- df.options in (self.flags.ignore_children_type or [])
- or frappe.get_meta(df.options).is_virtual == 1
- ):
- # do not delete rows for this because of flags
- # hack for docperm :(
- return
+ all_rows = self.get(df.fieldname)
# delete rows that do not match the ones in the document
- tbl = frappe.qb.DocType(df.options)
- qry = (
- frappe.qb.from_(tbl)
- .where(tbl.parent == self.name)
- .where(tbl.parenttype == self.doctype)
- .where(tbl.parentfield == fieldname)
- .delete()
- )
+ # if the doctype isn't in ignore_children_type flag and isn't virtual
+ if not (
+ df.options in (self.flags.ignore_children_type or ())
+ or frappe.get_meta(df.options).is_virtual == 1
+ ):
+ existing_row_names = [row.name for row in all_rows if row.name and not row.is_new()]
- if rows:
- qry = qry.where(tbl.name.notin(rows))
+ tbl = frappe.qb.DocType(df.options)
+ qry = (
+ frappe.qb.from_(tbl)
+ .where(tbl.parent == self.name)
+ .where(tbl.parenttype == self.doctype)
+ .where(tbl.parentfield == fieldname)
+ .delete()
+ )
- qry.run()
+ if existing_row_names:
+ qry = qry.where(tbl.name.notin(existing_row_names))
+
+ qry.run()
+
+ # update / insert
+ for d in all_rows:
+ d: Document
+ d.db_update()
def get_doc_before_save(self) -> "Document":
return getattr(self, "_doc_before_save", None)
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index cc51a55d90..c089b8fa74 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -150,12 +150,13 @@ def apply_workflow(doc, action):
@frappe.whitelist()
def can_cancel_document(doctype):
workflow = get_workflow(doctype)
- for state_doc in workflow.states:
- if state_doc.doc_status == "2":
- for transition in workflow.transitions:
- if transition.next_state == state_doc.state:
- return False
- return True
+ cancelling_states = [s.state for s in workflow.states if s.doc_status == "2"]
+ if not cancelling_states:
+ return True
+
+ for transition in workflow.transitions:
+ if transition.next_state in cancelling_states:
+ return False
return True
diff --git a/frappe/permissions.py b/frappe/permissions.py
index a334cc5722..3aac4197fa 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -1,6 +1,7 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
import copy
+import functools
import frappe
import frappe.share
@@ -37,22 +38,41 @@ AUTOMATIC_ROLES = (GUEST_ROLE, ALL_USER_ROLE, SYSTEM_USER_ROLE, ADMIN_ROLE)
def print_has_permission_check_logs(func):
+ @functools.wraps(func)
def inner(*args, **kwargs):
- frappe.flags["has_permission_check_logs"] = []
- result = func(*args, **kwargs)
- self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == frappe.session.user
raise_exception = kwargs.get("raise_exception", True)
+ self_perm_check = True if not kwargs.get("user") else kwargs.get("user") == frappe.session.user
+
+ if raise_exception:
+ frappe.flags["has_permission_check_logs"] = []
+
+ result = func(*args, **kwargs)
# print only if access denied
# and if user is checking his own permission
if not result and self_perm_check and raise_exception:
msgprint(("
").join(frappe.flags.get("has_permission_check_logs", [])))
- frappe.flags.pop("has_permission_check_logs", None)
+
+ if raise_exception:
+ frappe.flags.pop("has_permission_check_logs", None)
return result
return inner
+def _debug_log(log: str):
+ if not hasattr(frappe.local, "permission_debug_log"):
+ frappe.local.permission_debug_log = []
+ frappe.local.permission_debug_log.append(log)
+
+
+def _pop_debug_log() -> list[str]:
+ if log := getattr(frappe.local, "permission_debug_log", None):
+ del frappe.local.permission_debug_log
+ return log
+ return []
+
+
@print_has_permission_check_logs
def has_permission(
doctype,
@@ -62,7 +82,8 @@ def has_permission(
raise_exception=True,
*,
parent_doctype=None,
-):
+ debug=False,
+) -> bool:
"""Return True if user has permission `ptype` for given `doctype`.
If `doc` is passed, also check user, share and owner permissions.
@@ -83,9 +104,13 @@ def has_permission(
user = frappe.session.user
if user == "Administrator":
+ debug and _debug_log("Allowed everything because user is Administrator")
return True
if ptype == "share" and frappe.get_system_settings("disable_document_sharing"):
+ debug and _debug_log(
+ "User can't share because sharing is disabled globally from system settings"
+ )
return False
if not doc and hasattr(doctype, "doctype"):
@@ -94,88 +119,105 @@ def has_permission(
doctype = doc.doctype
if frappe.is_table(doctype):
- return has_child_permission(doctype, ptype, doc, user, raise_exception, parent_doctype)
+ return has_child_permission(
+ doctype, ptype, doc, user, raise_exception, parent_doctype, debug=debug
+ )
meta = frappe.get_meta(doctype)
if doc:
if isinstance(doc, (str, int)):
doc = frappe.get_doc(meta.name, doc)
- perm = get_doc_permissions(doc, user=user, ptype=ptype).get(ptype)
+ perm = get_doc_permissions(doc, user=user, ptype=ptype, debug=debug).get(ptype)
if not perm:
+ debug and _debug_log(
+ "Permission check failed from role permission system. Check if user's role grant them permission to the document."
+ )
msg = _("User {0} does not have access to this document").format(frappe.bold(user))
if frappe.has_permission(doc.doctype):
msg += f": {_(doc.doctype)} - {doc.name}"
- push_perm_check_log(msg)
+ push_perm_check_log(msg, debug=debug)
else:
if ptype == "submit" and not cint(meta.is_submittable):
- push_perm_check_log(_("Document Type is not submittable"))
+ push_perm_check_log(_("Document Type is not submittable"), debug=debug)
return False
if ptype == "import" and not cint(meta.allow_import):
- push_perm_check_log(_("Document Type is not importable"))
+ push_perm_check_log(_("Document Type is not importable"), debug=debug)
return False
- role_permissions = get_role_permissions(meta, user=user)
+ role_permissions = get_role_permissions(meta, user=user, debug=debug)
+ debug and _debug_log(
+ "User has following permissions using role permission system: "
+ + frappe.as_json(role_permissions, indent=8)
+ )
+
perm = role_permissions.get(ptype)
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)
- )
+ ),
+ debug=debug,
)
def false_if_not_shared():
- if ptype in ("read", "write", "share", "submit", "email", "print"):
+ if ptype not in ("read", "write", "share", "submit", "email", "print"):
+ debug and _debug_log(f"Permission type {ptype} can not be shared")
+ return False
- rights = ["read" if ptype in ("email", "print") else ptype]
+ rights = ["read" if ptype in ("email", "print") else ptype]
- if doc:
- doc_name = get_doc_name(doc)
- shared = frappe.share.get_shared(
- doctype,
- user,
- rights=rights,
- filters=[["share_name", "=", doc_name]],
- limit=1,
- )
+ if doc:
+ doc_name = get_doc_name(doc)
+ shared = frappe.share.get_shared(
+ doctype,
+ user,
+ rights=rights,
+ filters=[["share_name", "=", doc_name]],
+ limit=1,
+ )
+ debug and _debug_log(f"Document is shared with user for {ptype}? {bool(shared)}")
+ return bool(shared)
- if shared:
- if ptype in ("read", "write", "share", "submit") or meta.permissions[0].get(ptype):
- return True
-
- elif frappe.share.get_shared(doctype, user, rights=rights, limit=1):
- # if atleast one shared doc of that type, then return True
- # this is used in db_query to check if permission on DocType
- return True
+ elif frappe.share.get_shared(doctype, user, rights=rights, limit=1):
+ # if atleast one shared doc of that type, then return True
+ # this is used in db_query to check if permission on DocType
+ debug and _debug_log(f"At least one document is shared with user with perm: {rights}")
+ return True
return False
if not perm:
+ debug and _debug_log("Checking if document/doctype is explicitly shared with user")
perm = false_if_not_shared()
return bool(perm)
-def get_doc_permissions(doc, user=None, ptype=None):
+def get_doc_permissions(doc, user=None, ptype=None, debug=False):
"""Return a dict of evaluated permissions for given `doc` like `{"read":1, "write":1}`"""
if not user:
user = frappe.session.user
- if frappe.is_table(doc.doctype):
- return {"read": 1, "write": 1}
-
meta = frappe.get_meta(doc.doctype)
def is_user_owner():
return (doc.get("owner") or "").lower() == user.lower()
- if has_controller_permissions(doc, ptype, user=user) is False:
- push_perm_check_log(_("Not allowed via controller permission check"))
+ if not has_controller_permissions(doc, ptype, user=user, debug=debug):
+ push_perm_check_log(_("Not allowed via controller permission check"), debug=debug)
return {ptype: 0}
- permissions = copy.deepcopy(get_role_permissions(meta, user=user, is_owner=is_user_owner()))
+ permissions = copy.deepcopy(
+ get_role_permissions(meta, user=user, is_owner=is_user_owner(), debug=debug)
+ )
+
+ debug and _debug_log(
+ "User has following permissions using role permission system: "
+ + frappe.as_json(permissions, indent=8)
+ )
if not cint(meta.is_submittable):
permissions["submit"] = 0
@@ -189,20 +231,29 @@ def get_doc_permissions(doc, user=None, ptype=None):
# some access might be only for the owner
# eg. everyone might have read access but only owner can delete
permissions.update(permissions.get("if_owner", {}))
+ debug and _debug_log(
+ "User is owner of document, so permissions are updated to: " + frappe.as_json(permissions)
+ )
- if not has_user_permission(doc, user):
+ if not has_user_permission(doc, user, debug=debug):
if is_user_owner():
# replace with owner permissions
permissions = permissions.get("if_owner", {})
# if_owner does not come with create rights...
permissions["create"] = 0
+ debug and _debug_log("User has only 'If owner' permissions because of User Permissions")
else:
+ debug and _debug_log("User has no permissions because of User Permissions")
permissions = {}
+ debug and _debug_log(
+ "Final applicable permissions after evaluating user permissions: "
+ + frappe.as_json(permissions, indent=8)
+ )
return permissions
-def get_role_permissions(doctype_meta, user=None, is_owner=None):
+def get_role_permissions(doctype_meta, user=None, is_owner=None, debug=False):
"""
Return dict of evaluated role permissions like:
{
@@ -225,12 +276,14 @@ def get_role_permissions(doctype_meta, user=None, is_owner=None):
cache_key = (doctype_meta.name, user, bool(is_owner))
if user == "Administrator":
+ debug and _debug_log("all permissions granted because user is Administrator")
return allow_everything()
- if not frappe.local.role_permissions.get(cache_key):
+ if not frappe.local.role_permissions.get(cache_key) or debug:
perms = frappe._dict(if_owner={})
roles = frappe.get_roles(user)
+ debug and _debug_log("User has following roles: " + str(roles))
def is_perm_applicable(perm):
return perm.role in roles and cint(perm.permlevel) == 0
@@ -271,7 +324,7 @@ def get_user_permissions(user):
return get_user_permissions(user)
-def has_user_permission(doc, user=None):
+def has_user_permission(doc, user=None, debug=False):
"""Return True if User is allowed to view considering User Permissions."""
from frappe.core.doctype.user_permission.user_permission import get_user_permissions
@@ -279,13 +332,17 @@ def has_user_permission(doc, user=None):
if not user_permissions:
# no user permission rules specified for this doctype
+ debug and _debug_log("User is not affected by any user permissions")
return True
# user can create own role permissions, so nothing applies
if get_role_permissions("User Permission", user=user).get("write"):
+ debug and _debug_log("User permission bypassed because user can modify user permissions.")
return True
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
+ if apply_strict_user_permissions:
+ debug and _debug_log("Strict user permissions will be applied")
doctype = doc.get("doctype")
docname = doc.get("name")
@@ -300,8 +357,14 @@ def has_user_permission(doc, user=None):
# only check if allowed_docs is not empty
if allowed_docs and docname not in allowed_docs:
# no user permissions for this doc specified
- push_perm_check_log(_("Not allowed for {0}: {1}").format(_(doctype), docname))
+ debug and _debug_log(
+ "User doesn't have access to this document because of User Permissions, allowed documents: "
+ + str(allowed_docs)
+ )
+ push_perm_check_log(_("Not allowed for {0}: {1}").format(_(doctype), docname), debug=debug)
return False
+ else:
+ debug and _debug_log(f"User Has access to {docname} via User Permissions.")
# STEP 2: ---------------------------------
# check user permissions in all link fields
@@ -357,7 +420,7 @@ def has_user_permission(doc, user=None):
_(field.label) if field.label else field.fieldname,
)
- push_perm_check_log(msg)
+ push_perm_check_log(msg, debug=debug)
return False
@@ -373,23 +436,27 @@ def has_user_permission(doc, user=None):
return True
-def has_controller_permissions(doc, ptype, user=None):
- """Return controller permissions if defined, None if not defined."""
+def has_controller_permissions(doc, ptype, user=None, debug=False) -> bool:
+ """Return controller permissions if denied, True if not defined.
+
+ Controllers can only deny permission, they can not explicitly grant any permission that wasn't
+ already present."""
if not user:
user = frappe.session.user
methods = frappe.get_hooks("has_permission").get(doc.doctype, [])
if not methods:
- return None
+ return True
for method in reversed(methods):
- controller_permission = frappe.call(frappe.get_attr(method), doc=doc, ptype=ptype, user=user)
+ controller_permission = frappe.call(method, doc=doc, ptype=ptype, user=user, debug=debug)
+ debug and _debug_log(f"Controller permission check from {method}: {controller_permission}")
if controller_permission is not None:
- return controller_permission
+ return bool(controller_permission)
- # controller permissions could not decide on True or False
- return None
+ # None of the controller hooks returned anything conclusive
+ return True
def get_doctypes_with_read():
@@ -678,7 +745,8 @@ def filter_allowed_docs_for_doctype(user_permissions, doctype, with_default_doc=
return (allowed_doc, default_doc) if with_default_doc else allowed_doc
-def push_perm_check_log(log):
+def push_perm_check_log(log, debug=False):
+ debug and _debug_log(log)
if frappe.flags.get("has_permission_check_logs") is None:
return
@@ -692,7 +760,10 @@ def has_child_permission(
user=None,
raise_exception=True,
parent_doctype=None,
-):
+ *,
+ debug=False,
+) -> bool:
+ debug and _debug_log("This doctype is a child table, permissions will be checked on parent.")
if isinstance(child_doc, str):
child_doc = frappe.db.get_value(
child_doctype,
@@ -706,7 +777,8 @@ def has_child_permission(
if not parent_doctype:
push_perm_check_log(
- _("Please specify a valid parent DocType for {0}").format(frappe.bold(child_doctype))
+ _("Please specify a valid parent DocType for {0}").format(frappe.bold(child_doctype)),
+ debug=debug,
)
return False
@@ -720,7 +792,8 @@ def has_child_permission(
push_perm_check_log(
_("{0} is not a valid parent DocType for {1}").format(
frappe.bold(parent_doctype), frappe.bold(child_doctype)
- )
+ ),
+ debug=debug,
)
return False
@@ -730,7 +803,8 @@ def has_child_permission(
push_perm_check_log(
_("Parentfield not specified in {0}: {1}").format(
frappe.bold(child_doctype), frappe.bold(child_doc.name)
- )
+ ),
+ debug=debug,
)
return False
@@ -738,14 +812,19 @@ def has_child_permission(
push_perm_check_log(
_("{0} is not a valid parentfield for {1}").format(
frappe.bold(parentfield), frappe.bold(child_doctype)
- )
+ ),
+ debug=debug,
)
return False
permlevel = parent_meta.get_field(parentfield).permlevel
- if permlevel > 0 and permlevel not in parent_meta.get_permlevel_access(ptype, user=user):
+ accessible_permlevels = parent_meta.get_permlevel_access(ptype, user=user)
+ if permlevel > 0 and permlevel not in accessible_permlevels:
push_perm_check_log(
- _("Insufficient Permission Level for {0}").format(frappe.bold(parent_doctype))
+ _("Insufficient Permission Level for {0}").format(frappe.bold(parent_doctype)), debug=debug
+ )
+ debug and _debug_log(
+ f"This table is perm level {permlevel} but user only has access to {accessible_permlevels}"
)
return False
@@ -755,6 +834,7 @@ def has_child_permission(
doc=child_doc and getattr(child_doc, "parent_doc", child_doc.parent),
user=user,
raise_exception=raise_exception,
+ debug=debug,
)
diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js
index 64cd36a727..cb9bd28570 100644
--- a/frappe/printing/doctype/print_format/print_format.js
+++ b/frappe/printing/doctype/print_format/print_format.js
@@ -79,7 +79,7 @@ frappe.ui.form.on("Print Format", {
frappe.model.with_doctype(doctype, () => {
const meta = frappe.get_meta(doctype);
const has_int_float_currency_field = meta.fields.filter((df) =>
- in_list(["Int", "Float", "Currency"], df.fieldtype)
+ ["Int", "Float", "Currency"].includes(df.fieldtype)
);
frm.toggle_display("absolute_value", has_int_float_currency_field.length);
});
diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js
index 46313ef992..5b70f8e2ff 100644
--- a/frappe/printing/page/print_format_builder/print_format_builder.js
+++ b/frappe/printing/page/print_format_builder/print_format_builder.js
@@ -280,7 +280,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
set_section(f.label);
} else if (f.fieldtype === "Column Break") {
set_column();
- } else if (!in_list(frappe.model.layout_fields, f.fieldtype)) {
+ } else if (!frappe.model.layout_fields.includes(f.fieldtype)) {
if (!column) set_column();
if (f.fieldtype === "Table") {
@@ -317,7 +317,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
f.visible_columns = [];
$.each(frappe.get_meta(f.options).fields, function (i, _f) {
if (
- !in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) &&
+ !["Section Break", "Column Break", "Tab Break"].includes(_f.fieldtype) &&
!_f.print_hide &&
f.label
) {
@@ -636,7 +636,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
// add field which are in column_names first to preserve order
var fields = [];
$.each(column_names, function (i, v) {
- if (in_list(Object.keys(docfields_by_name), v)) {
+ if (Object.keys(docfields_by_name).includes(v)) {
fields.push(docfields_by_name[v]);
}
});
@@ -644,8 +644,8 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder {
$.each(doc_fields, function (j, f) {
if (
f &&
- !in_list(column_names, f.fieldname) &&
- !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) &&
+ !column_names.includes(f.fieldname) &&
+ !["Section Break", "Column Break", "Tab Break"].includes(f.fieldtype) &&
f.label
) {
fields.push(f);
diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js
index 97aa89b58c..91afafa707 100644
--- a/frappe/public/js/form_builder/store.js
+++ b/frappe/public/js/form_builder/store.js
@@ -171,7 +171,7 @@ export const useStore = defineStore("form-builder-store", () => {
}
// Link & Table fields should always have options set
- if (in_list(["Link", ...frappe.model.table_fields], df.fieldtype) && !df.options) {
+ if (["Link", ...frappe.model.table_fields].includes(df.fieldtype) && !df.options) {
error_message = __(
"Options is required for field {0} of type {1}",
get_field_data(df)
@@ -187,7 +187,7 @@ export const useStore = defineStore("form-builder-store", () => {
}
// In List View is not allowed for some fieldtypes
- if (df.in_list_view && in_list(not_allowed_in_list_view, df.fieldtype)) {
+ if (df.in_list_view && not_allowed_in_list_view.includes(df.fieldtype)) {
error_message = __(
"'In List View' is not allowed for field {0} of type {1}",
get_field_data(df)
@@ -195,7 +195,7 @@ export const useStore = defineStore("form-builder-store", () => {
}
// In Global Search is not allowed for no_value_type fields
- if (df.in_global_search && in_list(frappe.model.no_value_type, df.fieldtype)) {
+ if (df.in_global_search && frappe.model.no_value_type.includes(df.fieldtype)) {
error_message = __(
"'In Global Search' is not allowed for field {0} of type {1}",
get_field_data(df)
diff --git a/frappe/public/js/form_builder/utils.js b/frappe/public/js/form_builder/utils.js
index 6856432c2a..838b934a80 100644
--- a/frappe/public/js/form_builder/utils.js
+++ b/frappe/public/js/form_builder/utils.js
@@ -119,7 +119,7 @@ export async function get_table_columns(df, child_doctype) {
1,
]);
for (let tf of table_fields) {
- if (!in_list(frappe.model.layout_fields, tf.fieldtype) && tf.in_list_view && tf.label) {
+ if (!frappe.model.layout_fields.includes(tf.fieldtype) && tf.in_list_view && tf.label) {
let colsize;
if (tf.columns) {
@@ -281,7 +281,7 @@ export function scrub_field_names(fields) {
if (d.fieldname.endsWith("?")) {
d.fieldname = d.fieldname.slice(0, -1);
}
- if (in_list(frappe.model.restricted_fields, d.fieldname)) {
+ if (frappe.model.restricted_fields.includes(d.fieldname)) {
d.fieldname = d.fieldname + "1";
}
if (d.fieldtype == "Section Break") {
@@ -298,7 +298,7 @@ export function scrub_field_names(fields) {
frappe.utils.get_random(4);
}
} else {
- if (in_list(frappe.model.restricted_fields, d.fieldname)) {
+ if (frappe.model.restricted_fields.includes(d.fieldname)) {
frappe.throw(__("Fieldname {0} is restricted", [d.fieldname]));
}
}
diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue
index b4725da83d..5bfcab8148 100644
--- a/frappe/public/js/frappe/file_uploader/FileUploader.vue
+++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue
@@ -473,7 +473,7 @@ function check_restrictions(file) {
return is_correct_type && valid_file_size;
}
-function upload_files() {
+function upload_files(dialog) {
if (show_file_browser.value) {
return upload_via_file_browser();
}
@@ -483,6 +483,14 @@ function upload_files() {
if (props.as_dataurl) {
return return_as_dataurl();
}
+ if (!files.value.length) {
+ frappe.msgprint(__("Please select a file first."));
+ return Promise.reject();
+ }
+
+ dialog?.get_primary_btn().prop("disabled", true);
+ dialog?.get_secondary_btn().prop("disabled", true);
+
return frappe.run_serially(files.value.map((file, i) => () => upload_file(file, i)));
}
function upload_via_file_browser() {
diff --git a/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js b/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js
index 0e63eef469..b886c7703d 100644
--- a/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js
+++ b/frappe/public/js/frappe/file_uploader/file_uploader.bundle.js
@@ -113,9 +113,7 @@ class FileUploader {
}
upload_files() {
- this.dialog && this.dialog.get_primary_btn().prop("disabled", true);
- this.dialog && this.dialog.get_secondary_btn().prop("disabled", true);
- return this.uploader.upload_files();
+ return this.uploader.upload_files(this.dialog);
}
make_dialog(title) {
diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js
index 825cfb5320..b7bf5687d6 100644
--- a/frappe/public/js/frappe/form/controls/autocomplete.js
+++ b/frappe/public/js/frappe/form/controls/autocomplete.js
@@ -85,33 +85,15 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
};
}
- init_option_cache() {
- if (!this.$input.cache) {
- this.$input.cache = {};
- }
- if (!this.$input.cache[this.doctype]) {
- this.$input.cache[this.doctype] = {};
- }
- if (!this.$input.cache[this.doctype][this.df.fieldname]) {
- this.$input.cache[this.doctype][this.df.fieldname] = {};
- }
- }
-
setup_awesomplete() {
this.awesomplete = new Awesomplete(this.input, this.get_awesomplete_settings());
$(this.input_area).find(".awesomplete ul").css("min-width", "100%");
- this.init_option_cache();
-
this.$input.on(
"input",
frappe.utils.debounce((e) => {
- const cached_options =
- this.$input.cache[this.doctype][this.df.fieldname][e.target.value];
- if (cached_options && cached_options.length) {
- this.set_data(cached_options);
- } else if (this.get_query || this.df.get_query) {
+ if (this.get_query || this.df.get_query) {
this.execute_query_if_exists(e.target.value);
} else {
this.awesomplete.list = this.get_data();
@@ -245,7 +227,6 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui
if (!this.$input.is(":focus")) {
return;
}
- this.$input.cache[this.doctype][this.df.fieldname][term] = message;
this.set_data(message);
},
});
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 88669744fa..88aceb56a3 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -87,7 +87,7 @@ frappe.ui.form.Control = class BaseControl {
if (
status === "Read" &&
is_null(value) &&
- !in_list(["HTML", "Image", "Button"], this.df.fieldtype)
+ !["HTML", "Image", "Button"].includes(this.df.fieldtype)
)
status = "Read";
@@ -115,7 +115,7 @@ frappe.ui.form.Control = class BaseControl {
let value = frappe.model.get_value(this.doctype, this.docname, this.df.fieldname);
- if (in_list(["Date", "Datetime"], this.df.fieldtype) && value) {
+ if (["Date", "Datetime"].includes(this.df.fieldtype) && value) {
value = frappe.datetime.str_to_user(value);
}
@@ -127,7 +127,7 @@ frappe.ui.form.Control = class BaseControl {
status === "Read" &&
!this.only_input &&
is_null(value) &&
- !in_list(["HTML", "Image", "Button", "Geolocation"], this.df.fieldtype)
+ !["HTML", "Image", "Button", "Geolocation"].includes(this.df.fieldtype)
) {
if (explain) console.log("By Hide Read-only, null fields: None");
status = "None";
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 5aacc193d1..2e29cc79a9 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -138,7 +138,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
set_disp_area(value) {
if (
- in_list(["Currency", "Int", "Float"], this.df.fieldtype) &&
+ ["Currency", "Int", "Float"].includes(this.df.fieldtype) &&
(this.value === 0 || value === 0)
) {
// to set the 0 value in readonly for currency, int, float field
@@ -172,7 +172,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
if (
!this.df.label ||
!this.df?.documentation_url ||
- in_list(unsupported_fieldtypes, this.df.fieldtype)
+ unsupported_fieldtypes.includes(this.df.fieldtype)
)
return;
diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js
index db040d7c58..05e8fde29c 100644
--- a/frappe/public/js/frappe/form/controls/code.js
+++ b/frappe/public/js/frappe/form/controls/code.js
@@ -161,6 +161,7 @@ frappe.ui.form.ControlCode = class ControlCode extends frappe.ui.form.ControlTex
Golang: "ace/mode/golang",
Go: "ace/mode/golang",
Jinja: "ace/mode/django",
+ SQL: "ace/mode/sql",
};
const language = this.df.options;
diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js
index 2d0f506917..b090ed3cf2 100644
--- a/frappe/public/js/frappe/form/controls/data.js
+++ b/frappe/public/js/frappe/form/controls/data.js
@@ -215,8 +215,7 @@ frappe.ui.form.ControlData = class ControlData extends frappe.ui.form.ControlInp
}
set_input_attributes() {
if (
- in_list(
- ["Data", "Link", "Dynamic Link", "Password", "Select", "Read Only"],
+ ["Data", "Link", "Dynamic Link", "Password", "Select", "Read Only"].includes(
this.df.fieldtype
)
) {
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index 5d87c209e9..96f2f885d2 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -16,7 +16,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
}
get_start_date() {
- this.value = this.value == null ? undefined : this.value;
+ this.value = this.value == null || this.value == "" ? undefined : this.value;
let value = frappe.datetime.convert_to_user_tz(this.value);
return frappe.datetime.str_to_obj(value);
}
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index f60656273f..3125a0ca59 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -87,10 +87,10 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
return this.is_translatable() ? __(value) : value;
}
is_translatable() {
- return in_list(frappe.boot?.translated_doctypes || [], this.get_options());
+ return frappe.boot?.translated_doctypes || [].includes(this.get_options());
}
is_title_link() {
- return in_list(frappe.boot?.link_title_doctypes || [], this.get_options());
+ return frappe.boot?.link_title_doctypes || [].includes(this.get_options());
}
async set_link_title(value) {
const doctype = this.get_options();
diff --git a/frappe/public/js/frappe/form/controls/multicheck.js b/frappe/public/js/frappe/form/controls/multicheck.js
index 4b2987cf45..e29aa0a23e 100644
--- a/frappe/public/js/frappe/form/controls/multicheck.js
+++ b/frappe/public/js/frappe/form/controls/multicheck.js
@@ -75,13 +75,15 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for
make_checkboxes() {
this.$load_state.hide();
this.$checkbox_area.empty();
- this.options.forEach((option) => {
- let checkbox = this.get_checkbox_element(option).appendTo(this.$checkbox_area);
- if (option.danger) {
- checkbox.find(".label-area").addClass("text-danger");
- }
- option.$checkbox = checkbox;
- });
+ this.options
+ .sort((a, b) => cstr(a.label).localeCompare(cstr(b.label)))
+ .forEach((option) => {
+ let checkbox = this.get_checkbox_element(option).appendTo(this.$checkbox_area);
+ if (option.danger) {
+ checkbox.find(".label-area").addClass("text-danger");
+ }
+ option.$checkbox = checkbox;
+ });
if (this.df.select_all) {
this.setup_select_all();
}
@@ -152,7 +154,7 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for
`);
diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js
index ab2bda7d6d..1ae49c1feb 100644
--- a/frappe/public/js/frappe/form/controls/rating.js
+++ b/frappe/public/js/frappe/form/controls/rating.js
@@ -1,4 +1,4 @@
-frappe.ui.form.ControlRating = class ControlRating extends frappe.ui.form.ControlInt {
+frappe.ui.form.ControlRating = class ControlRating extends frappe.ui.form.ControlFloat {
make_input() {
super.make_input();
let stars = "";
diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js
index 09bf21c827..50e94651eb 100644
--- a/frappe/public/js/frappe/form/controls/table_multiselect.js
+++ b/frappe/public/js/frappe/form/controls/table_multiselect.js
@@ -166,7 +166,7 @@ frappe.ui.form.ControlTableMultiSelect = class ControlTableMultiSelect extends (
let me = this;
awesomplete.filter = function (item) {
- if (in_list(me._rows_list, item.value)) {
+ if (me._rows_list.includes(item.value)) {
return false;
}
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index 98a03eff97..fa7ab49dd1 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -295,11 +295,11 @@ class FormTimeline extends BaseTimeline {
set_communication_doc_status(doc) {
let indicator_color = "red";
- if (in_list(["Sent", "Clicked"], doc.delivery_status)) {
+ if (["Sent", "Clicked"].includes(doc.delivery_status)) {
indicator_color = "green";
} else if (["Sending", "Scheduled"].includes(doc.delivery_status)) {
indicator_color = "orange";
- } else if (in_list(["Opened", "Read"], doc.delivery_status)) {
+ } else if (["Opened", "Read"].includes(doc.delivery_status)) {
indicator_color = "blue";
} else if (doc.delivery_status == "Error") {
indicator_color = "red";
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 4f42b3ecaa..410a185ba2 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1412,7 +1412,7 @@ frappe.ui.form.Form = class FrappeForm {
is_form_builder() {
return (
- in_list(["DocType", "Customize Form"], this.doctype) &&
+ ["DocType", "Customize Form"].includes(this.doctype) &&
this.get_active_tab().label == "Form"
);
}
diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js
index b5942c7f46..8809d7b838 100644
--- a/frappe/public/js/frappe/form/grid.js
+++ b/frappe/public/js/frappe/form/grid.js
@@ -158,7 +158,7 @@ export default class Grid {
if (
!this.df.label ||
!this.df?.documentation_url ||
- in_list(unsupported_fieldtypes, this.df.fieldtype)
+ unsupported_fieldtypes.includes(this.df.fieldtype)
)
return;
@@ -685,7 +685,7 @@ export default class Grid {
get_modal_data() {
return this.df.get_data
? this.df.get_data().filter((data) => {
- if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) {
+ if (!this.deleted_docs || !this.deleted_docs.includes(data.name)) {
return data;
}
})
@@ -940,7 +940,7 @@ export default class Grid {
!df.hidden &&
(this.editable_fields || df.in_list_view) &&
((this.frm && this.frm.get_perm(df.permlevel, "read")) || !this.frm) &&
- !in_list(frappe.model.layout_fields, df.fieldtype)
+ !frappe.model.layout_fields.includes(df.fieldtype)
) {
if (df.columns) {
df.colsize = df.columns;
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index a168729c66..d403becaeb 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -429,10 +429,10 @@ export default class GridRow {
$(`
-
+
${__("Fieldname").bold()}
-
+
${__("Column Width").bold()}
@@ -500,7 +500,7 @@ export default class GridRow {
fields.push({
label: column.label,
value: column.fieldname,
- checked: selected_fields ? in_list(selected_fields, column.fieldname) : false,
+ checked: selected_fields ? selected_fields.includes(column.fieldname) : false,
});
}
});
@@ -522,13 +522,13 @@ export default class GridRow {
data-label='${docfield.label}' data-type='${docfield.fieldtype}'>
-
+
-
+
${__(docfield.label)}
-
-
+
@@ -1135,8 +1135,8 @@ export default class GridRow {
let ignore_fieldtypes = ["Text", "Small Text", "Code", "Text Editor", "HTML Editor"];
if (field.$input) {
field.$input.on("keydown", function (e) {
- var { TAB, UP: UP_ARROW, DOWN: DOWN_ARROW } = frappe.ui.keyCode;
- if (!in_list([TAB, UP_ARROW, DOWN_ARROW], e.which)) {
+ var { ESCAPE, TAB, UP: UP_ARROW, DOWN: DOWN_ARROW } = frappe.ui.keyCode;
+ if (![TAB, UP_ARROW, DOWN_ARROW, ESCAPE].includes(e.which)) {
return;
}
@@ -1145,7 +1145,7 @@ export default class GridRow {
var fieldtype = $(this).attr("data-fieldtype");
let ctrl_key = e.metaKey || e.ctrlKey;
- if (!in_list(ignore_fieldtypes, fieldtype) && ctrl_key && e.which !== TAB) {
+ if (!ignore_fieldtypes.includes(fieldtype) && ctrl_key && e.which !== TAB) {
me.add_new_row_using_keys(e);
return;
}
@@ -1156,7 +1156,7 @@ export default class GridRow {
}
var move_up_down = function (base) {
- if (in_list(ignore_fieldtypes, fieldtype) && !e.altKey) {
+ if (ignore_fieldtypes.includes(fieldtype) && !e.altKey) {
return false;
}
if (field.autocomplete_open) {
@@ -1171,6 +1171,14 @@ export default class GridRow {
return true;
};
+ // ESC
+ if (e.which === ESCAPE && !e.shiftKey) {
+ if (me.doc.__unedited) {
+ me.grid.grid_rows[me.doc.idx - 1].remove();
+ }
+ return false;
+ }
+
// TAB
if (e.which === TAB && !e.shiftKey) {
var last_column = me.wrapper.find(":input:enabled:last").get(0);
@@ -1441,8 +1449,8 @@ export default class GridRow {
!df.hidden &&
df.in_list_view &&
me.grid.frm.get_perm(df.permlevel, "read") &&
- !in_list(frappe.model.layout_fields, df.fieldtype) &&
- !in_list(blacklist, df.fieldname);
+ !frappe.model.layout_fields.includes(df.fieldtype) &&
+ !blacklist.includes(df.fieldname);
return visible ? df : null;
});
diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js
index 5f7b5e1035..bfec12c225 100644
--- a/frappe/public/js/frappe/form/grid_row_form.js
+++ b/frappe/public/js/frappe/form/grid_row_form.js
@@ -134,7 +134,7 @@ export default class GridRowForm {
var first = me.form_area.find("input:first");
if (
first.length &&
- !in_list(["Date", "Datetime", "Time"], first.attr("data-fieldtype"))
+ !["Date", "Datetime", "Time"].includes(first.attr("data-fieldtype"))
) {
try {
first.get(0).focus();
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index b7f0770a72..d2c78f0ee8 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -621,7 +621,7 @@ frappe.ui.form.Layout = class Layout {
// show grid row (if exists)
field.grid.grid_rows[0].show_form();
return true;
- } else if (!in_list(frappe.model.no_value_type, field.df.fieldtype)) {
+ } else if (!frappe.model.no_value_type.includes(field.df.fieldtype)) {
this.set_focus(field);
return true;
}
diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js
index a1a4893061..947fb14988 100644
--- a/frappe/public/js/frappe/form/toolbar.js
+++ b/frappe/public/js/frappe/form/toolbar.js
@@ -372,7 +372,7 @@ frappe.ui.form.Toolbar = class Toolbar {
}
// duplicate
- if (in_list(frappe.boot.user.can_create, me.frm.doctype) && !me.frm.meta.allow_copy) {
+ if (frappe.boot.user.can_create.includes(me.frm.doctype) && !me.frm.meta.allow_copy) {
this.page.add_menu_item(
__("Duplicate"),
function () {
diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js
index b9a66a8110..aa1cb31427 100644
--- a/frappe/public/js/frappe/list/list_settings.js
+++ b/frappe/public/js/frappe/list/list_settings.js
@@ -114,13 +114,13 @@ export default class ListSettings {
data-label="${me.fields[idx].label}" data-type="${me.fields[idx].type}">
-
+
${frappe.utils.icon("drag", "xs", "", "", "sortable-handle " + show_sortable_handle)}
-
+
${me.fields[idx].label}
-
+
${frappe.utils.icon("delete", "xs")}
@@ -316,7 +316,7 @@ export default class ListSettings {
meta.fields.forEach((field) => {
if (
field.in_list_view &&
- !in_list(frappe.model.no_value_type, field.fieldtype) &&
+ !frappe.model.no_value_type.includes(field.fieldtype) &&
me.subject_field.fieldname != field.fieldname
) {
me.fields.push({
@@ -363,11 +363,11 @@ export default class ListSettings {
let multiselect_fields = [];
meta.fields.forEach((field) => {
- if (!in_list(frappe.model.no_value_type, field.fieldtype)) {
+ if (!frappe.model.no_value_type.includes(field.fieldtype)) {
multiselect_fields.push({
label: field.label,
value: field.fieldname,
- checked: in_list(fields, field.fieldname),
+ checked: fields.includes(field.fieldname),
});
}
});
@@ -384,7 +384,7 @@ export default class ListSettings {
}
existing_fields.forEach((column) => {
- if (!in_list(new_fields, column)) {
+ if (!new_fields.includes(column)) {
removed_fields.push(column);
}
});
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 929c7b9812..491867edc9 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -478,7 +478,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
"/assets/frappe/images/ui-states/list-empty-state.svg";
const new_button = this.can_create
- ? `
-
+
{{ _("Verify") }}
`
);
// add event handler for submit button
verify_token();
+ $("#login_token").get(0)?.focus();
}
var continue_otp_app = function (setup, qrcode) {
diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py
index 176dd169aa..cf911d0ce3 100644
--- a/frappe/tests/test_document.py
+++ b/frappe/tests/test_document.py
@@ -533,6 +533,63 @@ class TestDocumentWebView(FrappeTestCase):
# Logged-in user can access the page without key
self.assertEqual(self.get(url_without_key, "Administrator").status, "200 OK")
+ def test_base_class_set_correctly_on_has_web_view_change(self):
+ from pathlib import Path
+
+ from frappe.modules.utils import get_doc_path, scrub
+
+ frappe.flags.allow_doctype_export = True
+
+ frappe.delete_doc_if_exists("DocType", "Test WebViewDocType", force=1)
+ test_doctype = new_doctype(
+ "Test WebViewDocType",
+ custom=0,
+ fields=[
+ {"fieldname": "test_field", "fieldtype": "Data"},
+ {"fieldname": "route", "fieldtype": "Data"},
+ {"fieldname": "is_published", "fieldtype": "Check"},
+ ],
+ )
+ test_doctype.insert()
+
+ doc_path = Path(get_doc_path(test_doctype.module, test_doctype.doctype, test_doctype.name))
+ controller_file_path = doc_path / f"{scrub(test_doctype.name)}.py"
+
+ # enable web view
+ test_doctype.has_web_view = 1
+ test_doctype.is_published_field = "is_published"
+ test_doctype.save()
+
+ # check if base class was updated to "WebsiteGenerator"
+ with open(controller_file_path) as f:
+ file_content = f.read()
+ self.assertIn(
+ "import WebsiteGenerator",
+ file_content,
+ "`WebsiteGenerator` not imported when web view is enabled!",
+ )
+ self.assertIn(
+ "(WebsiteGenerator)",
+ file_content,
+ "`Document` class not replaced with `WebsiteGenerator` when web view is enabled!",
+ )
+
+ # disable web view
+ test_doctype.has_web_view = 0
+ test_doctype.save()
+
+ # check if base class was updated to "Document" again
+ with open(controller_file_path) as f:
+ file_content = f.read()
+ self.assertIn(
+ "import Document", file_content, "`Document` not imported when web view is disabled!"
+ )
+ self.assertIn(
+ "(Document)",
+ file_content,
+ "`WebsiteGenerator` class not replaced with `Document` when web view is disabled!",
+ )
+
def test_bulk_inserts(self):
from frappe.model.document import bulk_insert
diff --git a/frappe/tests/test_email.py b/frappe/tests/test_email.py
index 13f385a22d..bd17a523ba 100644
--- a/frappe/tests/test_email.py
+++ b/frappe/tests/test_email.py
@@ -361,8 +361,10 @@ class TestEmailIntegrationTest(FrappeTestCase):
subject = "checking if email works"
content = "is email working?"
- frappe.sendmail(sender=sender, recipients=recipients, subject=subject, content=content, now=True)
- email = frappe.get_last_doc("Email Queue")
+ email = frappe.sendmail(
+ sender=sender, recipients=recipients, subject=subject, content=content, now=True
+ )
+ email.reload()
self.assertEqual(email.sender, sender)
self.assertEqual(len(email.recipients), 2)
self.assertEqual(email.status, "Sent")
diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py
index 970699d01c..14bd1affca 100644
--- a/frappe/tests/test_hooks.py
+++ b/frappe/tests/test_hooks.py
@@ -95,6 +95,80 @@ class TestHooks(FrappeTestCase):
event.delete()
+ def test_fixture_prefix(self):
+ import os
+ import shutil
+
+ from frappe import hooks
+ from frappe.utils.fixtures import export_fixtures
+
+ app = "frappe"
+ if os.path.isdir(frappe.get_app_path(app, "fixtures")):
+ shutil.rmtree(frappe.get_app_path(app, "fixtures"))
+
+ # use any set of core doctypes for test purposes
+ hooks.fixtures = [
+ {"dt": "User"},
+ {"dt": "Contact"},
+ {"dt": "Role"},
+ ]
+ hooks.fixture_auto_order = False
+ # every call to frappe.get_hooks loads the hooks module into cache
+ # therefor the cache has to be invalidated after every manual overwriting of hooks
+ # TODO replace with a more elegant solution if there is one or build a util function for this purpose
+ if frappe._load_app_hooks.__wrapped__ in frappe.local.request_cache.keys():
+ del frappe.local.request_cache[frappe._load_app_hooks.__wrapped__]
+ self.assertEqual([False], frappe.get_hooks("fixture_auto_order", app_name=app))
+ self.assertEqual(
+ [
+ {"dt": "User"},
+ {"dt": "Contact"},
+ {"dt": "Role"},
+ ],
+ frappe.get_hooks("fixtures", app_name=app),
+ )
+
+ export_fixtures(app)
+ # use assertCountEqual (replaced assertItemsEqual), beacuse os.listdir might return the list in a different order, depending on OS
+ self.assertCountEqual(
+ ["user.json", "contact.json", "role.json"], os.listdir(frappe.get_app_path(app, "fixtures"))
+ )
+
+ hooks.fixture_auto_order = True
+ del frappe.local.request_cache[frappe._load_app_hooks.__wrapped__]
+ self.assertEqual([True], frappe.get_hooks("fixture_auto_order", app_name=app))
+
+ shutil.rmtree(frappe.get_app_path(app, "fixtures"))
+ export_fixtures(app)
+ self.assertCountEqual(
+ ["1_user.json", "2_contact.json", "3_role.json"],
+ os.listdir(frappe.get_app_path(app, "fixtures")),
+ )
+
+ hooks.fixtures = [
+ {"dt": "User", "prefix": "my_prefix"},
+ {"dt": "Contact"},
+ {"dt": "Role"},
+ ]
+ hooks.fixture_auto_order = False
+
+ del frappe.local.request_cache[frappe._load_app_hooks.__wrapped__]
+ shutil.rmtree(frappe.get_app_path(app, "fixtures"))
+ export_fixtures(app)
+ self.assertCountEqual(
+ ["my_prefix_user.json", "contact.json", "role.json"],
+ os.listdir(frappe.get_app_path(app, "fixtures")),
+ )
+
+ hooks.fixture_auto_order = True
+ del frappe.local.request_cache[frappe._load_app_hooks.__wrapped__]
+ shutil.rmtree(frappe.get_app_path(app, "fixtures"))
+ export_fixtures(app)
+ self.assertCountEqual(
+ ["1_my_prefix_user.json", "2_contact.json", "3_role.json"],
+ os.listdir(frappe.get_app_path(app, "fixtures")),
+ )
+
class TestAPIHooks(FrappeAPITestCase):
def test_auth_hook(self):
diff --git a/frappe/tests/test_nestedset.py b/frappe/tests/test_nestedset.py
index 340b53bf38..d308388646 100644
--- a/frappe/tests/test_nestedset.py
+++ b/frappe/tests/test_nestedset.py
@@ -5,6 +5,7 @@ from unittest.mock import patch
import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
+from frappe.desk.treeview import get_children
from frappe.query_builder import Field
from frappe.query_builder.functions import Max
from frappe.tests.utils import FrappeTestCase
@@ -144,7 +145,7 @@ class TestNestedSet(FrappeTestCase):
leaf_node.reload()
def test_rebuild_tree(self):
- rebuild_tree(TEST_DOCTYPE, "parent_test_tree_doctype")
+ rebuild_tree(TEST_DOCTYPE)
self.test_basic_tree()
def test_move_group_into_another(self):
@@ -296,3 +297,43 @@ class TestNestedSet(FrappeTestCase):
self.assertNotIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=exclusive_link)))
self.assertIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=inclusive_link)))
+
+ def test_disabled_records_in_treeview(self):
+ """
+ Tests the `get_children` util for showing / skipping disabled records in treeview
+ """
+ doctype = (
+ new_doctype(
+ fields=[
+ {
+ "label": "Some Field",
+ "fieldname": "some_fieldname",
+ "fieldtype": "Data",
+ },
+ {
+ "label": "Disabled",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ },
+ ],
+ is_tree=True,
+ autoname="field:some_fieldname",
+ )
+ .insert()
+ .name
+ )
+
+ for record in [
+ {"some_fieldname": "Root", "disabled": 0, "is_group": 1},
+ {"some_fieldname": "Sub Tree 1", "disabled": 1, "parent_" + doctype: "Root", "is_group": 0},
+ ]:
+ d = frappe.new_doc(doctype)
+ d.update(record)
+ d.insert()
+
+ # Check if all records are fetched when flag is set to True
+ self.assertEqual(len(get_children(doctype, include_disabled=True)), 2)
+
+ # Check if disabled records are skipped is set to False
+ # Children of disabled records are automatically skipped in recursion
+ self.assertEqual(len(get_children(doctype)), 1)
diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py
index 5349f15dbf..817b5319f7 100644
--- a/frappe/tests/test_recorder.py
+++ b/frappe/tests/test_recorder.py
@@ -122,7 +122,13 @@ class TestRecorder(FrappeTestCase):
frappe.recorder.post_process()
requests = frappe.recorder.get()
- request = frappe.recorder.get(requests[0]["uuid"])
+ request = frappe.recorder.get(
+ next(
+ request
+ for request in requests
+ if request["event_type"] == frappe.recorder.RecorderEvent.HTTP_REQUEST
+ )["uuid"]
+ )
for query, call in zip(queries, request["calls"]):
self.assertEqual(call["exact_copies"], query[1])
@@ -152,6 +158,7 @@ class TestQueryNormalization(FrappeTestCase):
"select * from `user` where a > 5": "select * from `user` where a > ?",
"select `name` from `user`": "select `name` from `user`",
"select `name` from `user` limit 10": "select `name` from `user` limit ?",
+ "select `name` from `user` where name in ('a', 'b', 'c')": "select `name` from `user` where name in (?)",
}
for query, normalized in test_cases.items():
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index b0db0c7b91..c85463dde2 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -5,6 +5,7 @@ import functools
import hashlib
import io
import os
+import shutil
import sys
import traceback
from collections import deque
@@ -452,7 +453,12 @@ def execute_in_shell(cmd, verbose=False, low_priority=False, check_exit_code=Fal
cmd = shlex.join(cmd)
with (tempfile.TemporaryFile() as stdout, tempfile.TemporaryFile() as stderr):
- kwargs = {"shell": True, "stdout": stdout, "stderr": stderr}
+ kwargs = {
+ "shell": True,
+ "stdout": stdout,
+ "stderr": stderr,
+ "executable": shutil.which("bash") or "/bin/bash",
+ }
if low_priority:
kwargs["preexec_fn"] = lambda: os.nice(10)
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index d89003a81d..71097a3a58 100755
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -9,6 +9,7 @@ from typing import Any, NoReturn
from uuid import uuid4
import redis
+import setproctitle
from redis.exceptions import BusyLoadingError, ConnectionError
from rq import Callback, Queue, Worker
from rq.exceptions import NoSuchJobError
@@ -198,7 +199,10 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True,
method_name = method
method = frappe.get_attr(method)
else:
- method_name = cstr(method.__name__)
+ method_name = f"{method.__module__}.{method.__qualname__}"
+
+ actual_func_name = kwargs.get("job_type") if "run_scheduled_job" in method_name else method_name
+ setproctitle.setproctitle(f"rq: Started running {actual_func_name} at {time.time()}")
frappe.local.job = frappe._dict(
site=site,
@@ -288,6 +292,49 @@ def start_worker(
if quiet:
logging_level = "WARNING"
+ # Always initialize sentry SDK if the DSN is sent
+ if sentry_dsn := os.getenv("FRAPPE_SENTRY_DSN"):
+ import sentry_sdk
+ from sentry_sdk.integrations.argv import ArgvIntegration
+ from sentry_sdk.integrations.atexit import AtexitIntegration
+ from sentry_sdk.integrations.dedupe import DedupeIntegration
+ from sentry_sdk.integrations.excepthook import ExcepthookIntegration
+ from sentry_sdk.integrations.modules import ModulesIntegration
+ from sentry_sdk.integrations.rq import RqIntegration
+
+ from frappe.utils.sentry import FrappeIntegration, before_send
+
+ integrations = [
+ AtexitIntegration(),
+ ExcepthookIntegration(),
+ DedupeIntegration(),
+ ModulesIntegration(),
+ ArgvIntegration(),
+ RqIntegration(),
+ ]
+
+ experiments = {}
+ kwargs = {}
+
+ if os.getenv("ENABLE_SENTRY_DB_MONITORING"):
+ integrations.append(FrappeIntegration())
+ experiments["record_sql_params"] = True
+
+ if tracing_sample_rate := os.getenv("SENTRY_TRACING_SAMPLE_RATE"):
+ kwargs["traces_sample_rate"] = float(tracing_sample_rate)
+
+ sentry_sdk.init(
+ dsn=sentry_dsn,
+ before_send=before_send,
+ attach_stacktrace=True,
+ release=frappe.__version__,
+ auto_enabling_integrations=False,
+ default_integrations=False,
+ integrations=integrations,
+ _experiments=experiments,
+ **kwargs,
+ )
+
worker = Worker(queues, name=get_worker_name(queue_name), connection=redis_connection)
worker.work(
logging_level=logging_level,
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index bdba23ac2f..d48e3a9a67 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -347,7 +347,7 @@ class BackupGenerator:
backup_path = self.backup_path_files if folder == "public" else self.backup_path_private_files
if self.compress_files:
- cmd_string = "self=$$; ( tar cf - {1} || kill $self ) | gzip > {0}"
+ cmd_string = "set -o pipefail; tar cf - {1} | gzip > {0}"
else:
cmd_string = "tar -cf {0} {1}"
@@ -411,13 +411,6 @@ class BackupGenerator:
with gzip.open(self.backup_path_db, "wt") as f:
f.write(generated_header)
- # Remember process of this shell and kill it if mysqldump exits w/ non-zero code
- def wrap(cmd):
- ret = ["self=$$;", "("]
- ret.extend(cmd)
- ret.extend(["||", "kill", "$self", ")", "|", gzip_exc, ">>", self.backup_path_db])
- return ret
-
cmd = []
extra = []
if self.db_type == "mariadb":
@@ -451,7 +444,7 @@ class BackupGenerator:
cmd.append(bin)
cmd.extend(args)
- command = " ".join(wrap(cmd))
+ command = " ".join(["set -o pipefail;"] + cmd + ["|", gzip_exc, ">>", self.backup_path_db])
if self.verbose:
print(command.replace(frappe.utils.esc(self.password, "$ "), "*" * 10) + "\n")
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index bd4dc894fe..e810224d59 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -2485,6 +2485,8 @@ def get_imaginary_pixel_response():
def is_site_link(link: str) -> bool:
+ if not link:
+ return False
if link.startswith("/"):
return True
return urlparse(link).netloc == urlparse(frappe.utils.get_url()).netloc
diff --git a/frappe/utils/diff.py b/frappe/utils/diff.py
index 2fbe555e12..883f888a89 100644
--- a/frappe/utils/diff.py
+++ b/frappe/utils/diff.py
@@ -48,10 +48,19 @@ def _get_value_from_version(version_name: int | str, fieldname: str):
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def version_query(doctype, txt, searchfield, start, page_len, filters):
+ version_filters = {
+ "docname": filters["docname"],
+ "ref_doctype": filters["ref_doctype"],
+ }
+
+ if fieldname := filters.get("fieldname"):
+ # This helps filter version logs which contain changes to the field.
+ version_filters["data"] = ("LIKE", f'%"{fieldname}"%')
+
results = frappe.get_list(
"Version",
fields=["name", "modified"],
- filters=filters,
+ filters=version_filters,
limit_start=start,
limit_page_length=page_len,
order_by="modified desc",
diff --git a/frappe/utils/fixtures.py b/frappe/utils/fixtures.py
index ddd8650451..120d452cc0 100644
--- a/frappe/utils/fixtures.py
+++ b/frappe/utils/fixtures.py
@@ -38,7 +38,7 @@ def import_fixtures(app):
file_path = frappe.get_app_path(app, "fixtures", fname)
try:
- import_doc(file_path)
+ import_doc(file_path, sort=True)
except (ImportError, frappe.DoesNotExistError) as e:
# fixture syncing for missing doctypes
print(f"Skipping fixture syncing from the file {fname}. Reason: {e}")
@@ -67,20 +67,34 @@ def export_fixtures(app=None):
else:
apps = frappe.get_installed_apps()
for app in apps:
- for fixture in frappe.get_hooks("fixtures", app_name=app):
+ fixture_auto_order = bool(
+ next(iter(frappe.get_hooks("fixture_auto_order", app_name=app)), False)
+ )
+ fixtures = frappe.get_hooks("fixtures", app_name=app)
+ for index, fixture in enumerate(fixtures, start=1):
filters = None
or_filters = None
if isinstance(fixture, dict):
filters = fixture.get("filters")
or_filters = fixture.get("or_filters")
+ prefix = fixture.get("prefix")
fixture = fixture.get("doctype") or fixture.get("dt")
print(f"Exporting {fixture} app {app} filters {(filters if filters else or_filters)}")
if not os.path.exists(frappe.get_app_path(app, "fixtures")):
os.mkdir(frappe.get_app_path(app, "fixtures"))
+ filename = frappe.scrub(fixture)
+ if prefix:
+ filename = f"{prefix}_{filename}"
+ if fixture_auto_order:
+ number_of_digits = len(str(len(fixtures)))
+ # add zero padding so files can be sorted lexicographically with filename.
+ file_number = str(index).zfill(number_of_digits)
+ filename = f"{file_number}_{filename}"
+
export_json(
fixture,
- frappe.get_app_path(app, "fixtures", frappe.scrub(fixture) + ".json"),
+ frappe.get_app_path(app, "fixtures", filename + ".json"),
filters=filters,
or_filters=or_filters,
order_by="idx asc, creation asc",
diff --git a/frappe/utils/nestedset.py b/frappe/utils/nestedset.py
index 2beaa63447..60565581c9 100644
--- a/frappe/utils/nestedset.py
+++ b/frappe/utils/nestedset.py
@@ -168,11 +168,8 @@ def update_move_node(doc: Document, parent_field: str):
@frappe.whitelist()
-def rebuild_tree(doctype, parent_field):
- """
- call rebuild_node for all root nodes
- """
-
+def rebuild_tree(doctype: str) -> None:
+ """Call rebuild_node for all root nodes."""
# Check for perm if called from client-side
if frappe.request and frappe.local.form_dict.cmd == "rebuild_tree":
frappe.only_for("System Manager")
@@ -184,6 +181,8 @@ def rebuild_tree(doctype, parent_field):
title=_("Invalid Action"),
)
+ parent_field = meta.nsm_parent_field or f"parent_{frappe.scrub(doctype)}"
+
# get all roots
right = 1
table = DocType(doctype)
@@ -327,7 +326,7 @@ class NestedSet(Document):
)
if merge:
- rebuild_tree(self.doctype, parent_field)
+ rebuild_tree(self.doctype)
def validate_one_root(self):
if not self.get(self.nsm_parent_field):
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index cce341f926..927e3f3b38 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -56,6 +56,7 @@ def report_error(status_code):
response = build_response("json")
response.status_code = status_code
+
return response
@@ -168,8 +169,9 @@ def _make_logs_v1():
from frappe.utils.error import guess_exception_source
response = frappe.local.response
+ allow_traceback = frappe.get_system_settings("allow_error_traceback") if frappe.db else False
- if frappe.error_log:
+ if frappe.error_log and allow_traceback:
if source := guess_exception_source(frappe.local.error_log and frappe.local.error_log[0]["exc"]):
response["_exc_source"] = source
response["exc"] = json.dumps([frappe.utils.cstr(d["exc"]) for d in frappe.local.error_log])
diff --git a/frappe/utils/scheduler.py b/frappe/utils/scheduler.py
index 88cb85b667..ca2950a156 100755
--- a/frappe/utils/scheduler.py
+++ b/frappe/utils/scheduler.py
@@ -14,6 +14,8 @@ import random
import time
from typing import NoReturn
+import setproctitle
+
# imports - module imports
import frappe
from frappe.utils import cint, get_datetime, get_sites, now_datetime
@@ -31,6 +33,10 @@ def cprint(*args, **kwargs):
pass
+def _proctitle(message):
+ setproctitle.setproctitle(f"frappe-scheduler: {message}")
+
+
def start_scheduler() -> NoReturn:
"""Run enqueue_events_for_all_sites based on scheduler tick.
Specify scheduler_interval in seconds in common_site_config.json"""
@@ -39,6 +45,7 @@ def start_scheduler() -> NoReturn:
set_niceness()
while True:
+ _proctitle("idle")
time.sleep(tick)
enqueue_events_for_all_sites()
@@ -68,6 +75,7 @@ def enqueue_events_for_site(site: str) -> None:
frappe.logger("scheduler").error(f"Exception in Enqueue Events for Site {site}", exc_info=True)
try:
+ _proctitle(f"scheduling events for {site}")
frappe.init(site=site)
frappe.connect()
if is_scheduler_inactive():
diff --git a/frappe/utils/sentry.py b/frappe/utils/sentry.py
index 8ca66a15d7..61ba48eecc 100644
--- a/frappe/utils/sentry.py
+++ b/frappe/utils/sentry.py
@@ -1,18 +1,98 @@
import os
import sys
+from datetime import datetime
+import rq
from sentry_sdk import capture_message as sentry_capture_message
from sentry_sdk.hub import Hub
+from sentry_sdk.integrations import Integration
from sentry_sdk.integrations.wsgi import _make_wsgi_event_processor
from sentry_sdk.tracing import SOURCE_FOR_STYLE
-from sentry_sdk.utils import event_from_exception
+from sentry_sdk.tracing_utils import record_sql_queries
+from sentry_sdk.utils import capture_internal_exceptions, event_from_exception
import frappe
import frappe.monitor
+from frappe.database.database import Database, EmptyQueryValues
+
+
+class FrappeIntegration(Integration):
+ identifier = "frappe"
+
+ @staticmethod
+ def setup_once():
+ real_connect = Database.connect
+ real_sql = Database.sql
+
+ def sql(self, query, values=None, *args, **kwargs):
+ hub = Hub.current
+
+ if not self._conn:
+ self.connect()
+
+ with record_sql_queries(
+ hub, self._cursor, query, values, paramstyle="pyformat", executemany=False
+ ):
+ return real_sql(self, query, values or EmptyQueryValues, *args, **kwargs)
+
+ def connect(self):
+ hub = Hub.current
+ with capture_internal_exceptions():
+ hub.add_breadcrumb(message="connect", category="query")
+
+ with hub.start_span(op="db", description="connect"):
+ return real_connect(self)
+
+ Database.connect = connect
+ Database.sql = sql
+
+
+def set_scope(scope):
+ if job := rq.get_current_job():
+ kwargs = job._kwargs
+ transaction_name = str(kwargs["method"])
+ context = frappe._dict({"scheduled": False, "wait": 0})
+ if "run_scheduled_job" in transaction_name:
+ transaction_name = kwargs.get("kwargs", {}).get("job_type", "")
+ context.scheduled = True
+
+ waitdiff = datetime.utcnow() - job.enqueued_at
+ context.uuid = job.id
+ context.wait = waitdiff.total_seconds()
+
+ scope.set_extra("job", context)
+ scope.set_transaction_name(transaction_name)
+ else:
+ if frappe.form_dict.cmd:
+ path = f"/api/method/{frappe.form_dict.cmd}"
+ else:
+ path = frappe.request.path
+
+ scope.set_transaction_name(
+ path,
+ source=SOURCE_FOR_STYLE["endpoint"],
+ )
+
+ scope.set_user({"id": frappe.local.site})
+ user = getattr(frappe.session, "user", "Unidentified")
+ scope.set_tag("frappe_user", user)
+ # Extract `X-Frappe-Request-ID` to store as a separate field if its present
+ if trace_id := frappe.monitor.get_trace_id():
+ scope.set_tag("frappe_trace_id", trace_id)
+
+
+def set_sentry_context():
+ if not frappe.get_system_settings("enable_telemetry"):
+ return
+
+ hub = Hub.current
+ with hub.configure_scope() as scope:
+ set_scope(scope)
def before_send(event, hint):
- # Not doing anything here for now - we can add some checks to clean up the data, strip PII, etc.
+ if event.get("logger", "") == "CSSUTILS":
+ return None
return event
@@ -27,27 +107,27 @@ def capture_exception(message: str | None = None) -> None:
return
try:
hub = Hub.current
-
if frappe.request:
with hub.configure_scope() as scope:
- scope.set_transaction_name(
- frappe.request.path,
- source=SOURCE_FOR_STYLE["endpoint"],
- )
-
+ if (
+ os.getenv("ENABLE_SENTRY_DB_MONITORING") is None
+ or os.getenv("SENTRY_TRACING_SAMPLE_RATE") is None
+ ):
+ set_scope(scope)
evt_processor = _make_wsgi_event_processor(frappe.request.environ, False)
scope.add_event_processor(evt_processor)
- scope.set_tag("site", frappe.local.site)
- user = getattr(frappe.session, "user", "Unidentified")
- scope.set_user({"id": user, "email": user})
-
- # Extract `X-Frappe-Request-ID` to store as a separate field if its present
- if trace_id := frappe.monitor.get_trace_id():
- scope.set_tag("frappe_trace_id", trace_id)
+ if frappe.request.is_json:
+ scope.set_context("JSON Body", frappe.request.json)
+ elif frappe.request.form:
+ scope.set_context("Form Data", frappe.request.form)
if client := hub.client:
exc_info = sys.exc_info()
if any(exc_info):
+ # Don't report validation errors
+ if isinstance(exc_info[0], frappe.ValidationError):
+ return
+
event, hint = event_from_exception(
exc_info,
client_options=client.options,
diff --git a/frappe/website/doctype/blog_post/blog_post_list.js b/frappe/website/doctype/blog_post/blog_post_list.js
index 426cd4ef08..0d617654ca 100644
--- a/frappe/website/doctype/blog_post/blog_post_list.js
+++ b/frappe/website/doctype/blog_post/blog_post_list.js
@@ -2,9 +2,9 @@ frappe.listview_settings["Blog Post"] = {
add_fields: ["title", "published", "blogger", "blog_category"],
get_indicator: function (doc) {
if (doc.published) {
- return [__("Published"), "green", "published,=,Yes"];
+ return [__("Published"), "green", "published,=,1"];
} else {
- return [__("Not Published"), "gray", "published,=,Yes"];
+ return [__("Not Published"), "gray", "published,=,0"];
}
},
};
diff --git a/frappe/website/doctype/help_article/help_article.json b/frappe/website/doctype/help_article/help_article.json
index 520e9dedd0..ffced5cc3d 100644
--- a/frappe/website/doctype/help_article/help_article.json
+++ b/frappe/website/doctype/help_article/help_article.json
@@ -113,7 +113,7 @@
"index_web_pages_for_search": 1,
"is_published_field": "published",
"links": [],
- "modified": "2022-12-15 20:05:11.317400",
+ "modified": "2024-01-02 11:34:43.594309",
"modified_by": "Administrator",
"module": "Website",
"name": "Help Article",
@@ -136,10 +136,6 @@
"read": 1,
"role": "Knowledge Base Contributor",
"write": 1
- },
- {
- "read": 1,
- "role": "Guest"
}
],
"sort_field": "modified",
diff --git a/frappe/website/doctype/help_article/help_article.py b/frappe/website/doctype/help_article/help_article.py
index 78dbb49377..d2914dd07a 100644
--- a/frappe/website/doctype/help_article/help_article.py
+++ b/frappe/website/doctype/help_article/help_article.py
@@ -29,6 +29,7 @@ class HelpArticle(WebsiteGenerator):
route: DF.Data | None
title: DF.Data
# end: auto-generated types
+
def validate(self):
self.set_route()
diff --git a/frappe/website/doctype/help_article/test_help_article.py b/frappe/website/doctype/help_article/test_help_article.py
index a7576c7168..b17a8eda8f 100644
--- a/frappe/website/doctype/help_article/test_help_article.py
+++ b/frappe/website/doctype/help_article/test_help_article.py
@@ -39,6 +39,18 @@ class TestHelpArticle(FrappeTestCase):
self.assertEqual(self.help_article.helpful, 1)
self.assertEqual(self.help_article.not_helpful, 1)
+ def test_category_disable(self):
+ self.help_article.load_from_db()
+ self.help_article.published = 1
+ self.help_article.save()
+
+ self.help_category.load_from_db()
+ self.help_category.published = 0
+ self.help_category.save()
+
+ self.help_article.load_from_db()
+ self.assertEqual(self.help_article.published, 0)
+
@classmethod
def tearDownClass(cls) -> None:
frappe.delete_doc(cls.help_article.doctype, cls.help_article.name)
diff --git a/frappe/website/doctype/help_category/help_category.py b/frappe/website/doctype/help_category/help_category.py
index 7c78ff3d46..10efcb30e7 100644
--- a/frappe/website/doctype/help_category/help_category.py
+++ b/frappe/website/doctype/help_category/help_category.py
@@ -32,6 +32,11 @@ class HelpCategory(WebsiteGenerator):
def validate(self):
self.set_route()
+ # disable help articles of this category
+ if not self.published:
+ for d in frappe.get_all("Help Article", dict(category=self.name)):
+ frappe.db.set_value("Help Article", d.name, "published", 0)
+
def set_route(self):
if not self.route:
self.route = "kb/" + self.scrub(self.category_name)
diff --git a/frappe/website/doctype/top_bar_item/top_bar_item.json b/frappe/website/doctype/top_bar_item/top_bar_item.json
index 3d23141c91..899b881b8a 100644
--- a/frappe/website/doctype/top_bar_item/top_bar_item.json
+++ b/frappe/website/doctype/top_bar_item/top_bar_item.json
@@ -11,8 +11,7 @@
"open_in_new_tab",
"right",
"column_break_5",
- "parent_label",
- "icon"
+ "parent_label"
],
"fields": [
{
@@ -50,12 +49,6 @@
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
- {
- "description": "If Icon is set, it will be shown instead of Label",
- "fieldname": "icon",
- "fieldtype": "Attach Image",
- "label": "Icon"
- },
{
"default": "0",
"depends_on": "eval:doc.url",
@@ -68,12 +61,13 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-26 20:59:42.142208",
+ "modified": "2024-01-08 12:05:25.782635",
"modified_by": "Administrator",
"module": "Website",
"name": "Top Bar Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "states": []
}
\ No newline at end of file
diff --git a/frappe/website/doctype/top_bar_item/top_bar_item.py b/frappe/website/doctype/top_bar_item/top_bar_item.py
index 13eaf77113..61401458d8 100644
--- a/frappe/website/doctype/top_bar_item/top_bar_item.py
+++ b/frappe/website/doctype/top_bar_item/top_bar_item.py
@@ -14,7 +14,6 @@ class TopBarItem(Document):
if TYPE_CHECKING:
from frappe.types import DF
- icon: DF.AttachImage | None
label: DF.Data
open_in_new_tab: DF.Check
parent: DF.Data
@@ -24,4 +23,5 @@ class TopBarItem(Document):
right: DF.Check
url: DF.Data | None
# end: auto-generated types
+
pass
diff --git a/frappe/website/doctype/web_form/templates/web_form.html b/frappe/website/doctype/web_form/templates/web_form.html
index e30284e1ca..9b0f23e185 100644
--- a/frappe/website/doctype/web_form/templates/web_form.html
+++ b/frappe/website/doctype/web_form/templates/web_form.html
@@ -17,7 +17,7 @@
{% block header_buttons %}
{% if allow_edit and in_view_mode %}
- {{ _("Edit Response", null, "Button in web form") }}
+ {{ _("Edit Response", context="Button in web form") }}
{% endif %}
{% if allow_print and in_view_mode %}
@@ -38,10 +38,10 @@
{% if not in_view_mode %}
- {{ _("Discard", null, "Button in web form") }}
+ {{ _("Discard", context="Button in web form") }}
- {{ button_label or _("Submit", null, "Button in web form") }}
+ {{ _(button_label, context="Button in web form") or _("Submit", context="Button in web form") }}
{% endif %}
{% endblock %}
@@ -147,10 +147,10 @@
{% else %}
{% if show_list %}
- {{ _("See previous responses", null, "Button in web form") }}
+ {{ _("See previous responses", context="Button in web form") }}
{% endif %}
{% if not login_required or allow_multiple %}
- {{ _("Submit another response", null, "Button in web form") }}
+ {{ _("Submit another response", context="Button in web form") }}
{% endif %}
{% endif %}
diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js
index b725c8db6c..dd73f09a0c 100644
--- a/frappe/website/doctype/web_form/web_form.js
+++ b/frappe/website/doctype/web_form/web_form.js
@@ -107,6 +107,7 @@ frappe.ui.form.on("Web Form", {
reqd: df.reqd,
default: df.default,
read_only: df.read_only,
+ precision: df.precision,
depends_on: df.depends_on,
mandatory_depends_on: df.mandatory_depends_on,
read_only_depends_on: df.read_only_depends_on,
diff --git a/frappe/website/doctype/web_form_field/web_form_field.json b/frappe/website/doctype/web_form_field/web_form_field.json
index 90ec99c3b8..860029cce2 100644
--- a/frappe/website/doctype/web_form_field/web_form_field.json
+++ b/frappe/website/doctype/web_form_field/web_form_field.json
@@ -17,6 +17,7 @@
"options",
"max_length",
"max_value",
+ "precision",
"property_depends_on_section",
"depends_on",
"mandatory_depends_on",
@@ -143,11 +144,19 @@
"fieldtype": "Code",
"label": "Read Only Depends On",
"options": "JS"
+ },
+ {
+ "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)",
+ "description": "Set non-standard precision for a Float or Currency field",
+ "fieldname": "precision",
+ "fieldtype": "Select",
+ "label": "Precision",
+ "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9"
}
],
"istable": 1,
"links": [],
- "modified": "2022-11-21 17:41:52.139191",
+ "modified": "2024-01-08 13:21:06.272248",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form Field",
diff --git a/frappe/website/doctype/web_form_field/web_form_field.py b/frappe/website/doctype/web_form_field/web_form_field.py
index c6fa4f5dc9..208d31096a 100644
--- a/frappe/website/doctype/web_form_field/web_form_field.py
+++ b/frappe/website/doctype/web_form_field/web_form_field.py
@@ -56,6 +56,7 @@ class WebFormField(Document):
parent: DF.Data
parentfield: DF.Data
parenttype: DF.Data
+ precision: DF.Literal["", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
read_only: DF.Check
read_only_depends_on: DF.Code | None
reqd: DF.Check
diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json
index 4707eef8df..fddd929380 100644
--- a/frappe/website/doctype/website_settings/website_settings.json
+++ b/frappe/website/doctype/website_settings/website_settings.json
@@ -19,6 +19,7 @@
"misc_section",
"app_name",
"disable_signup",
+ "show_footer_on_login",
"column_break_9",
"app_logo",
"section_break_6",
@@ -49,9 +50,9 @@
"footer",
"footer_items",
"footer_details_section",
- "hide_footer_signup",
"copyright",
"footer_logo",
+ "hide_footer_signup",
"column_break_37",
"address",
"footer_powered",
@@ -126,7 +127,7 @@
"fieldname": "website_theme_image_link",
"fieldtype": "Code",
"hidden": 1,
- "label": "Website Theme Image Link"
+ "label": "Website Theme image link"
},
{
"collapsible": 1,
@@ -211,7 +212,7 @@
"default": "0",
"fieldname": "hide_footer_signup",
"fieldtype": "Check",
- "label": "Hide Footer Signup"
+ "label": "Hide footer signup"
},
{
"collapsible": 1,
@@ -248,10 +249,10 @@
},
{
"default": "1",
- "description": "Disable Signups on site. New users will have to be manually registered by system managers.",
+ "description": "New users will have to be manually registered by system managers.",
"fieldname": "disable_signup",
"fieldtype": "Check",
- "label": "Disable Signup"
+ "label": "Disable signups"
},
{
"collapsible": 1,
@@ -282,20 +283,20 @@
"description": "To use Google Indexing, enable Google Settings.",
"fieldname": "enable_google_indexing",
"fieldtype": "Check",
- "label": "Enable Google Indexing"
+ "label": "Enable Google indexing"
},
{
"fieldname": "indexing_refresh_token",
"fieldtype": "Data",
"hidden": 1,
- "label": "Indexing Refresh Token",
+ "label": "Indexing refresh token",
"read_only": 1
},
{
"fieldname": "indexing_authorization_code",
"fieldtype": "Data",
"hidden": 1,
- "label": "Indexing Authorization Code",
+ "label": "Indexing authorization code",
"read_only": 1
},
{
@@ -308,7 +309,7 @@
"default": "0",
"fieldname": "enable_view_tracking",
"fieldtype": "Check",
- "label": "Enable In App Website Tracking"
+ "label": "Enable in-app website tracking"
},
{
"fieldname": "footer_logo",
@@ -373,7 +374,7 @@
"default": "1",
"fieldname": "google_analytics_anonymize_ip",
"fieldtype": "Check",
- "label": "Google Analytics Anonymize IP"
+ "label": "Google Analytics anonymise IP"
},
{
"default": "0",
@@ -408,13 +409,13 @@
"default": "0",
"fieldname": "show_account_deletion_link",
"fieldtype": "Check",
- "label": "Show Account Deletion Link in My Account Page"
+ "label": "Show account deletion link in My Account page"
},
{
"default": "72",
"fieldname": "auto_account_deletion",
"fieldtype": "Int",
- "label": "Auto Account Deletion within (Hours)"
+ "label": "Automatically delete account within (hours)"
},
{
"fieldname": "footer_powered",
@@ -469,6 +470,12 @@
"fieldname": "analytics_section",
"fieldtype": "Section Break",
"label": "Analytics"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_footer_on_login",
+ "fieldtype": "Check",
+ "label": "Show footer on login"
}
],
"icon": "fa fa-cog",
@@ -476,7 +483,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-12-08 15:52:37.525003",
+ "modified": "2024-01-08 11:50:34.750809",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",
diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py
index d19ac375a5..70c5b04295 100644
--- a/frappe/website/doctype/website_settings/website_settings.py
+++ b/frappe/website/doctype/website_settings/website_settings.py
@@ -56,6 +56,7 @@ class WebsiteSettings(Document):
robots_txt: DF.Code | None
route_redirects: DF.Table[WebsiteRouteRedirect]
show_account_deletion_link: DF.Check
+ show_footer_on_login: DF.Check
show_language_picker: DF.Check
splash_image: DF.AttachImage | None
subdomain: DF.SmallText | None
@@ -63,8 +64,8 @@ class WebsiteSettings(Document):
top_bar_items: DF.Table[TopBarItem]
website_theme: DF.Link | None
website_theme_image_link: DF.Code | None
-
# end: auto-generated types
+
def validate(self):
self.validate_top_bar_items()
self.validate_footer_items()
diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py
index 83d55f7a9a..e07c850ed4 100644
--- a/frappe/website/page_renderers/document_page.py
+++ b/frappe/website/page_renderers/document_page.py
@@ -25,7 +25,10 @@ class DocumentPage(BaseTemplatePage):
def search_in_doctypes_with_web_view(self):
if document := _find_matching_document_webview(self.path):
self.doctype, self.docname = document
- return True
+ doc = frappe.get_cached_doc(self.doctype, self.docname)
+ return (
+ doc.meta.allow_guest_to_view or doc.has_permission() or frappe.has_website_permission(doc)
+ )
def search_web_page_dynamic_routes(self):
d = get_page_info_from_web_page_with_dynamic_routes(self.path)
diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py
index fefec2b4eb..b17031c4bb 100644
--- a/frappe/website/path_resolver.py
+++ b/frappe/website/path_resolver.py
@@ -38,7 +38,11 @@ class PathResolver:
except frappe.Redirect as e:
return frappe.flags.redirect_location, RedirectPage(self.path, e.http_status_code)
- endpoint = resolve_path(self.path)
+ if frappe.get_hooks("website_path_resolver"):
+ for handler in frappe.get_hooks("website_path_resolver"):
+ endpoint = frappe.get_attr(handler)(self.path)
+ else:
+ endpoint = resolve_path(self.path)
# WARN: Hardcoded for better performance
if endpoint == "app":
diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py
index 00670a2c4d..36e11072fa 100644
--- a/frappe/workflow/doctype/workflow/test_workflow.py
+++ b/frappe/workflow/doctype/workflow/test_workflow.py
@@ -150,32 +150,6 @@ class TestWorkflow(FrappeTestCase):
self.assertEqual(workflow_actions[0].status, "Completed")
frappe.set_user("Administrator")
- def test_update_docstatus(self):
- todo = create_new_todo()
- apply_workflow(todo, "Approve")
-
- self.workflow._update_state_docstatus = True
- self.workflow.states[1].doc_status = 0
- self.workflow.save()
- todo.reload()
- self.assertEqual(todo.docstatus, 0)
- self.workflow.states[1].doc_status = 1
- self.workflow.save()
- todo.reload()
- self.assertEqual(todo.docstatus, 1)
-
- self.workflow.states[1].doc_status = 0
- self.workflow.save()
-
- self.workflow._update_state_docstatus = False
- self.workflow.states[1].doc_status = 1
- self.workflow.save()
- todo.reload()
- self.assertEqual(todo.docstatus, 0)
-
- self.workflow.states[1].doc_status = 0
- self.workflow.save()
-
def test_if_workflow_set_on_action(self):
self.workflow._update_state_docstatus = True
self.workflow.states[1].doc_status = 1
diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py
index 16c440e99b..22c9efed8a 100644
--- a/frappe/workflow/doctype/workflow/workflow.py
+++ b/frappe/workflow/doctype/workflow/workflow.py
@@ -38,7 +38,6 @@ class Workflow(Document):
self.validate_docstatus()
def on_update(self):
- self.update_doc_status()
frappe.clear_cache(doctype=self.document_type)
def create_custom_field_for_workflow_state(self):
@@ -85,34 +84,6 @@ class Workflow(Document):
docstatus_map[d.doc_status] = d.state
- def update_doc_status(self):
- """
- Checks if the docstatus of a state was updated.
- If yes then the docstatus of the document with same state will be updated
- """
-
- if not self.get("_update_state_docstatus"):
- return
-
- doc_before_save = self.get_doc_before_save()
- before_save_states, new_states = {}, {}
- if doc_before_save:
- for d in doc_before_save.states:
- before_save_states[d.state] = d
- for d in self.states:
- new_states[d.state] = d
-
- for key in new_states:
- if key in before_save_states:
- if new_states[key].doc_status != before_save_states[key].doc_status:
- frappe.db.set_value(
- self.document_type,
- {self.workflow_state_field: before_save_states[key].state},
- "docstatus",
- new_states[key].doc_status,
- update_modified=False,
- )
-
def validate_docstatus(self):
def get_state(state):
for s in self.states:
diff --git a/frappe/www/login.py b/frappe/www/login.py
index 7cf89e7c3c..ff9c0d2854 100644
--- a/frappe/www/login.py
+++ b/frappe/www/login.py
@@ -37,6 +37,7 @@ def get_context(context):
context["hide_login"] = True # dont show login link on login page again.
context["provider_logins"] = []
context["disable_signup"] = cint(frappe.get_website_settings("disable_signup"))
+ context["show_footer_on_login"] = cint(frappe.get_website_settings("show_footer_on_login"))
context["disable_user_pass_login"] = cint(frappe.get_system_settings("disable_user_pass_login"))
context["logo"] = frappe.get_website_settings("app_logo") or frappe.get_hooks("app_logo_url")[-1]
context["app_name"] = (
diff --git a/package.json b/package.json
index 5ec8402fe3..83887161f3 100644
--- a/package.json
+++ b/package.json
@@ -20,7 +20,7 @@
},
"homepage": "https://frappeframework.com",
"dependencies": {
- "@editorjs/editorjs": "~2.26.3",
+ "@editorjs/editorjs": "^2.28.2",
"@frappe/esbuild-plugin-postcss2": "^0.1.3",
"@headlessui/vue": "^1.7.16",
"@popperjs/core": "^2.11.2",
@@ -47,7 +47,7 @@
"fast-deep-equal": "^2.0.1",
"fast-glob": "^3.2.5",
"frappe-charts": "2.0.0-rc22",
- "frappe-datatable": "^1.17.9",
+ "frappe-datatable": "1.17.14",
"frappe-gantt": "^0.6.0",
"highlight.js": "^10.4.1",
"html5-qrcode": "^2.3.8",
diff --git a/pyproject.toml b/pyproject.toml
index 2631778a85..228715a35d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -59,6 +59,7 @@ dependencies = [
"rauth~=0.7.3",
"redis~=5.0.1",
"hiredis~=2.2.3",
+ "setproctitle~=1.3.3",
"requests-oauthlib~=1.3.1",
"requests~=2.31.0",
"rq~=1.15.1",
diff --git a/yarn.lock b/yarn.lock
index 98a9877253..fea6ebf608 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -31,21 +31,10 @@
"@babel/helper-validator-identifier" "^7.22.20"
to-fast-properties "^2.0.0"
-"@codexteam/icons@0.1.0":
- version "0.1.0"
- resolved "https://registry.yarnpkg.com/@codexteam/icons/-/icons-0.1.0.tgz#a02885fe8699f69902d05b077b5f1cd48a2ca6b9"
- integrity sha512-jW1fWnwtWzcP4FBGsaodbJY3s1ZaRU+IJy1pvJ7ygNQxkQinybJcwXoyt0a5mWwu/4w30A42EWhCrZn8lp4fdw==
-
-"@editorjs/editorjs@~2.26.3":
- version "2.26.5"
- resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.26.5.tgz#ee0f1dbd3a3c6ba97d3ed30f13ab7d2e7b29dbe4"
- integrity sha512-imwXZi9NmzxKjNosa1xQf286liJYsTe2J2DWCiV5TwKhvYZ1INg5Y+FietcM2v65QmeLqP7wgBUhoI7wiCB+yQ==
- dependencies:
- "@codexteam/icons" "0.1.0"
- codex-notifier "^1.1.2"
- codex-tooltip "^1.0.5"
- html-janitor "^2.0.4"
- nanoid "^3.1.22"
+"@editorjs/editorjs@^2.28.2":
+ version "2.28.2"
+ resolved "https://registry.yarnpkg.com/@editorjs/editorjs/-/editorjs-2.28.2.tgz#a265c7d10e83adef81813e4dc0f01fe3464dff50"
+ integrity sha512-g6V0Nd3W9IIWMpvxDNTssQ6e4kxBp1Y0W4GIf8cXRlmcBp3TUjrgCYJQmNy3l2a6ZzhyBAoVSe8krJEq4g7PQw==
"@esbuild/linux-loong64@0.14.54":
version "0.14.54"
@@ -680,16 +669,6 @@ cluster-key-slot@1.1.2:
resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac"
integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==
-codex-notifier@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/codex-notifier/-/codex-notifier-1.1.2.tgz#a733079185f4c927fa296f1d71eb8753fe080895"
- integrity sha512-DCp6xe/LGueJ1N5sXEwcBc3r3PyVkEEDNWCVigfvywAkeXcZMk9K41a31tkEFBW0Ptlwji6/JlAb49E3Yrxbtg==
-
-codex-tooltip@^1.0.5:
- version "1.0.5"
- resolved "https://registry.yarnpkg.com/codex-tooltip/-/codex-tooltip-1.0.5.tgz#ba25fd5b3a58ba2f73fd667c2b46987ffd1edef2"
- integrity sha512-IuA8LeyLU5p1B+HyhOsqR6oxyFQ11k3i9e9aXw40CrHFTRO2Y1npNBVU3W1SvhKAbUU7R/YikUBdcYFP0RcJag==
-
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@@ -1497,10 +1476,10 @@ frappe-charts@2.0.0-rc22:
resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc22.tgz#9a5a747febdc381a1d4d7af96e89cf519dfba8c0"
integrity sha512-N7f/8979wJCKjusOinaUYfMxB80YnfuVLrSkjpj4LtyqS0BGS6SuJxUnb7Jl4RWUFEIs7zEhideIKnyLeFZF4Q==
-frappe-datatable@^1.17.9:
- version "1.17.9"
- resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.9.tgz#5ef4e5d335079ab5bf2abfecc916e31ecf17a5cb"
- integrity sha512-C1U5YKk7kP32eiHVnv1AdY5LafKKoGrcDpbErqM95PYrhanaq2Uvkvdsjo6yioLpPfnvFD8Vihm4JoGc8FjDcw==
+frappe-datatable@1.17.14:
+ version "1.17.14"
+ resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.14.tgz#5fe99fa45089d6f2a54d13215608ef777bc947ec"
+ integrity sha512-rrUyk+8ueX9ADDXwaHobBGmAWK86lF3P3yc3KYGHyhNiNTwKpUW08zQeuTUzJnWv0OSZ/zXYePzrjFKG7ZR4Wg==
dependencies:
hyperlist "^1.0.0-beta"
lodash "^4.17.5"
@@ -1704,11 +1683,6 @@ homedir-polyfill@^1.0.0, homedir-polyfill@^1.0.1:
dependencies:
parse-passwd "^1.0.0"
-html-janitor@^2.0.4:
- version "2.0.4"
- resolved "https://registry.yarnpkg.com/html-janitor/-/html-janitor-2.0.4.tgz#ae5a115cdf3331cd5501edd7b5471b18ea44cdbb"
- integrity sha512-92J5h9jNZRk30PMHapjHEJfkrBWKCOy0bq3oW2pBungky6lzYSoboBGPMvxl1XRKB2q+kniQmsLsPbdpY7RM2g==
-
html5-qrcode@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/html5-qrcode/-/html5-qrcode-2.3.8.tgz#0b0cdf7a9926cfd4be530e13a51db47592adfa0d"
@@ -2263,7 +2237,7 @@ ms@^2.1.1:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
-nanoid@^3.1.22, nanoid@^3.3.6:
+nanoid@^3.3.6:
version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==