From 7464b91bed46e690853d4837f95f87c7bbe30d53 Mon Sep 17 00:00:00 2001 From: fadilsid Date: Mon, 20 Nov 2023 03:01:53 +0000 Subject: [PATCH 001/134] fix: numbercard layout --- frappe/public/scss/desk/desktop.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 8de66e43d5..58a216fcff 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -133,7 +133,7 @@ body { } .widget-head { - @include flex(flex, flex-start, center, null); + @include flex(flex, space-between, center, null); .widget-label { min-width: 0px; From 6babd025423719e5d5a5e17ef43489fad51ce57a Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 4 Dec 2023 13:07:35 +0530 Subject: [PATCH 002/134] feat: add include disabled check --- frappe/public/js/frappe/views/treeview.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index 22810e8e35..0096a333bd 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -106,6 +106,17 @@ frappe.views.TreeView = class TreeView { $(this.page[0]).addClass("frappe-card"); } + if (frappe.meta.has_field(me.doctype, "disabled")) { + $( + "
" + ).appendTo(this.page.inner_toolbar); + this.page.inner_toolbar + .addClass("flex align-center") + .on("click", "input[type='checkbox']", function () { + me.rebuild_tree(); + }); + } + if (this.opts.show_expand_all) { this.page.add_inner_button(__("Collapse All"), function () { me.tree.load_children(me.tree.root_node, false); @@ -189,6 +200,9 @@ frappe.views.TreeView = class TreeView { if (use_value == null) { use_value = use_label; } + this.args["include_disabled"] = this.page.inner_toolbar + .find("input[type='checkbox']") + .prop("checked"); this.tree = new frappe.ui.Tree({ parent: this.body, label: use_label, From f8d4014a0024d27137034a9ca65d432ef2b72ed3 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 4 Dec 2023 13:08:33 +0530 Subject: [PATCH 003/134] feat: add disabled filter in get_children --- frappe/desk/treeview.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index f8b2a67c82..9bc195fa9b 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -37,13 +37,15 @@ 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): + return _get_children(doctype, parent, include_disabled) -def _get_children(doctype, parent="", ignore_permissions=False): +def _get_children(doctype, include_disabled, parent="", ignore_permissions=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) From 76a200028ed25fec99e7f28ca81673015e0a4150 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Mon, 4 Dec 2023 19:00:24 +0530 Subject: [PATCH 004/134] fix: make internal parameter optional --- frappe/desk/treeview.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 9bc195fa9b..4a77c6560b 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -38,10 +38,12 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): @frappe.whitelist() def get_children(doctype, parent="", include_disabled=False, **filters): - return _get_children(doctype, parent, include_disabled) + if isinstance(include_disabled, str): + include_disabled = bool(include_disabled) + return _get_children(doctype, parent, include_disabled=include_disabled) -def _get_children(doctype, include_disabled, 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: From 796e958910724dafe904827c13b1029d5f3c5a03 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Fri, 15 Dec 2023 15:00:36 +0530 Subject: [PATCH 005/134] test: treeview for disabled records --- frappe/tests/test_nestedset.py | 41 ++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/frappe/tests/test_nestedset.py b/frappe/tests/test_nestedset.py index 340b53bf38..53304a5bb3 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 @@ -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) From c8d06c4ad56c013a1fb6948c69377ddb0127f37e Mon Sep 17 00:00:00 2001 From: "avazquez@ctgalega.com" Date: Sun, 17 Dec 2023 20:03:10 +0100 Subject: [PATCH 006/134] fix: Consider allDay on Calendar View --- frappe/public/js/frappe/views/calendar/calendar.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 4b05cdd1ed..a7e4dad874 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -377,8 +377,11 @@ frappe.views.Calendar = class Calendar { $.each(me.field_map, function (target, source) { d[target] = d[source]; }); + + if (typeof(d.allDay) === "undefined") { + d.allDay = me.field_map.allDay + } - if (!me.field_map.allDay) d.allDay = 1; if (!me.field_map.convertToUserTz) d.convertToUserTz = 1; // convert to user tz From c65e866ec4f0b540d5cd9b2d40126d817e4df9dc Mon Sep 17 00:00:00 2001 From: "avazquez@ctgalega.com" Date: Sun, 17 Dec 2023 20:19:12 +0100 Subject: [PATCH 007/134] fix: linters --- frappe/public/js/frappe/views/calendar/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index a7e4dad874..22aebef2b4 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -379,7 +379,7 @@ frappe.views.Calendar = class Calendar { }); if (typeof(d.allDay) === "undefined") { - d.allDay = me.field_map.allDay + d.allDay = me.field_map.allDay; } if (!me.field_map.convertToUserTz) d.convertToUserTz = 1; From 7aa3f4f869678007f87d3a6dea549e2fbfe15774 Mon Sep 17 00:00:00 2001 From: "avazquez@ctgalega.com" Date: Sun, 17 Dec 2023 20:24:40 +0100 Subject: [PATCH 008/134] fix: linters --- frappe/public/js/frappe/views/calendar/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 22aebef2b4..8e9e843b39 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -378,7 +378,7 @@ frappe.views.Calendar = class Calendar { d[target] = d[source]; }); - if (typeof(d.allDay) === "undefined") { + if (typeof d.allDay === "undefined") { d.allDay = me.field_map.allDay; } From a15b1dee9582dcaba361529ec99efed4cd425b40 Mon Sep 17 00:00:00 2001 From: Gursheen Anand Date: Thu, 21 Dec 2023 18:30:16 +0530 Subject: [PATCH 009/134] fix: correctly typecast disabled flag --- frappe/desk/treeview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 4a77c6560b..77f86e2662 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -39,7 +39,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): @frappe.whitelist() def get_children(doctype, parent="", include_disabled=False, **filters): if isinstance(include_disabled, str): - include_disabled = bool(include_disabled) + include_disabled = frappe.json.loads(include_disabled) return _get_children(doctype, parent, include_disabled=include_disabled) From 067104ca9cf3f5e0ed59f7623edab730b0d796d2 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 18 Dec 2023 12:02:13 +0530 Subject: [PATCH 010/134] refactor(sentry): sync up with FC implementation Co-authored-by: Aditya Hase Signed-off-by: Akhil Narang --- frappe/__init__.py | 26 ---------- frappe/app.py | 46 ++++++++++++++++- frappe/hooks.py | 2 + frappe/utils/background_jobs.py | 43 ++++++++++++++++ frappe/utils/sentry.py | 91 ++++++++++++++++++++++++++++----- 5 files changed, 167 insertions(+), 41 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 158d5c9fa4..c450e26578 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""" 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/hooks.py b/frappe/hooks.py index ec7436ac19..de4254584f 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -422,11 +422,13 @@ before_request = [ "frappe.recorder.record", "frappe.monitor.start", "frappe.rate_limiter.apply", + "frappe.utils.sentry.set_sentry_context", ] # Background Job Hooks before_job = [ "frappe.monitor.start", + "frappe.utils.sentry.set_sentry_context", ] after_job = [ "frappe.monitor.stop", diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index d89003a81d..8ab529284d 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -288,6 +288,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/sentry.py b/frappe/utils/sentry.py index 8ca66a15d7..bcc6a3ab4b 100644 --- a/frappe/utils/sentry.py +++ b/frappe/utils/sentry.py @@ -1,18 +1,93 @@ import os import sys +from datetime import datetime +import rq from sentry_sdk import capture_message as sentry_capture_message +from sentry_sdk import configure_scope 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_sentry_context(): + with configure_scope() as 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_tag("site", frappe.local.site) + user = getattr(frappe.session, "user", "Unidentified") + if "@" not in user: + user = f"{user}@{frappe.local.site}" + 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) 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 @@ -30,20 +105,8 @@ def capture_exception(message: str | None = None) -> None: if frappe.request: with hub.configure_scope() as scope: - scope.set_transaction_name( - frappe.request.path, - source=SOURCE_FOR_STYLE["endpoint"], - ) - 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 client := hub.client: exc_info = sys.exc_info() From 3ee00cf1e4b04e9f74c65e53bad3db0a2e9f8412 Mon Sep 17 00:00:00 2001 From: Fierflame Date: Tue, 26 Dec 2023 20:17:58 +0800 Subject: [PATCH 011/134] fix: Solve the problem that the filter conditions are regarded as the same when the filter values are equivalent arrays in the filter --- frappe/public/js/frappe/ui/filters/filter_list.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index bdf471cf1b..3bc02861e4 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -265,10 +265,7 @@ frappe.ui.FilterGroup = class { let value = filter_value[3]; let equal = frappe.utils.arrays_equal; - if ( - equal(f_value.slice(0, 4), filter_value.slice(0, 4)) || - (Array.isArray(value) && equal(value, f_value[3])) - ) { + if (equal(f_value.slice(0, 4), filter_value.slice(0, 4))) { exists = true; } }); From a16db1be3228b51ee2f241723e520f26296ecbb0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Dec 2023 10:46:30 +0530 Subject: [PATCH 012/134] fix: show relative path in loaded JS (#23964) [skip ci] --- frappe/desk/form/meta.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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) From 64b63d59699cf9eeed9f1499eb7d728a11dccc42 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 27 Dec 2023 11:01:52 +0530 Subject: [PATCH 013/134] fix(sentry): don't set context if not enabled Signed-off-by: Akhil Narang --- frappe/hooks.py | 11 +++++-- frappe/utils/sentry.py | 75 ++++++++++++++++++++++++------------------ 2 files changed, 52 insertions(+), 34 deletions(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index de4254584f..e82714389b 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -1,3 +1,5 @@ +import os + from . import __version__ as app_version app_name = "frappe" @@ -422,14 +424,19 @@ before_request = [ "frappe.recorder.record", "frappe.monitor.start", "frappe.rate_limiter.apply", - "frappe.utils.sentry.set_sentry_context", ] # Background Job Hooks before_job = [ "frappe.monitor.start", - "frappe.utils.sentry.set_sentry_context", ] + +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.monitor.stop", "frappe.utils.file_lock.release_document_locks", diff --git a/frappe/utils/sentry.py b/frappe/utils/sentry.py index bcc6a3ab4b..5486b47ef1 100644 --- a/frappe/utils/sentry.py +++ b/frappe/utils/sentry.py @@ -4,7 +4,6 @@ from datetime import datetime import rq from sentry_sdk import capture_message as sentry_capture_message -from sentry_sdk import configure_scope from sentry_sdk.hub import Hub from sentry_sdk.integrations import Integration from sentry_sdk.integrations.wsgi import _make_wsgi_event_processor @@ -48,41 +47,49 @@ class FrappeIntegration(Integration): Database.sql = sql -def set_sentry_context(): - with configure_scope() as 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 +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() + 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) + 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: - if frappe.form_dict.cmd: - path = f"/api/method/{frappe.form_dict.cmd}" - else: - path = frappe.request.path + path = frappe.request.path - scope.set_transaction_name( - path, - source=SOURCE_FOR_STYLE["endpoint"], - ) + scope.set_transaction_name( + path, + source=SOURCE_FOR_STYLE["endpoint"], + ) - scope.set_tag("site", frappe.local.site) - user = getattr(frappe.session, "user", "Unidentified") - if "@" not in user: - user = f"{user}@{frappe.local.site}" - 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) + scope.set_tag("site", frappe.local.site) + user = getattr(frappe.session, "user", "Unidentified") + if "@" not in user: + user = f"{user}@{frappe.local.site}" + 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) + + +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): @@ -102,9 +109,13 @@ def capture_exception(message: str | None = None) -> None: return try: hub = Hub.current - if frappe.request: with hub.configure_scope() as scope: + 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) From fe906966d4b1eca364f2abfba4b1fddb29fb370f Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Wed, 27 Dec 2023 12:02:37 +0530 Subject: [PATCH 014/134] fix(minor): Onboarding check if element available (#23947) only try to find parent_element_selector if element exists --- frappe/public/js/onboarding_tours/onboarding_tours.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js index 3f6202d068..6bd3a62012 100644 --- a/frappe/public/js/onboarding_tours/onboarding_tours.js +++ b/frappe/public/js/onboarding_tours/onboarding_tours.js @@ -124,7 +124,7 @@ frappe.ui.OnboardingTour = class OnboardingTour { } = step_info; let element = cur_page?.page.querySelector(element_selector); !element && (element = document.querySelector(element_selector)); - if (parent_element_selector) { + if (element && parent_element_selector) { element = element.closest(parent_element_selector); } if (element && (next_on_click || hide_buttons || modal_trigger)) { From 547523c7b409047fb1091d61bb9f7ef57039c8e6 Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Wed, 27 Dec 2023 13:16:02 +0530 Subject: [PATCH 015/134] fix(minor): mobile menu class (#23970) removed `.collapse.show` class as it is removed during opening animation of menu. --- frappe/public/scss/website/navbar.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/website/navbar.scss b/frappe/public/scss/website/navbar.scss index f7f8ffc64f..c460e9fb5f 100644 --- a/frappe/public/scss/website/navbar.scss +++ b/frappe/public/scss/website/navbar.scss @@ -29,7 +29,7 @@ } } @media (max-width: map-get($grid-breakpoints, "sm")) { - .navbar-collapse.collapse.show { + .navbar-collapse { .navbar-nav { align-items: flex-start; } From 39136b01b6aff6c1d0f17fe151b694e4d2007a22 Mon Sep 17 00:00:00 2001 From: Maharshi Patel Date: Wed, 27 Dec 2023 13:32:04 +0530 Subject: [PATCH 016/134] fix: shortcut-widget-box should have flex-start As we changed align-items to space-between for widget head, we need to override .shortcut-widget-box to flex-start --- frappe/public/scss/desk/desktop.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 58a216fcff..8b192b8c0b 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -549,7 +549,7 @@ body { &.shortcut-widget-box { cursor: pointer; - + align-items: flex-start; &:hover { --icon-stroke: var(--invert-neutral); .widget-title { From 6367194620e3e6ae7ebb9f65b7c3062a5e6b0e1a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Dec 2023 14:15:20 +0530 Subject: [PATCH 017/134] perf: skip network call if no doc (#23967) If link field has no value then its title will always be undefined. --- frappe/public/js/frappe/utils/utils.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 02305870cd..4531304a47 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1545,6 +1545,9 @@ Object.assign(frappe.utils, { }, fetch_link_title(doctype, name) { + if (!doctype || !name) { + return; + } try { return frappe .xcall("frappe.desk.search.get_link_title", { From 2e730d70568eccc93c2d499a7d303ab22a56ba09 Mon Sep 17 00:00:00 2001 From: 14987 Date: Wed, 27 Dec 2023 15:06:24 +0530 Subject: [PATCH 018/134] fix(Data Import): show failed import logs --- frappe/core/doctype/data_import/data_import.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index b3fa136eb4..a93671e3e3 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}
` : ""; From 91405493e5a6d70123615615c4430aa602f22997 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Wed, 27 Dec 2023 11:33:18 +0100 Subject: [PATCH 019/134] fix(text_editor): Fix bubble's link tooltip clipping (#23911) * fix(text_editor): Use body as bounds to avoid bubble's tooltip clipping * fix(text_editor): Allow overflow for link tooltip --- frappe/public/js/frappe/form/controls/text_editor.js | 2 +- frappe/public/scss/common/quill.scss | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 15e11cd9e4..01e5a9aa30 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -208,7 +208,7 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for }, theme: this.df.theme || "snow", readOnly: this.disabled, - bounds: this.quill_container[0], + bounds: document.body, placeholder: this.df.placeholder || "", }; } diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 135fe0dcf8..18bf9ac24d 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -12,6 +12,7 @@ font-family: var(--font-stack); color: var(--text-color); line-height: 1.6; + overflow: visible; h1, h2, h3, From 445da4319bc5bfcb90d07e39f58264cfbd27c9b7 Mon Sep 17 00:00:00 2001 From: hhharsha36 <28597188+hhharsha36@users.noreply.github.com> Date: Wed, 27 Dec 2023 23:34:21 +1300 Subject: [PATCH 020/134] fix: error - permission denied for schema public from Postgres >= 15 during initial DB setup (#23799) Error from Postgres: ERROR: permission denied for schema public at character 14 Error from Frappe: psql:/home/frappe/frappe-bench/apps/frappe/frappe/database/postgres/framework_postgres.sql:72: ERROR: permission denied for schema public Error Causer: Starting Postgres version >= 15, all users will have the `CREATE` permission revoked by default. Fix: Grant relevant privileges to the database in question for the user. In this case, DB owner privilege. Note: the below two permission attempts were unsuccessful and still caused the same public schema permission error GRANT USAGE, CREATE ON SCHEMA public TO {frappe.conf.db_name}; GRANT ALL ON SCHEMA public TO {frappe.conf.db_name} References: https://stackoverflow.com/questions/74110708/postgres-15-permission-denied-for-schema-public https://stackoverflow.com/questions/67276391/why-am-i-getting-a-permission-denied-error-for-schema-public-on-pgadmin-4 --- frappe/database/postgres/setup_db.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index f5f3b14006..3323a68312 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -13,6 +13,9 @@ 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)) + psql_version = root_conn.sql(f"SELECT VERSION()", as_dict=True) + if psql_version and psql_version[0].get("version", "PostgreSQL 14").split()[1] >= "15": + root_conn.sql("ALTER DATABASE `{0}` OWNER TO {0}".format(frappe.conf.db_name)) root_conn.close() From 4996b8ac7b7b6ff2be407def4970a70a10729f01 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Dec 2023 16:12:40 +0530 Subject: [PATCH 021/134] refactor: postgres version check --- frappe/database/postgres/setup_db.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 3323a68312..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,9 +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)) - psql_version = root_conn.sql(f"SELECT VERSION()", as_dict=True) - if psql_version and psql_version[0].get("version", "PostgreSQL 14").split()[1] >= "15": - root_conn.sql("ALTER DATABASE `{0}` OWNER 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() From 32469b5a8126841b97cbc31ad7892bb68e50684b Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Wed, 27 Dec 2023 16:17:31 +0530 Subject: [PATCH 022/134] fix: roles editor (#23976) --- frappe/public/js/frappe/roles_editor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/roles_editor.js b/frappe/public/js/frappe/roles_editor.js index 32f1f07b1f..691acb035d 100644 --- a/frappe/public/js/frappe/roles_editor.js +++ b/frappe/public/js/frappe/roles_editor.js @@ -3,7 +3,7 @@ frappe.RoleEditor = class { this.frm = frm; this.wrapper = wrapper; this.disable = disable; - let user_roles = this.frm.doc.roles.map((a) => a.role); + let user_roles = this.frm.doc.roles ? this.frm.doc.roles.map((a) => a.role) : []; this.multicheck = frappe.ui.form.make_control({ parent: wrapper, df: { From 6e914dfdf60c39a01e03c10f4b3e3d6b86165130 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Dec 2023 16:45:42 +0530 Subject: [PATCH 023/134] test: use doctype that supports prompt naming (#23980) --- cypress/integration/navigation.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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"); }); From 34f03a2de24a2e34985a1e2e5599e287081f3cf2 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 27 Dec 2023 16:58:04 +0530 Subject: [PATCH 024/134] fix(data_import): respect the value of show_failed_logs checkbox Signed-off-by: Akhil Narang --- frappe/core/doctype/data_import/data_import.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index a93671e3e3..9f0e11a7b7 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -506,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; From 0fadb6d9fc925a904819315d0ccebcd67d57b576 Mon Sep 17 00:00:00 2001 From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com> Date: Wed, 27 Dec 2023 18:40:44 +0530 Subject: [PATCH 025/134] fix: use sbool for better typecasting Co-authored-by: Akhil Narang --- frappe/desk/treeview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 77f86e2662..77a8454849 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -39,7 +39,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): @frappe.whitelist() def get_children(doctype, parent="", include_disabled=False, **filters): if isinstance(include_disabled, str): - include_disabled = frappe.json.loads(include_disabled) + include_disabled = frappe.sbool(include_disabled) return _get_children(doctype, parent, include_disabled=include_disabled) From d5d35704d87d69247f911c6a49240f937813fcf3 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Wed, 27 Dec 2023 18:43:33 +0530 Subject: [PATCH 026/134] feat(sentry): skip validation errors Signed-off-by: Akhil Narang --- frappe/utils/sentry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/utils/sentry.py b/frappe/utils/sentry.py index 5486b47ef1..d3a76bed42 100644 --- a/frappe/utils/sentry.py +++ b/frappe/utils/sentry.py @@ -122,6 +122,10 @@ def capture_exception(message: str | None = None) -> None: 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, From c7e5afee6a46b5db6159137afd4fe6a071eec11e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 27 Dec 2023 18:51:20 +0530 Subject: [PATCH 027/134] refactor: Use arr.includes(item) instead of in_list(arr, item) --- frappe/core/doctype/user/user.js | 4 ++-- frappe/custom/doctype/custom_field/custom_field.js | 2 +- .../custom/doctype/customize_form/customize_form.js | 2 +- .../doctype/property_setter/property_setter.js | 2 +- frappe/email/doctype/newsletter/newsletter.js | 2 +- frappe/email/doctype/notification/notification.js | 8 ++++---- frappe/printing/doctype/print_format/print_format.js | 2 +- .../print_format_builder/print_format_builder.js | 10 +++++----- frappe/public/js/form_builder/store.js | 6 +++--- frappe/public/js/form_builder/utils.js | 6 +++--- .../public/js/frappe/form/controls/base_control.js | 6 +++--- frappe/public/js/frappe/form/controls/base_input.js | 4 ++-- frappe/public/js/frappe/form/controls/data.js | 3 +-- frappe/public/js/frappe/form/controls/link.js | 4 ++-- .../js/frappe/form/controls/table_multiselect.js | 2 +- frappe/public/js/frappe/form/footer/form_timeline.js | 4 ++-- frappe/public/js/frappe/form/form.js | 2 +- frappe/public/js/frappe/form/grid.js | 6 +++--- frappe/public/js/frappe/form/grid_row.js | 12 ++++++------ frappe/public/js/frappe/form/grid_row_form.js | 2 +- frappe/public/js/frappe/form/layout.js | 2 +- frappe/public/js/frappe/form/toolbar.js | 2 +- frappe/public/js/frappe/list/list_settings.js | 8 ++++---- frappe/public/js/frappe/list/list_view.js | 2 +- frappe/public/js/frappe/model/create_new.js | 12 ++++++------ frappe/public/js/frappe/model/meta.js | 6 +++--- frappe/public/js/frappe/model/model.js | 2 +- frappe/public/js/frappe/model/perm.js | 2 +- frappe/public/js/frappe/ui/field_group.js | 2 +- frappe/public/js/frappe/ui/filters/filter.js | 2 +- frappe/public/js/frappe/ui/group_by/group_by.js | 2 +- frappe/public/js/frappe/ui/toolbar/search_utils.js | 6 +++--- frappe/public/js/frappe/ui/toolbar/toolbar.js | 2 +- frappe/public/js/frappe/utils/common.js | 4 ++-- frappe/public/js/frappe/utils/datatype.js | 2 +- frappe/public/js/frappe/views/communication.js | 2 +- frappe/public/js/frappe/views/inbox/inbox_view.js | 4 ++-- frappe/public/js/frappe/views/kanban/kanban_view.js | 2 +- .../public/js/frappe/views/reports/query_report.js | 2 +- frappe/public/js/frappe/views/reports/report_view.js | 2 +- frappe/public/js/print_format_builder/utils.js | 2 +- 41 files changed, 79 insertions(+), 80 deletions(-) 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/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/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/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/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/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/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/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/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/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..feca4783bf 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -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,
 				});
 			}
 		});
@@ -1136,7 +1136,7 @@ export default class GridRow {
 		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)) {
+				if (![TAB, UP_ARROW, DOWN_ARROW].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) {
@@ -1441,8 +1441,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..d81e722ebc 100644
--- a/frappe/public/js/frappe/list/list_settings.js
+++ b/frappe/public/js/frappe/list/list_settings.js
@@ -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..e6cc574955 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -755,7 +755,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
 			: value;
 
 		let translated_doctypes = frappe.boot?.translated_doctypes || [];
-		if (in_list(translated_doctypes, df.options)) {
+		if (translated_doctypes.includes(df.options)) {
 			value_display = __(value_display);
 		}
 
diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js
index d519fd5fc1..9fa55fe5cd 100644
--- a/frappe/public/js/frappe/model/create_new.js
+++ b/frappe/public/js/frappe/model/create_new.js
@@ -62,7 +62,7 @@ $.extend(frappe.model, {
 				var df = frappe.meta.has_field(doctype, fieldname);
 				if (
 					df &&
-					in_list(["Link", "Data", "Select", "Dynamic Link"], df.fieldtype) &&
+					["Link", "Data", "Select", "Dynamic Link"].includes(df.fieldtype) &&
 					!df.no_copy
 				) {
 					doc[fieldname] = value;
@@ -89,12 +89,12 @@ $.extend(frappe.model, {
 		var updated = [];
 		for (var fid = 0; fid < docfields.length; fid++) {
 			var f = docfields[fid];
-			if (!in_list(frappe.model.no_value_type, f.fieldtype) && doc[f.fieldname] == null) {
+			if (!frappe.model.no_value_type.includes(f.fieldtype) && doc[f.fieldname] == null) {
 				if (f.no_default) continue;
 				var v = frappe.model.get_default_value(f, doc, parent_doc);
 				if (v) {
-					if (in_list(["Int", "Check"], f.fieldtype)) v = cint(v);
-					else if (in_list(["Currency", "Float"], f.fieldtype)) v = flt(v);
+					if (["Int", "Check"].includes(f.fieldtype)) v = cint(v);
+					else if (["Currency", "Float"].includes(f.fieldtype)) v = flt(v);
 
 					doc[f.fieldname] = v;
 					updated.push(f.fieldname);
@@ -102,7 +102,7 @@ $.extend(frappe.model, {
 					f.fieldtype == "Select" &&
 					f.options &&
 					typeof f.options === "string" &&
-					!in_list(["[Select]", "Loading..."], f.options)
+					!["[Select]", "Loading..."].includes(f.options)
 				) {
 					doc[f.fieldname] = f.options.split("\n")[0];
 				}
@@ -280,7 +280,7 @@ $.extend(frappe.model, {
 			if (
 				df &&
 				key.substr(0, 2) != "__" &&
-				!in_list(no_copy_list, key) &&
+				!no_copy_list.includes(key) &&
 				!(df && !from_amend && cint(df.no_copy) == 1)
 			) {
 				var value = doc[key] || [];
diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js
index a27060512f..2f209e048d 100644
--- a/frappe/public/js/frappe/model/meta.js
+++ b/frappe/public/js/frappe/model/meta.js
@@ -154,7 +154,7 @@ $.extend(frappe.meta, {
 
 	get_doctype_for_field: function (doctype, key) {
 		var out = null;
-		if (in_list(frappe.model.std_fields_list, key)) {
+		if (frappe.model.std_fields_list.includes(key)) {
 			// standard
 			out = doctype;
 		} else if (frappe.meta.has_field(doctype, key)) {
@@ -164,7 +164,7 @@ $.extend(frappe.meta, {
 			frappe.meta.get_table_fields(doctype).every(function (d) {
 				if (
 					frappe.meta.has_field(d.options, key) ||
-					in_list(frappe.model.child_table_field_list, key)
+					frappe.model.child_table_field_list.includes(key)
 				) {
 					out = d.options;
 					return false;
@@ -264,7 +264,7 @@ $.extend(frappe.meta, {
 			});
 		$.each(print_formats, function (i, d) {
 			if (
-				!in_list(print_format_list, d.name) &&
+				!print_format_list.includes(d.name) &&
 				d.print_format_type !== "JS" &&
 				(cint(enable_raw_printing) || !d.raw_printing)
 			) {
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 715f3c7533..4064ce1fee 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -550,7 +550,7 @@ $.extend(frappe.model, {
 				tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger));
 			} else {
 				// execute link triggers (want to reselect to execute triggers)
-				if (in_list(["Link", "Dynamic Link"], fieldtype) && doc) {
+				if (["Link", "Dynamic Link"].includes(fieldtype) && doc) {
 					tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger));
 				}
 			}
diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js
index 1534521670..22be220af2 100644
--- a/frappe/public/js/frappe/model/perm.js
+++ b/frappe/public/js/frappe/model/perm.js
@@ -240,7 +240,7 @@ $.extend(frappe.perm, {
 			// fields updated by workflow must be read-only
 			if (
 				cint(cur_frm.read_only) ||
-				in_list(cur_frm.states.update_fields, df.fieldname) ||
+				cur_frm.states.update_fields.includes(df.fieldname) ||
 				df.fieldname == cur_frm.state_fieldname
 			) {
 				status = "Read";
diff --git a/frappe/public/js/frappe/ui/field_group.js b/frappe/public/js/frappe/ui/field_group.js
index e80dff942a..27fe08439c 100644
--- a/frappe/public/js/frappe/ui/field_group.js
+++ b/frappe/public/js/frappe/ui/field_group.js
@@ -55,7 +55,7 @@ frappe.ui.FieldGroup = class FieldGroup extends frappe.ui.form.Layout {
 	focus_on_first_input() {
 		if (this.no_focus) return;
 		$.each(this.fields_list, function (i, f) {
-			if (!in_list(["Date", "Datetime", "Time", "Check"], f.df.fieldtype) && f.set_focus) {
+			if (!["Date", "Datetime", "Time", "Check"].includes(f.df.fieldtype) && f.set_focus) {
 				f.set_focus();
 				return false;
 			}
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index a88d7d01f2..d45e44df35 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -454,7 +454,7 @@ frappe.ui.filter_utils = {
 	},
 
 	get_selected_label(field) {
-		if (in_list(["Link", "Dynamic Link"], field.df.fieldtype)) {
+		if (["Link", "Dynamic Link"].includes(field.df.fieldtype)) {
 			return field.get_label_value();
 		}
 	},
diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js
index 2f62904813..a4471c788c 100644
--- a/frappe/public/js/frappe/ui/group_by/group_by.js
+++ b/frappe/public/js/frappe/ui/group_by/group_by.js
@@ -391,7 +391,7 @@ frappe.ui.GroupBy = class {
 		this.all_fields[this.doctype] = this.report_view.meta.fields;
 
 		const standard_fields_filter = (df) =>
-			!in_list(frappe.model.no_value_type, df.fieldtype) && !df.report_hide;
+			!frappe.model.no_value_type.includes(df.fieldtype) && !df.report_hide;
 
 		const table_fields = frappe.meta.get_table_fields(this.doctype).filter((df) => !df.hidden);
 
diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js
index 4577becf41..d910e8ada6 100644
--- a/frappe/public/js/frappe/ui/toolbar/search_utils.js
+++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js
@@ -113,7 +113,7 @@ frappe.search.utils = {
 	get_search_in_list: function (keywords) {
 		var me = this;
 		var out = [];
-		if (in_list(keywords.split(" "), "in") && keywords.slice(-2) !== "in") {
+		if (keywords.split(" ").includes("in") && keywords.slice(-2) !== "in") {
 			var parts = keywords.split(" in ");
 			frappe.boot.user.can_read.forEach(function (item) {
 				if (frappe.boot.user.can_search.includes(item)) {
@@ -190,11 +190,11 @@ frappe.search.utils = {
 			({ score, marked_string } = search_result);
 			if (score) {
 				target = item;
-				if (in_list(frappe.boot.single_types, item)) {
+				if (frappe.boot.single_types.includes(item)) {
 					out.push(option("", ["Form", item, item], 0.05));
 				} else if (frappe.boot.user.can_search.includes(item)) {
 					// include 'making new' option
-					if (in_list(frappe.boot.user.can_create, item)) {
+					if (frappe.boot.user.can_create.includes(item)) {
 						var match = item;
 						out.push({
 							type: "New",
diff --git a/frappe/public/js/frappe/ui/toolbar/toolbar.js b/frappe/public/js/frappe/ui/toolbar/toolbar.js
index d95c2f4a5b..2f6f5b7993 100644
--- a/frappe/public/js/frappe/ui/toolbar/toolbar.js
+++ b/frappe/public/js/frappe/ui/toolbar/toolbar.js
@@ -247,7 +247,7 @@ frappe.ui.toolbar.setup_session_defaults = function () {
 			fields = JSON.parse(data.message);
 			let perms = frappe.perm.get_perm("Session Default Settings");
 			//add settings button only if user is a System Manager or has permission on 'Session Default Settings'
-			if (in_list(frappe.user_roles, "System Manager") || perms[0].read == 1) {
+			if (frappe.user_roles.includes("System Manager") || perms[0].read == 1) {
 				fields[fields.length] = {
 					fieldname: "settings",
 					fieldtype: "Button",
diff --git a/frappe/public/js/frappe/utils/common.js b/frappe/public/js/frappe/utils/common.js
index 10e97a3ffd..8f397785ab 100644
--- a/frappe/public/js/frappe/utils/common.js
+++ b/frappe/public/js/frappe/utils/common.js
@@ -219,7 +219,7 @@ window.lstrip = function lstrip(s, chars) {
 	if (!chars) chars = ["\n", "\t", " "];
 	// strip left
 	let first_char = s.substr(0, 1);
-	while (in_list(chars, first_char)) {
+	while (chars.includes(first_char)) {
 		s = s.substr(1);
 		first_char = s.substr(0, 1);
 	}
@@ -229,7 +229,7 @@ window.lstrip = function lstrip(s, chars) {
 window.rstrip = function (s, chars) {
 	if (!chars) chars = ["\n", "\t", " "];
 	let last_char = s.substr(s.length - 1);
-	while (in_list(chars, last_char)) {
+	while (chars.includes(last_char)) {
 		s = s.substr(0, s.length - 1);
 		last_char = s.substr(s.length - 1);
 	}
diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js
index dc84a38334..25b8bac51a 100644
--- a/frappe/public/js/frappe/utils/datatype.js
+++ b/frappe/public/js/frappe/utils/datatype.js
@@ -75,7 +75,7 @@ window.has_words = function (list, item) {
 window.has_common = function (list1, list2) {
 	if (!list1 || !list2) return false;
 	for (var i = 0, j = list1.length; i < j; i++) {
-		if (in_list(list2, list1[i])) return true;
+		if (list2.includes(list1[i])) return true;
 	}
 	return false;
 };
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index f847778b3a..fb4fe913c1 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -156,7 +156,7 @@ frappe.views.CommunicationComposer = class {
 		// add from if user has access to multiple email accounts
 		const email_accounts = frappe.boot.email_accounts.filter((account) => {
 			return (
-				!in_list(["All Accounts", "Sent", "Spam", "Trash"], account.email_account) &&
+				!["All Accounts", "Sent", "Spam", "Trash"].includes(account.email_account) &&
 				account.enable_outgoing
 			);
 		});
diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js
index 22523cf825..0a5484a159 100644
--- a/frappe/public/js/frappe/views/inbox/inbox_view.js
+++ b/frappe/public/js/frappe/views/inbox/inbox_view.js
@@ -141,7 +141,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView {
 				["Communication", "sent_or_received", "=", "Sent", true],
 				["Communication", "email_status", "not in", "Spam,Trash", true],
 			]);
-		} else if (in_list(["Spam", "Trash"], email_account)) {
+		} else if (["Spam", "Trash"].includes(email_account)) {
 			filters = default_filters.concat([
 				["Communication", "email_status", "=", email_account, true],
 				["Communication", "email_account", "in", frappe.boot.all_accounts, true],
@@ -167,7 +167,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView {
 	get_no_result_message() {
 		var email_account = this.email_account;
 		var args;
-		if (in_list(["Spam", "Trash"], email_account)) {
+		if (["Spam", "Trash"].includes(email_account)) {
 			return __("No {0} mail", [email_account]);
 		} else if (!email_account && !frappe.boot.email_accounts.length) {
 			// email account is not configured
diff --git a/frappe/public/js/frappe/views/kanban/kanban_view.js b/frappe/public/js/frappe/views/kanban/kanban_view.js
index 25b875c2f9..5781052ca3 100644
--- a/frappe/public/js/frappe/views/kanban/kanban_view.js
+++ b/frappe/public/js/frappe/views/kanban/kanban_view.js
@@ -229,7 +229,7 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
 
 		this.meta.fields.forEach((df) => {
 			const is_valid_field =
-				in_list(["Data", "Text", "Small Text", "Text Editor"], df.fieldtype) && !df.hidden;
+				["Data", "Text", "Small Text", "Text Editor"].includes(df.fieldtype) && !df.hidden;
 
 			if (is_valid_field && !title_field) {
 				// can be mapped to textarea
diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js
index caad1f99be..67cc7e5c2a 100644
--- a/frappe/public/js/frappe/views/reports/query_report.js
+++ b/frappe/public/js/frappe/views/reports/query_report.js
@@ -1226,7 +1226,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
 					if (column.colIndex === index && !value) {
 						value = "Total";
 						column = { fieldtype: "Data" }; // avoid type issues for value if Date column
-					} else if (in_list(["Currency", "Float"], column.fieldtype)) {
+					} else if (["Currency", "Float"].includes(column.fieldtype)) {
 						// proxy for currency and float
 						data = this.data[0];
 					}
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index fd425ebc30..10f3143dac 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -890,7 +890,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
 	get_columns_for_picker() {
 		let out = {};
 
-		const standard_fields_filter = (df) => !in_list(frappe.model.no_value_type, df.fieldtype);
+		const standard_fields_filter = (df) => !frappe.model.no_value_type.includes(df.fieldtype);
 
 		let doctype_fields = frappe.meta
 			.get_docfields(this.doctype)
diff --git a/frappe/public/js/print_format_builder/utils.js b/frappe/public/js/print_format_builder/utils.js
index ea6fd1ff01..695fc853e1 100644
--- a/frappe/public/js/print_format_builder/utils.js
+++ b/frappe/public/js/print_format_builder/utils.js
@@ -94,7 +94,7 @@ export function get_table_columns(df) {
 	let total_width = 0;
 	for (let tf of table_fields) {
 		if (
-			!in_list(["Section Break", "Column Break"], tf.fieldtype) &&
+			!["Section Break", "Column Break"].includes(tf.fieldtype) &&
 			!tf.print_hide &&
 			df.label &&
 			total_width < 100

From e814af0aeb2b209b3cf55895a3591736b391e98a Mon Sep 17 00:00:00 2001
From: Ankush Menat 
Date: Wed, 27 Dec 2023 19:12:09 +0530
Subject: [PATCH 028/134] fix!: Init child tables as empty array on client side

---
 frappe/public/js/frappe/model/create_new.js | 57 +++++++++++++--------
 1 file changed, 35 insertions(+), 22 deletions(-)

diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js
index 9fa55fe5cd..159e39e3a7 100644
--- a/frappe/public/js/frappe/model/create_new.js
+++ b/frappe/public/js/frappe/model/create_new.js
@@ -84,30 +84,39 @@ $.extend(frappe.model, {
 	},
 
 	set_default_values: function (doc, parent_doc) {
-		var doctype = doc.doctype;
-		var docfields = frappe.meta.get_docfields(doctype);
-		var updated = [];
-		for (var fid = 0; fid < docfields.length; fid++) {
-			var f = docfields[fid];
-			if (!frappe.model.no_value_type.includes(f.fieldtype) && doc[f.fieldname] == null) {
-				if (f.no_default) continue;
-				var v = frappe.model.get_default_value(f, doc, parent_doc);
-				if (v) {
-					if (["Int", "Check"].includes(f.fieldtype)) v = cint(v);
-					else if (["Currency", "Float"].includes(f.fieldtype)) v = flt(v);
+		let doctype = doc.doctype;
+		let docfields = frappe.meta.get_docfields(doctype);
+		let updated = [];
 
-					doc[f.fieldname] = v;
-					updated.push(f.fieldname);
-				} else if (
-					f.fieldtype == "Select" &&
-					f.options &&
-					typeof f.options === "string" &&
-					!["[Select]", "Loading..."].includes(f.options)
-				) {
-					doc[f.fieldname] = f.options.split("\n")[0];
-				}
+		// Table types should be initialized
+		let fieldtypes_without_default = frappe.model.no_value_type.filter(
+			(fieldtype) => !frappe.model.table_fields.includes(fieldtype)
+		);
+		docfields.forEach((f) => {
+			if (
+				fieldtypes_without_default.includes(f.fieldtype) ||
+				doc[f.fieldname] != null ||
+				f.no_default
+			) {
+				return;
 			}
-		}
+
+			let v = frappe.model.get_default_value(f, doc, parent_doc);
+			if (v) {
+				if (["Int", "Check"].includes(f.fieldtype)) v = cint(v);
+				else if (["Currency", "Float"].includes(f.fieldtype)) v = flt(v);
+
+				doc[f.fieldname] = v;
+				updated.push(f.fieldname);
+			} else if (
+				f.fieldtype == "Select" &&
+				f.options &&
+				typeof f.options === "string" &&
+				!["[Select]", "Loading..."].includes(f.options)
+			) {
+				doc[f.fieldname] = f.options.split("\n")[0];
+			}
+		});
 		return updated;
 	},
 
@@ -142,6 +151,10 @@ $.extend(frappe.model, {
 			df.ignore_user_permissions != 1 &&
 			allowed_records.length;
 
+		if (frappe.model.table_fields.includes(df.fieldtype)) {
+			value = [];
+		}
+
 		// don't set defaults for "User" link field using User Permissions!
 		if (df.fieldtype === "Link" && df.options !== "User") {
 			// If user permission has Is Default enabled or single-user permission has found against respective doctype.

From 37803a00d4027612446545cc210aff31f0c8cb72 Mon Sep 17 00:00:00 2001
From: Revant Nandgaonkar 
Date: Thu, 28 Dec 2023 10:45:06 +0530
Subject: [PATCH 029/134] fix: strip exc from json response if traceback not
 allowed (#23989)

* fix: strip exc from json response if traceback not allowed

* fix: use pop instead of del to avoid key error

* fix: Avoid showing exc when traceback is disabled

---------

Co-authored-by: Ankush Menat 
---
 frappe/utils/response.py | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

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])

From 8583c01cad9b5fd693ae158388aca0db0c4a9f7f Mon Sep 17 00:00:00 2001
From: Anas AlGhanem 
Date: Thu, 28 Dec 2023 00:33:48 -0500
Subject: [PATCH 030/134] fix(Geo): Add timezone for palestinian territory
 country (#23986)

* Add timezone for palestinian territory country. It's blocking the first start wizard from completion.

* add currency and additional timezone for Palestine


[skip ci]
---
 frappe/geo/country_info.json | 11 ++++++++++-
 1 file changed, 10 insertions(+), 1 deletion(-)

diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json
index eba166d8fc..b027dc38d5 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",

From 3527489a0442f23c50a33407876cb21b6280d180 Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Thu, 28 Dec 2023 06:37:49 +0100
Subject: [PATCH 031/134] feat: sort multicheck by label (#23985)

* feat: sort multicheck by label

* fix: avoid double translation

* fix: always cast to string before comparing

---------

Co-authored-by: Ankush Menat 
---
 .../js/frappe/form/controls/multicheck.js      | 18 ++++++++++--------
 1 file changed, 10 insertions(+), 8 deletions(-)

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
 			
`); From 3c015a1fd770dc163f211f3c3d298b83a9434de9 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 11:40:03 +0530 Subject: [PATCH 032/134] fix: sudan country info (#23709) (#23998) * Update country_info.json Update Sudan Currency * chore: fix currency symbol --------- Co-authored-by: Ankush Menat [skip ci] (cherry picked from commit 6b24b9aa13df00a257adfcd40892e4acce688fd4) Co-authored-by: Ayman Mustafa <84941140+MEClouds@users.noreply.github.com> --- frappe/geo/country_info.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index b027dc38d5..8ff39c37d7 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -2550,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", From 5d51ec2ba12696057c4a81056cad155e38be0495 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 12:21:09 +0530 Subject: [PATCH 033/134] Revert "fix(text_editor): Fix bubble's link tooltip clipping (#23911)" (#23999) This reverts commit 91405493e5a6d70123615615c4430aa602f22997. --- frappe/public/js/frappe/form/controls/text_editor.js | 2 +- frappe/public/scss/common/quill.scss | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index 01e5a9aa30..15e11cd9e4 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -208,7 +208,7 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for }, theme: this.df.theme || "snow", readOnly: this.disabled, - bounds: document.body, + bounds: this.quill_container[0], placeholder: this.df.placeholder || "", }; } diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 18bf9ac24d..135fe0dcf8 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -12,7 +12,6 @@ font-family: var(--font-stack); color: var(--text-color); line-height: 1.6; - overflow: visible; h1, h2, h3, From ad8ad088c50ced942336ae9e10ada3e46ea9fb78 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 13:48:35 +0530 Subject: [PATCH 034/134] fix: force `[]` as default for child tables (#24000) --- frappe/public/js/frappe/model/create_new.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 159e39e3a7..23bd43d663 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -151,10 +151,6 @@ $.extend(frappe.model, { df.ignore_user_permissions != 1 && allowed_records.length; - if (frappe.model.table_fields.includes(df.fieldtype)) { - value = []; - } - // don't set defaults for "User" link field using User Permissions! if (df.fieldtype === "Link" && df.options !== "User") { // If user permission has Is Default enabled or single-user permission has found against respective doctype. @@ -232,6 +228,10 @@ $.extend(frappe.model, { value = frappe.datetime.now_time(); } + if (frappe.model.table_fields.includes(df.fieldtype)) { + value = []; + } + // set it here so we know it was set as a default df.__default_value = value; From a9bb994f15858f6a4820a99bf9ff69dd5ff91f7c Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 28 Dec 2023 14:24:45 +0530 Subject: [PATCH 035/134] fix!: deterministic fixture import order (#22210) * feat: #20753 fixture export prefix and fixture import order (cherry picked from commit 6a9c56a568e4ccf181fe9cb4153d0b9e4f02ac3d) * refactor: clarify prefix logic, see https://github.com/frappe/frappe/pull/20852/files/c4866921dfaa397bdea9a6c1a01c85d21218b593#r1196249038 (cherry picked from commit cd2519e71e5545bd4c706369df3ea05843a0bfd9) * style: format * refactor: conditionally sort documents when importing * refactor: simplify code * feat: Unittest as requested in https://github.com/frappe/frappe/pull/22210#discussion_r1331587501 --------- Co-authored-by: To Finke Co-authored-by: Ankush Menat --- .../core/doctype/data_import/data_import.py | 4 +- frappe/tests/test_hooks.py | 64 +++++++++++++++++++ frappe/utils/fixtures.py | 18 +++++- 3 files changed, 82 insertions(+), 4 deletions(-) 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/tests/test_hooks.py b/frappe/tests/test_hooks.py index 970699d01c..c1fcd23982 100644 --- a/frappe/tests/test_hooks.py +++ b/frappe/tests/test_hooks.py @@ -95,6 +95,70 @@ 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/utils/fixtures.py b/frappe/utils/fixtures.py index ddd8650451..2a9fb541b6 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,32 @@ 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", From acf032f6075bf9bab3e00a3d702658bf9f8f8251 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 14:50:16 +0530 Subject: [PATCH 036/134] refactor: simplify duplicate filter checking code keeping track of `exists` variable is weird to read and prone to bugs... what if next field makes exist = false, even if one filter already exists? --- .../public/js/frappe/ui/filters/filter_list.js | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter_list.js b/frappe/public/js/frappe/ui/filters/filter_list.js index 3bc02861e4..69712caf3f 100644 --- a/frappe/public/js/frappe/ui/filters/filter_list.js +++ b/frappe/public/js/frappe/ui/filters/filter_list.js @@ -252,24 +252,15 @@ frappe.ui.FilterGroup = class { filter_exists(filter_value) { // filter_value of form: [doctype, fieldname, condition, value] - let exists = false; - this.filters + return this.filters .filter((f) => f.field) - .map((f) => { + .some((f) => { let f_value = f.get_value(); if (filter_value.length === 2) { - exists = filter_value[0] === f_value[0] && filter_value[1] === f_value[1]; - return; - } - - let value = filter_value[3]; - let equal = frappe.utils.arrays_equal; - - if (equal(f_value.slice(0, 4), filter_value.slice(0, 4))) { - exists = true; + return filter_value[0] === f_value[0] && filter_value[1] === f_value[1]; } + return frappe.utils.arrays_equal(f_value.slice(0, 4), filter_value.slice(0, 4)); }); - return exists; } get_filters() { @@ -278,7 +269,6 @@ frappe.ui.FilterGroup = class { .map((f) => { return f.get_value(); }); - // {}: this.list.update_standard_filters(values); } update_filters() { From 1501c5e1d38a512d394d9bde8076de1d1e9308be Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 28 Dec 2023 15:58:44 +0530 Subject: [PATCH 037/134] fix(minor): spacing for sidebar item with child items --- frappe/public/scss/desk/desktop.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index ed0ed37cf2..2172a7add7 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -1034,7 +1034,7 @@ body { } .drop-icon { - padding: 10px 12px 10px 2px; + padding: 0px 12px 0px 2px; } svg { From bcd5d78dabe8b93febba7f6e05d3ff6415dcc995 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 17:58:25 +0530 Subject: [PATCH 038/134] fix: skip undefined path --- frappe/public/js/frappe/router.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js index 09675b62a1..448bb9c08b 100644 --- a/frappe/public/js/frappe/router.js +++ b/frappe/public/js/frappe/router.js @@ -107,6 +107,7 @@ frappe.router = { layout_mapped: {}, is_app_route(path) { + if (!path) return; // desk paths must begin with /app or doctype route if (path.substr(0, 1) === "/") path = path.substr(1); path = path.split("/"); From 0f099b1f1f901d1e98e54b8dd29854fa41a78e67 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 18:02:53 +0530 Subject: [PATCH 039/134] fix: unconditionally pop ignore_user_type filter This isn't a real column. SENTRY: FRAPPE-13Z --- frappe/core/doctype/user/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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( From 1ae79badf761eaab007556e32f71b2463d83a3de Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 18:04:44 +0530 Subject: [PATCH 040/134] fix: only set change listener if available --- frappe/desk/page/setup_wizard/setup_wizard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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" && From 4a8fb6c43cfec1fa06218b2fb9c69453a5021f00 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 19:16:11 +0530 Subject: [PATCH 041/134] style: format --- frappe/tests/test_hooks.py | 42 +++++++++++++++++++++++--------------- frappe/utils/fixtures.py | 4 +++- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py index c1fcd23982..14bd1affca 100644 --- a/frappe/tests/test_hooks.py +++ b/frappe/tests/test_hooks.py @@ -98,11 +98,12 @@ class TestHooks(FrappeTestCase): 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"))): + 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 @@ -118,16 +119,20 @@ class TestHooks(FrappeTestCase): 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)) + 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"))) - + 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__] @@ -135,13 +140,13 @@ class TestHooks(FrappeTestCase): 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"))) - + 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": "User", "prefix": "my_prefix"}, {"dt": "Contact"}, {"dt": "Role"}, ] @@ -150,14 +155,19 @@ class TestHooks(FrappeTestCase): 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"))) - + 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"))) + self.assertCountEqual( + ["1_my_prefix_user.json", "2_contact.json", "3_role.json"], + os.listdir(frappe.get_app_path(app, "fixtures")), + ) class TestAPIHooks(FrappeAPITestCase): diff --git a/frappe/utils/fixtures.py b/frappe/utils/fixtures.py index 2a9fb541b6..120d452cc0 100644 --- a/frappe/utils/fixtures.py +++ b/frappe/utils/fixtures.py @@ -67,7 +67,9 @@ def export_fixtures(app=None): else: apps = frappe.get_installed_apps() for app in apps: - fixture_auto_order = bool(next(iter(frappe.get_hooks("fixture_auto_order", app_name=app)), False)) + 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 From 00f20f43c6fc5fea18b7c36ea579a797261f5a2c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 20:50:27 +0530 Subject: [PATCH 042/134] build: add setproctitle as dependency (#24007) * build: add setproctitle as dependency RQ and other tools use it to automatically set a useful proc title * ci: print all bench logs after running tests This can help reveal failures from background jobs etc --- .github/workflows/server-tests.yml | 20 +++++++++++++------- .github/workflows/ui-tests.yml | 17 ++++++++++++----- frappe/utils/background_jobs.py | 4 ++++ pyproject.toml | 1 + 4 files changed, 30 insertions(+), 12 deletions(-) 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/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 8ab529284d..839c196966 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 @@ -200,6 +201,9 @@ def execute_job(site, method, event, job_name, kwargs, user=None, is_async=True, else: method_name = cstr(method.__name__) + 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, method=method_name, 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", From 754d0f91523d23dbe6c63a07f334d81270003193 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 28 Dec 2023 21:12:34 +0530 Subject: [PATCH 043/134] test: less flaky datet picker test (#24015) --- cypress/integration/control_date.js | 3 +++ 1 file changed, 3 insertions(+) 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() ); From a51a150376fa9c07e25b896177840f651222cf64 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 29 Dec 2023 06:33:59 +0530 Subject: [PATCH 044/134] feat: Hook to add custom route resolver --- frappe/website/path_resolver.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py index a91a51c0da..41c2bc9e3f 100644 --- a/frappe/website/path_resolver.py +++ b/frappe/website/path_resolver.py @@ -38,7 +38,11 @@ class PathResolver: except frappe.Redirect: return frappe.flags.redirect_location, RedirectPage(self.path) - 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": From d8e97f2dc042f24c8fbfd3b7172417b3776b7605 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 29 Dec 2023 10:26:16 +0530 Subject: [PATCH 045/134] fix: Do not change global search label to lower case --- frappe/public/js/frappe/ui/toolbar/search_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/toolbar/search_utils.js b/frappe/public/js/frappe/ui/toolbar/search_utils.js index d910e8ada6..0ceb9c53de 100644 --- a/frappe/public/js/frappe/ui/toolbar/search_utils.js +++ b/frappe/public/js/frappe/ui/toolbar/search_utils.js @@ -613,7 +613,7 @@ frappe.search.utils = { const target = item.label.toLowerCase(); const txt = keywords.toLowerCase(); if (txt === target || target.indexOf(txt) === 0) { - const search_result = this.fuzzy_search(txt, target, true); + const search_result = this.fuzzy_search(txt, item.label, true); results.push({ type: "Executable", value: search_result.marked_string, From d423d2ace5b46a2ee222a059af7bf3d8c6686719 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Dec 2023 11:33:14 +0530 Subject: [PATCH 046/134] fix(DX): filter version logs with changes to field (#24023) Diff view shows all versions. E.g. if you enable/disable a script it shows up in possible diff targets. This PR filters versiont to only show the ones that have the field changed somehow. --- frappe/public/js/frappe/utils/diffview.js | 6 +++++- frappe/utils/diff.py | 11 ++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/utils/diffview.js b/frappe/public/js/frappe/utils/diffview.js index 73bd4c1eae..210e582b7a 100644 --- a/frappe/public/js/frappe/utils/diffview.js +++ b/frappe/public/js/frappe/utils/diffview.js @@ -16,7 +16,11 @@ frappe.ui.DiffView = class DiffView { make_dialog() { const get_query = () => ({ query: "frappe.utils.diff.version_query", - filters: { docname: this.docname, ref_doctype: this.doctype }, + filters: { + docname: this.docname, + ref_doctype: this.doctype, + fieldname: this.fieldname, + }, }); const onchange = () => this.compute_diff(); return new frappe.ui.Dialog({ 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", From bdc495f5d1b4da62f872bdcfcd8cb01343dd0cc1 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Fri, 29 Dec 2023 11:54:03 +0530 Subject: [PATCH 047/134] fix: show right permission for user cannot create doctypes (#24027) --- frappe/core/page/permission_manager/permission_manager.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); }); From 31d72c33b4b550e98b9c6509c9c2bda113c34ada Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Dec 2023 11:55:26 +0530 Subject: [PATCH 048/134] chore: use site as "user" (#24026) For us, a user is single site. This is because logic like "issue affects more than 1 user" only makes sense for site. Same client/server script can affect multiple user on same site but it's not a useful error for us. [skip ci] --- frappe/public/js/sentry.bundle.js | 4 ++-- frappe/utils/sentry.py | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/frappe/public/js/sentry.bundle.js b/frappe/public/js/sentry.bundle.js index 9757595af4..e02764b930 100644 --- a/frappe/public/js/sentry.bundle.js +++ b/frappe/public/js/sentry.bundle.js @@ -6,8 +6,8 @@ Sentry.init({ autoSessionTracking: false, initialScope: { // don't use frappe.session.user, it's set much later and will fail because of async loading - user: { id: frappe.boot.user.name ?? "Unidentified" }, - tags: { site: frappe.boot.sitename }, + user: { id: frappe.boot.sitename }, + tags: { frappe_user: frappe.boot.user.name ?? "Unidentified" }, }, beforeSend(event, hint) { // Check if it was caused by frappe.throw() diff --git a/frappe/utils/sentry.py b/frappe/utils/sentry.py index d3a76bed42..5b41311c54 100644 --- a/frappe/utils/sentry.py +++ b/frappe/utils/sentry.py @@ -73,11 +73,9 @@ def set_scope(scope): source=SOURCE_FOR_STYLE["endpoint"], ) - scope.set_tag("site", frappe.local.site) + scope.set_user({"id": frappe.local.site}) user = getattr(frappe.session, "user", "Unidentified") - if "@" not in user: - user = f"{user}@{frappe.local.site}" - scope.set_user({"id": user, "email": user}) + 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) From 585200988bf4a021b56bcbd5566309b3ca2e6a0e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Dec 2023 14:35:33 +0530 Subject: [PATCH 049/134] feat: simple procline for scheduler Makes it easy to tell if scheduler is for some reason "stuck". --- frappe/utils/scheduler.py | 8 ++++++++ 1 file changed, 8 insertions(+) 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(): From 38314aec59e5191b51a3977987cef84521e06829 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Dec 2023 17:07:17 +0530 Subject: [PATCH 050/134] fix: Check permissions before rendering web view (#24032) --- frappe/website/page_renderers/document_page.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py index 83d55f7a9a..938aea94df 100644 --- a/frappe/website/page_renderers/document_page.py +++ b/frappe/website/page_renderers/document_page.py @@ -25,7 +25,7 @@ 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 + return frappe.get_cached_doc(self.doctype, self.docname).has_permission() def search_web_page_dynamic_routes(self): d = get_page_info_from_web_page_with_dynamic_routes(self.path) @@ -45,6 +45,7 @@ class DocumentPage(BaseTemplatePage): @cache_html def get_html(self): self.doc = frappe.get_cached_doc(self.doctype, self.docname) + self.doc.check_permission() self.init_context() self.update_context() self.post_process_context() From 8c515a9a5d902f528a0b57f5fca87359acfd1de6 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Dec 2023 17:23:50 +0530 Subject: [PATCH 051/134] fix: correct perm check for guests Allow viewing document page if doctype allows guest to view --- frappe/website/page_renderers/document_page.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py index 938aea94df..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 frappe.get_cached_doc(self.doctype, self.docname).has_permission() + 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) @@ -45,7 +48,6 @@ class DocumentPage(BaseTemplatePage): @cache_html def get_html(self): self.doc = frappe.get_cached_doc(self.doctype, self.docname) - self.doc.check_permission() self.init_context() self.update_context() self.post_process_context() From 19e0fe65f5c40c2aff9c2dfd627586f22950f752 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 29 Dec 2023 18:04:29 +0530 Subject: [PATCH 052/134] fix: allow guests to view published Newsletters --- frappe/email/doctype/newsletter/newsletter.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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", From ce3e029769ce35a355dc9f50518e223fbc52d7bb Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 29 Dec 2023 21:12:21 +0530 Subject: [PATCH 053/134] fix: incorrect class tags generation --- frappe/core/doctype/doctype/doctype.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f0f7f96bfc..f8720eb5be 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -593,7 +593,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( From b9fef7eb8dd468919cafbc24bb6ee190c6aee76e Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 29 Dec 2023 22:02:38 +0530 Subject: [PATCH 054/134] test: base class update on has web view update --- frappe/tests/test_document.py | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) 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 From cd37bf0337d924ea5194ed1bd83eb6ba57877116 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 30 Dec 2023 01:37:53 +0100 Subject: [PATCH 055/134] fix(Blog Post): correct indicator filter --- frappe/website/doctype/blog_post/blog_post_list.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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"]; } }, }; From 64c221343ee86f60e4cad7d26fada081f948799d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 30 Dec 2023 12:09:32 +0530 Subject: [PATCH 056/134] perf: skip ifnull checks on `modified` field (#24042) --- frappe/model/db_query.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 From 1fe3b5d5bcc88c2d59ad7e3d2d1c3119b487b857 Mon Sep 17 00:00:00 2001 From: Xiaoguang Sun Date: Mon, 1 Jan 2024 14:08:40 +0800 Subject: [PATCH 057/134] refactor(workspace): Optimize save call on workspace doc (#24052) Optimize Workspace.save_page to call doc.save() only once therefore eliminate sending unnecessary queries to database. Signed-off-by: Xiaoguang Sun --- frappe/desk/desktop.py | 2 +- frappe/desk/doctype/workspace/workspace.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) 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/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) From c00d5ac258bcba630bfa6d0129a5fe84014feb3b Mon Sep 17 00:00:00 2001 From: avc <94137451+git-avc@users.noreply.github.com> Date: Mon, 1 Jan 2024 07:19:24 +0100 Subject: [PATCH 058/134] feat(UX): cancel new row with escape key (#23928) * feat: (UX) cancel new row with escape key * fix: linters * fix: more linters --------- Co-authored-by: Ankush Menat --- frappe/public/js/frappe/form/grid_row.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index feca4783bf..ba913e53f0 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -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 (![TAB, UP_ARROW, DOWN_ARROW].includes(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; } @@ -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); From 05225e077e9d2e15490545931ff3226d99043a6a Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Mon, 1 Jan 2024 11:56:09 +0530 Subject: [PATCH 059/134] fix: broken `dynamic_link` to `link` formatting in query report --- frappe/public/js/frappe/views/reports/query_report.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 67cc7e5c2a..32353b8a02 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1259,13 +1259,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { width: parseInt(column.width) || null, editable: false, compareValue: compareFn, - format: (value, row, column, data, filter, data1) => { + format: (value, row, column, data, filter) => { if (this.report_settings.formatter) { return this.report_settings.formatter( value, row, column, - data1, + data, format_cell, filter ); From d5a727dc58cd17f7c46d29b95812e9048a3d3fde Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Jan 2024 12:13:14 +0530 Subject: [PATCH 060/134] fix: Rate control read only mode (#24055) - It should inherit ControlFloat as underlying value is float. closes https://github.com/frappe/frappe/issues/24053 --- frappe/public/js/frappe/form/controls/rating.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 = ""; From 78cd62fec0b8680904052f88ff0ed98774288d5c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Jan 2024 12:39:36 +0530 Subject: [PATCH 061/134] chore: faster total rows estimation (#24058) --- frappe/commands/site.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 48a4feea57..0a96dc8105 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -570,7 +570,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 +610,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: From 8a7707e3fac802e799ef1eb5079e672c0659271a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Jan 2024 12:46:47 +0530 Subject: [PATCH 062/134] fix(DX): store tracebacks with context (#24059) --- frappe/commands/site.py | 4 ++-- frappe/core/doctype/prepared_report/prepared_report.py | 2 +- frappe/core/doctype/scheduled_job_type/scheduled_job_type.py | 2 +- frappe/desk/page/setup_wizard/setup_wizard.py | 2 +- frappe/email/doctype/notification/notification.py | 3 +-- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 0a96dc8105..9b2ad747a9 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -448,7 +448,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 @@ -899,7 +899,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/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/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/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index 229eae4f1f..50bdc92b0d 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/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index d89a2bbfbd..6b3fe209df 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -505,8 +505,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")) From e163dbe5e2b64ca12940bbdabbb7965908dce75b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Jan 2024 12:54:04 +0530 Subject: [PATCH 063/134] chore: disable v13 releases --- .github/workflows/initiate_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b71d01e1b44434a8f2241327f4109c0f6f209c6a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Jan 2024 19:14:03 +0530 Subject: [PATCH 064/134] fix: View logging fails if no referrer ``` AttributeError: 'NoneType' object has no attribute 'startswith' File "frappe/app.py", line 110, in application response = frappe.api.handle(request) File "frappe/api/__init__.py", line 49, in handle data = endpoint(**arguments) File "frappe/api/v1.py", line 36, in handle_rpc_call return frappe.handler.handle() File "frappe/handler.py", line 49, in handle data = execute_cmd(cmd) File "frappe/handler.py", line 85, in execute_cmd return frappe.call(method, **frappe.form_dict) File "__init__.py", line 1680, in call return fn(*args, **newargs) File "frappe/utils/typing_validations.py", line 31, in wrapper return func(*args, **kwargs) File "frappe/website/doctype/web_page_view/web_page_view.py", line 58, in make_view_log if not frappe.utils.is_site_link(path): File "frappe/utils/data.py", line 2488, in is_site_link if link.startswith("/"): ``` --- frappe/utils/data.py | 2 ++ 1 file changed, 2 insertions(+) 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 From e296a81ed9ea3a857a4b48c859fd0b0aa06723b0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Jan 2024 11:25:15 +0530 Subject: [PATCH 065/134] refactor: remove dead handling for "fnargs" (#24064) This was added for some function here: https://github.com/frappe/frappe/commit/2dd28afcb7e89cf6968886717f413d6620602d87#diff-4a7c68f7105f3a52a7504ce52ab85bd106df91e69ab07a9f12fe23dc258870a6R266 It's not used anywhere. --- frappe/__init__.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index c450e26578..16db0a4477 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1694,17 +1694,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: From da8e88e66c82d9cfb58ca0aec77929c9dea244c7 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Tue, 2 Jan 2024 11:39:16 +0530 Subject: [PATCH 066/134] feat(sentry): log request json body / form data Signed-off-by: Akhil Narang --- frappe/utils/sentry.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/utils/sentry.py b/frappe/utils/sentry.py index 5b41311c54..61ba48eecc 100644 --- a/frappe/utils/sentry.py +++ b/frappe/utils/sentry.py @@ -116,6 +116,10 @@ def capture_exception(message: str | None = None) -> None: set_scope(scope) evt_processor = _make_wsgi_event_processor(frappe.request.environ, False) scope.add_event_processor(evt_processor) + 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() From 2ef75be7ffdfd3012f736fa78c4ab147551b63bf Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Tue, 2 Jan 2024 12:35:10 +0530 Subject: [PATCH 067/134] chore: update datatable --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5ec8402fe3..2f2f257a2a 100644 --- a/package.json +++ b/package.json @@ -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.10", "frappe-gantt": "^0.6.0", "highlight.js": "^10.4.1", "html5-qrcode": "^2.3.8", diff --git a/yarn.lock b/yarn.lock index 98a9877253..cb1c54da48 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1497,10 +1497,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.10: + version "1.17.10" + resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.10.tgz#8526e0e360b493a72114afd2a4dc4c155b4a14dd" + integrity sha512-D537Kfs9F/P8kw7tk/yvdNc/mexRShOlCJc4dL1yPXk6wXIXthFiniq+R8P8jh61aHLqLQr0LmPahAI40+9v+w== dependencies: hyperlist "^1.0.0-beta" lodash "^4.17.5" From 2b23b8d663ecc359ff82b280d5c2b5168879f200 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 2 Jan 2024 12:49:20 +0530 Subject: [PATCH 068/134] fix: validate server scripts with FrappeTransformer (#24068) --- frappe/core/doctype/server_script/server_script.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) 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")) From c58e2c5eb333acd209d34a028e5bbbab2c23a5ee Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Tue, 2 Jan 2024 13:55:48 +0530 Subject: [PATCH 069/134] fix: lint Signed-off-by: Akhil Narang --- frappe/public/js/frappe/views/calendar/calendar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/calendar/calendar.js b/frappe/public/js/frappe/views/calendar/calendar.js index 8e9e843b39..2ba8bb8440 100644 --- a/frappe/public/js/frappe/views/calendar/calendar.js +++ b/frappe/public/js/frappe/views/calendar/calendar.js @@ -377,7 +377,7 @@ frappe.views.Calendar = class Calendar { $.each(me.field_map, function (target, source) { d[target] = d[source]; }); - + if (typeof d.allDay === "undefined") { d.allDay = me.field_map.allDay; } From 4756bfda2040c732eb4d203cc9ae720c7772c46c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Tue, 2 Jan 2024 15:25:28 +0530 Subject: [PATCH 070/134] fix(minor): Remove unused Settings workspace and some minor style fixes --- frappe/core/workspace/settings/settings.json | 383 ------------------- frappe/public/js/frappe/list/list_view.js | 2 +- frappe/public/scss/desk/desktop.scss | 5 + frappe/public/scss/desk/list.scss | 2 +- 4 files changed, 7 insertions(+), 385 deletions(-) delete mode 100644 frappe/core/workspace/settings/settings.json 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/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index e6cc574955..06bf9fef00 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 - ? `

- + {% 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 %} From 745080c56e7466791837bdf10c2d099981b72031 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Sat, 6 Jan 2024 09:42:53 +0530 Subject: [PATCH 098/134] fix(test_recorder): get the correct request (#24143) Signed-off-by: Akhil Narang --- frappe/recorder.py | 13 ++++++++++--- frappe/tests/test_recorder.py | 8 +++++++- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/frappe/recorder.py b/frappe/recorder.py index be0a1e4969..079251997f 100644 --- a/frappe/recorder.py +++ b/frappe/recorder.py @@ -8,6 +8,7 @@ import re import time from collections import Counter from collections.abc import Callable +from enum import Enum import sqlparse @@ -21,6 +22,12 @@ RECORDER_REQUEST_HASH = "recorder-requests" TRACEBACK_PATH_PATTERN = re.compile(".*/apps/") +class RecorderEvent(str, Enum): + HTTP_REQUEST = "HTTP Request" + BACKGROUND_JOB = "Background Job" + INVALID = "Invalid" + + def sql(*args, **kwargs): start_time = time.monotonic() result = frappe.db._sql(*args, **kwargs) @@ -154,16 +161,16 @@ class Recorder: self.method = frappe.request.method self.headers = dict(frappe.local.request.headers) self.form_dict = frappe.local.form_dict - self.event_type = "HTTP Request" + self.event_type = RecorderEvent.HTTP_REQUEST elif frappe.job: - self.event_type = "Background Job" + self.event_type = RecorderEvent.BACKGROUND_JOB self.path = frappe.job.method self.cmd = None self.method = None self.headers = None self.form_dict = None else: - self.event_type = None + self.event_type = RecorderEvent.INVALID self.path = None self.cmd = None self.method = None diff --git a/frappe/tests/test_recorder.py b/frappe/tests/test_recorder.py index 40e8bdc127..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]) From c6bdc7fe459db93b5b29deed6fc32c29d06d7c86 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Sun, 7 Jan 2024 08:06:55 +0100 Subject: [PATCH 099/134] fix(workspace): Fix widget rendering for EditorJS v2.28 (#23918) * fix(workspace): Ensure Block.render() always return an element If `has_data` is false, then `this.wrapper` is still empty (because it was just created and `this.make` did nothing), so it's safe to give it back to EditorJS for rendering. * build(deps): Bump EditorJS --- .../js/frappe/views/workspace/blocks/card.js | 2 +- .../js/frappe/views/workspace/blocks/chart.js | 2 +- .../views/workspace/blocks/custom_block.js | 2 +- .../views/workspace/blocks/number_card.js | 2 +- .../views/workspace/blocks/onboarding.js | 2 +- .../views/workspace/blocks/quick_list.js | 2 +- .../frappe/views/workspace/blocks/shortcut.js | 2 +- package.json | 2 +- yarn.lock | 36 +++---------------- 9 files changed, 13 insertions(+), 39 deletions(-) diff --git a/frappe/public/js/frappe/views/workspace/blocks/card.js b/frappe/public/js/frappe/views/workspace/blocks/card.js index 840339dab8..6d3507c454 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/card.js +++ b/frappe/public/js/frappe/views/workspace/blocks/card.js @@ -32,7 +32,7 @@ export default class Card extends Block { if (this.data && this.data.card_name) { let has_data = this.make("card", this.data.card_name, "links"); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/chart.js b/frappe/public/js/frappe/views/workspace/blocks/chart.js index a396472da9..924ad39dfb 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/chart.js +++ b/frappe/public/js/frappe/views/workspace/blocks/chart.js @@ -33,7 +33,7 @@ export default class Chart extends Block { if (this.data && this.data.chart_name) { let has_data = this.make("chart", this.data.chart_name); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/custom_block.js b/frappe/public/js/frappe/views/workspace/blocks/custom_block.js index b0b6a77b25..35bf69a084 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/custom_block.js +++ b/frappe/public/js/frappe/views/workspace/blocks/custom_block.js @@ -32,7 +32,7 @@ export default class CustomBlock extends Block { if (this.data && this.data.custom_block_name) { let has_data = this.make("custom_block", this.data.custom_block_name); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/number_card.js b/frappe/public/js/frappe/views/workspace/blocks/number_card.js index e560f85a51..28e964fe1b 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/number_card.js +++ b/frappe/public/js/frappe/views/workspace/blocks/number_card.js @@ -33,7 +33,7 @@ export default class NumberCard extends Block { if (this.data && this.data.number_card_name) { let has_data = this.make("number_card", this.data.number_card_name); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js index c279da79e5..534fe9eb37 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -113,7 +113,7 @@ export default class Onboarding extends Block { if (this.data && this.data.onboarding_name) { let has_data = this.make("onboarding", this.data.onboarding_name); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/quick_list.js b/frappe/public/js/frappe/views/workspace/blocks/quick_list.js index 5dcb8d66ec..1edaf43c34 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/quick_list.js +++ b/frappe/public/js/frappe/views/workspace/blocks/quick_list.js @@ -33,7 +33,7 @@ export default class QuickList extends Block { if (this.data && this.data.quick_list_name) { let has_data = this.make("quick_list", this.data.quick_list_name); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js index 56b4d18d62..6322cb22b7 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/shortcut.js +++ b/frappe/public/js/frappe/views/workspace/blocks/shortcut.js @@ -52,7 +52,7 @@ export default class Shortcut extends Block { if (this.data && this.data.shortcut_name) { let has_data = this.make("shortcut", this.data.shortcut_name); - if (!has_data) return; + if (!has_data) return this.wrapper; } if (!this.readOnly) { diff --git a/package.json b/package.json index 9968ba36dc..6c382dab28 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", diff --git a/yarn.lock b/yarn.lock index cac489329f..7c648aa992 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" @@ -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== From d0157f78624b7c275d2bc6648ea05473ff471178 Mon Sep 17 00:00:00 2001 From: Safwan Samsudeen <62411302+safwansamsudeen@users.noreply.github.com> Date: Sun, 7 Jan 2024 18:17:30 +0530 Subject: [PATCH 100/134] fix: mobile issue with form pop up (#24125) * fix: mobile configure columns issue * fix: correct grid view issue * fix header too --- frappe/public/js/frappe/form/grid_row.js | 12 ++++++------ frappe/public/js/frappe/list/list_settings.js | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index ba913e53f0..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()}
@@ -522,13 +522,13 @@ export default class GridRow { data-label='${docfield.label}' data-type='${docfield.fieldtype}'>
-
+ -
+
${__(docfield.label)}
-
-
+
diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js index d81e722ebc..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")} From cb2e803d0c217fcf5a3463930a5a983dd2257bcc Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 8 Jan 2024 11:39:02 +0530 Subject: [PATCH 101/134] fix(minor): Unpublish article when category is unpublished #15361 --- .../doctype/help_article/test_help_article.py | 12 ++++++++++++ .../website/doctype/help_category/help_category.py | 5 +++++ 2 files changed, 17 insertions(+) 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) From 04646024cb5fa9109e4273b9f7ec2a70e4ac36d1 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 8 Jan 2024 11:56:51 +0530 Subject: [PATCH 102/134] fix(minor): show footer on login via Website Settings, fixes #24153 --- frappe/public/scss/login.bundle.scss | 1 + frappe/templates/includes/login/login.js | 5 +++ .../website_settings/website_settings.json | 33 +++++++++++-------- .../website_settings/website_settings.py | 3 +- frappe/www/login.py | 1 + 5 files changed, 29 insertions(+), 14 deletions(-) diff --git a/frappe/public/scss/login.bundle.scss b/frappe/public/scss/login.bundle.scss index 66834017f4..25c6c5e70f 100644 --- a/frappe/public/scss/login.bundle.scss +++ b/frappe/public/scss/login.bundle.scss @@ -5,6 +5,7 @@ body { background-color: var(--bg-light-gray); } .web-footer { + margin-top: 3rem; display: none; } } diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index 90e12cf3d0..b61d4c6e61 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -2,6 +2,7 @@ // don't remove this line (used in test) window.disable_signup = {{ disable_signup and "true" or "false" }}; +window.show_footer_on_login = {{ show_footer_on_login and "true" or "false" }}; window.login = {}; @@ -305,6 +306,10 @@ frappe.ready(function () { $(window).trigger("hashchange"); } + if (window.show_footer_on_login) { + $("body .web-footer").show(); + } + $(".form-signup, .form-forgot, .form-login-with-email-link").removeClass("hide"); $(document).trigger('login_rendered'); }); 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/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"] = ( From 5275f393c7b619a04b7d56ef6d830072bc7fc314 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 8 Jan 2024 12:07:10 +0530 Subject: [PATCH 103/134] fix(minor): drop icon column, unsupported, fixes #23826 --- .../website/doctype/top_bar_item/top_bar_item.json | 14 ++++---------- .../website/doctype/top_bar_item/top_bar_item.py | 2 +- 2 files changed, 5 insertions(+), 11 deletions(-) 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 From c1d687ddbb85c1ba2ceaa8ad0f23434ed9aa5308 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 8 Jan 2024 12:42:13 +0530 Subject: [PATCH 104/134] fix: ignore user settings on report doc (#24162) --- frappe/public/js/frappe/views/reports/report_view.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 10f3143dac..ad9ce770f7 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -111,10 +111,9 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { //Setup groupby for reports this.group_by_control = new frappe.ui.GroupBy(this); - if (this.report_doc && this.report_doc.json.group_by) { + if (this.report_doc?.json?.group_by) { this.group_by_control.apply_settings(this.report_doc.json.group_by); - } - if (this.view_user_settings && this.view_user_settings.group_by) { + } else if (this.view_user_settings?.group_by) { this.group_by_control.apply_settings(this.view_user_settings.group_by); } } From f6388f6357b39b6942d00564254f945b97f99704 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 8 Jan 2024 12:50:34 +0530 Subject: [PATCH 105/134] fix: Skip no-value fields for rename (#24163) --- frappe/custom/doctype/custom_field/custom_field.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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. From eb0e8262a795694e790b217ac58360006d82d7aa Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 8 Jan 2024 12:51:32 +0530 Subject: [PATCH 106/134] fix(minor): Don't load user_info for Administrator twice. fixes #22211 (#24160) --- frappe/boot.py | 3 --- 1 file changed, 3 deletions(-) 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 From b9923571dc82115bc2573a349a26046c3fb36b61 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 8 Jan 2024 12:51:45 +0530 Subject: [PATCH 107/134] fix: check correct part of backup header `-- Backup generated by Frappe 15.1.0 on branch fix-backup-restore` Signed-off-by: Akhil Narang --- frappe/installer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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: From 2969b6eff13eb79aeb38bc08cbc8fa89fa3dd1ea Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 8 Jan 2024 14:24:16 +0530 Subject: [PATCH 108/134] fix: added precision field in webform field --- .../doctype/web_form_field/web_form_field.json | 11 ++++++++++- .../website/doctype/web_form_field/web_form_field.py | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) 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 From 20986060ca1b57831a65f2df2e0d72ecdca6cc68 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 8 Jan 2024 14:32:25 +0530 Subject: [PATCH 109/134] fix(style): set min height correctly --- frappe/public/scss/login.bundle.scss | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/scss/login.bundle.scss b/frappe/public/scss/login.bundle.scss index 25c6c5e70f..15cae7debf 100644 --- a/frappe/public/scss/login.bundle.scss +++ b/frappe/public/scss/login.bundle.scss @@ -4,8 +4,10 @@ body { @include media-breakpoint-up(sm) { background-color: var(--bg-light-gray); } + .page-content-wrapper { + min-height: calc(100vh - 220px); + } .web-footer { - margin-top: 3rem; display: none; } } From 50e43df26b2ba757d690b61d9b5a822a366a2b35 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 8 Jan 2024 14:32:56 +0530 Subject: [PATCH 110/134] fix: include Web Form Field in core_doctypes --- frappe/core/doctype/doctype/doctype.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index f8720eb5be..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", ] From e2bc379b239cbb0e87ab79e2ad78b6d608586b9b Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 8 Jan 2024 14:33:31 +0530 Subject: [PATCH 111/134] fix: get precision data while getting fields --- frappe/website/doctype/web_form/web_form.js | 1 + 1 file changed, 1 insertion(+) 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, From c1ebfc64b60499739d4f0bba61500314b2b916e7 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 8 Jan 2024 15:08:13 +0530 Subject: [PATCH 112/134] fix: copy content on reply --- frappe/core/doctype/communication/communication.js | 3 +++ 1 file changed, 3 insertions(+) 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); From 216b5fa9bfd9fd6e3cfb63b7b255f87b3184d6cf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 8 Jan 2024 15:12:59 +0530 Subject: [PATCH 113/134] chore: Skip explicit opt out We dont load posthog js if user opts out, so this starts failing. --- frappe/public/js/telemetry/index.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/telemetry/index.js b/frappe/public/js/telemetry/index.js index d15101b3d1..548a7c7ce5 100644 --- a/frappe/public/js/telemetry/index.js +++ b/frappe/public/js/telemetry/index.js @@ -39,7 +39,6 @@ class TelemetryManager { disable() { this.enabled = false; - posthog.opt_out_capturing(); } can_enable() { From e948afe938b17e739f56bcf99db59bd1cf0040f4 Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Mon, 8 Jan 2024 15:31:21 +0530 Subject: [PATCH 114/134] build(deps): bump frappe-datable to v1.17.14 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6c382dab28..83887161f3 100644 --- a/package.json +++ b/package.json @@ -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.13", + "frappe-datatable": "1.17.14", "frappe-gantt": "^0.6.0", "highlight.js": "^10.4.1", "html5-qrcode": "^2.3.8", diff --git a/yarn.lock b/yarn.lock index 7c648aa992..fea6ebf608 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1476,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.13: - version "1.17.13" - resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.13.tgz#349a15fd102b8abe55ab903c65de074aa66a26d6" - integrity sha512-rHzLjuAbWdbqEZbU6RCcimyeTswdIKtl8WJLjctj5zze7w5DFSQX0iDTbKNBG7TehS7YqP4k+ySIhvkHRELq3w== +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" From 4e188ec531ce3764f128da3bc406b60141e06a45 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 8 Jan 2024 15:37:04 +0530 Subject: [PATCH 115/134] fix: webhook error logging fails (#24172) These variables can be unset if webhook fails while creating them. --- frappe/integrations/doctype/webhook/webhook.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) 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}) From b58f6a7420bc8cd56c142cf391f76c23acfb2fa1 Mon Sep 17 00:00:00 2001 From: V Shankar Date: Mon, 8 Jan 2024 15:59:34 +0530 Subject: [PATCH 116/134] fix: Make Autocomplete option cache like link field (#23888) * fix: execute query even though input cache exists * fix: remove invalid autocomplete caching This cache can get stale when document value changes. --------- Co-authored-by: Shankar Co-authored-by: Ankush Menat --- .../js/frappe/form/controls/autocomplete.js | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) 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); }, }); From e3bafe15b684e6c95183aaddd619688f2ebac229 Mon Sep 17 00:00:00 2001 From: Akhil Narang Date: Mon, 8 Jan 2024 16:31:03 +0530 Subject: [PATCH 117/134] fix(communication): don't change email case Signed-off-by: Akhil Narang --- frappe/core/doctype/communication/communication.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index de2dfb7702..48c8d3cd12 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.""" From 54ecae0656653806e3325fde11ac747bd002f420 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:09:13 +0530 Subject: [PATCH 118/134] fix: Allowed standard fields in data export ( creation, owner) (#24187) Co-authored-by: Pavan Kumar Yekabote --- .../core/doctype/data_export/data_export.js | 6 ++++++ frappe/core/doctype/data_export/exporter.py | 16 ++++++++++++++- .../doctype/data_export/test_data_exporter.py | 4 ++-- frappe/model/__init__.py | 20 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) 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/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): """ From 64e37ec046fc0fe7025edcebcbfe87f76634ba0b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 8 Jan 2024 17:19:40 +0530 Subject: [PATCH 119/134] fix: Handle empty value of datetime control --- frappe/public/js/frappe/form/controls/datetime.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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); } From 9b8a8c155d3f1a06987552ec533c07da01ca5baf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 8 Jan 2024 19:15:19 +0530 Subject: [PATCH 120/134] fix: Correct logic for can_cancel (#24196) You can cancel a document IFF: - Document has no workflow - Document has workflow but it doesn't have state with docstatus=2. closes https://github.com/frappe/frappe/issues/19075 --- frappe/model/workflow.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) 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 From 0932bbf10c7bf642ffec7cf6fee7c6a6b040a04e Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Mon, 8 Jan 2024 17:08:16 +0100 Subject: [PATCH 121/134] fix(OTP): Autofocus OTP input, fix placeholder (#24203) * fix(OTP): Autofocus OTP input DOM insertion does not trigger autofocus, only initial load does. * fix(OTP): Fix placeholder of OTP input --- frappe/templates/includes/login/login.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/templates/includes/login/login.js b/frappe/templates/includes/login/login.js index b61d4c6e61..3cff2e4652 100644 --- a/frappe/templates/includes/login/login.js +++ b/frappe/templates/includes/login/login.js @@ -339,13 +339,14 @@ var request_otp = function (r) { {{ _("Verification") }}
- +
` ); // add event handler for submit button verify_token(); + $("#login_token").get(0)?.focus(); } var continue_otp_app = function (setup, qrcode) { From 417fce091a2e673f02a02f8f9ed0e190ea9243de Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 8 Jan 2024 16:30:54 +0000 Subject: [PATCH 122/134] fix: delete existing children first to avoid `UniqueValidationError` (#24140) --- frappe/model/document.py | 49 ++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 1201d3755b..ec0799e99f 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -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) From 207962aadc6a7a68feb5fe3a8b7ad8bd59c8af1b Mon Sep 17 00:00:00 2001 From: RitvikSardana Date: Tue, 9 Jan 2024 00:04:48 +0530 Subject: [PATCH 123/134] fix: count correction when a tag is deleted --- frappe/desk/doctype/tag/tag.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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) From c56c1cc2f76198ec467d5a73374bf619fcf50d11 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 9 Jan 2024 10:17:29 +0530 Subject: [PATCH 124/134] fix: bom creator tree view not working (#24200) --- frappe/public/js/frappe/views/treeview.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index c255bca9dc..bbf35104d1 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -200,9 +200,13 @@ frappe.views.TreeView = class TreeView { if (use_value == null) { use_value = use_label; } - this.args["include_disabled"] = this.page.inner_toolbar - .find("input[type='checkbox']") - .prop("checked"); + + if (this.page?.inner_toolbar) { + this.args["include_disabled"] = this.page.inner_toolbar + .find("input[type='checkbox']") + .prop("checked"); + } + this.tree = new frappe.ui.Tree({ parent: this.body, label: use_label, From 610bc43a1399835827bd666dd82c85eda1750f57 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 9 Jan 2024 14:50:58 +0530 Subject: [PATCH 125/134] fix: added invalid conditions for number fields --- frappe/public/js/frappe/ui/filters/filter.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index d45e44df35..70c919e7f7 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -52,7 +52,9 @@ frappe.ui.Filter = class { "Markdown Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"], Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"], Rating: ["like", "not like", "Between", "in", "not in", "Timespan"], + Int: ["like", "not like", "Between", "in", "not in", "Timespan"], Float: ["like", "not like", "Between", "in", "not in", "Timespan"], + Percent: ["like", "not like", "Between", "in", "not in", "Timespan"], }; } From 02031fc6f43dfe247d755229e40e249a88964f9c Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 9 Jan 2024 15:02:03 +0530 Subject: [PATCH 126/134] fix: don't update docstatus (#24216) This doesn't make sense. Updating docstatus without resaving document is a bad idea. Co-authored-by: rohitwaghchaure --- .../doctype/workflow/test_workflow.py | 26 ----------------- frappe/workflow/doctype/workflow/workflow.py | 29 ------------------- 2 files changed, 55 deletions(-) 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: From 724fd1a291fd3f75454ad0b05b4ad068d4ebf491 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:08:45 +0530 Subject: [PATCH 127/134] chore: linter fix --- frappe/email/doctype/notification/notification.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 4d86528e40..18e4a76635 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -339,7 +339,9 @@ def get_context(context): # For sending emails to specified role if recipient.receiver_by_role: - emails = get_info_based_on_role(recipient.receiver_by_role, "email", ignore_permissions=True) + emails = get_info_based_on_role( + recipient.receiver_by_role, "email", ignore_permissions=True + ) for email in emails: recipients = recipients + email.split("\n") From 2da9ba820b1f14e2fa9a92a96adacde133c5d865 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 9 Jan 2024 15:45:14 +0530 Subject: [PATCH 128/134] fix: added validation if upload is clicked without uploading file --- frappe/public/js/frappe/file_uploader/FileUploader.vue | 10 +++++++++- .../js/frappe/file_uploader/file_uploader.bundle.js | 4 +--- 2 files changed, 10 insertions(+), 4 deletions(-) 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) { From 2fe507c58c5d03bcf4c8ef3d480f9b36f58016e5 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 9 Jan 2024 15:55:04 +0530 Subject: [PATCH 129/134] chore: linter fix --- frappe/email/doctype/notification/notification.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 18e4a76635..28a45ce35e 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -339,9 +339,7 @@ def get_context(context): # For sending emails to specified role if recipient.receiver_by_role: - emails = get_info_based_on_role( - recipient.receiver_by_role, "email", ignore_permissions=True - ) + emails = get_info_based_on_role(recipient.receiver_by_role, "email", ignore_permissions=True) for email in emails: recipients = recipients + email.split("\n") @@ -368,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", ignore_permissions=True) + receiver_list += get_info_based_on_role( + recipient.receiver_by_role, "mobile_no", ignore_permissions=True + ) return receiver_list From 3884fa3c63447936b0a1c5fe4e9fe6a544e4c05f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 9 Jan 2024 16:08:27 +0530 Subject: [PATCH 130/134] fix: duplicate field in filters (#24189) * fix: duplicate field in filters * test: clear filters using new clear filter button --- cypress/integration/routing.js | 1 + cypress/support/commands.js | 23 ++--------------------- frappe/public/js/frappe/list/list_view.js | 10 +++++++--- 3 files changed, 10 insertions(+), 24 deletions(-) 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/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/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 06bf9fef00..491867edc9 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -2016,9 +2016,13 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { parse_filters_from_route_options() { const filters = []; - for (let field in frappe.route_options) { + let params = new URLSearchParams(window.location.search); + if (!params.toString() && frappe.route_options) { + params = new Map(Object.entries(frappe.route_options)); + } + + params.forEach((value, field) => { let doctype = null; - let value = frappe.route_options[field]; let value_array; if ($.isArray(value) && value[0].startsWith("[") && value[0].endsWith("]")) { @@ -2060,7 +2064,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { filters.push([doctype, field, "=", value]); } } - } + }); return filters; } From 238085d865dc982d9a7880ae62d947d247013cd7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 10 Jan 2024 12:27:49 +0530 Subject: [PATCH 131/134] fix: pass parent doctype on dashboard chart (#24236) --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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) From 3a8fc90961dde363b32e424fef319d0c29f048c5 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 10 Jan 2024 14:59:13 +0530 Subject: [PATCH 132/134] feat: permission debugger (#24239) * feat: permission debugger This PR adds a virtual doctype that can run has_permission for doctype-docname-user-ptype combinations and spit out detailed log for why/where some permissionw as denied or granted. This isn't supposed to be programatic, it's just textual dump of what code is doing. IMO a better debugger can be written but that will require extensive rewrite of perm checks first. All debugging, error messages in current systems are bolted on top with hacks to avoid messing with implementation. * fix: capture UP pass check * fix: reset docname on changing doctype * fix: docname is optional * fix: debug doctype perms --- .../doctype/permission_debugger/__init__.py | 0 .../permission_debugger.js | 24 +++ .../permission_debugger.json | 90 ++++++++++ .../permission_debugger.py | 75 ++++++++ .../test_permission_debugger.py | 9 + frappe/permissions.py | 161 +++++++++++++----- 6 files changed, 315 insertions(+), 44 deletions(-) create mode 100644 frappe/core/doctype/permission_debugger/__init__.py create mode 100644 frappe/core/doctype/permission_debugger/permission_debugger.js create mode 100644 frappe/core/doctype/permission_debugger/permission_debugger.json create mode 100644 frappe/core/doctype/permission_debugger/permission_debugger.py create mode 100644 frappe/core/doctype/permission_debugger/test_permission_debugger.py diff --git a/frappe/core/doctype/permission_debugger/__init__.py b/frappe/core/doctype/permission_debugger/__init__.py new file mode 100644 index 0000000000..e69de29bb2 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/permissions.py b/frappe/permissions.py index c54637a77c..27190d597e 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -60,6 +60,19 @@ def print_has_permission_check_logs(func): 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, @@ -69,6 +82,7 @@ def has_permission( raise_exception=True, *, parent_doctype=None, + debug=False, ): """Return True if user has permission `ptype` for given `doctype`. If `doc` is passed, also check user, share and owner permissions. @@ -90,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"): @@ -101,71 +119,84 @@ 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 @@ -175,11 +206,18 @@ def get_doc_permissions(doc, user=None, ptype=None): 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 has_controller_permissions(doc, ptype, user=user, debug=debug) is False: + 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 @@ -193,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: { @@ -229,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 @@ -275,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 @@ -283,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") @@ -304,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 @@ -361,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 @@ -377,7 +436,7 @@ def has_user_permission(doc, user=None): return True -def has_controller_permissions(doc, ptype, user=None): +def has_controller_permissions(doc, ptype, user=None, debug=False): """Return controller permissions if defined, None if not defined.""" if not user: user = frappe.session.user @@ -389,6 +448,7 @@ def has_controller_permissions(doc, ptype, user=None): for method in reversed(methods): controller_permission = frappe.call(frappe.get_attr(method), doc=doc, ptype=ptype, user=user) + debug and _debug_log(f"Controller permission check from {method}: {controller_permission}") if controller_permission is not None: return controller_permission @@ -682,7 +742,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 @@ -696,7 +757,10 @@ def has_child_permission( user=None, raise_exception=True, parent_doctype=None, + *, + debug=False, ): + 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, @@ -710,7 +774,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 @@ -724,7 +789,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 @@ -734,7 +800,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 @@ -742,14 +809,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 @@ -759,6 +831,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, ) From 914406d31bba60e0a03e73cd7f31c1fd51de4fc7 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 10 Jan 2024 14:53:32 +0530 Subject: [PATCH 133/134] feat: extend perm debugging to popular controllers - [x] File - [x] Communication --- frappe/__init__.py | 2 ++ frappe/core/doctype/communication/communication.py | 7 ++++--- frappe/core/doctype/file/file.py | 8 ++++---- frappe/core/doctype/file/file_list.js | 0 frappe/model/document.py | 4 ++-- frappe/permissions.py | 2 +- 6 files changed, 13 insertions(+), 10 deletions(-) delete mode 100644 frappe/core/doctype/file/file_list.js diff --git a/frappe/__init__.py b/frappe/__init__.py index 3c83ed335f..1568aa9f20 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -975,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`. @@ -999,6 +1000,7 @@ def has_permission( user=user, raise_exception=throw, parent_doctype=parent_doctype, + debug=debug, ) if throw and not out: diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py index 48c8d3cd12..516356308e 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -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/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/file/file_list.js b/frappe/core/doctype/file/file_list.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/model/document.py b/frappe/model/document.py index ec0799e99f..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`.""" diff --git a/frappe/permissions.py b/frappe/permissions.py index 27190d597e..f10e6b5e2b 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -447,7 +447,7 @@ def has_controller_permissions(doc, ptype, user=None, debug=False): return None 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 From b3532024b5087244ff6f00c42075f43113945e61 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 10 Jan 2024 15:38:26 +0530 Subject: [PATCH 134/134] fix: Accept "Falsy" values from perm controllers --- frappe/permissions.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/frappe/permissions.py b/frappe/permissions.py index f10e6b5e2b..3aac4197fa 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -83,7 +83,7 @@ def has_permission( *, 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. @@ -206,7 +206,7 @@ def get_doc_permissions(doc, user=None, ptype=None, debug=False): def is_user_owner(): return (doc.get("owner") or "").lower() == user.lower() - if has_controller_permissions(doc, ptype, user=user, debug=debug) is False: + 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} @@ -436,24 +436,27 @@ def has_user_permission(doc, user=None, debug=False): return True -def has_controller_permissions(doc, ptype, user=None, debug=False): - """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(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(): @@ -759,7 +762,7 @@ def has_child_permission( 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(