From 2ef5e6bd1d2bba43de3c2b9ecb02838db88cd596 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Tue, 2 May 2023 17:40:09 +0200 Subject: [PATCH 01/42] fix: file permissions --- frappe/core/doctype/file/file.json | 8 +----- frappe/core/doctype/file/file.py | 44 +++++++++++++++--------------- frappe/hooks.py | 1 + 3 files changed, 24 insertions(+), 29 deletions(-) diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index d6c4a99bc3..6c64bfe274 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -174,7 +174,7 @@ "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2022-09-13 15:50:15.508251", + "modified": "2023-05-02 15:42:14.274901", "modified_by": "Administrator", "module": "Core", "name": "File", @@ -196,14 +196,8 @@ { "create": 1, "delete": 1, - "email": 1, - "export": 1, - "if_owner": 1, - "print": 1, "read": 1, - "report": 1, "role": "All", - "share": 1, "write": 1 } ], diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 1323359030..2e88591f94 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -16,6 +16,7 @@ import frappe from frappe import _ from frappe.database.schema import SPECIAL_CHAR_PATTERN from frappe.model.document import Document +from frappe.permissions import get_doctypes_with_read from frappe.utils import call_hook_method, cint, get_files_path, get_hook_method, get_url from frappe.utils.file_manager import is_safe_path from frappe.utils.image import optimize_image, strip_exif_data @@ -703,40 +704,39 @@ def on_doctype_update(): def has_permission(doc, ptype=None, user=None): - has_access = False user = user or frappe.session.user if ptype == "create": - has_access = frappe.has_permission("File", "create", user=user) + return frappe.has_permission("File", "create", user=user) - if not doc.is_private or doc.owner in [user, "Guest"] or user == "Administrator": - has_access = True + if not doc.is_private or doc.owner == user or user == "Administrator": + return True if doc.attached_to_doctype and doc.attached_to_name: attached_to_doctype = doc.attached_to_doctype attached_to_name = doc.attached_to_name - try: - ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name) + ref_doc = frappe.get_doc(attached_to_doctype, attached_to_name) - if ptype in ["write", "create", "delete"]: - has_access = ref_doc.has_permission("write") + if ptype in ["write", "create", "delete"]: + return ref_doc.has_permission("write") + else: + return ref_doc.has_permission("read") - if ptype == "delete" and not has_access: - frappe.throw( - _( - "Cannot delete file as it belongs to {0} {1} for which you do not have permissions" - ).format(doc.attached_to_doctype, doc.attached_to_name), - frappe.PermissionError, - ) - else: - has_access = ref_doc.has_permission("read") - except frappe.DoesNotExistError: - # if parent doc is not created before file is created - # we cannot check its permission so we will use file's permission - pass + return False - return has_access + +def get_permission_query_conditions(user: str = None) -> str: + user = user or frappe.session.user + if user == "Administrator": + return "" + + readable_doctypes = ", ".join(repr(dt) for dt in get_doctypes_with_read()) + return f""" + (`tabFile`.`is_private` = 0) + OR (`tabFile`.`attached_to_doctype` IS NULL AND `tabFile`.`owner` = {user !r}) + OR (`tabFile`.`attached_to_doctype` IN ({readable_doctypes})) + """ # Note: kept at the end to not cause circular, partial imports & maintain backwards compatibility diff --git a/frappe/hooks.py b/frappe/hooks.py index 5967486824..e30b300a58 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -108,6 +108,7 @@ permission_query_conditions = { "Communication": "frappe.core.doctype.communication.communication.get_permission_query_conditions_for_communication", "Workflow Action": "frappe.workflow.doctype.workflow_action.workflow_action.get_permission_query_conditions", "Prepared Report": "frappe.core.doctype.prepared_report.prepared_report.get_permission_query_condition", + "File": "frappe.core.doctype.file.file.get_permission_query_conditions", } has_permission = { From 9c785569f9d4d6cb3d8b03739d6c149f683ae947 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 3 May 2023 11:28:20 +0200 Subject: [PATCH 02/42] test: file permissions --- frappe/core/doctype/file/test_file.py | 66 +++++++++++++++++++++------ 1 file changed, 53 insertions(+), 13 deletions(-) diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index 51e065f710..dbab111257 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -601,25 +601,27 @@ class TestAttachmentsAccess(FrappeTestCase): def setUp(self) -> None: frappe.db.delete("File", {"is_folder": 0}) - def test_attachments_access(self): + def test_list_private_attachments(self): frappe.set_user("test4@example.com") self.attached_to_doctype, self.attached_to_docname = make_test_doc() frappe.get_doc( { "doctype": "File", - "file_name": "test_user.txt", + "file_name": "test_user_attachment.txt", "attached_to_doctype": self.attached_to_doctype, "attached_to_name": self.attached_to_docname, "content": "Testing User", + "is_private": 1, } ).insert() frappe.get_doc( { "doctype": "File", - "file_name": "test_user_home.txt", + "file_name": "test_user_standalone.txt", "content": "User Home", + "is_private": 1, } ).insert() @@ -628,18 +630,20 @@ class TestAttachmentsAccess(FrappeTestCase): frappe.get_doc( { "doctype": "File", - "file_name": "test_system_manager.txt", + "file_name": "test_sm_attachment.txt", "attached_to_doctype": self.attached_to_doctype, "attached_to_name": self.attached_to_docname, "content": "Testing System Manager", + "is_private": 1, } ).insert() frappe.get_doc( { "doctype": "File", - "file_name": "test_sm_home.txt", + "file_name": "test_sm_standalone.txt", "content": "System Manager Home", + "is_private": 1, } ).insert() @@ -654,15 +658,51 @@ class TestAttachmentsAccess(FrappeTestCase): file.file_name for file in get_files_in_folder("Home/Attachments")["files"] ] - self.assertIn("test_sm_home.txt", system_manager_files) - self.assertNotIn("test_sm_home.txt", user_files) - self.assertIn("test_user_home.txt", system_manager_files) - self.assertIn("test_user_home.txt", user_files) + self.assertIn("test_sm_standalone.txt", system_manager_files) + self.assertNotIn("test_sm_standalone.txt", user_files) - self.assertIn("test_system_manager.txt", system_manager_attachments_files) - self.assertNotIn("test_system_manager.txt", user_attachments_files) - self.assertIn("test_user.txt", system_manager_attachments_files) - self.assertIn("test_user.txt", user_attachments_files) + self.assertIn("test_user_standalone.txt", user_files) + self.assertNotIn("test_user_standalone.txt", system_manager_files) + + self.assertIn("test_sm_attachment.txt", system_manager_attachments_files) + self.assertIn("test_sm_attachment.txt", user_attachments_files) + self.assertIn("test_user_attachment.txt", system_manager_attachments_files) + self.assertIn("test_user_attachment.txt", user_attachments_files) + + def test_list_public_single_file(self): + """Ensure that users are able to list public standalone files.""" + frappe.set_user("test@example.com") + frappe.get_doc( + { + "doctype": "File", + "file_name": "test_public_single.txt", + "content": "Public single File", + "is_private": 0, + } + ).insert() + + frappe.set_user("test4@example.com") + files = [file.file_name for file in get_files_in_folder("Home")["files"]] + self.assertIn("test_public_single.txt", files) + + def test_list_public_attachment(self): + """Ensure that users are able to list public attachments.""" + frappe.set_user("test@example.com") + self.attached_to_doctype, self.attached_to_docname = make_test_doc() + frappe.get_doc( + { + "doctype": "File", + "file_name": "test_public_attachment.txt", + "attached_to_doctype": self.attached_to_doctype, + "attached_to_name": self.attached_to_docname, + "content": "Public Attachment", + "is_private": 0, + } + ).insert() + + frappe.set_user("test4@example.com") + files = [file.file_name for file in get_files_in_folder("Home/Attachments")["files"]] + self.assertIn("test_public_attachment.txt", files) def tearDown(self) -> None: frappe.set_user("Administrator") From bcdc483a13ec61f83aac4230ce262bd42e6b8fff Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Thu, 15 Jun 2023 18:36:30 +0200 Subject: [PATCH 03/42] fix(test): Fix test_never_render to get path as string, exclude PYC files from static downloads This test code never actually tested the behaviour for two reasons: - first, the page had an error which meant that a 500 Error page was returned (because `path` is not a string) - second, every page contains the string "400" because it's contained in some of the icons.svg icons! I also found a minor related bug in static_page.py, allowing people to download PYC files (pycache) --- frappe/tests/test_website.py | 5 +++-- frappe/website/page_renderers/static_page.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 6c319fff0a..1031a46c80 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -340,8 +340,9 @@ class TestWebsite(FrappeTestCase): FILES_TO_SKIP = choices(list(WWW.glob("**/*.py*")), k=10) for suffix in FILES_TO_SKIP: - content = get_response_content(suffix.relative_to(WWW)) - self.assertIn("404", content) + path: str = suffix.relative_to(WWW).as_posix() + content = get_response_content(path) + self.assertIn("Not Found", content) def test_metatags(self): content = get_response_content("/_test/_test_metatags") diff --git a/frappe/website/page_renderers/static_page.py b/frappe/website/page_renderers/static_page.py index 04e58ff217..d6de2f2991 100644 --- a/frappe/website/page_renderers/static_page.py +++ b/frappe/website/page_renderers/static_page.py @@ -8,7 +8,7 @@ import frappe from frappe.website.page_renderers.base_renderer import BaseRenderer from frappe.website.utils import is_binary_file -UNSUPPORTED_STATIC_PAGE_TYPES = ("html", "md", "js", "xml", "css", "txt", "py", "json") +UNSUPPORTED_STATIC_PAGE_TYPES = ("html", "md", "js", "xml", "css", "txt", "py", "pyc", "json") class StaticPage(BaseRenderer): From 9afedfae253896be463d52efe35eac035ac5f313 Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Fri, 16 Jun 2023 13:27:49 +0200 Subject: [PATCH 04/42] fix(test): Remove frappe.local.request between requests `frappe.local.request` was not cleared between tests, which would not be a problem if all tests did set it to another Request object. But, some tests directly fetch the response content using get_response_content without first setting the frappe.local.request object (using set_request). --- frappe/tests/test_website.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py index 1031a46c80..16e82850de 100644 --- a/frappe/tests/test_website.py +++ b/frappe/tests/test_website.py @@ -11,10 +11,16 @@ from frappe.website.utils import build_response, clear_website_cache, get_home_p class TestWebsite(FrappeTestCase): def setUp(self): frappe.set_user("Guest") + self._clearRequest() def tearDown(self): frappe.db.delete("Access Log") frappe.set_user("Administrator") + self._clearRequest() + + def _clearRequest(self): + if hasattr(frappe.local, "request"): + delattr(frappe.local, "request") def test_home_page(self): frappe.set_user("Administrator") From 23846434ee03223f2460fa95b55974c06c51ab8a Mon Sep 17 00:00:00 2001 From: Corentin Flr <10946971+cogk@users.noreply.github.com> Date: Fri, 16 Jun 2023 14:39:03 +0200 Subject: [PATCH 05/42] fix(path_resolver): Avoid 200 OK for NotFoundPage renderer --- frappe/website/path_resolver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py index 37bfb3ee56..50ef2afcb4 100644 --- a/frappe/website/path_resolver.py +++ b/frappe/website/path_resolver.py @@ -51,7 +51,6 @@ class PathResolver: TemplatePage, ListPage, PrintPage, - NotFoundPage, ] for renderer in renderers: From 85ac64ddd9794f3f77115b25e51f441041ad0acc Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 19 Jun 2023 13:18:25 +0530 Subject: [PATCH 06/42] fix: log errors while getting headers and data --- .../integrations/doctype/webhook/webhook.py | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 6fa24bfb67..711a565971 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -112,16 +112,21 @@ def get_context(doc): def enqueue_webhook(doc, webhook) -> None: - webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name")) - headers = get_webhook_headers(doc, webhook) - data = get_webhook_data(doc, webhook) + try: + webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name")) + headers = get_webhook_headers(doc, webhook) + data = get_webhook_data(doc, webhook) - if webhook.is_dynamic_url: - request_url = frappe.render_template(webhook.request_url, get_context(doc)) - else: - 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 + + r = None + except Exception as e: + frappe.logger().debug({"enqueue_webhook_error": e}) + log_request(webhook.name, doc.name, request_url, headers, data, r) - r = None for i in range(3): try: r = requests.request( From 74cc21013fbd954229d98cf8d802ad56706562d3 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 19 Jun 2023 13:46:27 +0530 Subject: [PATCH 07/42] fix: validate webhook secret --- frappe/integrations/doctype/webhook/webhook.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 711a565971..2431fe2886 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -26,6 +26,7 @@ class Webhook(Document): self.validate_request_url() self.validate_request_body() self.validate_repeating_fields() + self.validate_secret() self.preview_document = None def on_update(self): @@ -74,6 +75,13 @@ class Webhook(Document): if len(webhook_data) != len(set(webhook_data)): frappe.throw(_("Same Field is entered more than once")) + def validate_secret(self): + if self.enable_security and self.webhook_secret: + try: + self.get_password("webhook_secret", False).encode("utf8") + except Exception: + frappe.throw(_("Invalid Webhook Secret")) + @frappe.whitelist() def generate_preview(self): # This function doesn't need to do anything specific as virtual fields From 3f792a80b1036a7e2c1c28cdd9f1e3f817968f42 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 23 Jun 2023 16:29:39 +0530 Subject: [PATCH 08/42] fix: return if exception occur before executing webhook --- frappe/integrations/doctype/webhook/webhook.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index 2431fe2886..c798c64c2c 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -130,10 +130,10 @@ def enqueue_webhook(doc, webhook) -> None: else: request_url = webhook.request_url - r = None except Exception as e: frappe.logger().debug({"enqueue_webhook_error": e}) - log_request(webhook.name, doc.name, request_url, headers, data, r) + log_request(webhook.name, doc.name, request_url, headers, data) + return for i in range(3): try: From fe27b8c7e0c622365fe2b034ca5b7bf37bcd7464 Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Fri, 23 Jun 2023 16:31:55 +0530 Subject: [PATCH 09/42] fix: removed redundant condition Co-authored-by: Ankush Menat --- frappe/integrations/doctype/webhook/webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index c798c64c2c..5b8c653198 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -76,7 +76,7 @@ class Webhook(Document): frappe.throw(_("Same Field is entered more than once")) def validate_secret(self): - if self.enable_security and self.webhook_secret: + if self.enable_security: try: self.get_password("webhook_secret", False).encode("utf8") except Exception: From e26152f0dc3fed4ca797a12c7086b61a3dc82207 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Jun 2023 13:28:57 +0530 Subject: [PATCH 10/42] chore: use node18 for github workflow [skip ci] --- frappe/utils/boilerplate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 0e646f9992..e2aaa26f22 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -593,7 +593,7 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 16 + node-version: 18 check-latest: true - name: Cache pip From ee74830460a13e1042a9f587de423d61b0b3594b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Jun 2023 17:32:02 +0530 Subject: [PATCH 11/42] chore: add github star CTA on server script sidebar [skip ci] --- .../server_script/server_script_list.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 frappe/core/doctype/server_script/server_script_list.js diff --git a/frappe/core/doctype/server_script/server_script_list.js b/frappe/core/doctype/server_script/server_script_list.js new file mode 100644 index 0000000000..0df447a0eb --- /dev/null +++ b/frappe/core/doctype/server_script/server_script_list.js @@ -0,0 +1,38 @@ +frappe.listview_settings["Server Script"] = { + onload: function (listview) { + add_github_star_cta(listview); + }, +}; + +function add_github_star_cta(listview) { + try { + const key = "show_github_star_banner"; + if (localStorage.getItem(key) == "false") { + return; + } + + if (listview.github_star_banner) { + listview.github_star_banner.remove(); + } + + const message = "Loving Frappe Framework?"; + const link = "https://github.com/frappe/frappe"; + const cta = "Star us on GitHub"; + + listview.github_star_banner = $(` +
+
+ ${message}
${cta} → +
+
+ + + +
+
+ `).appendTo(listview.page.sidebar); + } catch (error) { + console.error(error); + } +} From ca95b591ae8581a48dfc28f3560670da36f8da0e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Jun 2023 17:36:53 +0530 Subject: [PATCH 12/42] refactor: Pass redis connection directly --- frappe/utils/background_jobs.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index 6b0249e720..b2a3fbab24 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -9,7 +9,7 @@ from uuid import uuid4 import redis from redis.exceptions import BusyLoadingError, ConnectionError -from rq import Connection, Queue, Worker +from rq import Queue, Worker from rq.exceptions import NoSuchJobError from rq.job import Job, JobStatus from rq.logutils import setup_loghandlers @@ -253,17 +253,16 @@ def start_worker( WorkerKlass = DEQUEUE_STRATEGIES.get(strategy, Worker) - with Connection(redis_connection): - logging_level = "INFO" - if quiet: - logging_level = "WARNING" - worker = WorkerKlass(queues, name=get_worker_name(queue_name)) - worker.work( - logging_level=logging_level, - burst=burst, - date_format="%Y-%m-%d %H:%M:%S", - log_format="%(asctime)s,%(msecs)03d %(message)s", - ) + logging_level = "INFO" + if quiet: + logging_level = "WARNING" + worker = WorkerKlass(queues, name=get_worker_name(queue_name), connection=redis_connection) + worker.work( + logging_level=logging_level, + burst=burst, + date_format="%Y-%m-%d %H:%M:%S", + log_format="%(asctime)s,%(msecs)03d %(message)s", + ) def get_worker_name(queue): From 7fbc6e8175da239a230428d936b2af7f58ceeff9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Jun 2023 17:42:18 +0530 Subject: [PATCH 13/42] refactor: Simplify dequeue_strategy selection Classes arent required anymore, it can just be a parm to worker class isntead. --- frappe/utils/background_jobs.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index b2a3fbab24..a713163a7b 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -4,7 +4,7 @@ import socket import time from collections import defaultdict from functools import lru_cache -from typing import Any, Callable, Literal, NoReturn +from typing import Any, Callable, NoReturn from uuid import uuid4 import redis @@ -13,7 +13,7 @@ from rq import Queue, Worker from rq.exceptions import NoSuchJobError from rq.job import Job, JobStatus from rq.logutils import setup_loghandlers -from rq.worker import RandomWorker, RoundRobinWorker +from rq.worker import DequeueStrategy from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed import frappe @@ -230,10 +230,12 @@ def start_worker( rq_username: str | None = None, rq_password: str | None = None, burst: bool = False, - strategy: Literal["round_robin", "random"] | None = None, + strategy: DequeueStrategy | None = DequeueStrategy.DEFAULT, ) -> NoReturn | None: # pragma: no cover """Wrapper to start rq worker. Connects to redis and monitors these queues.""" - DEQUEUE_STRATEGIES = {"round_robin": RoundRobinWorker, "random": RandomWorker} + + if not strategy: + strategy = DequeueStrategy.DEFAULT if frappe._tune_gc: gc.collect() @@ -251,17 +253,17 @@ def start_worker( if os.environ.get("CI"): setup_loghandlers("ERROR") - WorkerKlass = DEQUEUE_STRATEGIES.get(strategy, Worker) - logging_level = "INFO" if quiet: logging_level = "WARNING" - worker = WorkerKlass(queues, name=get_worker_name(queue_name), connection=redis_connection) + + worker = Worker(queues, name=get_worker_name(queue_name), connection=redis_connection) worker.work( logging_level=logging_level, burst=burst, date_format="%Y-%m-%d %H:%M:%S", log_format="%(asctime)s,%(msecs)03d %(message)s", + dequeue_strategy=strategy, ) From 73bca16d77acbdf9c78eb38f0e4132a0518094ff Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Jun 2023 17:59:28 +0530 Subject: [PATCH 14/42] feat: RQ `WorkerPool` RQ now has experimental support for workerpools. When to use this? Roughly when you have more than 2 workers a workerpool might make sense, below 2 it's overhead as master "pool" process will need to run to manager workerpool itself. Why is it any better? Currently we just let supervisor duplicate the worker process N number of times. This is inefficient from shared memory POV. Forking the original process to create workers enables sharing of more memory thus leading upwards of 60-70% reduction in memory usage with pool size of 8 workers. --- frappe/commands/scheduler.py | 22 +++++++++++ frappe/core/doctype/rq_job/test_rq_job.py | 11 ++++++ frappe/utils/background_jobs.py | 46 +++++++++++++++++++++-- 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py index 36fa81f8a5..6af3a2403e 100755 --- a/frappe/commands/scheduler.py +++ b/frappe/commands/scheduler.py @@ -215,6 +215,27 @@ def start_worker( ) +@click.command("worker-pool") +@click.option( + "--queue", + type=str, + help="Queue to consume from. Multiple queues can be specified using comma-separated string. If not specified all queues are consumed.", +) +@click.option("--num-workers", type=int, default=2, help="Number of workers to spawn in pool.") +@click.option("--quiet", is_flag=True, default=False, help="Hide Log Outputs") +@click.option("--burst", is_flag=True, default=False, help="Run Worker in Burst mode.") +def start_worker_pool(queue, quiet=False, num_workers=2, burst=False): + """Start a backgrond worker""" + from frappe.utils.background_jobs import start_worker_pool + + start_worker_pool( + queue=queue, + quiet=quiet, + burst=burst, + num_workers=num_workers, + ) + + @click.command("ready-for-migration") @click.option("--site", help="site name") @pass_context @@ -251,5 +272,6 @@ commands = [ show_pending_jobs, start_scheduler, start_worker, + start_worker_pool, trigger_scheduler_event, ] diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index c39717cfd8..6512902fb3 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -96,6 +96,17 @@ class TestRQJob(FrappeTestCase): _, stderr = execute_in_shell("bench worker --queue short,default --burst", check_exit_code=True) self.assertIn("quitting", cstr(stderr)) + @timeout(20) + def test_multi_queue_burst_consumption_worker_pool(self): + for _ in range(3): + for q in ["default", "short"]: + frappe.enqueue(self.BG_JOB, sleep=1, queue=q) + + _, stderr = execute_in_shell( + "bench worker-pool --queue short,default --burst --num-workers=4", check_exit_code=True + ) + self.assertIn("quitting", cstr(stderr)) + @timeout(20) def test_job_id_dedup(self): job_id = "test_dedup" diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index a713163a7b..9008ba00ee 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -14,6 +14,7 @@ from rq.exceptions import NoSuchJobError from rq.job import Job, JobStatus from rq.logutils import setup_loghandlers from rq.worker import DequeueStrategy +from rq.worker_pool import WorkerPool from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed import frappe @@ -237,9 +238,7 @@ def start_worker( if not strategy: strategy = DequeueStrategy.DEFAULT - if frappe._tune_gc: - gc.collect() - gc.freeze() + _freeze_gc() with frappe.init_site(): # empty init is required to get redis_queue from common_site_config.json @@ -267,6 +266,47 @@ def start_worker( ) +def start_worker_pool( + queue: str | None = None, + num_workers: int = 1, + quiet: bool = False, + burst: bool = False, +) -> NoReturn: + """Start worker pool with specified number of workers. + + WARNING: This feature is considered "EXPERIMENTAL". + """ + + _freeze_gc() + + with frappe.init_site(): + redis_connection = get_redis_conn() + + if queue: + queue = [q.strip() for q in queue.split(",")] + queues = get_queue_list(queue, build_queue_name=True) + + if os.environ.get("CI"): + setup_loghandlers("ERROR") + + logging_level = "INFO" + if quiet: + logging_level = "WARNING" + + pool = WorkerPool( + queues=queues, + connection=redis_connection, + num_workers=num_workers, + ) + pool.start(logging_level=logging_level, burst=burst) + + +def _freeze_gc(): + if frappe._tune_gc: + gc.collect() + gc.freeze() + + def get_worker_name(queue): """When limiting worker to a specific queue, also append queue name to default worker name""" name = None From 4b046f0b1116830ba2326ba36fea06465782c6fb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 26 Jun 2023 15:53:15 +0200 Subject: [PATCH 15/42] refactor: use new_doc instead of get_doc --- frappe/core/doctype/file/test_file.py | 84 ++++++++++++--------------- 1 file changed, 36 insertions(+), 48 deletions(-) diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index b07e344dc0..1e7e698062 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -615,46 +615,38 @@ class TestAttachmentsAccess(FrappeTestCase): frappe.set_user("test4@example.com") self.attached_to_doctype, self.attached_to_docname = make_test_doc() - frappe.get_doc( - { - "doctype": "File", - "file_name": "test_user_attachment.txt", - "attached_to_doctype": self.attached_to_doctype, - "attached_to_name": self.attached_to_docname, - "content": "Testing User", - "is_private": 1, - } + frappe.new_doc( + "File", + file_name="test_user_attachment.txt", + attached_to_doctype=self.attached_to_doctype, + attached_to_name=self.attached_to_docname, + content="Testing User", + is_private=1, ).insert() - frappe.get_doc( - { - "doctype": "File", - "file_name": "test_user_standalone.txt", - "content": "User Home", - "is_private": 1, - } + frappe.new_doc( + "File", + file_name="test_user_standalone.txt", + content="User Home", + is_private=1, ).insert() frappe.set_user("test@example.com") - frappe.get_doc( - { - "doctype": "File", - "file_name": "test_sm_attachment.txt", - "attached_to_doctype": self.attached_to_doctype, - "attached_to_name": self.attached_to_docname, - "content": "Testing System Manager", - "is_private": 1, - } + frappe.new_doc( + "File", + file_name="test_sm_attachment.txt", + attached_to_doctype=self.attached_to_doctype, + attached_to_name=self.attached_to_docname, + content="Testing System Manager", + is_private=1, ).insert() - frappe.get_doc( - { - "doctype": "File", - "file_name": "test_sm_standalone.txt", - "content": "System Manager Home", - "is_private": 1, - } + frappe.new_doc( + "File", + file_name="test_sm_standalone.txt", + content="System Manager Home", + is_private=1, ).insert() system_manager_files = [file.file_name for file in get_files_in_folder("Home")["files"]] @@ -682,13 +674,11 @@ class TestAttachmentsAccess(FrappeTestCase): def test_list_public_single_file(self): """Ensure that users are able to list public standalone files.""" frappe.set_user("test@example.com") - frappe.get_doc( - { - "doctype": "File", - "file_name": "test_public_single.txt", - "content": "Public single File", - "is_private": 0, - } + frappe.new_doc( + "File", + file_name="test_public_single.txt", + content="Public single File", + is_private=0, ).insert() frappe.set_user("test4@example.com") @@ -699,15 +689,13 @@ class TestAttachmentsAccess(FrappeTestCase): """Ensure that users are able to list public attachments.""" frappe.set_user("test@example.com") self.attached_to_doctype, self.attached_to_docname = make_test_doc() - frappe.get_doc( - { - "doctype": "File", - "file_name": "test_public_attachment.txt", - "attached_to_doctype": self.attached_to_doctype, - "attached_to_name": self.attached_to_docname, - "content": "Public Attachment", - "is_private": 0, - } + frappe.new_doc( + "File", + file_name="test_public_attachment.txt", + attached_to_doctype=self.attached_to_doctype, + attached_to_name=self.attached_to_docname, + content="Public Attachment", + is_private=0, ).insert() frappe.set_user("test4@example.com") From e2c468df6026992a706116c35074c3bcd6153a2f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Jun 2023 19:56:04 +0530 Subject: [PATCH 16/42] fix: set prepared report in background (#21485) --- frappe/core/doctype/report/report.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 8cdbc24074..dba82bada5 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -135,7 +135,7 @@ class Report(Document): # automatically set as prepared execution_time = (datetime.datetime.now() - start_time).total_seconds() if execution_time > threshold and not self.prepared_report: - self.db_set("prepared_report", 1) + frappe.enqueue(enable_prepared_report, report=self.name) frappe.cache.hset("report_execution_time", self.name, execution_time) @@ -382,3 +382,7 @@ def get_group_by_column_label(args, meta): function=sql_fn_map[args.aggregate_function], fieldlabel=aggregate_on_label ) return label + + +def enable_prepared_report(report: str): + frappe.db.set_value("Report", report, "prepared_report", 1) From b9bd05581323085d44b365b2027ad011e180bc04 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 27 Jun 2023 17:10:46 +0530 Subject: [PATCH 17/42] fix(WebForm): auto-increment link field --- frappe/public/js/frappe/form/controls/autocomplete.js | 9 +++++++++ frappe/website/doctype/web_form/web_form.py | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 27bf75e807..c0674956e9 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -174,6 +174,15 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui if (typeof options[0] === "string") { options = options.map((o) => ({ label: o, value: o })); } + + options = options.map((o) => { + if (typeof o !== "string") { + o.label = o.label.toString(); + o.value = o.value.toString(); + } + return o; + }); + return options; } diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index fd9949c45f..75f8793b4a 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -631,7 +631,7 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals if title_field and show_title_field_in_link: return json.dumps(link_options, default=str) else: - return "\n".join([doc.value for doc in link_options]) + return "\n".join([str(doc.value) for doc in link_options]) else: raise frappe.PermissionError( From b9f000e1f9f460e1e800a6bef381883c3694d9e4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Jun 2023 10:07:14 +0530 Subject: [PATCH 18/42] refactor!: Log 5xx error to error log instead of error snapshot Also move log_error function to right location --- frappe/__init__.py | 37 +----- frappe/app.py | 4 +- frappe/hooks.py | 1 - frappe/utils/error.py | 275 ++++++------------------------------------ 4 files changed, 43 insertions(+), 274 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 998d881a13..13e9448109 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -2213,41 +2213,6 @@ def logger( ) -def log_error(title=None, message=None, reference_doctype=None, reference_name=None): - """Log error to Error Log""" - # Parameter ALERT: - # the title and message may be swapped - # the better API for this is log_error(title, message), and used in many cases this way - # this hack tries to be smart about whats a title (single line ;-)) and fixes it - - traceback = None - if message: - if "\n" in title: # traceback sent as title - traceback, title = title, message - else: - traceback = message - - title = title or "Error" - traceback = as_unicode(traceback or get_traceback(with_context=True)) - - if not db: - print(f"Failed to log error in db: {title}") - return - - error_log = get_doc( - doctype="Error Log", - error=traceback, - method=title, - reference_doctype=reference_doctype, - reference_name=reference_name, - ) - - if flags.read_only: - error_log.deferred_insert() - else: - return error_log.insert(ignore_permissions=True) - - def get_desk_link(doctype, name): html = ( '{doctype_local} {name}' @@ -2439,6 +2404,8 @@ def validate_and_sanitize_search_inputs(fn): return wrapper +from frappe.utils.error import log_error # noqa: backward compatibility + if _tune_gc: # generational GC gets triggered after certain allocs (g0) which is 700 by default. # This number is quite small for frappe where a single query can potentially create 700+ diff --git a/frappe/app.py b/frappe/app.py index 5113c858a5..1cbdca1361 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -22,7 +22,7 @@ from frappe import _ from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest from frappe.middlewares import StaticDataMiddleware from frappe.utils import cint, get_site_name, sanitize_html -from frappe.utils.error import make_error_snapshot +from frappe.utils.error import log_error_snapshot from frappe.website.serve import get_response local_manager = LocalManager(frappe.local) @@ -346,7 +346,7 @@ def handle_exception(e): frappe.local.login_manager.clear_cookies() if http_status_code >= 500: - make_error_snapshot(e) + log_error_snapshot(e) if return_as_message: response = get_response("message", http_status_code=http_status_code) diff --git a/frappe/hooks.py b/frappe/hooks.py index f160d93ecc..85a28feb39 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -214,7 +214,6 @@ scheduler_events = { "hourly": [ "frappe.model.utils.link_count.update_link_count", "frappe.model.utils.user_settings.sync_user_settings", - "frappe.utils.error.collect_error_snapshots", "frappe.desk.page.backups.backups.delete_downloadable_backups", "frappe.deferred_insert.save_to_db", "frappe.desk.form.document_follow.send_hourly_updates", diff --git a/frappe/utils/error.py b/frappe/utils/error.py index a9891cb532..d44bdef47f 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -1,17 +1,10 @@ # Copyright (c) 2015, Maxwell Morais and contributors # License: MIT. See LICENSE -import datetime import functools import inspect -import json -import linecache -import os -import sys -import traceback import frappe -from frappe.utils import cstr, encode EXCLUDE_EXCEPTIONS = ( frappe.AuthenticationError, @@ -37,194 +30,57 @@ def _is_ldap_exception(e): return False -def make_error_snapshot(exception): - if frappe.conf.disable_error_snapshot: +def log_error( + title=None, message=None, reference_doctype=None, reference_name=None, *, defer_insert=False +): + """Log error to Error Log""" + # Parameter ALERT: + # the title and message may be swapped + # the better API for this is log_error(title, message), and used in many cases this way + # this hack tries to be smart about whats a title (single line ;-)) and fixes it + + traceback = None + if message: + if "\n" in title: # traceback sent as title + traceback, title = title, message + else: + traceback = message + + title = title or "Error" + traceback = frappe.as_unicode(traceback or frappe.get_traceback(with_context=True)) + + if not frappe.db: + print(f"Failed to log error in db: {title}") return + error_log = frappe.get_doc( + doctype="Error Log", + error=traceback, + method=title, + reference_doctype=reference_doctype, + reference_name=reference_name, + ) + + if frappe.flags.read_only or defer_insert: + error_log.deferred_insert() + else: + return error_log.insert(ignore_permissions=True) + + +def log_error_snapshot(exception: Exception): + if isinstance(exception, EXCLUDE_EXCEPTIONS) or _is_ldap_exception(exception): return logger = frappe.logger(with_more_info=True) try: - error_id = "{timestamp:s}-{ip:s}-{hash:s}".format( - timestamp=cstr(datetime.datetime.now()), - ip=frappe.local.request_ip or "127.0.0.1", - hash=frappe.generate_hash(length=3), - ) - snapshot_folder = get_error_snapshot_path() - frappe.create_folder(snapshot_folder) - - snapshot_file_path = os.path.join(snapshot_folder, f"{error_id}.json") - snapshot = get_snapshot(exception) - - with open(encode(snapshot_file_path), "wb") as error_file: - error_file.write(encode(frappe.as_json(snapshot))) - - logger.error(f"New Exception collected with id: {error_id}") - + log_error(title=str(exception), defer_insert=True) + logger.error("New Exception collected in error log") except Exception as e: logger.error(f"Could not take error snapshot: {e}", exc_info=True) -def get_snapshot(exception, context=10): - import pydoc - - """ - Return a dict describing a given traceback (based on cgitb.text) - """ - - etype, evalue, etb = sys.exc_info() - if isinstance(etype, type): - etype = etype.__name__ - - # creates a snapshot dict with some basic information - - s = { - "pyver": "Python {version:s}: {executable:s} (prefix: {prefix:s})".format( - version=sys.version.split(maxsplit=1)[0], executable=sys.executable, prefix=sys.prefix - ), - "timestamp": cstr(datetime.datetime.now()), - "traceback": traceback.format_exc(), - "frames": [], - "etype": cstr(etype), - "evalue": cstr(repr(evalue)), - "exception": {}, - "locals": {}, - } - - # start to process frames - records = inspect.getinnerframes(etb, 5) - - for frame, file, lnum, func, lines, index in records: - file = file and os.path.abspath(file) or "?" - args, varargs, varkw, locals = inspect.getargvalues(frame) - call = "" - - if func != "?": - call = inspect.formatargvalues( - args, varargs, varkw, locals, formatvalue=lambda value: f"={pydoc.text.repr(value)}" - ) - - # basic frame information - f = {"file": file, "func": func, "call": call, "lines": {}, "lnum": lnum} - - def reader(lnum=[lnum]): # noqa - try: - # B023: function is evaluated immediately, binding not necessary - return linecache.getline(file, lnum[0]) # noqa: B023 - finally: - lnum[0] += 1 - - vars = _scanvars(reader, frame, locals) - - # if it is a view, replace with generated code - # if file.endswith('html'): - # lmin = lnum > context and (lnum - context) or 0 - # lmax = lnum + context - # lines = code.split("\n")[lmin:lmax] - # index = min(context, lnum) - 1 - - if index is not None: - i = lnum - index - for line in lines: - f["lines"][i] = line.rstrip() - i += 1 - - # dump local variable (referenced in current line only) - f["dump"] = {} - for name, where, value in vars: - if name in f["dump"]: - continue - if value is not __UNDEF__: - if where == "global": - name = f"global {name:s}" - elif where != "local": - name = where + " " + name.split(".")[-1] - f["dump"][name] = pydoc.text.repr(value) - else: - f["dump"][name] = "undefined" - - s["frames"].append(f) - - # add exception type, value and attributes - if isinstance(evalue, BaseException): - for name in dir(evalue): - if name != "messages" and not name.startswith("__"): - value = pydoc.text.repr(getattr(evalue, name)) - s["exception"][name] = encode(value) - - # add all local values (of last frame) to the snapshot - for name, value in locals.items(): - s["locals"][name] = value if isinstance(value, str) else pydoc.text.repr(value) - - return s - - -def collect_error_snapshots(): - """Scheduled task to collect error snapshots from files and push into Error Snapshot table""" - if frappe.conf.disable_error_snapshot: - return - - try: - path = get_error_snapshot_path() - if not os.path.exists(path): - return - - for fname in os.listdir(path): - fullpath = os.path.join(path, fname) - - try: - with open(fullpath) as filedata: - data = json.load(filedata) - - except ValueError: - # empty file - os.remove(fullpath) - continue - - for field in ["locals", "exception", "frames"]: - data[field] = frappe.as_json(data[field]) - - doc = frappe.new_doc("Error Snapshot") - doc.update(data) - doc.save() - - frappe.db.commit() - - os.remove(fullpath) - - clear_old_snapshots() - - except Exception as e: - make_error_snapshot(e) - - # prevent creation of unlimited error snapshots - raise - - -def clear_old_snapshots(): - """Clear snapshots that are older than a month""" - from frappe.query_builder import DocType, Interval - from frappe.query_builder.functions import Now - - ErrorSnapshot = DocType("Error Snapshot") - frappe.db.delete(ErrorSnapshot, filters=(ErrorSnapshot.creation < (Now() - Interval(months=1)))) - - path = get_error_snapshot_path() - today = datetime.datetime.now() - - for file in os.listdir(path): - p = os.path.join(path, file) - ctime = datetime.datetime.fromtimestamp(os.path.getctime(p)) - if (today - ctime).days > 31: - os.remove(os.path.join(path, p)) - - -def get_error_snapshot_path(): - return frappe.get_site_path("error-snapshots") - - def get_default_args(func): """Get default arguments of a function from its signature.""" signature = inspect.signature(func) @@ -270,56 +126,3 @@ def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None): return wrapper_raise_error_on_no_output return decorator_raise_error_on_no_output - - -# Vendored from cgitb standard library reused under PSF License: -# https://github.com/python/cpython/blob/main/LICENSE - - -import keyword -import tokenize - -__UNDEF__ = [] # a special sentinel object - - -def _scanvars(reader, frame, locals): - """Scan one logical line of Python and look up values of variables used.""" - vars, lasttoken, parent, prefix, value = [], None, None, "", __UNDEF__ - for ttype, token, start, end, line in tokenize.generate_tokens(reader): - if ttype == tokenize.NEWLINE: - break - if ttype == tokenize.NAME and token not in keyword.kwlist: - if lasttoken == ".": - if parent is not __UNDEF__: - value = getattr(parent, token, __UNDEF__) - vars.append((prefix + token, prefix, value)) - else: - where, value = _lookup(token, frame, locals) - vars.append((token, where, value)) - elif token == ".": - prefix += lasttoken + "." - parent = value - else: - parent, prefix = None, "" - lasttoken = token - return vars - - -def _lookup(name, frame, locals): - """Find the value for a given name in the given environment.""" - if name in locals: - return "local", locals[name] - if name in frame.f_globals: - return "global", frame.f_globals[name] - if "__builtins__" in frame.f_globals: - builtins = frame.f_globals["__builtins__"] - if type(builtins) is type({}): # noqa - if name in builtins: - return "builtin", builtins[name] - else: - if hasattr(builtins, name): - return "builtin", getattr(builtins, name) - return None, __UNDEF__ - - -# end: vendored code From ae8ee5064c972f66411030bcf7e9d6f9a4236743 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Jun 2023 10:25:26 +0530 Subject: [PATCH 19/42] refactor!: Remove error snapshot --- .../core/doctype/error_snapshot/__init__.py | 0 .../doctype/error_snapshot/error_object.html | 12 -- .../error_snapshot/error_snapshot.html | 77 ----------- .../doctype/error_snapshot/error_snapshot.js | 20 --- .../error_snapshot/error_snapshot.json | 130 ------------------ .../doctype/error_snapshot/error_snapshot.py | 40 ------ .../error_snapshot/error_snapshot_list.js | 19 --- .../error_snapshot/test_error_snapshot.py | 11 -- .../core/doctype/log_settings/log_settings.py | 2 - .../doctype/log_settings/test_log_settings.py | 1 - frappe/core/notifications.py | 1 - frappe/core/workspace/build/build.json | 128 ++++++++--------- frappe/installer.py | 1 - frappe/patches.txt | 3 +- .../v14_0/clear_long_pending_stale_logs.py | 1 - 15 files changed, 60 insertions(+), 386 deletions(-) delete mode 100644 frappe/core/doctype/error_snapshot/__init__.py delete mode 100644 frappe/core/doctype/error_snapshot/error_object.html delete mode 100644 frappe/core/doctype/error_snapshot/error_snapshot.html delete mode 100644 frappe/core/doctype/error_snapshot/error_snapshot.js delete mode 100644 frappe/core/doctype/error_snapshot/error_snapshot.json delete mode 100644 frappe/core/doctype/error_snapshot/error_snapshot.py delete mode 100644 frappe/core/doctype/error_snapshot/error_snapshot_list.js delete mode 100644 frappe/core/doctype/error_snapshot/test_error_snapshot.py diff --git a/frappe/core/doctype/error_snapshot/__init__.py b/frappe/core/doctype/error_snapshot/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/core/doctype/error_snapshot/error_object.html b/frappe/core/doctype/error_snapshot/error_object.html deleted file mode 100644 index 450bfacfc6..0000000000 --- a/frappe/core/doctype/error_snapshot/error_object.html +++ /dev/null @@ -1,12 +0,0 @@ -{% if (Object.prototype.toString.call(x) === "[object Object]") { %} - - {% for (var key in x) { %} - - - - - {% } %} -
{{ key }}{{ x[key] }}
-{% } else { %} - {{ x }} -{% } %} diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.html b/frappe/core/doctype/error_snapshot/error_snapshot.html deleted file mode 100644 index 6f449e0fe9..0000000000 --- a/frappe/core/doctype/error_snapshot/error_snapshot.html +++ /dev/null @@ -1,77 +0,0 @@ - -{% function id(){ return id._old_id++; }; id._old_id = 0; %} -

{{ __("Error Report") }}

-

{{ doc.pyver }}

-
-
{{ __("Timestamp") }}:
-
{{ doc.timestamp }}
-
{{ __("Relapsed") }}
-
{{ doc.relapses }}
-
- -

{{ __("Exception") }}

-{{ frappe.render_template("error_object", {x: JSON.parse(doc.exception)}) }} - -

{{ __("Locals") }}

-{{ frappe.render_template("error_object", {x: JSON.parse(doc.locals)} )}} - -

{{ __("Traceback") }}

-{% var frames = JSON.parse(doc.frames); %} -{% for (var i in frames) { %} - {% var frameid = id(), frame = frames[i] %} -

{{ frame.file }}: {{ frame.lnum }} -

-
-
- {% for (var index in frame.lines) { %} - {% var line = frame.lines[index] %} -
- {{ index }} - {{ line }} -
- {% } %} -
-
- - {{ __("Locals") }} - -
-
-
-
-
-

{{ __("Locals") }}

- {{ frappe.render_template("error_object", {x: frame.dump }) }} -
-
-

-{% } %} diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.js b/frappe/core/doctype/error_snapshot/error_snapshot.js deleted file mode 100644 index f8a7e3ded5..0000000000 --- a/frappe/core/doctype/error_snapshot/error_snapshot.js +++ /dev/null @@ -1,20 +0,0 @@ -frappe.ui.form.on("Error Snapshot", "load", function (frm) { - frm.set_read_only(true); -}); - -frappe.ui.form.on("Error Snapshot", "refresh", function (frm) { - frm.set_df_property( - "view", - "options", - frappe.render_template("error_snapshot", { doc: frm.doc }) - ); - - if (frm.doc.relapses) { - frm.add_custom_button(__("Show Relapses"), function () { - frappe.route_options = { - parent_error_snapshot: frm.doc.name, - }; - frappe.set_route("List", "Error Snapshot"); - }); - } -}); diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.json b/frappe/core/doctype/error_snapshot/error_snapshot.json deleted file mode 100644 index b92db8f99a..0000000000 --- a/frappe/core/doctype/error_snapshot/error_snapshot.json +++ /dev/null @@ -1,130 +0,0 @@ -{ - "actions": [], - "creation": "2015-11-28 00:57:39.766888", - "doctype": "DocType", - "document_type": "System", - "engine": "InnoDB", - "field_order": [ - "view", - "seen", - "evalue", - "timestamp", - "relapses", - "etype", - "traceback", - "parent_error_snapshot", - "pyver", - "exception", - "locals", - "frames" - ], - "fields": [ - { - "fieldname": "view", - "fieldtype": "HTML", - "label": "Snapshot View" - }, - { - "default": "0", - "fieldname": "seen", - "fieldtype": "Check", - "hidden": 1, - "in_filter": 1, - "label": "Seen" - }, - { - "fieldname": "evalue", - "fieldtype": "Code", - "hidden": 1, - "in_list_view": 1, - "label": "Friendly Title", - "read_only": 1 - }, - { - "fieldname": "timestamp", - "fieldtype": "Datetime", - "hidden": 1, - "label": "Timestamp", - "read_only": 1 - }, - { - "default": "1", - "fieldname": "relapses", - "fieldtype": "Int", - "hidden": 1, - "in_list_view": 1, - "label": "Relapses", - "read_only": 1 - }, - { - "fieldname": "etype", - "fieldtype": "Data", - "hidden": 1, - "label": "Exception Type", - "read_only": 1 - }, - { - "fieldname": "traceback", - "fieldtype": "Code", - "hidden": 1, - "label": "Traceback", - "read_only": 1 - }, - { - "fieldname": "parent_error_snapshot", - "fieldtype": "Data", - "hidden": 1, - "label": "Parent Error Snapshot" - }, - { - "fieldname": "pyver", - "fieldtype": "Code", - "hidden": 1, - "label": "Pyver", - "read_only": 1 - }, - { - "fieldname": "exception", - "fieldtype": "Code", - "hidden": 1, - "label": "Exception" - }, - { - "fieldname": "locals", - "fieldtype": "Code", - "hidden": 1, - "label": "Locals" - }, - { - "fieldname": "frames", - "fieldtype": "Code", - "hidden": 1, - "label": "Frames" - } - ], - "in_create": 1, - "links": [], - "modified": "2022-08-03 12:20:53.504160", - "modified_by": "Administrator", - "module": "Core", - "name": "Error Snapshot", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "share": 1, - "write": 1 - } - ], - "sort_field": "timestamp", - "sort_order": "DESC", - "states": [], - "title_field": "evalue" -} \ No newline at end of file diff --git a/frappe/core/doctype/error_snapshot/error_snapshot.py b/frappe/core/doctype/error_snapshot/error_snapshot.py deleted file mode 100644 index acc49c78cd..0000000000 --- a/frappe/core/doctype/error_snapshot/error_snapshot.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors -# License: MIT. See LICENSE - -import frappe -from frappe.model.document import Document -from frappe.query_builder import Interval -from frappe.query_builder.functions import Now - - -class ErrorSnapshot(Document): - no_feed_on_delete = True - - def onload(self): - if not self.parent_error_snapshot: - self.db_set("seen", 1, update_modified=False) - - for relapsed in frappe.get_all("Error Snapshot", filters={"parent_error_snapshot": self.name}): - frappe.db.set_value("Error Snapshot", relapsed.name, "seen", 1, update_modified=False) - - frappe.local.flags.commit = True - - def validate(self): - parent = frappe.get_all( - "Error Snapshot", - filters={"evalue": self.evalue, "parent_error_snapshot": ""}, - fields=["name", "relapses", "seen"], - limit_page_length=1, - ) - - if parent: - parent = parent[0] - self.update({"parent_error_snapshot": parent["name"]}) - frappe.db.set_value("Error Snapshot", parent["name"], "relapses", parent["relapses"] + 1) - if parent["seen"]: - frappe.db.set_value("Error Snapshot", parent["name"], "seen", 0) - - @staticmethod - def clear_old_logs(days=30): - table = frappe.qb.DocType("Error Snapshot") - frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days)))) diff --git a/frappe/core/doctype/error_snapshot/error_snapshot_list.js b/frappe/core/doctype/error_snapshot/error_snapshot_list.js deleted file mode 100644 index b331788852..0000000000 --- a/frappe/core/doctype/error_snapshot/error_snapshot_list.js +++ /dev/null @@ -1,19 +0,0 @@ -frappe.listview_settings["Error Snapshot"] = { - add_fields: ["parent_error_snapshot", "relapses", "seen"], - filters: [ - ["parent_error_snapshot", "=", null], - ["seen", "=", false], - ], - get_indicator: function (doc) { - if (doc.parent_error_snapshot && doc.parent_error_snapshot.length) { - return [__("Relapsed"), !doc.seen ? "orange" : "blue", "parent_error_snapshot,!=,"]; - } else { - return [__("First Level"), !doc.seen ? "red" : "green", "parent_error_snapshot,=,"]; - } - }, - onload: function (listview) { - frappe.require("logtypes.bundle.js", () => { - frappe.utils.logtypes.show_log_retention_message(cur_list.doctype); - }); - }, -}; diff --git a/frappe/core/doctype/error_snapshot/test_error_snapshot.py b/frappe/core/doctype/error_snapshot/test_error_snapshot.py deleted file mode 100644 index 4779d56c7b..0000000000 --- a/frappe/core/doctype/error_snapshot/test_error_snapshot.py +++ /dev/null @@ -1,11 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: MIT. See LICENSE -from frappe.tests.utils import FrappeTestCase -from frappe.utils.logger import sanitized_dict - -# test_records = frappe.get_test_records('Error Snapshot') - - -class TestErrorSnapshot(FrappeTestCase): - def test_form_dict_sanitization(self): - self.assertNotEqual(sanitized_dict({"pwd": "SECRET", "usr": "WHAT"}).get("pwd"), "SECRET") diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index 832be49f3c..c4d311cb3d 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -14,7 +14,6 @@ DEFAULT_LOGTYPES_RETENTION = { "Error Log": 30, "Activity Log": 90, "Email Queue": 30, - "Error Snapshot": 30, "Scheduled Job Log": 90, "Route History": 90, "Submission Queue": 30, @@ -156,7 +155,6 @@ LOG_DOCTYPES = [ "Route History", "Email Queue", "Email Queue Recipient", - "Error Snapshot", "Error Log", ] diff --git a/frappe/core/doctype/log_settings/test_log_settings.py b/frappe/core/doctype/log_settings/test_log_settings.py index d7f43a181d..edee098553 100644 --- a/frappe/core/doctype/log_settings/test_log_settings.py +++ b/frappe/core/doctype/log_settings/test_log_settings.py @@ -62,7 +62,6 @@ class TestLogSettings(FrappeTestCase): "Activity Log", "Email Queue", "Route History", - "Error Snapshot", "Scheduled Job Log", ] diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py index 093418e345..26e920bca9 100644 --- a/frappe/core/notifications.py +++ b/frappe/core/notifications.py @@ -11,7 +11,6 @@ def get_notification_config(): "Communication": {"status": "Open", "communication_type": "Communication"}, "ToDo": "frappe.core.notifications.get_things_todo", "Event": "frappe.core.notifications.get_todays_events", - "Error Snapshot": {"seen": 0, "parent_error_snapshot": None}, "Workflow Action": {"status": "Open"}, }, } diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json index b917f88e27..12bef0ed89 100644 --- a/frappe/core/workspace/build/build.json +++ b/frappe/core/workspace/build/build.json @@ -155,74 +155,6 @@ "onboard": 0, "type": "Link" }, - { - "hidden": 0, - "is_query_report": 0, - "label": "System Logs", - "link_count": 6, - "onboard": 0, - "type": "Card Break" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Background Jobs", - "link_count": 0, - "link_to": "RQ Job", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Scheduled Jobs Logs", - "link_count": 0, - "link_to": "Scheduled Job Log", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Error Logs", - "link_count": 0, - "link_to": "Error Log", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Error Snapshot", - "link_count": 0, - "link_to": "Error Snapshot", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Communication Logs", - "link_count": 0, - "link_to": "Communication", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, - { - "hidden": 0, - "is_query_report": 0, - "label": "Activity Log", - "link_count": 0, - "link_to": "Activity Log", - "link_type": "DocType", - "onboard": 0, - "type": "Link" - }, { "hidden": 0, "is_query_report": 0, @@ -331,9 +263,67 @@ "link_type": "DocType", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "System Logs", + "link_count": 5, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Background Jobs", + "link_count": 0, + "link_to": "RQ Job", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Scheduled Jobs Logs", + "link_count": 0, + "link_to": "Scheduled Job Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Error Logs", + "link_count": 0, + "link_to": "Error Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Communication Logs", + "link_count": 0, + "link_to": "Communication", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Activity Log", + "link_count": 0, + "link_to": "Activity Log", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2023-05-24 14:47:24.395259", + "modified": "2023-06-28 10:30:17.228167", "modified_by": "Administrator", "module": "Core", "name": "Build", diff --git a/frappe/installer.py b/frappe/installer.py index 4f02e207bd..775e5b9b02 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -617,7 +617,6 @@ def make_site_dirs(): os.path.join("public", "files"), os.path.join("private", "backups"), os.path.join("private", "files"), - "error-snapshots", "locks", "logs", ]: diff --git a/frappe/patches.txt b/frappe/patches.txt index c26b1a74d7..ebdda9b220 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -31,7 +31,6 @@ execute:frappe.reload_doc('core', 'doctype', 'user') #2017-10-27 execute:frappe.reload_doc('core', 'doctype', 'report_column') execute:frappe.reload_doc('core', 'doctype', 'report_filter') execute:frappe.reload_doc('core', 'doctype', 'report') #2020-08-25 -execute:frappe.reload_doc('core', 'doctype', 'error_snapshot') execute:frappe.get_doc("User", "Guest").save() execute:frappe.delete_doc("DocType", "Control Panel", force=1) execute:frappe.delete_doc("DocType", "Tag") @@ -42,7 +41,6 @@ execute:frappe.db.sql("delete from `tabProperty Setter` where `property` = 'idx' execute:frappe.db.sql("delete from tabSessions where user is null") execute:frappe.delete_doc("DocType", "Backup Manager") execute:frappe.permissions.reset_perms("Web Page") -execute:frappe.permissions.reset_perms("Error Snapshot") execute:frappe.db.sql("delete from `tabWeb Page` where ifnull(template_path, '')!=''") execute:frappe.core.doctype.language.language.update_language_names() # 2017-04-12 execute:frappe.db.set_value("Print Settings", "Print Settings", "add_draft_heading", 1) @@ -227,3 +225,4 @@ frappe.patches.v15_0.remove_background_jobs_from_dropdown frappe.desk.doctype.form_tour.patches.introduce_ui_tours execute:frappe.delete_doc_if_exists("Workspace", "Customization") execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter") +execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot") diff --git a/frappe/patches/v14_0/clear_long_pending_stale_logs.py b/frappe/patches/v14_0/clear_long_pending_stale_logs.py index 53127cb197..e419b1e562 100644 --- a/frappe/patches/v14_0/clear_long_pending_stale_logs.py +++ b/frappe/patches/v14_0/clear_long_pending_stale_logs.py @@ -15,7 +15,6 @@ def execute(): "Email Queue": get_current_setting("clear_email_queue_after") or 30, # child table on email queue "Email Queue Recipient": get_current_setting("clear_email_queue_after") or 30, - "Error Snapshot": get_current_setting("clear_error_log_after") or 90, # newly added "Scheduled Job Log": 90, } From 37577b752ef025f9815e0996ca88417ca608d7a1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Jun 2023 10:57:54 +0530 Subject: [PATCH 20/42] fix: log settings should delete delted doctypes [skip ci] --- frappe/core/doctype/log_settings/log_settings.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/log_settings/log_settings.py b/frappe/core/doctype/log_settings/log_settings.py index c4d311cb3d..623e358b6c 100644 --- a/frappe/core/doctype/log_settings/log_settings.py +++ b/frappe/core/doctype/log_settings/log_settings.py @@ -44,11 +44,11 @@ def _supports_log_clearing(doctype: str) -> bool: class LogSettings(Document): def validate(self): - self._remove_unsupported_doctypes() + self.remove_unsupported_doctypes() self._deduplicate_entries() self.add_default_logtypes() - def _remove_unsupported_doctypes(self): + def remove_unsupported_doctypes(self): for entry in list(self.logs_to_clear): if _supports_log_clearing(entry.ref_doctype): continue @@ -113,6 +113,7 @@ class LogSettings(Document): def run_log_clean_up(): doc = frappe.get_doc("Log Settings") + doc.remove_unsupported_doctypes() doc.add_default_logtypes() doc.save() doc.clear_logs() From 537d5511127235b41b44f7c4fc15df340f89d5de Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 28 Jun 2023 11:17:17 +0530 Subject: [PATCH 21/42] refactor: Deprecate broken-img mixin --- frappe/public/scss/common/mixins.scss | 41 ++----------------------- frappe/public/scss/desk/file_view.scss | 5 --- frappe/public/scss/desk/image_view.scss | 5 --- frappe/public/scss/desk/kanban.scss | 5 --- 4 files changed, 2 insertions(+), 54 deletions(-) diff --git a/frappe/public/scss/common/mixins.scss b/frappe/public/scss/common/mixins.scss index 6d71ea9d6f..55b54d9de3 100644 --- a/frappe/public/scss/common/mixins.scss +++ b/frappe/public/scss/common/mixins.scss @@ -45,42 +45,5 @@ $background-color: var(--bg-color), $border-radius: var(--border-radius), ) { - - @if $content { - img:after { - content: url($content); - } - } @else { - img:after { - content: url("data:image/svg+xml;utf8,"); - } - } - - img[alt]:after { - height: $height; - top: $top; - left: $left; - background-color: $background-color; - border-radius: $border-radius; - width: 100%; - position: absolute; - @include flex(); - z-index: 1; - } -} - -// @mixin img-foreground() { -// content: "\f1c5"; -// display: block; -// font-style: normal; -// font-family: FontAwesome; -// font-size: 32px; -// color: var(--text-muted); - -// position: absolute; -// top: 50%; -// transform: translateY(-50%); -// left: 0; -// width: 100%; -// text-align: center; -// } \ No newline at end of file + // Deprecated: Does not work as expected anymore. Also, this never worked in Safari. +} \ No newline at end of file diff --git a/frappe/public/scss/desk/file_view.scss b/frappe/public/scss/desk/file_view.scss index d074a7efdd..29e49ab65e 100644 --- a/frappe/public/scss/desk/file_view.scss +++ b/frappe/public/scss/desk/file_view.scss @@ -97,11 +97,6 @@ color: transparent; position: relative; } - - @include broken-img( - $height: 70px, - $top: -15px, - ); } } diff --git a/frappe/public/scss/desk/image_view.scss b/frappe/public/scss/desk/image_view.scss index 3b9d033406..063af27923 100644 --- a/frappe/public/scss/desk/image_view.scss +++ b/frappe/public/scss/desk/image_view.scss @@ -153,11 +153,6 @@ position: relative; width: 100%; } - - @include broken-img( - $height: 175px, - $border-radius: 0 - ); } .image-title { diff --git a/frappe/public/scss/desk/kanban.scss b/frappe/public/scss/desk/kanban.scss index 8f286b7b35..5a748c581c 100644 --- a/frappe/public/scss/desk/kanban.scss +++ b/frappe/public/scss/desk/kanban.scss @@ -181,11 +181,6 @@ color: transparent; position: relative; } - - @include broken-img( - $height: 125px, - $top: -4px, - ) } .kanban-card-body { From f3c876e43e0c8182220e6d5a0e6365efcfa4fd97 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 28 Jun 2023 11:43:23 +0530 Subject: [PATCH 22/42] chore: ignore pyo files too --- frappe/website/page_renderers/static_page.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/website/page_renderers/static_page.py b/frappe/website/page_renderers/static_page.py index d6de2f2991..f992743c6a 100644 --- a/frappe/website/page_renderers/static_page.py +++ b/frappe/website/page_renderers/static_page.py @@ -8,7 +8,7 @@ import frappe from frappe.website.page_renderers.base_renderer import BaseRenderer from frappe.website.utils import is_binary_file -UNSUPPORTED_STATIC_PAGE_TYPES = ("html", "md", "js", "xml", "css", "txt", "py", "pyc", "json") +UNSUPPORTED_STATIC_PAGE_TYPES = ("html", "md", "js", "xml", "css", "txt", "py", "pyc", "json", "pyo") class StaticPage(BaseRenderer): From aaf1bd7f4bcd21db2e36888ac48d91d2260f772f Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 28 Jun 2023 11:46:17 +0530 Subject: [PATCH 23/42] style: fix linter whitespace issue --- frappe/public/js/frappe/form/controls/autocomplete.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index c0674956e9..7b91a2031c 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -182,7 +182,7 @@ frappe.ui.form.ControlAutocomplete = class ControlAutoComplete extends frappe.ui } return o; }); - + return options; } From 814265d24581ca6024ebdf8c4e53df95ac060726 Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Wed, 28 Jun 2023 13:34:36 +0530 Subject: [PATCH 24/42] fix(minor): workflow state indicator (#21508) In List view only `Status` type was considered for indicator. Added fields with `Workflow State` as options --- frappe/public/js/frappe/list/list_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index c0eef27b9d..1ca72a4e45 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -711,7 +711,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } get_column_html(col, doc) { - if (col.type === "Status") { + if (col.type === "Status" || col.df?.options == "Workflow State") { return `