From 032df946bead39b603e1eda6acd2049475bde757 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Nov 2022 14:08:35 +0530 Subject: [PATCH 01/43] test: bg jobs test cleanup (#18767) * test: fix flaky RQ job tests Sometimes stop_job doesn't succeed and causes tests to timeout. Reduced sleep time to avoid this in tests. We are still testing all the important features - monitoring. * build(deps): Bump RQ to latest version Minor bugfixes that affect us ref: https://github.com/rq/rq/releases * test: sanity tests for scheduled job types * test(test_runner): dont set bench_id globally * refactor: stop_job shouldn't throw error The intention of use here is to stop stuck jobs or long running jobs, if for some reason they were stopped by the time command gets executed, there's no need to throw error. --- frappe/core/doctype/rq_job/rq_job.py | 7 +++++- frappe/core/doctype/rq_job/test_rq_job.py | 15 ++++++------- frappe/test_runner.py | 1 - frappe/tests/test_redis.py | 4 ++++ frappe/tests/test_zbg_job_sanity_test.py | 27 +++++++++++++++++++++++ pyproject.toml | 2 +- 6 files changed, 45 insertions(+), 11 deletions(-) create mode 100644 frappe/tests/test_zbg_job_sanity_test.py diff --git a/frappe/core/doctype/rq_job/rq_job.py b/frappe/core/doctype/rq_job/rq_job.py index 7e1c35a0e6..f05611fe7d 100644 --- a/frappe/core/doctype/rq_job/rq_job.py +++ b/frappe/core/doctype/rq_job/rq_job.py @@ -5,10 +5,12 @@ import functools import re from rq.command import send_stop_job_command +from rq.exceptions import InvalidJobOperation from rq.job import Job from rq.queue import Queue import frappe +from frappe import _ from frappe.model.document import Document from frappe.utils import ( cint, @@ -93,7 +95,10 @@ class RQJob(Document): @check_permissions def stop_job(self): - send_stop_job_command(connection=get_redis_conn(), job_id=self.job_id) + try: + send_stop_job_command(connection=get_redis_conn(), job_id=self.job_id) + except InvalidJobOperation: + frappe.msgprint(_("Job is not running."), title=_("Invalid Operation")) @staticmethod def get_count(args) -> int: diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index ae0691fa61..460aa08941 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -19,12 +19,11 @@ class TestRQJob(FrappeTestCase): @timeout(seconds=20) def check_status(self, job: Job, status, wait=True): - if wait: - while True: - if job.is_queued or job.is_started: - time.sleep(0.2) - else: - break + while wait: + if not (job.is_queued or job.is_started): + break + time.sleep(0.2) + self.assertEqual(frappe.get_doc("RQ Job", job.id).status, status) def test_serialization(self): @@ -69,7 +68,7 @@ class TestRQJob(FrappeTestCase): self.assertGreaterEqual(len(non_failed_jobs), 1) # Create a slow job and check if it's stuck in "Started" - job = frappe.enqueue(method=self.BG_JOB, queue="short", sleep=1000) + job = frappe.enqueue(method=self.BG_JOB, queue="short", sleep=10) time.sleep(3) self.check_status(job, "started", wait=False) stop_job(job_id=job.id) @@ -84,8 +83,8 @@ class TestRQJob(FrappeTestCase): def test_is_enqueued(self): + dummy_job = frappe.enqueue(self.BG_JOB, sleep=10, queue="short") job_name = "uniq_test_job" - dummy_job = frappe.enqueue(self.BG_JOB, sleep=100, queue="short") actual_job = frappe.enqueue(self.BG_JOB, job_name=job_name, queue="short") self.assertTrue(is_job_queued(job_name)) diff --git a/frappe/test_runner.py b/frappe/test_runner.py index 1e3573336a..7e2c7e724f 100644 --- a/frappe/test_runner.py +++ b/frappe/test_runner.py @@ -86,7 +86,6 @@ def main( frappe.utils.scheduler.disable_scheduler() set_test_email_config() - frappe.conf.update({"bench_id": "test_bench", "use_rq_auth": False}) if not frappe.flags.skip_before_tests: if verbose: diff --git a/frappe/tests/test_redis.py b/frappe/tests/test_redis.py index ca59af98b0..19001028f6 100644 --- a/frappe/tests/test_redis.py +++ b/frappe/tests/test_redis.py @@ -1,4 +1,5 @@ import functools +from unittest.mock import patch import redis @@ -30,12 +31,14 @@ def skip_if_redis_version_lt(version): class TestRedisAuth(FrappeTestCase): @skip_if_redis_version_lt("6.0") + @patch.dict(frappe.conf, {"bench_id": "test_bench", "use_rq_auth": False}) def test_rq_gen_acllist(self): """Make sure that ACL list is genrated""" acl_list = RedisQueue.gen_acl_list() self.assertEqual(acl_list[1]["bench"][0], get_bench_id()) @skip_if_redis_version_lt("6.0") + @patch.dict(frappe.conf, {"bench_id": "test_bench", "use_rq_auth": False}) def test_adding_redis_user(self): acl_list = RedisQueue.gen_acl_list() username, password = acl_list[1]["bench"] @@ -47,6 +50,7 @@ class TestRedisAuth(FrappeTestCase): conn.acl_deluser(username) @skip_if_redis_version_lt("6.0") + @patch.dict(frappe.conf, {"bench_id": "test_bench", "use_rq_auth": False}) def test_rq_namespace(self): """Make sure that user can access only their respective namespace.""" # Current bench ID diff --git a/frappe/tests/test_zbg_job_sanity_test.py b/frappe/tests/test_zbg_job_sanity_test.py new file mode 100644 index 0000000000..19dc168c04 --- /dev/null +++ b/frappe/tests/test_zbg_job_sanity_test.py @@ -0,0 +1,27 @@ +""" smoak tests to check that all registered background jobs execute without error. + +Note: Filename is intentional to run this test roughly at end. Don't change.""" + +import time + +import frappe +from frappe.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs +from frappe.tests.utils import FrappeTestCase, timeout + + +class TestScheduledJobSanity(FrappeTestCase): + def setUp(self): + remove_failed_jobs() + + @timeout(90) + def test_bg_jobs_run(self): + """Enqueue all scheduled jobs, wait for finish and verify that none failed.""" + for scheduled_job_type in frappe.get_all("Scheduled Job Type", pluck="name"): + frappe.get_doc("Scheduled Job Type", scheduled_job_type).enqueue(force=True) + + while RQJob.get_list({"filters": [["RQ Job", "status", "in", ("queued", "started")]]}): + time.sleep(0.5) + + # Check no failed, if failed print full details + failed_jobs = RQJob.get_list({"filters": [["RQ Job", "status", "=", "failed"]]}) + self.assertEqual(len(failed_jobs), 0, "Jobs failed: " + str(failed_jobs)) diff --git a/pyproject.toml b/pyproject.toml index 6f744ca186..348353003c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,7 +57,7 @@ dependencies = [ "hiredis~=2.0.0", "requests-oauthlib~=1.3.0", "requests~=2.27.1", - "rq~=1.10.1", + "rq~=1.11.1", "rsa>=4.1", "semantic-version~=2.10.0", "sqlparse~=0.4.1", From 43ccb40d0e4598bf71fe8af2f1fec6dda1e8adc8 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Nov 2022 18:33:50 +0530 Subject: [PATCH 02/43] chore: rollback to 3.10 for vuln checks cython dependency fails [skip ci] --- .github/workflows/linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 1d8e736538..01b5407489 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -79,7 +79,7 @@ jobs: steps: - uses: actions/setup-python@v4 with: - python-version: '3.11' + python-version: '3.10' - uses: actions/checkout@v3 - run: | pip install pip-audit From 313605d328d192078b41fd196b41af21754373cf Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Nov 2022 20:37:43 +0530 Subject: [PATCH 03/43] fix: standard dashboards not loading --- frappe/public/js/frappe/views/breadcrumbs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 5236c21ac2..53a3300a94 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -137,13 +137,13 @@ frappe.breadcrumbs = { const doctype_meta = frappe.get_doc("DocType", doctype); if ( (doctype === "User" && !frappe.user.has_role("System Manager")) || - (doctype_meta && doctype_meta.issingle) + doctype_meta?.issingle ) { // no user listview for non-system managers and single doctypes } else { let route; const doctype_route = frappe.router.slug(frappe.router.doctype_layout || doctype); - if (doctype_meta.is_tree) { + if (doctype_meta?.is_tree) { let view = frappe.model.user_settings[doctype].last_view || "Tree"; route = `${doctype_route}/view/${view}`; } else { From cd1bbccdc3a412478dc9ec1a4cddca937f0109db Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 5 Nov 2022 16:05:37 +0000 Subject: [PATCH 04/43] fix: dont release `frappe.local` twice (#18772) --- frappe/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/app.py b/frappe/app.py index 8cb32ff4bf..889c2e7cbe 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -86,7 +86,7 @@ def application(request: Request): log_request(request, response) process_response(response) - frappe.destroy() + frappe.db.close() return response From ff0c820c9c7c1b9c3173b0c2ce8d1be8fe576b76 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 5 Nov 2022 16:06:07 +0000 Subject: [PATCH 05/43] chore(socketio): remove duplicate event handler (#18770) --- socketio.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/socketio.js b/socketio.js index 711e0028cc..30e931d6bf 100644 --- a/socketio.js +++ b/socketio.js @@ -31,11 +31,6 @@ io.on("connection", function (socket) { socket.user = cookie.parse(socket.request.headers.cookie).user_id; - socket.on("task_subscribe", function (task_id) { - var room = get_task_room(socket, task_id); - socket.join(room); - }); - let retries = 0; let join_user_room = () => { request @@ -61,13 +56,13 @@ io.on("connection", function (socket) { join_user_room(); - socket.on("task_unsubscribe", function (task_id) { + socket.on("task_subscribe", function (task_id) { var room = get_task_room(socket, task_id); - socket.leave(room); + socket.join(room); }); socket.on("task_unsubscribe", function (task_id) { - var room = "task:" + task_id; + var room = get_task_room(socket, task_id); socket.leave(room); }); From 5d1d3564ef41ceec652c7f097ae9ef4150420ffa Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Nov 2022 20:45:02 +0530 Subject: [PATCH 06/43] test: Dashboard basic ui test + whitelisting --- cypress/integration/dashboard.js | 50 ++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 cypress/integration/dashboard.js diff --git a/cypress/integration/dashboard.js b/cypress/integration/dashboard.js new file mode 100644 index 0000000000..6eb28567bc --- /dev/null +++ b/cypress/integration/dashboard.js @@ -0,0 +1,50 @@ +describe("Dashboard view", { scrollBehavior: false }, () => { + before(() => { + cy.login(); + cy.visit("/app"); + }); + + it("should load", () => { + const chart = "TODO-YEARLY-TRENDS"; + const dashboard = "TODO-TEST-DASHBOARD"; // check slash in name intentionally. + + cy.insert_doc( + "Dashboard Chart", + { + is_standard: 0, + chart_name: chart, + chart_type: "Count", + document_type: "ToDo", + parent_document_type: "", + based_on: "creation", + group_by_type: "Count", + timespan: "Last Year", + time_interval: "Yearly", + timeseries: 1, + type: "Line", + filters_json: "[]", + }, + true + ); + + cy.insert_doc( + "Dashboard", + { + name: dashboard, + dashboard_name: dashboard, + is_standard: 0, + charts: [ + { + chart: chart, + }, + ], + }, + true + ); + + cy.visit(`/app/dashboard-view/${dashboard}`); + + // expect chart to be loaded + cy.findByText(chart).should("be.visible"); + }); +}); From 9a7e59e811366f5ea0aefc7d4a9d1e4e387920b0 Mon Sep 17 00:00:00 2001 From: Ponnusamy <95607086+Ponnusamy1-V@users.noreply.github.com> Date: Sun, 6 Nov 2022 15:00:53 +0530 Subject: [PATCH 07/43] fix: dashboard view from workspace --- frappe/public/js/frappe/views/breadcrumbs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 53a3300a94..2eaa70fe0f 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -29,7 +29,7 @@ frappe.breadcrumbs = { return localStorage["preferred_breadcrumbs:" + doctype]; }, - add(module, doctype, type) { + async add(module, doctype, type) { let obj; if (typeof module === "object") { obj = module; @@ -40,7 +40,7 @@ frappe.breadcrumbs = { type: type, }; } - + await frappe.model.with_doctype(doctype); this.all[frappe.breadcrumbs.current_page()] = obj; this.update(); }, From 09d35c74eb77f76cdca073bda3833160f1d83c79 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 6 Nov 2022 12:57:02 +0100 Subject: [PATCH 08/43] fix: remove redundant translation (#18775) --- frappe/client.py | 5 ----- frappe/core/page/permission_manager/permission_manager.py | 2 -- frappe/desk/desk_page.py | 5 ----- frappe/desk/query_report.py | 5 ----- 4 files changed, 17 deletions(-) diff --git a/frappe/client.py b/frappe/client.py index f42f73a529..404617b68c 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -333,11 +333,6 @@ def get_js(items): with open(contentpath) as srcfile: code = frappe.utils.cstr(srcfile.read()) - if frappe.local.lang != "en": - messages = frappe.get_lang_dict("jsfile", contentpath) - messages = json.dumps(messages) - code += f"\n\n$.extend(frappe._messages, {messages})" - out.append(code) return out diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 46c9e0aca2..45c1e44fa1 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -19,7 +19,6 @@ from frappe.permissions import ( setup_custom_perms, update_permission_property, ) -from frappe.translate import send_translations from frappe.utils.user import get_users_with_role as _get_user_with_role not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def", "Transaction Log"] @@ -28,7 +27,6 @@ not_allowed_in_permission_manager = ["DocType", "Patch Log", "Module Def", "Tran @frappe.whitelist() def get_roles_and_doctypes(): frappe.only_for("System Manager") - send_translations(frappe.get_lang_dict("doctype", "DocPerm")) active_domains = frappe.get_active_domains() diff --git a/frappe/desk/desk_page.py b/frappe/desk/desk_page.py index ad0bd549d8..bde27125f6 100644 --- a/frappe/desk/desk_page.py +++ b/frappe/desk/desk_page.py @@ -2,7 +2,6 @@ # License: MIT. See LICENSE import frappe -from frappe.translate import send_translations @frappe.whitelist() @@ -31,10 +30,6 @@ def getpage(): page = frappe.form_dict.get("name") doc = get(page) - # load translations - if frappe.lang != "en": - send_translations(frappe.get_lang_dict("page", page)) - frappe.response.docs.append(doc) diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 877fdbe5bc..d0bc63f858 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -14,7 +14,6 @@ from frappe.model.utils import render_include from frappe.modules import get_module_path, scrub from frappe.monitor import add_data_to_monitor from frappe.permissions import get_role_permissions -from frappe.translate import send_translations from frappe.utils import ( cint, cstr, @@ -202,10 +201,6 @@ def get_script(report_name): if not script: script = "frappe.query_reports['%s']={}" % report_name - # load translations - if frappe.lang != "en": - send_translations(frappe.get_lang_dict("report", report_name)) - return { "script": render_include(script), "html_format": html_format, From e02b90cd5b0be4d6dd40742fdeff2ac01e70b638 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Nov 2022 17:33:02 +0530 Subject: [PATCH 09/43] fix: dont allow reading attributes of unsafe objects (#18706) --- frappe/tests/test_safe_exec.py | 16 ++++++++++++++ frappe/utils/safe_exec.py | 39 ++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py index caa98737a2..fcd5832680 100644 --- a/frappe/tests/test_safe_exec.py +++ b/frappe/tests/test_safe_exec.py @@ -1,3 +1,5 @@ +import types + import frappe from frappe.tests.utils import FrappeTestCase from frappe.utils.safe_exec import get_safe_globals, safe_exec @@ -59,3 +61,17 @@ class TestSafeExec(FrappeTestCase): # enqueue whitelisted method safe_exec("""frappe.enqueue("ping", now=True)""") + + def test_ensure_getattrable_globals(self): + def check_safe(objects): + for obj in objects: + if isinstance(obj, (types.ModuleType, types.CodeType, types.TracebackType, types.FrameType)): + self.fail(f"{obj} wont work in safe exec.") + elif isinstance(obj, dict): + check_safe(obj.values()) + + check_safe(get_safe_globals().values()) + + def test_unsafe_objects(self): + unsafe_global = {"frappe": frappe} + self.assertRaises(SyntaxError, safe_exec, """frappe.msgprint("Hello")""", unsafe_global) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index afdd4694a8..c75a5fd12b 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -2,6 +2,9 @@ import copy import inspect import json import mimetypes +import types +from contextlib import contextmanager +from functools import lru_cache import RestrictedPython.Guards from RestrictedPython import compile_restricted, safe_globals @@ -64,14 +67,20 @@ def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=Fals exec_globals.frappe.db.pop("rollback", None) exec_globals.frappe.db.pop("add_index", None) - # execute script compiled by RestrictedPython - frappe.flags.in_safe_exec = True - exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used - frappe.flags.in_safe_exec = False + with safe_exec_flags(), patched_qb(): + # execute script compiled by RestrictedPython + exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used return exec_globals, _locals +@contextmanager +def safe_exec_flags(): + frappe.flags.in_safe_exec = True + yield + frappe.flags.in_safe_exec = False + + def get_safe_globals(): datautils = frappe._dict() @@ -258,6 +267,24 @@ def call_with_form_dict(function, kwargs): frappe.local.form_dict = form_dict +@contextmanager +def patched_qb(): + try: + _terms = frappe.qb.terms + frappe.qb.terms = _flatten(frappe.qb.terms) + yield + finally: + frappe.qb.terms = _terms + + +@lru_cache +def _flatten(module): + new_mod = NamespaceDict() + for name, obj in inspect.getmembers(module, lambda x: not inspect.ismodule(x)): + new_mod[name] = obj + return new_mod + + def get_python_builtins(): return { "abs": abs, @@ -350,6 +377,10 @@ def _getattr(object, name, default=None): if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES): raise SyntaxError(f"{name} is an unsafe attribute") + + if isinstance(object, (types.ModuleType, types.CodeType, types.TracebackType, types.FrameType)): + raise SyntaxError(f"Reading {object} attributes is not allowed") + return RestrictedPython.Guards.safer_getattr(object, name, default=default) From e3a0b007c21858d19e86d2aa26e55737eefe6d44 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sun, 6 Nov 2022 13:06:51 +0100 Subject: [PATCH 10/43] fix: translations (#18765) * fix: add missing translations * fix: capitalize label * fix: translate label on summary item --- frappe/public/js/frappe/utils/utils.js | 2 +- frappe/public/js/frappe/views/reports/query_report.js | 2 +- frappe/translations/de.csv | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 3d17e8a8f6..cf5b619c37 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1383,7 +1383,7 @@ Object.assign(frappe.utils, { : ""; return $(`
- ${summary.label} + ${__(summary.label)}
${value}
`); }, diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 1febf11b6c..dc5e285181 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1067,7 +1067,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { { fieldname: "sb_1", fieldtype: "Section Break", - label: "Y axis", + label: "Y Axis", }, { fieldname: "y_axis_fields", diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 78fdcb7925..b1000f3bee 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -4080,7 +4080,7 @@ Scheduler Event,Scheduler-Ereignis, Select Event Type,Wählen Sie den Ereignistyp, Schedule Script,Zeitplan-Skript, Duration,Dauer, -Donut,Krapfen, +Donut,Donut, Custom Options,Benutzerdefinierte Optionen, "Ex: ""colors"": [""#d1d8dd"", ""#ff5858""]","Beispiel: "Farben": ["# d1d8dd", "# ff5858"]", Confirmation Email Template,Bestätigungs-E-Mail-Vorlage, @@ -4818,3 +4818,8 @@ K,Tsd,Number system M,Mio,Number system B,Mrd,Number system T,Bio,Number system +Type of Chart,Diagrammtyp, +Preview Chart,Vorschau erzeugen, +Please select X and Y fields,Bitte Felder für die X- und Y-Achse wählen, +Notification sent to,Benachrichtigung gesendet an, +Add to this activity by mailing to {0},"Senden Sie eine E-Mail an {0}, damit sie hier erscheint", From db1ed206f310603e05e30f0c0c33f5a83e831dba Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Nov 2022 18:53:09 +0530 Subject: [PATCH 11/43] fix: only release db if it exists ref https://github.com/frappe/frappe/pull/18772 [skip ci] --- frappe/app.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/app.py b/frappe/app.py index 889c2e7cbe..0d7fdc1fe1 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -86,7 +86,8 @@ def application(request: Request): log_request(request, response) process_response(response) - frappe.db.close() + if frappe.db: + frappe.db.close() return response From 44a5bdc3f1d6b184797c41946aad29f9286ca289 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Nov 2022 19:18:30 +0530 Subject: [PATCH 12/43] fix: ignore internal methods (#18784) --- frappe/utils/safe_exec.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index c75a5fd12b..04aa134d39 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -281,7 +281,8 @@ def patched_qb(): def _flatten(module): new_mod = NamespaceDict() for name, obj in inspect.getmembers(module, lambda x: not inspect.ismodule(x)): - new_mod[name] = obj + if not name.startswith("_"): + new_mod[name] = obj return new_mod From 62c4a3c020dcd58550c6e36e1ab32730b4ff4b99 Mon Sep 17 00:00:00 2001 From: Daizy Modi Date: Sun, 6 Nov 2022 19:59:59 +0530 Subject: [PATCH 13/43] fix: check permission before print or download document (#18757) --- frappe/utils/weasyprint.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/utils/weasyprint.py b/frappe/utils/weasyprint.py index ceb064a955..d670cf96f5 100644 --- a/frappe/utils/weasyprint.py +++ b/frappe/utils/weasyprint.py @@ -9,6 +9,7 @@ import frappe @frappe.whitelist() def download_pdf(doctype, name, print_format, letterhead=None): doc = frappe.get_doc(doctype, name) + doc.check_permission("print") generator = PrintFormatGenerator(print_format, doc, letterhead) pdf = generator.render_pdf() @@ -21,6 +22,7 @@ def download_pdf(doctype, name, print_format, letterhead=None): def get_html(doctype, name, print_format, letterhead=None): doc = frappe.get_doc(doctype, name) + doc.check_permission("print") generator = PrintFormatGenerator(print_format, doc, letterhead) return generator.get_html_preview() From f04287ae83ed1531212c50defe52b75264d44d80 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 6 Nov 2022 20:21:04 +0530 Subject: [PATCH 14/43] fix: dont close static modal with keyboard closes https://github.com/frappe/frappe/issues/11926 [skip ci] --- frappe/public/js/frappe/ui/keyboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/ui/keyboard.js b/frappe/public/js/frappe/ui/keyboard.js index 14418528a6..2e3bd95616 100644 --- a/frappe/public/js/frappe/ui/keyboard.js +++ b/frappe/public/js/frappe/ui/keyboard.js @@ -331,7 +331,7 @@ function close_grid_and_dialog() { } // close open dialog - if (cur_dialog && !cur_dialog.no_cancel_flag) { + if (cur_dialog && !cur_dialog.no_cancel_flag && !cur_dialog.static) { cur_dialog.cancel(); return false; } From b2860e6f9ed45214631f655880899227d075f1e9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 7 Nov 2022 19:01:50 +0530 Subject: [PATCH 15/43] fix(UX): allow clicking on row to open in new tab (#18789) - ctrl+click on row is toggling checkbox instead of opening it in new row - This only happens on non-mac devices Root cause: incorrect grouping of predicate towards fixing https://github.com/frappe/frappe/issues/18788 [skip ci] --- 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 66ff5107a1..52d0026e37 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -1179,7 +1179,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { this.$result.on("click", ".list-row, .image-view-header, .file-header", (e) => { const $target = $(e.target); // tick checkbox if Ctrl/Meta key is pressed - if (e.ctrlKey || (e.metaKey && !$target.is("a"))) { + if ((e.ctrlKey || e.metaKey) && !$target.is("a")) { const $list_row = $(e.currentTarget); const $check = $list_row.find(".list-row-checkbox"); $check.prop("checked", !$check.prop("checked")); From ce360b6fcea2b10b0b2fa297746fc23595bb631f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 5 Nov 2022 16:58:01 +0530 Subject: [PATCH 16/43] feat: Set default SQL statement timeouts --- frappe/database/database.py | 36 ++++++++++++++++++++++++++-- frappe/database/mariadb/database.py | 7 ++++++ frappe/database/postgres/database.py | 8 +++++++ frappe/tests/test_db.py | 21 ++++++++++++++++ frappe/utils/background_jobs.py | 1 - 5 files changed, 70 insertions(+), 3 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 3cb47e853a..64ef994a50 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -7,7 +7,7 @@ import random import re import string import traceback -from contextlib import contextmanager +from contextlib import contextmanager, suppress from time import time from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder @@ -29,7 +29,7 @@ from frappe.exceptions import DoesNotExistError, ImplicitCommitError from frappe.model.utils.link_count import flush_local_link_count from frappe.query_builder.functions import Count from frappe.utils import cast as cast_fieldtype -from frappe.utils import get_datetime, get_table_name, getdate, now, sbool +from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE) INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*") @@ -114,6 +114,17 @@ class Database: self._cursor = self._conn.cursor() frappe.local.rollback_observers = [] + try: + if execution_timeout := get_ideal_query_execution_timeout(): + self.set_execution_timeout(execution_timeout) + except Exception as e: + frappe.logger("database").warning(f"Couldn't set execution timeout {e}") + + def set_execution_timeout(self, seconds: int): + """Set session speicifc timeout on exeuction of statements. + If any statement takes more time it will be killed along with entire transaction.""" + raise NotImplementedError + def use(self, db_name): """`USE` db_name.""" self._conn.select_db(db_name) @@ -1340,3 +1351,24 @@ def savepoint(catch: type | tuple[type, ...] = Exception): frappe.db.rollback(save_point=savepoint) else: frappe.db.release_savepoint(savepoint) + + +def get_ideal_query_execution_timeout() -> int: + """Get execution timeout based on current timeout in contexts. + + HTTP requests: HTTP timeout or a default (300) + Background jobs: Job timeout + + Note: Timeout adds 1.5x as "safety factor" + """ + from rq import get_current_job + + # Zero means no timeout, which is the default value in db. + timeout = 0 + with suppress(Exception): + if hasattr(frappe.local, "request"): + timeout = frappe.conf.http_timeout or 300 + elif job := get_current_job(): + timeout = job.timeout + + return int(cint(timeout) * 1.5) diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 1df9877eb1..322c355357 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -68,6 +68,10 @@ class MariaDBExceptionUtil: def is_syntax_error(e: pymysql.Error) -> bool: return e.args[0] == ER.PARSE_ERROR + @staticmethod + def is_statement_timeout(e: pymysql.Error) -> bool: + return e.args[0] == 1969 + @staticmethod def is_data_too_long(e: pymysql.Error) -> bool: return e.args[0] == ER.DATA_TOO_LONG @@ -102,6 +106,9 @@ class MariaDBConnectionUtil: def create_connection(self): return pymysql.connect(**self.get_connection_settings()) + def set_execution_timeout(self, seconds: int): + self.sql("set session max_statement_time = %s", int(seconds)) + def get_connection_settings(self) -> dict: conn_settings = { "host": self.host, diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 3b3612c0e4..d082afceaf 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -99,6 +99,10 @@ class PostgresExceptionUtil: def is_duplicate_fieldname(e): return getattr(e, "pgcode", None) == DUPLICATE_COLUMN + @staticmethod + def is_statement_timeout(e): + return PostgresDatabase.is_timedout(e) or isinstance(e, frappe.QueryTimeoutError) + @staticmethod def is_data_too_long(e): return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION @@ -161,6 +165,10 @@ class PostgresDatabase(PostgresExceptionUtil, Database): return conn + def set_execution_timeout(self, seconds: int): + # Postgres expects milliseconds as input + self.sql("set local statement_timeout = %s", int(seconds) * 1000) + def escape(self, s, percent=True): """Escape quotes and percent in given string.""" if isinstance(s, bytes): diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 08fef66bd0..9a7d086252 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -36,6 +36,27 @@ class TestDB(FrappeTestCase): def test_get_database_size(self): self.assertIsInstance(frappe.db.get_database_size(), (float, int)) + def test_db_statement_execution_timeout(self): + frappe.db.set_execution_timeout(2) + # Setting 0 means no timeout. + self.addCleanup(frappe.db.set_execution_timeout, 0) + + try: + savepoint = "statement_timeout" + frappe.db.savepoint(savepoint) + frappe.db.multisql( + { + "mariadb": "select sleep(10)", + "postgres": "select pg_sleep(10)", + } + ) + except Exception as e: + self.assertTrue(frappe.db.is_statement_timeout(e), f"exepcted {e} to be timeout error") + frappe.db.rollback(save_point=savepoint) + else: + frappe.db.rollback(save_point=savepoint) + self.fail("Long running queries not timing out") + def test_get_value(self): self.assertEqual(frappe.db.get_value("User", {"name": ["=", "Administrator"]}), "Administrator") self.assertEqual(frappe.db.get_value("User", {"name": ["like", "Admin%"]}), "Administrator") diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py index d416857588..12c2105df8 100755 --- a/frappe/utils/background_jobs.py +++ b/frappe/utils/background_jobs.py @@ -9,7 +9,6 @@ from uuid import uuid4 import redis from redis.exceptions import BusyLoadingError, ConnectionError from rq import Connection, Queue, Worker -from rq.command import send_stop_job_command from rq.logutils import setup_loghandlers from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed From f45656217e13f0e062707d98c3c01ef2479c50a0 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 8 Nov 2022 14:58:25 +0530 Subject: [PATCH 17/43] refactor: control statement timeout via flag --- frappe/database/database.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 64ef994a50..1934bc8c30 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -115,7 +115,7 @@ class Database: frappe.local.rollback_observers = [] try: - if execution_timeout := get_ideal_query_execution_timeout(): + if execution_timeout := get_query_execution_timeout(): self.set_execution_timeout(execution_timeout) except Exception as e: frappe.logger("database").warning(f"Couldn't set execution timeout {e}") @@ -1353,20 +1353,24 @@ def savepoint(catch: type | tuple[type, ...] = Exception): frappe.db.release_savepoint(savepoint) -def get_ideal_query_execution_timeout() -> int: - """Get execution timeout based on current timeout in contexts. +def get_query_execution_timeout() -> int: + """Get execution timeout based on current timeout in different contexts. - HTTP requests: HTTP timeout or a default (300) - Background jobs: Job timeout + HTTP requests: HTTP timeout or a default (300) + Background jobs: Job timeout + Console/Commands: No timeout = 0. - Note: Timeout adds 1.5x as "safety factor" + Note: Timeout adds 1.5x as "safety factor" """ from rq import get_current_job + if not frappe.conf.get("enable_db_statement_timeout"): + return 0 + # Zero means no timeout, which is the default value in db. timeout = 0 with suppress(Exception): - if hasattr(frappe.local, "request"): + if getattr(frappe.local, "request", None): timeout = frappe.conf.http_timeout or 300 elif job := get_current_job(): timeout = job.timeout From ad7c0816f8e5f455aeb869132dda46980fe78966 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 8 Nov 2022 21:27:41 +0530 Subject: [PATCH 18/43] fix: prevent deleting standard doctypes in prod (#18803) --- frappe/core/doctype/doctype/test_doctype.py | 3 +++ frappe/installer.py | 4 ++-- frappe/model/delete_doc.py | 2 ++ frappe/tests/test_modules.py | 2 +- frappe/tests/test_virtual_doctype.py | 2 +- frappe/tests/ui_test_helpers.py | 4 ++-- 6 files changed, 11 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 3722e5d1fa..2e74fd3a6a 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -670,6 +670,9 @@ class TestDocType(FrappeTestCase): self.assertEqual(test_json.test_json_field["hello"], "world") + def test_no_delete_doc(self): + self.assertRaises(frappe.ValidationError, frappe.delete_doc, "DocType", "Address") + @patch.dict(frappe.conf, {"developer_mode": 1}) def test_custom_field_deletion(self): """Custom child tables whose doctype doesn't exist should be auto deleted.""" diff --git a/frappe/installer.py b/frappe/installer.py index 4f1755c2a0..2a6c29a17f 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -402,7 +402,7 @@ def _delete_modules(modules: list[str], dry_run: bool) -> list[str]: if not dry_run: if doctype.issingle: - frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True) + frappe.delete_doc("DocType", doctype.name, ignore_on_trash=True, force=True) else: drop_doctypes.append(doctype.name) @@ -460,7 +460,7 @@ def _delete_doctypes(doctypes: list[str], dry_run: bool) -> None: for doctype in set(doctypes): print(f"* dropping Table for '{doctype}'...") if not dry_run: - frappe.delete_doc("DocType", doctype, ignore_on_trash=True) + frappe.delete_doc("DocType", doctype, ignore_on_trash=True, force=True) frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`") diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index d1120cc22d..5e8a12c345 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -92,6 +92,8 @@ def delete_doc( else: doc = frappe.get_doc(doctype, name) + if not (doc.custom or frappe.conf.developer_mode or frappe.flags.in_patch or force): + frappe.throw(_("Standard DocType can not be deleted.")) update_flags(doc, flags, ignore_permissions) check_permission_and_not_submitted(doc) diff --git a/frappe/tests/test_modules.py b/frappe/tests/test_modules.py index d6145afcf4..43c90456b8 100644 --- a/frappe/tests/test_modules.py +++ b/frappe/tests/test_modules.py @@ -69,7 +69,7 @@ class TestUtils(FrappeTestCase): delattr(self, "note") if self._testMethodName == "test_make_boilerplate": - self.doctype.delete() + self.doctype.delete(force=True) scrubbed = frappe.scrub(self.doctype.name) self.addCleanup( delete_path, diff --git a/frappe/tests/test_virtual_doctype.py b/frappe/tests/test_virtual_doctype.py index 22910cb64f..898c5e118c 100644 --- a/frappe/tests/test_virtual_doctype.py +++ b/frappe/tests/test_virtual_doctype.py @@ -87,7 +87,7 @@ class TestVirtualDoctypes(FrappeTestCase): cls.addClassCleanup(frappe.flags.pop, "allow_doctype_export", None) vdt = new_doctype(name=TEST_DOCTYPE_NAME, is_virtual=1, custom=0).insert() - cls.addClassCleanup(vdt.delete) + cls.addClassCleanup(vdt.delete, force=True) patch_virtual_doc = patch( "frappe.controllers", new={frappe.local.site: {TEST_DOCTYPE_NAME: VirtualDoctypeTest}} diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 297d9f5b12..ecb6b4da97 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -451,7 +451,7 @@ def create_test_user(): @frappe.whitelist() def setup_tree_doctype(): - frappe.delete_doc_if_exists("DocType", "Custom Tree") + frappe.delete_doc_if_exists("DocType", "Custom Tree", force=True) frappe.get_doc( { @@ -475,7 +475,7 @@ def setup_tree_doctype(): @frappe.whitelist() def setup_image_doctype(): - frappe.delete_doc_if_exists("DocType", "Custom Image") + frappe.delete_doc_if_exists("DocType", "Custom Image", force=True) frappe.get_doc( { From 15cae83c50b202e53c78c8b7de8ac56698c1e117 Mon Sep 17 00:00:00 2001 From: Ponnusamy <95607086+Ponnusamy1-V@users.noreply.github.com> Date: Wed, 9 Nov 2022 13:13:39 +0530 Subject: [PATCH 19/43] Update breadcrumbs.js --- frappe/public/js/frappe/views/breadcrumbs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/views/breadcrumbs.js b/frappe/public/js/frappe/views/breadcrumbs.js index 2eaa70fe0f..8cf9a6ffd7 100644 --- a/frappe/public/js/frappe/views/breadcrumbs.js +++ b/frappe/public/js/frappe/views/breadcrumbs.js @@ -29,7 +29,7 @@ frappe.breadcrumbs = { return localStorage["preferred_breadcrumbs:" + doctype]; }, - async add(module, doctype, type) { + add(module, doctype, type) { let obj; if (typeof module === "object") { obj = module; @@ -40,7 +40,6 @@ frappe.breadcrumbs = { type: type, }; } - await frappe.model.with_doctype(doctype); this.all[frappe.breadcrumbs.current_page()] = obj; this.update(); }, @@ -132,8 +131,9 @@ frappe.breadcrumbs = { } }, - set_list_breadcrumb(breadcrumbs) { + async set_list_breadcrumb(breadcrumbs) { const doctype = breadcrumbs.doctype; + await frappe.model.with_doctype(doctype); const doctype_meta = frappe.get_doc("DocType", doctype); if ( (doctype === "User" && !frappe.user.has_role("System Manager")) || From b71d93ef9f700a6a70a21df59314ed91c9647659 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Nov 2022 14:26:06 +0530 Subject: [PATCH 20/43] test: db timeout computation --- frappe/tests/test_db.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 9a7d086252..d8259975da 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -11,13 +11,13 @@ import frappe from frappe.core.utils import find from frappe.custom.doctype.custom_field.custom_field import create_custom_field from frappe.database import savepoint -from frappe.database.database import Database +from frappe.database.database import Database, get_query_execution_timeout from frappe.database.utils import FallBackDateTimeStr from frappe.query_builder import Field from frappe.query_builder.functions import Concat_ws from frappe.tests.test_query_builder import db_type_is, run_only_if from frappe.tests.utils import FrappeTestCase -from frappe.utils import add_days, cint, now, random_string +from frappe.utils import add_days, cint, now, random_string, set_request from frappe.utils.testutils import clear_custom_fields @@ -57,6 +57,13 @@ class TestDB(FrappeTestCase): frappe.db.rollback(save_point=savepoint) self.fail("Long running queries not timing out") + @patch.dict(frappe.conf, {"http_timeout": 20, "enable_db_statement_timeout": 1}) + def test_db_timeout_computation(self): + set_request(method="GET", path="/") + self.assertEqual(get_query_execution_timeout(), 30) + frappe.local.request = None + self.assertEqual(get_query_execution_timeout(), 0) + def test_get_value(self): self.assertEqual(frappe.db.get_value("User", {"name": ["=", "Administrator"]}), "Administrator") self.assertEqual(frappe.db.get_value("User", {"name": ["like", "Admin%"]}), "Administrator") From b0cb1adc013b58fe025faf90cb2e07297201211e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Nov 2022 15:02:03 +0530 Subject: [PATCH 21/43] ci: config cleanup and bump coverage --- .flake8 | 55 +++++++++++++++++++++++----- .github/helper/flake8.conf | 75 -------------------------------------- .pre-commit-config.yaml | 1 - .stylelintrc | 9 ----- bandit.yml | 1 - pyproject.toml | 2 +- 6 files changed, 47 insertions(+), 96 deletions(-) delete mode 100644 .github/helper/flake8.conf delete mode 100644 .stylelintrc delete mode 100644 bandit.yml diff --git a/.flake8 b/.flake8 index 4b852abd7c..2de7a154c9 100644 --- a/.flake8 +++ b/.flake8 @@ -1,37 +1,74 @@ [flake8] ignore = + B001, + B007, + B009, + B010, + B950, + E101, + E111, + E114, + E116, + E117, E121, + E122, + E123, + E124, + E125, E126, E127, E128, + E131, + E201, + E202, E203, + E211, + E221, + E222, + E223, + E224, E225, E226, + E228, E231, E241, + E242, E251, E261, + E262, E265, + E266, + E271, + E272, + E273, + E274, + E301, E302, E303, E305, + E306, E402, E501, + E502, + E701, + E702, + E703, E741, + F401, + F403, + F405, + W191, W291, W292, W293, W391, W503, W504, - F403, - B007, - B950, - W191, - E124, # closing bracket, irritating while writing QB code - E131, # continuation line unaligned for hanging indent - E123, # closing bracket does not match indentation of opening bracket's line - E101, # ensured by use of black + E711, + E129, + F841, + E713, + E712, max-line-length = 200 -exclude=.github/helper/semgrep_rules +exclude=,test_*.py diff --git a/.github/helper/flake8.conf b/.github/helper/flake8.conf deleted file mode 100644 index 20d4b912ca..0000000000 --- a/.github/helper/flake8.conf +++ /dev/null @@ -1,75 +0,0 @@ -[flake8] -ignore = - B001, - B007, - B009, - B010, - B950, - E101, - E111, - E114, - E116, - E117, - E121, - E122, - E123, - E124, - E125, - E126, - E127, - E128, - E131, - E201, - E202, - E203, - E211, - E221, - E222, - E223, - E224, - E225, - E226, - E228, - E231, - E241, - E242, - E251, - E261, - E262, - E265, - E266, - E271, - E272, - E273, - E274, - E301, - E302, - E303, - E305, - E306, - E402, - E501, - E502, - E701, - E702, - E703, - E741, - F401, - F403, - F405, - W191, - W291, - W292, - W293, - W391, - W503, - W504, - E711, - E129, - F841, - E713, - E712, - - -max-line-length = 200 -exclude=.github/helper/semgrep_rules,test_*.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 27fae671c9..0783e94457 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,6 @@ repos: hooks: - id: flake8 additional_dependencies: ['flake8-bugbear',] - args: ['--config', '.github/helper/flake8.conf'] ci: autoupdate_schedule: weekly diff --git a/.stylelintrc b/.stylelintrc deleted file mode 100644 index 1e05d1fb41..0000000000 --- a/.stylelintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": ["stylelint-config-recommended"], - "plugins": ["stylelint-scss"], - "rules": { - "at-rule-no-unknown": null, - "scss/at-rule-no-unknown": true, - "no-descending-specificity": null - } -} \ No newline at end of file diff --git a/bandit.yml b/bandit.yml deleted file mode 100644 index b8560e97c8..0000000000 --- a/bandit.yml +++ /dev/null @@ -1 +0,0 @@ -skips: ['E0203', 'B605', 'B404', 'B603', 'B607'] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 348353003c..6506bfa13c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -93,7 +93,7 @@ ensure_newline_before_comments = true indent = "\t" [tool.bench.dev-dependencies] -coverage = "~=6.4.1" +coverage = "~=6.5.0" Faker = "~=13.12.1" pyngrok = "~=5.0.5" unittest-xml-reporting = "~=3.0.4" From ffdd368fe298f7245070d5d6a362197dc9ef1f05 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Nov 2022 14:08:28 +0530 Subject: [PATCH 22/43] perf: faster as_list and as_dict --- frappe/database/database.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 1934bc8c30..8f8aa2bba2 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -7,6 +7,7 @@ import random import re import string import traceback +import warnings from contextlib import contextmanager, suppress from time import time @@ -273,6 +274,20 @@ class Database: if pluck: return [r[0] for r in self.last_result] + if as_utf8: + warnings.warn( + "as_utf8 parameter is deprecated and will be removed in version 15.", + DeprecationWarning, + stacklevel=2, + ) + + if formatted: + warnings.warn( + "formatted parameter is deprecated and will be removed in version 15.", + DeprecationWarning, + stacklevel=2, + ) + # scrub output if required if as_dict: ret = self.fetch_as_dict(formatted, as_utf8) @@ -391,10 +406,13 @@ class Database: def fetch_as_dict(self, formatted=0, as_utf8=0) -> list[frappe._dict]: """Internal. Converts results to dict.""" result = self.last_result - ret = [] if result: keys = [column[0] for column in self._cursor.description] + if not as_utf8: + return [frappe._dict(zip(keys, row)) for row in result] + + ret = [] for r in result: values = [] for value in r: @@ -429,6 +447,9 @@ class Database: @staticmethod def convert_to_lists(res, formatted=0, as_utf8=0): """Convert tuple output to lists (internal).""" + if not as_utf8: + return [[value for value in row] for row in res] + nres = [] for r in res: nr = [] From 66f5f4dd467087a7a4eeb19a9a6566201550ee89 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Nov 2022 14:28:52 +0530 Subject: [PATCH 23/43] refactor(db): deprecated unused functions --- frappe/database/database.py | 22 ++++++++++------------ frappe/utils/deprecations.py | 27 +++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 frappe/utils/deprecations.py diff --git a/frappe/database/database.py b/frappe/database/database.py index 8f8aa2bba2..91f73ba006 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -7,7 +7,6 @@ import random import re import string import traceback -import warnings from contextlib import contextmanager, suppress from time import time @@ -31,6 +30,7 @@ from frappe.model.utils.link_count import flush_local_link_count from frappe.query_builder.functions import Count from frappe.utils import cast as cast_fieldtype from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool +from frappe.utils.deprecations import deprecated, deprecation_warning IFNULL_PATTERN = re.compile(r"ifnull\(", flags=re.IGNORECASE) INDEX_PATTERN = re.compile(r"\s*\([^)]+\)\s*") @@ -275,18 +275,9 @@ class Database: return [r[0] for r in self.last_result] if as_utf8: - warnings.warn( - "as_utf8 parameter is deprecated and will be removed in version 15.", - DeprecationWarning, - stacklevel=2, - ) - + deprecation_warning("as_utf8 parameter is deprecated and will be removed in version 15.") if formatted: - warnings.warn( - "formatted parameter is deprecated and will be removed in version 15.", - DeprecationWarning, - stacklevel=2, - ) + deprecation_warning("formatted parameter is deprecated and will be removed in version 15.") # scrub output if required if as_dict: @@ -858,6 +849,7 @@ class Database: ).run(debug=debug, run=run, as_dict=as_dict) return {} + @deprecated def update(self, *args, **kwargs): """Update multiple values. Alias for `set_value`.""" return self.set_value(*args, **kwargs) @@ -897,6 +889,9 @@ class Database: modified_by = modified_by or frappe.session.user to_update.update({"modified": modified, "modified_by": modified_by}) + if for_update: + deprecation_warning("for_update parameter is deprecated and will be removed in v15.") + if is_single_doctype: frappe.db.delete( "Singles", filters={"field": ("in", tuple(to_update)), "doctype": dt}, debug=debug @@ -927,11 +922,13 @@ class Database: if dt in self.value_cache: del self.value_cache[dt] + @deprecated @staticmethod def set(doc, field, val): """Set value in document. **Avoid**""" doc.db_set(field, val) + @deprecated def touch(self, doctype, docname): """Update the modified timestamp of this document.""" modified = now() @@ -1254,6 +1251,7 @@ class Database: """ return self.sql_ddl(f"truncate `{get_table_name(doctype)}`") + @deprecated def clear_table(self, doctype): return self.truncate(doctype) diff --git a/frappe/utils/deprecations.py b/frappe/utils/deprecations.py new file mode 100644 index 0000000000..e98362198b --- /dev/null +++ b/frappe/utils/deprecations.py @@ -0,0 +1,27 @@ +""" Utils for deprecating functionality in Framework. + +WARNING: This file is internal, instead of depending just copy the code or use deprecation +libraries. +""" +import functools +import warnings + + +def deprecated(func): + """Decorator to wrap a function/method as deprecated.""" + + @functools.wraps(func) + def wrapper(*args, **kwargs): + deprecation_warning( + f"{func.__name__} is deprecated and will be removed in next major version.", + stacklevel=1, + ) + return func(*args, **kwargs) + + return wrapper + + +def deprecation_warning(message, category=DeprecationWarning, stacklevel=1): + """like warnings.warn but with auto incremented sane stacklevel.""" + + warnings.warn(message=message, category=category, stacklevel=stacklevel + 2) From a5240824d5cf25102d37ef3cc78d3045a11c8b3a Mon Sep 17 00:00:00 2001 From: Himanshu Shivhare Date: Wed, 9 Nov 2022 18:25:00 +0530 Subject: [PATCH 24/43] docs: youtube channel in "about" section (#18810) * YouTube channel addded in about section. I have added ERPNext YouTube channel reference in the about section. #Create an official Handle for YouTube Channel (Like @erpnext or erpnextofficial) * chore: handle [skip ci] Co-authored-by: Ankush Menat --- frappe/public/js/frappe/ui/toolbar/about.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/ui/toolbar/about.js b/frappe/public/js/frappe/ui/toolbar/about.js index e23706eff1..69cbbfaba0 100644 --- a/frappe/public/js/frappe/ui/toolbar/about.js +++ b/frappe/public/js/frappe/ui/toolbar/about.js @@ -19,6 +19,8 @@ frappe.ui.misc.about = function () { Facebook: https://facebook.com/erpnext

