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 ? `
${m.message}
` : ""; @@ -507,7 +506,13 @@ frappe.ui.form.on("Data Import", { }, show_import_log(frm) { + if (!frm.doc.show_failed_logs) { + frm.toggle_display("import_log_preview", false); + return; + } + frm.toggle_display("import_log_section", false); + frm.toggle_display("import_log_preview", true); if (frm.import_in_progress) { return; diff --git a/frappe/core/doctype/data_import/data_import.py b/frappe/core/doctype/data_import/data_import.py index f3dca2d5af..ac28f9091f 100644 --- a/frappe/core/doctype/data_import/data_import.py +++ b/frappe/core/doctype/data_import/data_import.py @@ -241,9 +241,11 @@ def import_file(doctype, file_path, import_type, submit_after_import=False, cons i.import_data() -def import_doc(path, pre_process=None): +def import_doc(path, pre_process=None, sort=False): if os.path.isdir(path): files = [os.path.join(path, f) for f in os.listdir(path)] + if sort: + files.sort() else: files = [path] diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f0f7f96bfc..3c3008ecd6 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -234,6 +234,7 @@ class DocType(Document): "DocPerm", "Custom Field", "Customize Form Field", + "Web Form Field", "DocField", ] @@ -593,7 +594,7 @@ class DocType(Document): if not self.has_value_changed("has_web_view"): return - despaced_name = self.name.replace(" ", "_") + despaced_name = self.name.replace(" ", "") scrubbed_name = frappe.scrub(self.name) scrubbed_module = frappe.scrub(self.module) controller_path = frappe.get_module_path( diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index f6c0b1defa..de4375ae6c 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -778,11 +778,11 @@ def on_doctype_update(): frappe.db.add_index("File", ["attached_to_doctype", "attached_to_name"]) -def has_permission(doc, ptype=None, user=None): +def has_permission(doc, ptype=None, user=None, debug=False): user = user or frappe.session.user if ptype == "create": - return frappe.has_permission("File", "create", user=user) + return frappe.has_permission("File", "create", user=user, debug=debug) if not doc.is_private or (user != "Guest" and doc.owner == user) or user == "Administrator": return True @@ -798,9 +798,9 @@ def has_permission(doc, ptype=None, user=None): return False if ptype in ["write", "create", "delete"]: - return ref_doc.has_permission("write") + return ref_doc.has_permission("write", debug=debug, user=user) else: - return ref_doc.has_permission("read") + return ref_doc.has_permission("read", debug=debug, user=user) return False diff --git a/frappe/core/doctype/page/test_page.py b/frappe/core/doctype/page/test_page.py index 61e7ed99a0..d8cf90b8d9 100644 --- a/frappe/core/doctype/page/test_page.py +++ b/frappe/core/doctype/page/test_page.py @@ -16,10 +16,6 @@ class TestPage(FrappeTestCase): frappe.NameError, frappe.get_doc(dict(doctype="Page", page_name="DocType", module="Core")).insert, ) - self.assertRaises( - frappe.NameError, - frappe.get_doc(dict(doctype="Page", page_name="Settings", module="Core")).insert, - ) @unittest.skipUnless( os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" diff --git a/frappe/core/doctype/file/file_list.js b/frappe/core/doctype/permission_debugger/__init__.py similarity index 100% rename from frappe/core/doctype/file/file_list.js rename to frappe/core/doctype/permission_debugger/__init__.py diff --git a/frappe/core/doctype/permission_debugger/permission_debugger.js b/frappe/core/doctype/permission_debugger/permission_debugger.js new file mode 100644 index 0000000000..c04c3b3e67 --- /dev/null +++ b/frappe/core/doctype/permission_debugger/permission_debugger.js @@ -0,0 +1,24 @@ +// Copyright (c) 2024, Frappe Technologies and contributors +// For license information, please see license.txt + +const call_debug = (frm) => { + frm.trigger("debug"); +}; + +frappe.ui.form.on("Permission Debugger", { + refresh(frm) { + frm.disable_save(); + }, + docname: call_debug, + ref_doctype(frm) { + frm.doc.docname = ""; // Usually doctype change invalidates docname + call_debug(frm); + }, + user: call_debug, + permission_type: call_debug, + debug(frm) { + if (frm.doc.ref_doctype && frm.doc.user) { + frm.call("debug"); + } + }, +}); diff --git a/frappe/core/doctype/permission_debugger/permission_debugger.json b/frappe/core/doctype/permission_debugger/permission_debugger.json new file mode 100644 index 0000000000..aff0ba5ebb --- /dev/null +++ b/frappe/core/doctype/permission_debugger/permission_debugger.json @@ -0,0 +1,90 @@ +{ + "actions": [], + "allow_rename": 1, + "beta": 1, + "creation": "2024-01-03 17:43:27.257317", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "ref_doctype", + "column_break_mcqo", + "docname", + "column_break_xbrd", + "user", + "column_break_nvaa", + "permission_type", + "section_break_hkjp", + "output" + ], + "fields": [ + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "docname", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Document", + "options": "ref_doctype" + }, + { + "fieldname": "column_break_mcqo", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_xbrd", + "fieldtype": "Column Break" + }, + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "reqd": 1 + }, + { + "fieldname": "section_break_hkjp", + "fieldtype": "Section Break" + }, + { + "fieldname": "output", + "fieldtype": "Code", + "label": "Output", + "read_only": 1 + }, + { + "fieldname": "column_break_nvaa", + "fieldtype": "Column Break" + }, + { + "fieldname": "permission_type", + "fieldtype": "Select", + "label": "Permission Type", + "options": "read\nwrite\ncreate\ndelete\nsubmit\ncancel\nselect\namend\nprint\nemail\nreport\nimport\nexport\nshare" + } + ], + "index_web_pages_for_search": 1, + "is_virtual": 1, + "issingle": 1, + "links": [], + "modified": "2024-01-10 14:17:49.722593", + "modified_by": "Administrator", + "module": "Core", + "name": "Permission Debugger", + "owner": "Administrator", + "permissions": [ + { + "read": 1, + "role": "System Manager", + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/permission_debugger/permission_debugger.py b/frappe/core/doctype/permission_debugger/permission_debugger.py new file mode 100644 index 0000000000..f548094707 --- /dev/null +++ b/frappe/core/doctype/permission_debugger/permission_debugger.py @@ -0,0 +1,75 @@ +# Copyright (c) 2024, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.permissions import _pop_debug_log, has_permission + + +class PermissionDebugger(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + docname: DF.DynamicLink | None + output: DF.Code | None + permission_type: DF.Literal[ + "read", + "write", + "create", + "delete", + "submit", + "cancel", + "select", + "amend", + "print", + "email", + "report", + "import", + "export", + "share", + ] + ref_doctype: DF.Link + user: DF.Link + # end: auto-generated types + + @frappe.whitelist() + def debug(self): + if not (self.ref_doctype and self.user): + return + + result = has_permission( + self.ref_doctype, ptype=self.permission_type, doc=self.docname, user=self.user, debug=True + ) + + self.output = "\n==============================\n".join(_pop_debug_log()) + self.output += "\n\n" + f"Ouput of has_permission: {result}" + + # None of these apply, overriden for sanity. + def load_from_db(self): + super(Document, self).__init__({"modified": None, "permission_type": "read"}) + + def db_insert(self, *args, **kwargs): + ... + + def db_update(self): + ... + + @staticmethod + def get_list(args): + ... + + @staticmethod + def get_count(args): + ... + + @staticmethod + def get_stats(args): + ... + + def delete(self): + ... diff --git a/frappe/core/doctype/permission_debugger/test_permission_debugger.py b/frappe/core/doctype/permission_debugger/test_permission_debugger.py new file mode 100644 index 0000000000..d66a26302a --- /dev/null +++ b/frappe/core/doctype/permission_debugger/test_permission_debugger.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestPermissionDebugger(FrappeTestCase): + pass diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index 48e10abdbd..8710e35f64 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -113,7 +113,7 @@ def generate_report(prepared_report): instance.status = "Completed" except Exception: instance.status = "Error" - instance.error_message = frappe.get_traceback() + instance.error_message = frappe.get_traceback(with_context=True) instance.report_end_time = frappe.utils.now() instance.save(ignore_permissions=True) diff --git a/frappe/core/doctype/recorder/recorder.json b/frappe/core/doctype/recorder/recorder.json index aa0d782811..391f808f31 100644 --- a/frappe/core/doctype/recorder/recorder.json +++ b/frappe/core/doctype/recorder/recorder.json @@ -14,6 +14,7 @@ "cmd", "time", "duration", + "event_type", "section_break_1skt", "request_headers", "section_break_sgro", @@ -30,6 +31,7 @@ "label": "Path" }, { + "depends_on": "eval:doc.event_type==\"HTTP Request\"", "fieldname": "cmd", "fieldtype": "Data", "in_standard_filter": 1, @@ -67,6 +69,7 @@ "fieldtype": "Section Break" }, { + "depends_on": "eval:doc.event_type==\"HTTP Request\"", "fieldname": "request_headers", "fieldtype": "Code", "label": "Request Headers" @@ -76,11 +79,13 @@ "fieldtype": "Section Break" }, { + "depends_on": "eval:doc.event_type==\"HTTP Request\"", "fieldname": "form_dict", "fieldtype": "Code", "label": "Form Dict" }, { + "depends_on": "eval:doc.event_type==\"HTTP Request\"", "fieldname": "method", "fieldtype": "Select", "in_standard_filter": 1, @@ -96,6 +101,12 @@ { "fieldname": "section_break_9jhm", "fieldtype": "Section Break" + }, + { + "fieldname": "event_type", + "fieldtype": "Data", + "hidden": 1, + "label": "Event Type" } ], "hide_toolbar": 1, @@ -103,7 +114,7 @@ "index_web_pages_for_search": 1, "is_virtual": 1, "links": [], - "modified": "2023-08-10 12:01:03.456643", + "modified": "2024-01-03 16:45:47.110048", "modified_by": "Administrator", "module": "Core", "name": "Recorder", diff --git a/frappe/core/doctype/recorder/recorder.py b/frappe/core/doctype/recorder/recorder.py index f5ef909a2a..347a237743 100644 --- a/frappe/core/doctype/recorder/recorder.py +++ b/frappe/core/doctype/recorder/recorder.py @@ -19,6 +19,7 @@ class Recorder(Document): cmd: DF.Data | None duration: DF.Float + event_type: DF.Data | None form_dict: DF.Code | None method: DF.Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] number_of_queries: DF.Int @@ -27,7 +28,6 @@ class Recorder(Document): sql_queries: DF.Table[RecorderQuery] time: DF.Datetime | None time_in_queries: DF.Float - # end: auto-generated types def load_from_db(self): diff --git a/frappe/core/doctype/report/report.json b/frappe/core/doctype/report/report.json index 1761b0d574..353e44c88b 100644 --- a/frappe/core/doctype/report/report.json +++ b/frappe/core/doctype/report/report.json @@ -108,14 +108,16 @@ "depends_on": "eval:doc.report_type==\"Query Report\"", "fieldname": "query", "fieldtype": "Code", - "label": "Query" + "label": "Query", + "options": "SQL" }, { "depends_on": "eval:doc.report_type==\"Script Report\" && doc.is_standard===\"No\"", "description": "JavaScript Format: frappe.query_reports['REPORTNAME'] = {}", "fieldname": "javascript", "fieldtype": "Code", - "label": "Javascript" + "label": "Javascript", + "options": "Javascript" }, { "depends_on": "eval:doc.report_type==\"Report Builder\" || \"Custom Report\"", @@ -141,11 +143,12 @@ "label": "Prepared Report" }, { - "depends_on": "eval:(doc.report_type===\"Script Report\" \n|| doc.report_type==\"Query Report\") \n&& doc.is_standard===\"No\"", + "depends_on": "eval:doc.report_type===\"Script Report\" && doc.is_standard===\"No\"", "description": "Filters will be accessible via filters.

Send output as 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 = $('
').appendTo( @@ -105,7 +105,7 @@ frappe.ui.form.on("User", { } if ( - in_list(["System User", "Website User"], frm.doc.user_type) && + ["System User", "Website User"].includes(frm.doc.user_type) && !frm.is_new() && !frm.roles_editor && frm.can_edit_roles diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 1a13a20e4e..f727d02120 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1052,7 +1052,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters): user_type_condition = "and user_type != 'Website User'" if filters and filters.get("ignore_user_type") and frappe.session.data.user_type == "System User": user_type_condition = "" - filters.pop("ignore_user_type") + filters and filters.pop("ignore_user_type", None) txt = f"%{txt}%" return frappe.db.sql( diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index f06e583bee..f25ec6d4ad 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -251,7 +251,7 @@ frappe.PermissionEngine = class PermissionEngine { this.rights.forEach((r) => { if (!d.is_submittable && ["submit", "cancel", "amend"].includes(r)) return; - if (d.in_create && ["create", "write", "delete"].includes(r)) return; + if (d.in_create && ["create", "delete"].includes(r)) return; this.add_check(perm_container, d, r); }); diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json index 81d86c346f..ad6cd2d287 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"id\":\"5nnLaQeoFa\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"HXRmktXYHy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"id\":\"pYALX3MwBW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"id\":\"XC78DuYB65\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"id\":\"XPm50Ppq3J\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"id\":\"yoU6nWiT83\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"id\":\"5UgFESBY0N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Format Builder\",\"col\":3}},{\"id\":\"0gE0s-S70E\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Frappe Framework\",\"col\":3}},{\"id\":\"62hseENHbd\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tOCrOgLW1G\",\"type\":\"header\",\"data\":{\"text\":\"Elements\",\"col\":12}},{\"id\":\"BIHjudL0T_\",\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"id\":\"cJ6CVsa8qW\",\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"id\":\"MmEJpjEdGR\",\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"id\":\"2ZdtgxQZqq\",\"type\":\"card\",\"data\":{\"card_name\":\"Customization\",\"col\":4}},{\"id\":\"NPFolijIcb\",\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"id\":\"iK3JQ9RXJE\",\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"id\":\"TiO9FCUUeC\",\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]", + "content": "[{\"id\":\"5nnLaQeoFa\",\"type\":\"header\",\"data\":{\"text\":\"Get started
\",\"col\":12}},{\"id\":\"HXRmktXYHy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"id\":\"pYALX3MwBW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"id\":\"XC78DuYB65\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"id\":\"XPm50Ppq3J\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"id\":\"yoU6nWiT83\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"id\":\"5UgFESBY0N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Format Builder\",\"col\":3}},{\"id\":\"0gE0s-S70E\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"62hseENHbd\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tOCrOgLW1G\",\"type\":\"header\",\"data\":{\"text\":\"Components to build your app\",\"col\":12}},{\"id\":\"cJ6CVsa8qW\",\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"id\":\"MmEJpjEdGR\",\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"id\":\"2ZdtgxQZqq\",\"type\":\"card\",\"data\":{\"card_name\":\"Customization\",\"col\":4}},{\"id\":\"NPFolijIcb\",\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"id\":\"BIHjudL0T_\",\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"id\":\"iK3JQ9RXJE\",\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"id\":\"TiO9FCUUeC\",\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]", "creation": "2021-01-02 10:51:16.579957", "custom_blocks": [], "docstatus": 0, @@ -8,7 +8,7 @@ "for_user": "", "hide_custom": 0, "icon": "tool", - "idx": 0, + "idx": 1, "is_hidden": 0, "label": "Build", "links": [ @@ -115,46 +115,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Modules", - "link_count": 3, - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Module Def", - "link_count": 0, - "link_to": "Module Def", - "link_type": "DocType", - "onboard": 0, - "only_for": "", - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Module Onboarding", - "link_count": 0, - "link_to": "Module Onboarding", - "link_type": "DocType", - "onboard": 0, - "only_for": "", - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Module Profile", - "link_count": 0, - "link_to": "Module Profile", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -321,9 +281,40 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Modules", + "link_count": 2, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Def", + "link_count": 0, + "link_to": "Module Def", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Onboarding", + "link_count": 0, + "link_to": "Module Onboarding", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" } ], - "modified": "2023-07-04 14:34:09.420325", + "modified": "2024-01-02 15:38:42.806824", "modified_by": "Administrator", "module": "Core", "name": "Build", @@ -346,8 +337,9 @@ { "color": "Grey", "doc_view": "List", - "label": "Learn Frappe Framework", - "type": "URL", + "label": "System Settings", + "link_to": "System Settings", + "type": "DocType", "url": "https://frappe.school/courses/frappe-framework-course?utm_source=in_app" }, { diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json deleted file mode 100644 index 24e534ce19..0000000000 --- a/frappe/core/workspace/settings/settings.json +++ /dev/null @@ -1,383 +0,0 @@ -{ - "charts": [], - "content": "[{\"id\":\"bc3WecV0uU\",\"type\":\"header\",\"data\":{\"text\":\"Settings\",\"col\":12}},{\"id\":\"_6Jxax2I11\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"rbf1Om8zJG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"xMytWpIImZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"id\":\"Q9DPlmrPpX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oVwctUh0gf\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"hC0b24aSJG\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"JA_iI4Z0yI\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"F1GxSqFKy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"vugObM_K_T\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"XwKthiuAAW\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"EQY7Sfmdxn\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]", - "creation": "2020-03-02 15:09:40.527211", - "custom_blocks": [], - "docstatus": 0, - "doctype": "Workspace", - "for_user": "", - "hide_custom": 0, - "icon": "setting", - "idx": 0, - "is_hidden": 0, - "label": "Settings", - "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Data", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Import Data", - "link_count": 0, - "link_to": "Data Import", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Export Data", - "link_count": 0, - "link_to": "Data Export", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Bulk Update", - "link_count": 0, - "link_to": "Bulk Update", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Download Backups", - "link_count": 0, - "link_to": "backups", - "link_type": "Page", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Deleted Documents", - "link_count": 0, - "link_to": "Deleted Document", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Email / Notifications", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Account", - "link_count": 0, - "link_to": "Email Account", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Domain", - "link_count": 0, - "link_to": "Email Domain", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Notification", - "link_count": 0, - "link_to": "Notification", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Email Template", - "link_count": 0, - "link_to": "Email Template", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "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" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Newsletter", - "link_count": 0, - "link_to": "Newsletter", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Notification Settings", - "link_count": 0, - "link_to": "Notification Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Website", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Website Settings", - "link_count": 0, - "link_to": "Website Settings", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Website Theme", - "link_count": 0, - "link_to": "Website Theme", - "link_type": "DocType", - "onboard": 1, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Website Script", - "link_count": 0, - "link_to": "Website Script", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "About Us Settings", - "link_count": 0, - "link_to": "About Us Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Contact Us Settings", - "link_count": 0, - "link_to": "Contact Us Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Printing", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Format Builder", - "link_count": 0, - "link_to": "print-format-builder", - "link_type": "Page", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Settings", - "link_count": 0, - "link_to": "Print Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Format", - "link_count": 0, - "link_to": "Print Format", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Print Style", - "link_count": 0, - "link_to": "Print Style", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Workflow", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow", - "link_count": 0, - "link_to": "Workflow", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow State", - "link_count": 0, - "link_to": "Workflow State", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Workflow Action", - "link_count": 0, - "link_to": "Workflow Action", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Core", - "link_count": 2, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "System Settings", - "link_count": 0, - "link_to": "System Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Domain Settings", - "link_count": 0, - "link_to": "Domain Settings", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - } - ], - "modified": "2023-05-24 14:58:44.010999", - "modified_by": "Administrator", - "module": "Core", - "name": "Settings", - "number_cards": [], - "owner": "Administrator", - "parent_page": "", - "public": 1, - "quick_lists": [], - "restrict_to_domain": "", - "roles": [], - "sequence_id": 18.0, - "shortcuts": [ - { - "icon": "setting", - "label": "System Settings", - "link_to": "System Settings", - "type": "DocType" - }, - { - "icon": "printer", - "label": "Print Settings", - "link_to": "Print Settings", - "type": "DocType" - }, - { - "icon": "website", - "label": "Website Settings", - "link_to": "Website Settings", - "type": "DocType" - } - ], - "title": "Settings" -} \ No newline at end of file diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json index 53ba10c0f9..3899ad534b 100644 --- a/frappe/core/workspace/users/users.json +++ b/frappe/core/workspace/users/users.json @@ -1,6 +1,6 @@ { "charts": [], - "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]", + "content": "[{\"id\":\"YpGCeLfign\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"id\":\"b7abeqw4NZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"id\":\"eghSJPhZRC\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"id\":\"uAzl_lT_C0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"id\":\"EpBz2lplSt\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"id\":\"vHWhzaFoAH\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"id\":\"oFB4l28FMU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yJNNylguxk\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"id\":\"NMpIkExl3i\",\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"id\":\"VepG3durKm\",\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"id\":\"S9FeWt7xXE\",\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]", "creation": "2020-03-02 15:12:16.754449", "custom_blocks": [], "docstatus": 0, @@ -12,47 +12,6 @@ "is_hidden": 0, "label": "Users", "links": [ - { - "hidden": 0, - "is_query_report": 0, - "label": "Users", - "link_count": 0, - "onboard": 0, - "type": "Card Break" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "User", - "link_count": 0, - "link_to": "User", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Role", - "link_count": 0, - "link_to": "Role", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "dependencies": "", - "hidden": 0, - "is_query_report": 0, - "label": "Role Profile", - "link_count": 0, - "link_to": "Role Profile", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -145,9 +104,61 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Users", + "link_count": 4, + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "User", + "link_count": 0, + "link_to": "User", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role", + "link_count": 0, + "link_to": "Role", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Role Profile", + "link_count": 0, + "link_to": "Role Profile", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Profile", + "link_count": 0, + "link_to": "Module Profile", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2023-05-24 14:47:23.619182", + "modified": "2024-01-02 15:39:13.811700", "modified_by": "Administrator", "module": "Core", "name": "Users", diff --git a/frappe/custom/doctype/custom_field/custom_field.js b/frappe/custom/doctype/custom_field/custom_field.js index 6e3ab9a249..38c234a507 100644 --- a/frappe/custom/doctype/custom_field/custom_field.js +++ b/frappe/custom/doctype/custom_field/custom_field.js @@ -67,7 +67,7 @@ frappe.ui.form.on("Custom Field", { return v.value; }); - if (insert_after == null || !in_list(fieldnames, insert_after)) { + if (insert_after == null || !fieldnames.includes(insert_after)) { insert_after = fieldnames[-1]; } diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index e84e0dd712..ce86da5e91 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -362,7 +362,8 @@ def rename_fieldname(custom_field: str, fieldname: str): frappe.msgprint(_("Old and new fieldnames are same."), alert=True) return - frappe.db.rename_column(parent_doctype, old_fieldname, new_fieldname) + if frappe.db.has_column(field.dt, old_fieldname): + frappe.db.rename_column(parent_doctype, old_fieldname, new_fieldname) # Update in DB after alter column is successful, alter column will implicitly commit, so it's # best to commit change on field too to avoid any possible mismatch between two. diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index e2fb630af3..92765653df 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -81,7 +81,7 @@ frappe.ui.form.on("Customize Form", { add_customize_child_table_button: function (frm) { frm.doc.fields.forEach(function (f) { - if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return; + if (!["Table", "Table MultiSelect"].includes(f.fieldtype)) return; frm.add_custom_button( __(f.options), diff --git a/frappe/custom/doctype/property_setter/property_setter.js b/frappe/custom/doctype/property_setter/property_setter.js index 955e01c33e..d8df5d1ec0 100644 --- a/frappe/custom/doctype/property_setter/property_setter.js +++ b/frappe/custom/doctype/property_setter/property_setter.js @@ -3,7 +3,7 @@ frappe.ui.form.on("Property Setter", { validate: function (frm) { - if (frm.doc.property_type == "Check" && !in_list(["0", "1"], frm.doc.value)) { + if (frm.doc.property_type == "Check" && !["0", "1"].includes(frm.doc.value)) { frappe.throw(__("Value for a check field can be either 0 or 1")); } }, diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index 01c18d69c4..4e1404084e 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -57,7 +57,7 @@ class DbManager: from frappe.database import get_command from frappe.utils import execute_in_shell - command = [] + command = ["set -o pipefail;"] if source.endswith(".gz"): if gzip := which("gzip"): diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index f5f3b14006..5118a38509 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -1,7 +1,9 @@ import os +import re import frappe from frappe.database.db_manager import DbManager +from frappe.utils import cint def setup_database(): @@ -13,6 +15,11 @@ def setup_database(): root_conn.sql(f"CREATE DATABASE `{frappe.conf.db_name}`") root_conn.sql(f"CREATE user {frappe.conf.db_name} password '{frappe.conf.db_password}'") root_conn.sql("GRANT ALL PRIVILEGES ON DATABASE `{0}` TO {0}".format(frappe.conf.db_name)) + if psql_version := root_conn.sql("SELECT VERSION()", as_dict=True): + version_string = psql_version[0].get("version") or "PostgreSQL 14" + major_version = cint(re.split(r"[\w\.]", version_string)[1]) + if major_version > 15: + root_conn.sql("ALTER DATABASE `{0}` OWNER TO {0}".format(frappe.conf.db_name)) root_conn.close() diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index a7c9a5ef0c..742bb15176 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -565,7 +565,7 @@ def save_new_widget(doc, page, blocks, new_widgets): page, json_config, e ) doc.log_error("Could not save customization", log) - return False + raise return True diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 2385860115..8e008e30c6 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -109,7 +109,7 @@ def get( refresh=None, ): if chart_name: - chart = frappe.get_doc("Dashboard Chart", chart_name) + chart: DashboardChart = frappe.get_doc("Dashboard Chart", chart_name) else: chart = frappe._dict(frappe.parse_json(chart)) @@ -207,13 +207,14 @@ def get_chart_config(chart, filters, timespan, timegrain, from_date, to_date): filters.append([doctype, datefield, ">=", from_date, False]) filters.append([doctype, datefield, "<=", to_date, False]) - data = frappe.db.get_list( + data = frappe.get_list( doctype, fields=[datefield, f"SUM({value_field})", "COUNT(*)"], filters=filters, group_by=datefield, order_by=datefield, as_list=True, + parent_doctype=chart.parent_document_type, ) result = get_result(data, timegrain, from_date, to_date, chart.chart_type) diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 9fc12a1e0d..eec086f3bb 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -10,7 +10,7 @@ from frappe.model.naming import append_number_if_name_exists from frappe.modules.export_file import export_to_files from frappe.query_builder import Criterion from frappe.query_builder.utils import DocType -from frappe.utils import cint +from frappe.utils import cint, flt class NumberCard(Document): @@ -165,7 +165,7 @@ def get_result(doc, filters, to_date=None): ) number = res[0]["result"] if res else 0 - return cint(number) + return flt(number) @frappe.whitelist() diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js index e04ab59fdb..c43f95ee19 100644 --- a/frappe/desk/doctype/system_console/system_console.js +++ b/frappe/desk/doctype/system_console/system_console.js @@ -41,6 +41,10 @@ frappe.ui.form.on("System Console", { frm.get_field("sql_output").html(""); } } + + const field = frm.get_field("console"); + field.df.options = frm.doc.type; + field.set_language(); }, render_sql_output: function (frm) { diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index f71afef6da..0ebfb3b9c4 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -109,7 +109,7 @@ class DocTags: tags = "" else: tl = unique(filter(lambda x: x, tl)) - tags = "," + ",".join(tl) + tags = ",".join(tl) try: frappe.db.sql( "update `tab{}` set _user_tags={} where name={}".format(self.dt, "%s", "%s"), (tags, dn) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 758681b0dc..cd0bb949ca 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -277,7 +277,6 @@ def save_page(title, public, new_widgets, blocks): doc = frappe.get_doc("Workspace", pages[0]) doc.content = blocks - doc.save(ignore_permissions=True) save_new_widget(doc, title, blocks, new_widgets) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 40497030a9..88b0b368ef 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -8,7 +8,7 @@ from frappe.build import scrub_html_template from frappe.model.meta import Meta from frappe.model.utils import render_include from frappe.modules import get_module_path, load_doctype_module, scrub -from frappe.utils import get_html_format +from frappe.utils import get_bench_path, get_html_format from frappe.utils.data import get_link_to_form ASSET_KEYS = ( @@ -120,7 +120,9 @@ class FormMeta(Meta): def _add_code(self, path, fieldname): js = get_js(path) if js: - comment = f"\n\n/* Adding {path} */\n\n" + bench_path = get_bench_path() + "/" + asset_path = path.replace(bench_path, "") + comment = f"\n\n/* Adding {asset_path} */\n\n" sourceURL = f"\n\n//# sourceURL={scrub(self.name) + fieldname}" self.set(fieldname, (self.get(fieldname) or "") + comment + js + sourceURL) diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 75865f6ae4..44960915b1 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -350,7 +350,7 @@ frappe.setup.SetupWizardSlide = class SetupWizardSlide extends frappe.ui.Slide { let me = this; this.fields.filter(frappe.model.is_value_type).forEach((field) => { field.fieldname && - me.get_input(field.fieldname)?.on("change", function () { + me.get_input(field.fieldname)?.on?.("change", function () { frappe.telemetry.capture(`${field.fieldname}_set`, "setup"); if ( field.fieldname == "enable_telemetry" && diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index dbad6f718d..22aa4245d8 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -157,7 +157,7 @@ def get_setup_complete_hooks(args): def handle_setup_exception(args): frappe.db.rollback() if args: - traceback = frappe.get_traceback() + traceback = frappe.get_traceback(with_context=True) print(traceback) for hook in frappe.get_hooks("setup_wizard_exception"): frappe.get_attr(hook)(traceback, args) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index dcce6f3850..b74582edc8 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -36,13 +36,17 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): @frappe.whitelist() -def get_children(doctype, parent="", **filters): - return _get_children(doctype, parent) +def get_children(doctype, parent="", include_disabled=False, **filters): + if isinstance(include_disabled, str): + include_disabled = frappe.sbool(include_disabled) + return _get_children(doctype, parent, include_disabled=include_disabled) -def _get_children(doctype, parent="", ignore_permissions=False): +def _get_children(doctype, parent="", ignore_permissions=False, include_disabled=False): parent_field = "parent_" + doctype.lower().replace(" ", "_") filters = [[f"ifnull(`{parent_field}`,'')", "=", parent], ["docstatus", "<", 2]] + if frappe.db.has_column(doctype, "disabled") and not include_disabled: + filters.append(["disabled", "=", False]) meta = frappe.get_meta(doctype) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 2f4c23eac6..0fa328b42d 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -268,15 +268,15 @@ class EmailAccount(Document): if not in_receive and self.use_imap: email_server.imap.logout() - # reset failed attempts count - self.set_failed_attempts_count(0) - return email_server def check_email_server_connection(self, email_server, in_receive): # tries to connect to email server and handles failure try: email_server.connect() + + # reset failed attempts count - do it after succesful connection + self.set_failed_attempts_count(0) except (error_proto, imaplib.IMAP4.error) as e: message = cstr(e).lower().replace(" ", "") auth_error_codes = [ @@ -294,6 +294,8 @@ class EmailAccount(Document): error_message = _( "Authentication failed while receiving emails from Email Account: {0}." ).format(self.name) + + error_message = _("Email Account Disabled.") + " " + error_message error_message += "
" + _("Message from server: {0}").format(cstr(e)) self.handle_incoming_connect_error(description=error_message) return None @@ -489,31 +491,35 @@ class EmailAccount(Document): state.pop("_smtp_server_instance", None) def handle_incoming_connect_error(self, description): - if self.get_failed_attempts_count() > 2: - self.db_set("enable_incoming", 0) - - for user in get_system_managers(only_name=True): - try: - assign_to.add( - { - "assign_to": user, - "doctype": self.doctype, - "name": self.name, - "description": description, - "priority": "High", - "notify": 1, - } - ) - except assign_to.DuplicateToDoError: - frappe.clear_last_message() + if self.get_failed_attempts_count() > 5: + # This is done in background to avoid committing here. + frappe.enqueue(self._disable_broken_incoming_account, description=description) else: self.set_failed_attempts_count(self.get_failed_attempts_count() + 1) + def _disable_broken_incoming_account(self, description): + self.db_set("enable_incoming", 0) + + for user in get_system_managers(only_name=True): + try: + assign_to.add( + { + "assign_to": [user], + "doctype": self.doctype, + "name": self.name, + "description": description, + "priority": "High", + "notify": 1, + } + ) + except assign_to.DuplicateToDoError: + pass + def set_failed_attempts_count(self, value): - frappe.cache.set(f"{self.name}:email-account-failed-attempts", value) + frappe.cache.set_value(f"{self.name}:email-account-failed-attempts", value) def get_failed_attempts_count(self): - return cint(frappe.cache.get(f"{self.name}:email-account-failed-attempts")) + return cint(frappe.cache.get_value(f"{self.name}:email-account-failed-attempts")) def receive(self): """Called by scheduler to receive emails from this EMail account using POP3/IMAP.""" diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js index 5b1a831759..385b31dfab 100644 --- a/frappe/email/doctype/newsletter/newsletter.js +++ b/frappe/email/doctype/newsletter/newsletter.js @@ -4,7 +4,7 @@ frappe.ui.form.on("Newsletter", { refresh(frm) { let doc = frm.doc; - let can_write = in_list(frappe.boot.user.can_write, doc.doctype); + let can_write = frappe.boot.user.can_write.includes(doc.doctype); if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) { frm.add_custom_button( __("Send a test email"), diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json index 7ac6203ada..e7c902697f 100644 --- a/frappe/email/doctype/newsletter/newsletter.json +++ b/frappe/email/doctype/newsletter/newsletter.json @@ -1,5 +1,6 @@ { "actions": [], + "allow_guest_to_view": 1, "allow_rename": 1, "creation": "2013-01-10 16:34:31", "description": "Create and Send Newsletters", @@ -253,7 +254,7 @@ "index_web_pages_for_search": 1, "is_published_field": "published", "links": [], - "modified": "2023-03-20 22:45:59.129630", + "modified": "2023-12-29 18:04:13.270523", "modified_by": "Administrator", "module": "Email", "name": "Newsletter", diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index 5be70b14b0..127f803110 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -34,7 +34,7 @@ frappe.notification = { let fields = frappe.get_doc("DocType", frm.doc.document_type).fields; let options = $.map(fields, function (d) { - return in_list(frappe.model.no_value_type, d.fieldtype) + return frappe.model.no_value_type.includes(d.fieldtype) ? null : get_select_options(d); }); @@ -66,7 +66,7 @@ frappe.notification = { : null; } }); - } else if (in_list(["WhatsApp", "SMS"], frm.doc.channel)) { + } else if (["WhatsApp", "SMS"].includes(frm.doc.channel)) { receiver_fields = $.map(fields, function (d) { return d.options == "Phone" ? get_select_options(d) : null; }); @@ -102,7 +102,7 @@ Last comment: {{ comments[-1].comment }} by {{ comments[-1].by }} </ul> `; - } else if (in_list(["Slack", "System Notification", "SMS"], frm.doc.channel)) { + } else if (["Slack", "System Notification", "SMS"].includes(frm.doc.channel)) { template = `
Message Example
*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 - ? `

- +
` ); // 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 %} - + {% 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==