Twitter: https://twitter.com/erpnext

+

+ YouTube: https://www.youtube.com/@erpnextofficial


${__("Installed Apps")}

${__("Loading versions...")}
From 1dd719b123d08e8cc1dbc856ca234abe274772f9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Nov 2022 19:37:35 +0530 Subject: [PATCH 25/43] fix: decorator ordering statcimethod doesn't return normal function so can't be "chained" the other way around [skip ci] --- frappe/database/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 91f73ba006..47ca451289 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -922,8 +922,8 @@ class Database: if dt in self.value_cache: del self.value_cache[dt] - @deprecated @staticmethod + @deprecated def set(doc, field, val): """Set value in document. **Avoid**""" doc.db_set(field, val) From a42ca7d8c1d66fd9fd90fbcbeffac5281ea58b0b Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 9 Nov 2022 14:15:41 +0000 Subject: [PATCH 26/43] fix: raise exception if doc before save is not found (#18796) * fix: raise exception if doc before save is not found * test: ensure error is raised when trying to save new doc using `doc.save()` * chore: add comment explaining condition * test: clearer name and docstring --- frappe/model/document.py | 12 ++++++++---- frappe/tests/test_document.py | 13 +++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 8dcc57e827..942c7005a2 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -744,12 +744,13 @@ class Document(BaseDocument): Will also validate document transitions (Save > Submit > Cancel) calling `self.check_docstatus_transition`.""" - self.load_doc_before_save() + self.load_doc_before_save(raise_exception=True) self._action = "save" - previous = self.get_doc_before_save() + previous = self._doc_before_save - if not previous or self.meta.get("is_virtual"): + # previous is None for new document insert + if not previous: self.check_docstatus_transition(0) return @@ -1048,7 +1049,7 @@ class Document(BaseDocument): self.set_title_field() - def load_doc_before_save(self): + def load_doc_before_save(self, *, raise_exception: bool = False): """load existing document from db before saving""" self._doc_before_save = None @@ -1059,6 +1060,9 @@ class Document(BaseDocument): try: self._doc_before_save = frappe.get_doc(self.doctype, self.name, for_update=True) except frappe.DoesNotExistError: + if raise_exception: + raise + frappe.clear_last_message() def run_post_save_methods(self): diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index cec7ee3bb0..3833e911a7 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -411,6 +411,19 @@ class TestDocument(FrappeTestCase): todo.save() self.assertEqual(todo.notify_update.call_count, 1) + def test_error_on_saving_new_doc_with_name(self): + """Trying to save a new doc with name should raise DoesNotExistError""" + + doc = frappe.get_doc( + { + "doctype": "ToDo", + "description": "this should raise frappe.DoesNotExistError", + "name": "lets-trick-doc-save", + } + ) + + self.assertRaises(frappe.DoesNotExistError, doc.save) + class TestDocumentWebView(FrappeTestCase): def get(self, path, user="Guest"): From 104942ee0a56809552992d37a8fa323e0ef16c0e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 11:25:44 +0530 Subject: [PATCH 27/43] build(deps): bump socket.io-parser from 4.0.4 to 4.0.5 (#18823) Bumps [socket.io-parser](https://github.com/socketio/socket.io-parser) from 4.0.4 to 4.0.5. - [Release notes](https://github.com/socketio/socket.io-parser/releases) - [Changelog](https://github.com/socketio/socket.io-parser/blob/main/CHANGELOG.md) - [Commits](https://github.com/socketio/socket.io-parser/compare/4.0.4...4.0.5) --- updated-dependencies: - dependency-name: socket.io-parser dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 263c531ca4..798300ae61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3170,9 +3170,9 @@ socket.io-client@^4.5.1: socket.io-parser "~4.2.0" socket.io-parser@~4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.4.tgz#9ea21b0d61508d18196ef04a2c6b9ab630f4c2b0" - integrity sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g== + version "4.0.5" + resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.0.5.tgz#cb404382c32324cc962f27f3a44058cf6e0552df" + integrity sha512-sNjbT9dX63nqUFIOv95tTVm6elyIU4RvB1m8dOeZt+IgWwcWklFDOdmGcfo3zSiRsnR/3pJkjY5lfoGqEe4Eig== dependencies: "@types/component-emitter" "^1.2.10" component-emitter "~1.3.0" From fcaa16bb213702355449a2bc3aeda5bd1ff0b827 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 9 Nov 2022 22:25:47 +0530 Subject: [PATCH 28/43] perf: faster generate_hash --- frappe/__init__.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index c03b87be1c..483316a15f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1018,19 +1018,12 @@ def get_precision( return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency) -def generate_hash(txt: str | None = None, length: int | None = None) -> str: - """Generates random hash for given text + current timestamp + random string.""" - import hashlib - import time +def generate_hash(txt: str | None = None, length: int | None = 56) -> str: + """Generate random hash using best available randomness source.""" + import math + import secrets - from .utils import random_string - - digest = hashlib.sha224( - ((txt or "") + repr(time.time()) + repr(random_string(8))).encode() - ).hexdigest() - if length: - digest = digest[:length] - return digest + return secrets.token_hex(math.ceil(length / 2))[:length] def reset_metadata_version(): From f34f7030a3372eb8dc2677bedcdf0948ffb20145 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 10 Nov 2022 11:34:06 +0530 Subject: [PATCH 29/43] refactor: remove `txt` param from generate_hash use --- frappe/__init__.py | 5 ++++- frappe/core/doctype/data_export/exporter.py | 2 +- frappe/core/doctype/data_import/test_importer.py | 2 +- frappe/model/naming.py | 2 +- frappe/patches/v11_0/remove_skip_for_doctype.py | 2 +- frappe/patches/v12_0/move_email_and_phone_to_child_table.py | 6 +++--- frappe/patches/v12_0/setup_tags.py | 2 +- frappe/templates/includes/navbar/navbar_items.html | 4 ++-- frappe/tests/test_modules.py | 2 +- frappe/utils/jinja_globals.py | 4 +--- frappe/utils/oauth.py | 2 +- frappe/website/doctype/website_theme/website_theme.py | 2 +- .../section_with_collapsible_content.html | 2 +- .../web_template/section_with_tabs/section_with_tabs.html | 4 ++-- frappe/website/web_template/slideshow/slideshow.html | 2 +- 15 files changed, 22 insertions(+), 21 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 483316a15f..84a27642a9 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1018,11 +1018,14 @@ def get_precision( return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency) -def generate_hash(txt: str | None = None, length: int | None = 56) -> str: +def generate_hash(txt: str | None = None, length: int = 56) -> str: """Generate random hash using best available randomness source.""" import math import secrets + if not length: + length = 56 + return secrets.token_hex(math.ceil(length / 2))[:length] diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index e3bf669630..611592531d 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -432,7 +432,7 @@ class DataExporter: row[_column_start_end.start + i + 1] = value def build_response_as_excel(self): - filename = frappe.generate_hash("", 10) + filename = frappe.generate_hash(length=10) with open(filename, "wb") as f: f.write(cstr(self.writer.getvalue()).encode("utf-8")) f = open(filename) diff --git a/frappe/core/doctype/data_import/test_importer.py b/frappe/core/doctype/data_import/test_importer.py index af8c711ab5..978f5792dd 100644 --- a/frappe/core/doctype/data_import/test_importer.py +++ b/frappe/core/doctype/data_import/test_importer.py @@ -97,7 +97,7 @@ class TestImporter(FrappeTestCase): def test_data_import_update(self): existing_doc = frappe.get_doc( doctype=doctype_name, - title=frappe.generate_hash(doctype_name, 8), + title=frappe.generate_hash(length=8), table_field_1=[{"child_title": "child title to update"}], ) existing_doc.save() diff --git a/frappe/model/naming.py b/frappe/model/naming.py index 6f7db5cf9d..93be2204b4 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -278,7 +278,7 @@ def make_autoname(key="", doctype="", doc=""): DE/09/01/00001 where 09 is the year, 01 is the month and 00001 is the series """ if key == "hash": - return frappe.generate_hash(doctype, 10) + return frappe.generate_hash(length=10) series = NamingSeries(key) return series.generate_next_name(doc) diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index e7c1d71a0a..b3471ca4e8 100644 --- a/frappe/patches/v11_0/remove_skip_for_doctype.py +++ b/frappe/patches/v11_0/remove_skip_for_doctype.py @@ -60,7 +60,7 @@ def execute(): # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes, creation, modified) new_user_permissions_list.append( ( - frappe.generate_hash("", 10), + frappe.generate_hash(length=10), user_permission.user, user_permission.allow, user_permission.for_value, diff --git a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py index 1a369b4e12..7283760c23 100644 --- a/frappe/patches/v12_0/move_email_and_phone_to_child_table.py +++ b/frappe/patches/v12_0/move_email_and_phone_to_child_table.py @@ -27,7 +27,7 @@ def execute(): email_values.append( ( 1, - frappe.generate_hash(contact_detail.email_id, 10), + frappe.generate_hash(length=10), contact_detail.email_id, "email_ids", "Contact", @@ -44,7 +44,7 @@ def execute(): phone_values.append( ( phone_counter, - frappe.generate_hash(contact_detail.email_id, 10), + frappe.generate_hash(length=10), contact_detail.phone, "phone_nos", "Contact", @@ -63,7 +63,7 @@ def execute(): phone_values.append( ( phone_counter, - frappe.generate_hash(contact_detail.email_id, 10), + frappe.generate_hash(length=10), contact_detail.mobile_no, "phone_nos", "Contact", diff --git a/frappe/patches/v12_0/setup_tags.py b/frappe/patches/v12_0/setup_tags.py index 6bff8d3dac..cb0d46a45d 100644 --- a/frappe/patches/v12_0/setup_tags.py +++ b/frappe/patches/v12_0/setup_tags.py @@ -28,7 +28,7 @@ def execute(): tag_list.append((tag.strip(), time, time, "Administrator")) - tag_link_name = frappe.generate_hash(_user_tags.name + tag.strip() + doctype.name, 10) + tag_link_name = frappe.generate_hash(length=10) tag_links.append( (tag_link_name, doctype.name, _user_tags.name, tag.strip(), time, time, "Administrator") ) diff --git a/frappe/templates/includes/navbar/navbar_items.html b/frappe/templates/includes/navbar/navbar_items.html index 8a10751441..99a40a02a0 100644 --- a/frappe/templates/includes/navbar/navbar_items.html +++ b/frappe/templates/includes/navbar/navbar_items.html @@ -3,7 +3,7 @@ {% if parent %} -{%- set dropdown_id = 'id-' + frappe.utils.generate_hash('Dropdown', 12) -%} +{%- set dropdown_id = 'id-' + frappe.utils.generate_hash(length=12) -%}