From c41b5e9511f81d0919ffd5ed9e149c5e7cbe8ee1 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 13 Jan 2023 17:00:15 +0530 Subject: [PATCH 01/56] fix: Report sidebar must consider Permission Query - On boot cache permissible reports, filter out reports blocked by Permission Query - Sidebar report selector uses boot cache to get allowed reports, which now respects Permission Query - Convert qb query to str and append permission query and then execute Co-authored-by: Gavin D'souza --- frappe/boot.py | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 31e101aedc..122de4fc95 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -3,6 +3,7 @@ """ bootstrap client session """ +from typing import TYPE_CHECKING import frappe import frappe.defaults @@ -12,6 +13,7 @@ from frappe.desk.doctype.route_history.route_history import frequently_visited_l from frappe.desk.form.load import get_meta_bundle from frappe.email.inbox import get_email_accounts from frappe.model.base_document import get_controller +from frappe.model.db_query import DatabaseQuery from frappe.query_builder import DocType from frappe.query_builder.functions import Count from frappe.query_builder.terms import ParameterizedValueWrapper, SubQuery @@ -24,6 +26,9 @@ from frappe.utils import add_user_info, cstr, get_time_zone from frappe.utils.change_log import get_versions from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled +if TYPE_CHECKING: + from pypika.dialects import MySQLQueryBuilder + def get_bootinfo(): """build and return boot info""" @@ -182,7 +187,8 @@ def get_user_pages_or_reports(parent, cache=False): & (customRole[parent.lower()].isnotnull()) & (hasRole.role.isin(roles)) ) - ).run(as_dict=True) + ) + pages_with_custom_roles = run_with_permission_query(parent, pages_with_custom_roles) for p in pages_with_custom_roles: has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype} @@ -208,7 +214,7 @@ def get_user_pages_or_reports(parent, cache=False): if parent == "Report": pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0) - pages_with_standard_roles = pages_with_standard_roles.run(as_dict=True) + pages_with_standard_roles = run_with_permission_query(parent, pages_with_standard_roles) for p in pages_with_standard_roles: if p.name not in has_role: @@ -222,12 +228,12 @@ def get_user_pages_or_reports(parent, cache=False): # pages with no role are allowed if parent == "Page": - pages_with_no_roles = ( frappe.qb.from_(parentTable) .select(parentTable.name, parentTable.modified, *columns) .where(no_of_roles == 0) - ).run(as_dict=True) + ) + pages_with_no_roles = run_with_permission_query(parent, pages_with_no_roles) for p in pages_with_no_roles: if p.name not in has_role: @@ -248,6 +254,21 @@ def get_user_pages_or_reports(parent, cache=False): return has_role +def run_with_permission_query(doctype: str, query: "MySQLQueryBuilder") -> list[dict]: + """ + Adds Permission Query (Server Script) conditions and runs/executes modified query + Note: Works only if 'WHERE' is the last clause in the query + """ + db_query = DatabaseQuery(doctype, frappe.session.user) + permission_query = db_query.get_permission_query_conditions() + + query = query.get_sql() + if permission_query: + query = query + " AND " + permission_query + + return frappe.db.sql(query, as_dict=True) + + def load_translations(bootinfo): bootinfo["lang"] = frappe.lang bootinfo["__messages"] = get_messages_for_boot() From 0ba158979de31bdae879b61aad6a05f9c4a41477 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 13 Jan 2023 18:36:12 +0530 Subject: [PATCH 02/56] fix: Make `run_with_permission_query` private (not a general util) - Also switch query and doctype parameter positions in `_run_with_permission_query` --- frappe/boot.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 122de4fc95..a84007a195 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -188,7 +188,7 @@ def get_user_pages_or_reports(parent, cache=False): & (hasRole.role.isin(roles)) ) ) - pages_with_custom_roles = run_with_permission_query(parent, pages_with_custom_roles) + pages_with_custom_roles = _run_with_permission_query(pages_with_custom_roles, parent) for p in pages_with_custom_roles: has_role[p.name] = {"modified": p.modified, "title": p.title, "ref_doctype": p.ref_doctype} @@ -214,7 +214,7 @@ def get_user_pages_or_reports(parent, cache=False): if parent == "Report": pages_with_standard_roles = pages_with_standard_roles.where(report.disabled == 0) - pages_with_standard_roles = run_with_permission_query(parent, pages_with_standard_roles) + pages_with_standard_roles = _run_with_permission_query(pages_with_standard_roles, parent) for p in pages_with_standard_roles: if p.name not in has_role: @@ -233,7 +233,7 @@ def get_user_pages_or_reports(parent, cache=False): .select(parentTable.name, parentTable.modified, *columns) .where(no_of_roles == 0) ) - pages_with_no_roles = run_with_permission_query(parent, pages_with_no_roles) + pages_with_no_roles = _run_with_permission_query(pages_with_no_roles, parent) for p in pages_with_no_roles: if p.name not in has_role: @@ -254,7 +254,7 @@ def get_user_pages_or_reports(parent, cache=False): return has_role -def run_with_permission_query(doctype: str, query: "MySQLQueryBuilder") -> list[dict]: +def _run_with_permission_query(query: "MySQLQueryBuilder", doctype: str) -> list[dict]: """ Adds Permission Query (Server Script) conditions and runs/executes modified query Note: Works only if 'WHERE' is the last clause in the query @@ -266,7 +266,7 @@ def run_with_permission_query(doctype: str, query: "MySQLQueryBuilder") -> list[ if permission_query: query = query + " AND " + permission_query - return frappe.db.sql(query, as_dict=True) + return frappe.db.sql(query, as_dict=True) # nosemgrep def load_translations(bootinfo): From 2702bf60aa76b7f30178c7989388ead8e5527c7e Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Jan 2023 16:48:24 +0530 Subject: [PATCH 03/56] refactor: Use `Query` instead of `MySQLQueryBuilder` - Use `Query` from database/utils which will consider postgres query type as well - Reduce LOC where unnecessary Co-authored-by: Gavin D'souza --- frappe/boot.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index a84007a195..09956ea0d7 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -27,7 +27,7 @@ from frappe.utils.change_log import get_versions from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled if TYPE_CHECKING: - from pypika.dialects import MySQLQueryBuilder + from frappe.database.utils import Query def get_bootinfo(): @@ -254,17 +254,14 @@ def get_user_pages_or_reports(parent, cache=False): return has_role -def _run_with_permission_query(query: "MySQLQueryBuilder", doctype: str) -> list[dict]: +def _run_with_permission_query(query: "Query", doctype: str) -> list[dict]: """ Adds Permission Query (Server Script) conditions and runs/executes modified query Note: Works only if 'WHERE' is the last clause in the query """ - db_query = DatabaseQuery(doctype, frappe.session.user) - permission_query = db_query.get_permission_query_conditions() - - query = query.get_sql() + permission_query = DatabaseQuery(doctype, frappe.session.user).get_permission_query_conditions() if permission_query: - query = query + " AND " + permission_query + query = f"{query.get_sql()} AND {permission_query}" return frappe.db.sql(query, as_dict=True) # nosemgrep From 8bd1b7b019844dec0b0261699cb0036a902a9b8a Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Jan 2023 17:09:21 +0530 Subject: [PATCH 04/56] fix: Remove unnecessary `get_sql` --- frappe/boot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/boot.py b/frappe/boot.py index 09956ea0d7..bb8393e8dc 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -261,7 +261,7 @@ def _run_with_permission_query(query: "Query", doctype: str) -> list[dict]: """ permission_query = DatabaseQuery(doctype, frappe.session.user).get_permission_query_conditions() if permission_query: - query = f"{query.get_sql()} AND {permission_query}" + query = f"{query} AND {permission_query}" return frappe.db.sql(query, as_dict=True) # nosemgrep From 968648e1b68c50d47028141404917ccf1cd09004 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Jan 2023 18:24:06 +0530 Subject: [PATCH 05/56] test: Test if permission query via server script is applied on cached allowed reports --- frappe/tests/test_boot.py | 50 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py index 0b688d6aee..7baebac140 100644 --- a/frappe/tests/test_boot.py +++ b/frappe/tests/test_boot.py @@ -1,5 +1,5 @@ import frappe -from frappe.boot import get_unseen_notes +from frappe.boot import get_unseen_notes, get_user_pages_or_reports from frappe.desk.doctype.note.note import mark_as_seen from frappe.tests.utils import FrappeTestCase @@ -26,3 +26,51 @@ class TestBootData(FrappeTestCase): mark_as_seen(note.name) unseen_notes = [d.title for d in get_unseen_notes()] self.assertListEqual(unseen_notes, []) + + def test_get_user_pages_or_reports_with_permission_query(self): + try: + # Create a ToDo custom report with admin user + frappe.set_user("Administrator") + frappe.get_doc( + { + "doctype": "Report", + "ref_doctype": "ToDo", + "report_name": "Test Admin Report", + "report_type": "Report Builder", + "is_standard": "No", + } + ).insert() + + # Add permission query such that each user can only see their own custom reports + frappe.get_doc( + dict( + doctype="Server Script", + name="test_report_permission_query", + script_type="Permission Query", + reference_doctype="Report", + script="""conditions = f"(`tabReport`.is_standard = 'Yes' or `tabReport`.owner = '{frappe.session.user}')" + """, + ) + ).insert() + + # Create a ToDo custom report with test user + frappe.set_user("test@example.com") + frappe.get_doc( + { + "doctype": "Report", + "ref_doctype": "ToDo", + "report_name": "Test User Report", + "report_type": "Report Builder", + "is_standard": "No", + } + ).insert() + + get_user_pages_or_reports("Report") + allowed_reports = frappe.cache().get_value("has_role:Report", user=frappe.session.user) + + # Test user must not see admin user's report + self.assertNotIn("Test Admin Report", allowed_reports) + self.assertIn("Test User Report", allowed_reports) + finally: + frappe.db.rollback() + frappe.set_user("Administrator") From f6a68062d9fe5e44ee31436bc5639cc7ed8e4caf Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Jan 2023 19:15:16 +0530 Subject: [PATCH 06/56] chore: Add comments to avoid incompatible queries with `_run_with_permission_query` --- frappe/boot.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/boot.py b/frappe/boot.py index bb8393e8dc..8585bddf90 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -174,6 +174,7 @@ def get_user_pages_or_reports(parent, cache=False): parentTable = DocType(parent) # get pages or reports set on custom role + # must end in a WHERE clause for `_run_with_permission_query` pages_with_custom_roles = ( frappe.qb.from_(customRole) .from_(hasRole) @@ -199,6 +200,7 @@ def get_user_pages_or_reports(parent, cache=False): .where(customRole[parent.lower()].isnotnull()) ) + # must end in a WHERE clause for `_run_with_permission_query` pages_with_standard_roles = ( frappe.qb.from_(hasRole) .from_(parentTable) @@ -228,6 +230,7 @@ def get_user_pages_or_reports(parent, cache=False): # pages with no role are allowed if parent == "Page": + # must end in a WHERE clause for `_run_with_permission_query` pages_with_no_roles = ( frappe.qb.from_(parentTable) .select(parentTable.name, parentTable.modified, *columns) From 076225720dc4929ad9735bdd0b8922393d1da4ca Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 18 Jan 2023 12:53:32 +0100 Subject: [PATCH 07/56] refactor: address template --- .../address_template/address_template.jinja | 10 ++++ .../address_template/address_template.py | 48 +++++++------------ .../address_template/test_address_template.py | 46 +++++++++--------- 3 files changed, 49 insertions(+), 55 deletions(-) create mode 100644 frappe/contacts/doctype/address_template/address_template.jinja diff --git a/frappe/contacts/doctype/address_template/address_template.jinja b/frappe/contacts/doctype/address_template/address_template.jinja new file mode 100644 index 0000000000..65ea58eb21 --- /dev/null +++ b/frappe/contacts/doctype/address_template/address_template.jinja @@ -0,0 +1,10 @@ +{{ address_line1 }}
+{% if address_line2 %}{{ address_line2 }}
{% endif -%} +{{ city }}
+{% if state %}{{ state }}
{% endif -%} +{% if pincode %}{{ pincode }}
{% endif -%} +{{ country }}
+
+{% if phone %}{{ _("Phone") }}: {{ phone }}
{% endif -%} +{% if fax %}{{ _("Fax") }}: {{ fax }}
{% endif -%} +{% if email_id %}{{ _("Email") }}: {{ email_id }}
{% endif -%} diff --git a/frappe/contacts/doctype/address_template/address_template.py b/frappe/contacts/doctype/address_template/address_template.py index a8806b336b..a33115b105 100644 --- a/frappe/contacts/doctype/address_template/address_template.py +++ b/frappe/contacts/doctype/address_template/address_template.py @@ -4,52 +4,36 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cint from frappe.utils.jinja import validate_template class AddressTemplate(Document): def validate(self): + validate_template(self.template) + if not self.template: self.template = get_default_address_template() - self.defaults = frappe.db.get_values( - "Address Template", {"is_default": 1, "name": ("!=", self.name)} - ) - if not self.is_default: - if not self.defaults: - self.is_default = 1 - if cint(frappe.db.get_single_value("System Settings", "setup_complete")): - frappe.msgprint(_("Setting this Address Template as default as there is no other default")) - - validate_template(self.template) + if not self.is_default and not self._get_previous_default(): + self.is_default = 1 + if frappe.db.get_single_value("System Settings", "setup_complete"): + frappe.msgprint(_("Setting this Address Template as default as there is no other default")) def on_update(self): - if self.is_default and self.defaults: - for d in self.defaults: - frappe.db.set_value("Address Template", d[0], "is_default", 0) + if self.is_default and (previous_default := self._get_previous_default()): + frappe.db.set_value("Address Template", previous_default, "is_default", 0) def on_trash(self): if self.is_default: frappe.throw(_("Default Address Template cannot be deleted")) + def _get_previous_default(self) -> str | None: + return frappe.db.get_value("Address Template", {"is_default": 1, "name": ("!=", self.name)}) + @frappe.whitelist() -def get_default_address_template(): - """Get default address template (translated)""" - return ( - """{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}\ -{{ city }}
-{% if state %}{{ state }}
{% endif -%} -{% if pincode %}{{ pincode }}
{% endif -%} -{{ country }}
-{% if phone %}""" - + _("Phone") - + """: {{ phone }}
{% endif -%} -{% if fax %}""" - + _("Fax") - + """: {{ fax }}
{% endif -%} -{% if email_id %}""" - + _("Email") - + """: {{ email_id }}
{% endif -%}""" - ) +def get_default_address_template() -> str: + """Return the default address template.""" + from pathlib import Path + + return (Path(__file__).parent / "address_template.jinja").read_text() diff --git a/frappe/contacts/doctype/address_template/test_address_template.py b/frappe/contacts/doctype/address_template/test_address_template.py index ee45ce98f8..c3c5b544d6 100644 --- a/frappe/contacts/doctype/address_template/test_address_template.py +++ b/frappe/contacts/doctype/address_template/test_address_template.py @@ -1,39 +1,39 @@ # Copyright (c) 2015, Frappe Technologies and Contributors # License: MIT. See LICENSE import frappe +from frappe.contacts.doctype.address_template.address_template import get_default_address_template from frappe.tests.utils import FrappeTestCase +from frappe.utils.jinja import validate_template class TestAddressTemplate(FrappeTestCase): - def setUp(self): - self.make_default_address_template() + def setUp(self) -> None: + frappe.db.delete("Address Template", {"country": "India"}) + frappe.db.delete("Address Template", {"country": "Brazil"}) + + def test_default_address_template(self): + validate_template(get_default_address_template()) def test_default_is_unset(self): - a = frappe.get_doc("Address Template", "India") - a.is_default = 1 - a.save() + frappe.get_doc({"doctype": "Address Template", "country": "India", "is_default": 1}).insert() - b = frappe.get_doc("Address Template", "Brazil") - b.is_default = 1 - b.save() + self.assertEqual(frappe.db.get_value("Address Template", "India", "is_default"), 1) + + frappe.get_doc({"doctype": "Address Template", "country": "Brazil", "is_default": 1}).insert() self.assertEqual(frappe.db.get_value("Address Template", "India", "is_default"), 0) + self.assertEqual(frappe.db.get_value("Address Template", "Brazil", "is_default"), 1) - def tearDown(self): - a = frappe.get_doc("Address Template", "India") - a.is_default = 1 - a.save() + def test_delete_address_template(self): + india = frappe.get_doc( + {"doctype": "Address Template", "country": "India", "is_default": 0} + ).insert() - @classmethod - def make_default_address_template(self): - template = """{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}{{ city }}
{% if state %}{{ state }}
{% endif -%}{% if pincode %}{{ pincode }}
{% endif -%}{{ country }}
{% if phone %}Phone: {{ phone }}
{% endif -%}{% if fax %}Fax: {{ fax }}
{% endif -%}{% if email_id %}Email: {{ email_id }}
{% endif -%}""" + brazil = frappe.get_doc( + {"doctype": "Address Template", "country": "Brazil", "is_default": 1} + ).insert() - if not frappe.db.exists("Address Template", "India"): - frappe.get_doc( - {"doctype": "Address Template", "country": "India", "is_default": 1, "template": template} - ).insert() + india.reload() # might have been modified by the second template + india.delete() # should not raise an error - if not frappe.db.exists("Address Template", "Brazil"): - frappe.get_doc( - {"doctype": "Address Template", "country": "Brazil", "template": template} - ).insert() + self.assertRaises(frappe.ValidationError, brazil.delete) From 85a7757779ca1c5e143e6df3cfb382bec80fafa3 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Tue, 8 Jun 2021 14:53:27 +0000 Subject: [PATCH 08/56] fix: listview stats speedup --- frappe/public/js/frappe/list/list_view.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 8faff07d16..4625f0aa8e 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -584,7 +584,9 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { `); this.setup_new_doc_event(); - this.list_sidebar && this.list_sidebar.reload_stats(); + if (this.list_view_settings && !this.list_view_settings.disable_sidebar_stats) { + this.list_sidebar && this.list_sidebar.reload_stats(); + } this.toggle_paging && this.$paging_area.toggle(true); } From 30941c49f52e723acd93d088de022dea47d6d344 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 21 Jan 2023 17:28:42 +0530 Subject: [PATCH 09/56] chore!: remove special local cache for documents --- frappe/__init__.py | 29 +++-------------------------- frappe/cache_manager.py | 2 -- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 52aa734f8a..4c1540e7a5 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -238,7 +238,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False) -> None: local.jenv = None local.jloader = None local.cache = {} - local.document_cache = {} local.form_dict = _dict() local.preload_assets = {"style": [], "script": []} local.session = _dict() @@ -1075,25 +1074,10 @@ def set_value(doctype, docname, fieldname, value=None): def get_cached_doc(*args, **kwargs) -> "Document": - def _respond(doc, from_redis=False): - if isinstance(doc, dict): - local.document_cache[key] = doc = get_doc(doc) - - elif from_redis: - local.document_cache[key] = doc - + if (key := can_cache_doc(args)) and (doc := cache().hget("document_cache", key)): return doc - if key := can_cache_doc(args): - # local cache - has "ready" `Document` objects - if doc := local.document_cache.get(key): - return _respond(doc) - - # redis cache - if doc := cache().hget("document_cache", key): - return _respond(doc, True) - - # Not found in local/redis, fetch from DB + # Not found in cache, fetch from DB doc = get_doc(*args, **kwargs) # Store in cache @@ -1106,14 +1090,7 @@ def get_cached_doc(*args, **kwargs) -> "Document": def _set_document_in_cache(key: str, doc: "Document") -> None: - local.document_cache[key] = doc - - # Avoid setting in local.cache since we're already using local.document_cache above - # Try pickling the doc object as-is first, else fallback to doc.as_dict() - try: - cache().hset("document_cache", key, doc, cache_locally=False) - except Exception: - cache().hset("document_cache", key, doc.as_dict(), cache_locally=False) + cache().hset("document_cache", key, doc) def can_cache_doc(args) -> str | None: diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 9c9f081c60..24a4c6a271 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -127,8 +127,6 @@ def clear_doctype_cache(doctype=None): for key in ("is_table", "doctype_modules", "document_cache"): cache.delete_value(key) - frappe.local.document_cache = {} - def clear_single(dt): for name in doctype_cache_keys: cache.hdel(name, dt) From 05c03a9345598aabfb7832219fe94fd2a02d3105 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 21 Jan 2023 17:32:24 +0530 Subject: [PATCH 10/56] chore!: remove `cache_locally` parameter --- frappe/utils/redis_wrapper.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index e470c83d75..ea91299cfc 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -45,7 +45,7 @@ class RedisWrapper(redis.Redis): return f"{frappe.conf.db_name}|{key}".encode() - def set_value(self, key, val, user=None, expires_in_sec=None, shared=False, cache_locally=True): + def set_value(self, key, val, user=None, expires_in_sec=None, shared=False): """Sets cache value. :param key: Cache key @@ -55,7 +55,7 @@ class RedisWrapper(redis.Redis): """ key = self.make_key(key, user, shared) - if not expires_in_sec and cache_locally: + if not expires_in_sec: frappe.local.cache[key] = val try: @@ -169,7 +169,6 @@ class RedisWrapper(redis.Redis): key: str, value, shared: bool = False, - cache_locally: bool = True, *args, **kwargs, ): @@ -179,8 +178,7 @@ class RedisWrapper(redis.Redis): _name = self.make_key(name, shared=shared) # set in local - if cache_locally: - frappe.local.cache.setdefault(_name, {})[key] = value + frappe.local.cache.setdefault(_name, {})[key] = value # set in redis try: From 23c9d8a42d0d62c779997962d82c36e938d460a7 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 21 Jan 2023 17:46:41 +0530 Subject: [PATCH 11/56] chore: remove old cache reference --- frappe/__init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 4c1540e7a5..d533e65b58 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1116,12 +1116,11 @@ def get_document_cache_key(doctype: str, name: str): def clear_document_cache(doctype, name): cache().hdel("last_modified", doctype) - key = get_document_cache_key(doctype, name) - if key in local.document_cache: - del local.document_cache[key] - cache().hdel("document_cache", key) + cache().hdel("document_cache", get_document_cache_key(doctype, name)) + if doctype == "System Settings" and hasattr(local, "system_settings"): delattr(local, "system_settings") + if doctype == "Website Settings" and hasattr(local, "website_settings"): delattr(local, "website_settings") From ce4450cd95c24b2dab9d4189297a0ec95b81a0dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riandrys=20G=C3=B3ngora=20Rom=C3=A1n?= Date: Mon, 23 Jan 2023 09:47:28 -0500 Subject: [PATCH 12/56] fix(child table): Update docfield property using set_df_property update docfield property using set_df_property set_df_property doesn't work when it's call it in form_render event of child table --- frappe/public/js/frappe/form/form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 8893c4b69e..9aa7529761 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1516,7 +1516,7 @@ frappe.ui.form.Form = class FrappeForm { if (this.fields_dict[fieldname].grid.grid_rows_by_docname[table_row_name]) { this.fields_dict[fieldname].grid.grid_rows_by_docname[ table_row_name - ].refresh_field(fieldname); + ].refresh_field(table_field); } } else { this.refresh_field(fieldname); From ba438fe4a6ee3e1232ba214e285a544bf5a71b38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Riandrys=20G=C3=B3ngora=20Rom=C3=A1n?= Date: Mon, 23 Jan 2023 11:08:43 -0500 Subject: [PATCH 13/56] test: update docfield property using set_df_property in child table add UI test with cypress on form.js update docfield property using set_df_property in child table --- cypress/integration/form.js | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/cypress/integration/form.js b/cypress/integration/form.js index fa0d758223..8186647a14 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -26,6 +26,11 @@ context("Form", () => { }); }); + beforeEach(() => { + cy.login(); + cy.visit("/app/website"); + }); + it("create a new form", () => { cy.visit("/app/todo/new"); cy.get_field("description", "Text Editor") @@ -172,4 +177,57 @@ context("Form", () => { send_welcome_email: 0, }); }); + + it("update docfield property using set_df_property in child table", () => { + cy.visit("/app/contact/Test Form Contact 1"); + cy.window() + .its("cur_frm") + .then((frm) => { + cy.get('.frappe-control[data-fieldname="phone_nos"]').as("table"); + + // set property before form_render event of child table + cy.get("@table") + .find('[data-idx="1"]') + .invoke("attr", "data-name") + .then((cdn) => { + frm.set_df_property( + "phone_nos", + "hidden", + 1, + "Contact Phone", + "is_primary_phone", + cdn + ); + }); + + cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.frappe-control[data-fieldname="is_primary_phone"]') + .should("be.hidden"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + + // set property on form_render event of child table + cy.get("@table").find('[data-idx="1"] .edit-grid-row').click(); + cy.get("@table") + .find('[data-idx="1"]') + .invoke("attr", "data-name") + .then((cdn) => { + frm.set_df_property( + "phone_nos", + "hidden", + 0, + "Contact Phone", + "is_primary_phone", + cdn + ); + }); + + cy.get(".grid-row-open").as("table-form"); + cy.get("@table-form") + .find('.frappe-control[data-fieldname="is_primary_phone"]') + .should("be.visible"); + cy.get("@table-form").find(".grid-footer-toolbar").click(); + }); + }); }); From 81d6b282a387800d92466417851fce05b1102d94 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Tue, 24 Jan 2023 11:26:31 +0530 Subject: [PATCH 14/56] chore: Remove errprint triggered by passing Query object to db.sql --- frappe/boot.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 8585bddf90..8eed64b2dc 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -3,12 +3,11 @@ """ bootstrap client session """ -from typing import TYPE_CHECKING - import frappe import frappe.defaults import frappe.desk.desk_page from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings +from frappe.database.utils import Query from frappe.desk.doctype.route_history.route_history import frequently_visited_links from frappe.desk.form.load import get_meta_bundle from frappe.email.inbox import get_email_accounts @@ -26,9 +25,6 @@ from frappe.utils import add_user_info, cstr, get_time_zone from frappe.utils.change_log import get_versions from frappe.website.doctype.web_page_view.web_page_view import is_tracking_enabled -if TYPE_CHECKING: - from frappe.database.utils import Query - def get_bootinfo(): """build and return boot info""" @@ -264,9 +260,8 @@ def _run_with_permission_query(query: "Query", doctype: str) -> list[dict]: """ permission_query = DatabaseQuery(doctype, frappe.session.user).get_permission_query_conditions() if permission_query: - query = f"{query} AND {permission_query}" - - return frappe.db.sql(query, as_dict=True) # nosemgrep + return frappe.db.sql(f"{query} AND {permission_query}", as_dict=True) # nosemgrep + return query.run(as_dict=True) def load_translations(bootinfo): From cf92c5ac849871d9d6503afa93c7f043f17c0a11 Mon Sep 17 00:00:00 2001 From: Sabu Siyad Date: Wed, 25 Jan 2023 16:05:36 +0530 Subject: [PATCH 15/56] fix(translation): empty string passed gets passed (#19776) It is reserved by GNU gettext: gettext("") returns the header entry with meta information, not the empty string --- frappe/custom/doctype/custom_field/custom_field.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index f14a4588a8..758d9c1e64 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -142,7 +142,7 @@ def get_fields_label(doctype=None): return frappe.msgprint(_("Custom Fields can only be added to a standard DocType.")) return [ - {"value": df.fieldname or "", "label": _(df.label or "")} + {"value": df.fieldname or "", "label": _(df.label) if df.label else ""} for df in frappe.get_meta(doctype).get("fields") ] From 599f5a0ce5ae2eda69bfeb2d8f2470c6f66a0c76 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 25 Jan 2023 17:25:26 +0530 Subject: [PATCH 16/56] build(deps): Bump frappe-datatable to 1.17.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 0028ad3e9e..514c119157 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "fast-deep-equal": "^2.0.1", "fast-glob": "^3.2.5", "frappe-charts": "2.0.0-rc22", - "frappe-datatable": "^1.16.4", + "frappe-datatable": "^1.17.0", "frappe-gantt": "^0.6.0", "highlight.js": "^10.4.1", "html5-qrcode": "^2.0.11", diff --git a/yarn.lock b/yarn.lock index d2ee8e62a5..6893ef3e07 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1435,10 +1435,10 @@ frappe-charts@2.0.0-rc22: resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc22.tgz#9a5a747febdc381a1d4d7af96e89cf519dfba8c0" integrity sha512-N7f/8979wJCKjusOinaUYfMxB80YnfuVLrSkjpj4LtyqS0BGS6SuJxUnb7Jl4RWUFEIs7zEhideIKnyLeFZF4Q== -frappe-datatable@^1.16.4: - version "1.16.4" - resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.16.4.tgz#cb26f197c3cd404a5b13f016ef81c394e06f56fe" - integrity sha512-VoiTLnkuObMa3FxITrvP32UYN9v4WQ0j4qlCiDuqdXha9/BVSxwDt2BTK+cvaRloGcds5G2Hm9IRbltRRGGhxA== +frappe-datatable@^1.17.0: + version "1.17.0" + resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.0.tgz#bf13553320408acdc3b491049e05c040ea74cab8" + integrity sha512-QstM7Qg9ZLOxBidU1LBzKEjzAIQBmRmtLakRfqEsrlH4snbeKbcNFIAhiHSOe28d49gEsE3p0kLW3KU0ByID4g== dependencies: hyperlist "^1.0.0-beta" lodash "^4.17.5" From 2a1c5f1fa92fc0f131623d01248dad85a6c80140 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Wed, 25 Jan 2023 18:45:10 +0530 Subject: [PATCH 17/56] build(deps): Bump frappe-datatable to 1.17.1 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 514c119157..589d2dd6c9 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "fast-deep-equal": "^2.0.1", "fast-glob": "^3.2.5", "frappe-charts": "2.0.0-rc22", - "frappe-datatable": "^1.17.0", + "frappe-datatable": "^1.17.1", "frappe-gantt": "^0.6.0", "highlight.js": "^10.4.1", "html5-qrcode": "^2.0.11", diff --git a/yarn.lock b/yarn.lock index 6893ef3e07..e5621d385f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1435,10 +1435,10 @@ frappe-charts@2.0.0-rc22: resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc22.tgz#9a5a747febdc381a1d4d7af96e89cf519dfba8c0" integrity sha512-N7f/8979wJCKjusOinaUYfMxB80YnfuVLrSkjpj4LtyqS0BGS6SuJxUnb7Jl4RWUFEIs7zEhideIKnyLeFZF4Q== -frappe-datatable@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.0.tgz#bf13553320408acdc3b491049e05c040ea74cab8" - integrity sha512-QstM7Qg9ZLOxBidU1LBzKEjzAIQBmRmtLakRfqEsrlH4snbeKbcNFIAhiHSOe28d49gEsE3p0kLW3KU0ByID4g== +frappe-datatable@^1.17.1: + version "1.17.1" + resolved "https://registry.yarnpkg.com/frappe-datatable/-/frappe-datatable-1.17.1.tgz#795ee79a420df07b963b7decf489045d5993cc0b" + integrity sha512-qqvmsaYbQUwCAtGnhmTN8jrdvXW6YfRLTZS6ufb3b1ibFEMUbE04rEFJF7TJRd2ugSk80seS2OPGTZGw+V2b0A== dependencies: hyperlist "^1.0.0-beta" lodash "^4.17.5" From ffc5447548716442d6682a91b66879d7e63b2efe Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 25 Jan 2023 20:33:55 +0530 Subject: [PATCH 18/56] fix: set-config without `-g` must specify site (#19782) --- frappe/commands/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 280e656f1c..2a1f7d3e5e 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -1071,6 +1071,8 @@ def set_config(context, key, value, global_=False, parse=False, as_dict=False): common_site_config_path = os.path.join(sites_path, "common_site_config.json") update_site_config(key, value, validate=False, site_config_path=common_site_config_path) else: + if not context.sites: + raise SiteNotSpecifiedError for site in context.sites: frappe.init(site=site) update_site_config(key, value, validate=False) From 9bccedc761413e8e967582296da4862a4012c715 Mon Sep 17 00:00:00 2001 From: V Shankar <95605398+Shankarv19bcr@users.noreply.github.com> Date: Thu, 26 Jan 2023 15:30:47 +0530 Subject: [PATCH 19/56] feat: change quick entry dialog size based on column breaks (#19715) Co-authored-by: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> --- frappe/public/js/frappe/ui/dialog.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js index ed42b81b68..9ec6922306 100644 --- a/frappe/public/js/frappe/ui/dialog.js +++ b/frappe/public/js/frappe/ui/dialog.js @@ -28,6 +28,8 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { this.get_close_btn().hide(); } + if (!this.size) this.set_modal_size(); + this.wrapper = this.$wrapper.find(".modal-dialog").get(0); if (this.size == "small") $(this.wrapper).addClass("modal-sm"); else if (this.size == "large") $(this.wrapper).addClass("modal-lg"); @@ -123,6 +125,31 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup { }); } + set_modal_size() { + if (!this.fields) { + this.size = ""; + return; + } + + let col_brk = 0; + let cur_col_brk = 0; + + // if fields have more than 2 Column Breaks before encountering Section Break, make it large + this.fields.forEach((field) => { + if (field.fieldtype == "Column Break") { + cur_col_brk++; + + if (cur_col_brk > col_brk) { + col_brk = cur_col_brk; + } + } else if (field.fieldtype == "Section Break") { + cur_col_brk = 0; + } + }); + + this.size = col_brk >= 4 ? "extra-large" : col_brk >= 2 ? "large" : ""; + } + get_primary_btn() { return this.standard_actions.find(".btn-primary"); } From 586613d94caa6c61828887136495392504d08069 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 26 Jan 2023 15:36:18 +0530 Subject: [PATCH 20/56] test: fixed failing control_color UI test --- cypress/integration/control_color.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/control_color.js b/cypress/integration/control_color.js index aa3a45eed8..e97dbe0f06 100644 --- a/cypress/integration/control_color.js +++ b/cypress/integration/control_color.js @@ -26,7 +26,7 @@ context("Control Color", () => { //Checking if the css attribute is correct cy.get(".color-map").should("have.css", "color", "rgb(79, 157, 217)"); - cy.get(".hue-map").should("have.css", "color", "rgb(0, 145, 255)"); + cy.get(".hue-map").should("have.css", "color", "rgb(0, 144, 255)"); //Checking if the correct color is being selected cy.get("@dialog").then((dialog) => { From ed86da416f71d41c048a198535fc78632bc9565a Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 26 Jan 2023 15:50:12 +0530 Subject: [PATCH 21/56] test: fixed failing web_form UI test --- cypress/support/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 0a25ff5cab..c067974d9f 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -285,7 +285,7 @@ Cypress.Commands.add("get_open_dialog", () => { Cypress.Commands.add("save", () => { cy.intercept("/api/method/frappe.desk.form.save.savedocs").as("save_call"); - cy.get(`button[data-label="Save"]:visible`).click({ scrollBehavior: "top", force: true }); + cy.get(`.page-container:visible button[data-label="Save"]`).click({ force: true }); cy.wait("@save_call"); }); Cypress.Commands.add("hide_dialog", () => { From 4bcb12617c6d14cd323d1fad7da295d8679eb2f3 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 26 Jan 2023 11:39:41 +0100 Subject: [PATCH 22/56] fix: assertAlmostEqual with precision --- frappe/model/base_document.py | 2 +- frappe/tests/utils.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 8cd074a4c0..783e879bff 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -1068,7 +1068,7 @@ class BaseDocument: def is_dummy_password(self, pwd): return "".join(set(pwd)) == "*" - def precision(self, fieldname, parentfield=None): + def precision(self, fieldname, parentfield=None) -> int | None: """Returns float precision for a particular field (or get global default). :param fieldname: Fieldname for which precision is required. diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py index 15e0c3d9c0..5f13c9cd11 100644 --- a/frappe/tests/utils.py +++ b/frappe/tests/utils.py @@ -55,12 +55,14 @@ class FrappeTestCase(unittest.TestCase): else: self._compare_field(value, actual.get(field), actual, field) - def _compare_field(self, expected, actual, doc, field): + def _compare_field(self, expected, actual, doc: BaseDocument, field: str): msg = f"{field} should be same." if isinstance(expected, float): precision = doc.precision(field) - self.assertAlmostEqual(expected, actual, f"{field} should be same to {precision} digits") + self.assertAlmostEqual( + expected, actual, places=precision, msg=f"{field} should be same to {precision} digits" + ) elif isinstance(expected, (bool, int)): self.assertEqual(expected, cint(actual), msg=msg) elif isinstance(expected, datetime_like_types): From ea306db2a2a5c6c20a53a725267df187411913e4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 26 Jan 2023 16:11:49 +0530 Subject: [PATCH 23/56] test: fix flaky view_routing UI test --- cypress/integration/view_routing.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cypress/integration/view_routing.js b/cypress/integration/view_routing.js index 1e3b841c79..9267974154 100644 --- a/cypress/integration/view_routing.js +++ b/cypress/integration/view_routing.js @@ -103,8 +103,9 @@ context("View", () => { }); it("Route to File View", () => { + cy.intercept("POST", "/api/method/frappe.desk.reportview.get").as("list_loaded"); cy.visit("app/file"); - cy.wait(500); + cy.wait("@list_loaded"); cy.window() .its("cur_list") .then((list) => { @@ -113,7 +114,7 @@ context("View", () => { }); cy.visit("app/file/view/home/Attachments"); - cy.wait(500); + cy.wait("@list_loaded"); cy.window() .its("cur_list") .then((list) => { From 28e9b44dbc804560fae99e2afa56067b88ac561a Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 26 Jan 2023 16:19:23 +0530 Subject: [PATCH 24/56] test: fix flaky folder_navigation UI test --- cypress/integration/folder_navigation.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js index 60fa46bc88..c5b3a44f0d 100644 --- a/cypress/integration/folder_navigation.js +++ b/cypress/integration/folder_navigation.js @@ -10,7 +10,7 @@ context("Folder Navigation", () => { cy.get(".filter-selector > .btn").findByText("1 filter").click(); cy.findByRole("button", { name: "Clear Filters" }).click(); cy.get(".filter-action-buttons > .text-muted").findByText("+ Add a Filter").click(); - cy.get(".fieldname-select-area > .awesomplete > .form-control").type("Fol{enter}"); + cy.get(".fieldname-select-area > .awesomplete > .form-control:last").type("Fol{enter}"); cy.get( ".filter-field > .form-group > .link-field > .awesomplete > .input-with-feedback" ).type("Home{enter}"); From 114db456f84549d7462a83c078bc3a468e3ce901 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 26 Jan 2023 16:22:57 +0530 Subject: [PATCH 25/56] test: fix flaky navigation UI test --- cypress/integration/navigation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js index 2302296f23..cf1b5dc89d 100644 --- a/cypress/integration/navigation.js +++ b/cypress/integration/navigation.js @@ -18,6 +18,7 @@ context("Navigation", () => { it.only("Navigate to previous page after login", () => { cy.visit("/app/todo"); cy.get(".page-head").findByTitle("To Do").should("be.visible"); + cy.clear_filters(); cy.request("/api/method/logout"); cy.reload().as("reload"); cy.get("@reload").get(".page-card .btn-primary").contains("Login").click(); From 177ca618eb06e6d93d22e6592d5a758da6e84947 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 26 Jan 2023 16:45:25 +0530 Subject: [PATCH 26/56] test: fix flaky control_link UI test --- cypress/integration/control_link.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index a5281d9b09..d3462492f6 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -229,19 +229,15 @@ context("Control Link", () => { ); cy.reload(); cy.new_form("ToDo"); - cy.fill_field("description", "new", "Text Editor"); - cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form"); - cy.findByRole("button", { name: "Save" }).click(); - cy.wait("@save_form"); + cy.fill_field("description", "new", "Text Editor").wait(200); + cy.save(); cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should( "contain", "Administrator" ); // if user clears default value explicitly, system should not reset default again cy.get_field("assigned_by").clear().blur(); - cy.intercept("POST", "/api/method/frappe.desk.form.save.savedocs").as("save_form"); - cy.findByRole("button", { name: "Save" }).click(); - cy.wait("@save_form"); + cy.save(); cy.get_field("assigned_by").should("have.value", ""); cy.get(".frappe-control[data-fieldname=assigned_by_full_name] .control-value").should( "contain", From 8fe6b8f3d99bc821325c68692abe9dd7e60b14a8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 26 Jan 2023 13:53:03 +0100 Subject: [PATCH 27/56] fix: add freeze message for bulk delete --- frappe/public/js/frappe/list/bulk_operations.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index c9e8f7d329..7f317137b9 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -146,6 +146,8 @@ export default class BulkOperations { .call({ method: "frappe.desk.reportview.delete_items", freeze: true, + freeze_message: + docnames.length <= 10 ? __("Deleting {0}...", [docnames.join(", ")]) : null, args: { items: docnames, doctype: this.doctype, From 9d9c06985ed3ac671c3bb96ccf597c7d068c59f5 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:49:50 +0100 Subject: [PATCH 28/56] fix: use count instead of concatenated docnames --- frappe/public/js/frappe/list/bulk_operations.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/list/bulk_operations.js b/frappe/public/js/frappe/list/bulk_operations.js index 7f317137b9..e7e3fb256f 100644 --- a/frappe/public/js/frappe/list/bulk_operations.js +++ b/frappe/public/js/frappe/list/bulk_operations.js @@ -147,7 +147,9 @@ export default class BulkOperations { method: "frappe.desk.reportview.delete_items", freeze: true, freeze_message: - docnames.length <= 10 ? __("Deleting {0}...", [docnames.join(", ")]) : null, + docnames.length <= 10 + ? __("Deleting {0} records...", [docnames.length]) + : null, args: { items: docnames, doctype: this.doctype, From e93345d20e1ead3f9ce5d46184c4b21f4bd4d55b Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 26 Jan 2023 16:20:43 -0500 Subject: [PATCH 29/56] fix: grid search without frm --- frappe/public/js/frappe/form/grid.js | 4 +--- frappe/public/js/frappe/form/grid_row.js | 12 ++++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index e80a07f8ac..9e7a8b58aa 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -580,9 +580,7 @@ export default class Grid { } get_filtered_data() { - if (!this.frm) return; - - let all_data = this.frm.doc[this.df.fieldname]; + let all_data = this.frm ? this.frm.doc[this.df.fieldname] : this.df.data; for (const field in this.filter) { all_data = all_data.filter((data) => { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 0b6ac5c208..b8bb8756d3 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -757,12 +757,12 @@ export default class GridRow { show_search_row() { // show or remove search columns based on grid rows - this.show_search = - this.frm && - this.frm.doc && - this.frm.doc[this.grid.df.fieldname] && - this.frm.doc[this.grid.df.fieldname].length >= 20; - !this.show_search && this.wrapper.remove(); + // this.show_search = + // this.frm && + // this.frm.doc && + // this.frm.doc[this.grid.df.fieldname] && + // this.frm.doc[this.grid.df.fieldname].length >= 20; + // !this.show_search && this.wrapper.remove(); return this.show_search; } From 67c5a8d75a16920cd574a6e70aff05dc8080c41f Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 26 Jan 2023 16:31:07 -0500 Subject: [PATCH 30/56] fix: remove incompatible logic --- frappe/public/js/frappe/form/grid_row.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index b8bb8756d3..b39b0b1641 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -756,13 +756,6 @@ export default class GridRow { } show_search_row() { - // show or remove search columns based on grid rows - // this.show_search = - // this.frm && - // this.frm.doc && - // this.frm.doc[this.grid.df.fieldname] && - // this.frm.doc[this.grid.df.fieldname].length >= 20; - // !this.show_search && this.wrapper.remove(); return this.show_search; } From 10104abeb2b5e3b83afb3d0d6f49a1dc9be10218 Mon Sep 17 00:00:00 2001 From: Devin Slauenwhite Date: Thu, 26 Jan 2023 17:15:22 -0500 Subject: [PATCH 31/56] fix: limit search rows to >= 20 --- frappe/public/js/frappe/form/grid_row.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index b39b0b1641..35caaa667f 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -756,6 +756,7 @@ export default class GridRow { } show_search_row() { + this.show_search = this.show_search && this.grid.data.length >= 20; return this.show_search; } From c0fab395a74ce283d4597f50d2adae15953d0f1e Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 27 Jan 2023 09:04:35 +0530 Subject: [PATCH 32/56] ci(vuln check): skip dropbox package and use cache --- .github/workflows/linters.yml | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index 01b5407489..eb775e01cd 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -80,7 +80,20 @@ jobs: - uses: actions/setup-python@v4 with: python-version: '3.10' + - uses: actions/checkout@v3 + + - name: Cache pip + uses: actions/cache@v3 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - run: | pip install pip-audit - pip-audit ${GITHUB_WORKSPACE} + cd ${GITHUB_WORKSPACE} + sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456 + pip-audit . From db9b25ec0e529ffb654faa97ff833b64442f78c5 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Fri, 27 Jan 2023 03:57:11 +0000 Subject: [PATCH 33/56] fix(MariaDBTable): dont attempt to drop index twice (#19783) --- frappe/database/mariadb/schema.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index 13525d2328..bbdd95d921 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -94,7 +94,7 @@ class MariaDBTable(DBTable): if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False): add_index_query.append(f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)") - for col in self.drop_index + self.drop_unique: + for col in {*self.drop_index, *self.drop_unique}: if col.fieldname == "name": continue From 70ee9272b100743ad2b75870a7b6f611483cddd4 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 27 Jan 2023 12:44:33 +0530 Subject: [PATCH 34/56] fix: sanitize traceback for common secrets (#19805) --- frappe/tests/test_utils.py | 13 ++++++++++++ frappe/utils/__init__.py | 43 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 54 insertions(+), 2 deletions(-) diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index c6f7b8302f..59df08dd91 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -960,3 +960,16 @@ class TestTypingValidations(FrappeTestCase): report.toggle_disable(changed_value) report.toggle_disable(current_value) + + +class TestTBSanitization(FrappeTestCase): + def test_traceback_sanitzation(self): + try: + password = "42" + args = {"password": "42", "pwd": "42", "safe": "safe_value"} + raise Exception + except Exception: + traceback = frappe.get_traceback(with_context=True) + self.assertNotIn("42", traceback) + self.assertIn("********", traceback) + self.assertIn("safe_value", traceback) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index c715097be2..d82c039b95 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -24,7 +24,6 @@ from typing import Any, Literal from urllib.parse import quote, urlparse from redis.exceptions import ConnectionError -from traceback_with_variables import iter_exc_lines from werkzeug.test import Client import frappe @@ -298,13 +297,15 @@ def get_traceback(with_context=False) -> str: """ Returns the traceback of the Exception """ + from traceback_with_variables import iter_exc_lines + exc_type, exc_value, exc_tb = sys.exc_info() if not any([exc_type, exc_value, exc_tb]): return "" if with_context: - trace_list = iter_exc_lines() + trace_list = iter_exc_lines(fmt=_get_sanitizer()) tb = "\n".join(trace_list) else: trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) @@ -314,6 +315,44 @@ def get_traceback(with_context=False) -> str: return tb.replace(bench_path, "") +@functools.lru_cache(maxsize=1) +def _get_sanitizer(): + from traceback_with_variables import Format + + blocklist = [ + "password", + "passwd", + "secret", + "token", + "key", + "pwd", + ] + + placeholder = "********" + + def dict_printer(v: dict) -> str: + from copy import deepcopy + + v = deepcopy(v) + for key in blocklist: + if key in v: + v[key] = placeholder + + return str(v) + + # Adapted from https://github.com/andy-landy/traceback_with_variables/blob/master/examples/format_customized.py + # Reused under MIT license: https://github.com/andy-landy/traceback_with_variables/blob/master/LICENSE + + return Format( + custom_var_printers=[ + # redact variables + *[(variable_name, lambda: placeholder) for variable_name in blocklist], + # redact dictionary keys + (["_secret", dict, lambda *a, **kw: False], dict_printer), + ], + ) + + def log(event, details): frappe.logger(event).info(details) From bc9ed4a422a8ab1c1617a45137f95dc822f59483 Mon Sep 17 00:00:00 2001 From: Ritwik Puri Date: Fri, 27 Jan 2023 14:19:05 +0530 Subject: [PATCH 35/56] chore: remove prepared report event from system settings controller (#19808) [skip ci] --- frappe/core/doctype/system_settings/system_settings.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js index 3db3eef299..1de4dd82e7 100644 --- a/frappe/core/doctype/system_settings/system_settings.js +++ b/frappe/core/doctype/system_settings/system_settings.js @@ -30,13 +30,6 @@ frappe.ui.form.on("System Settings", { frm.set_value("bypass_restrict_ip_check_if_2fa_enabled", 0); } }, - enable_prepared_report_auto_deletion: function (frm) { - if (frm.doc.enable_prepared_report_auto_deletion) { - if (!frm.doc.prepared_report_expiry_period) { - frm.set_value("prepared_report_expiry_period", 7); - } - } - }, on_update: function (frm) { if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) { // Clear cache after saving to refresh the values of boot. From b8deb7241104db1e702d35606b715e3481b2cf58 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 27 Jan 2023 14:26:54 +0530 Subject: [PATCH 36/56] fix: prepared report patch (#19807) - the patch is failing if system setting is updated and column is gone - the message is misleading "invalid column", it should be "missing field". --- frappe/database/database.py | 4 +++- frappe/patches.txt | 2 +- ...emove_prepared_report_settings_from_system_settings.py | 8 +------- 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index fe258be8d7..cf4ceef461 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -772,7 +772,9 @@ class Database: if not df: frappe.throw( - _("Invalid field name: {0}").format(frappe.bold(fieldname)), self.InvalidColumnName + _("Field {0} does not exist on {1}").format( + frappe.bold(fieldname), frappe.bold(doctype), self.InvalidColumnName + ) ) val = cast_fieldtype(df.fieldtype, val) diff --git a/frappe/patches.txt b/frappe/patches.txt index 7f7ab9bfe2..49b43f0558 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -196,7 +196,6 @@ frappe.patches.v14_0.setup_likes_from_feedback frappe.patches.v14_0.update_webforms frappe.patches.v14_0.delete_payment_gateways frappe.patches.v15_0.remove_event_streaming -frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report [post_model_sync] @@ -221,3 +220,4 @@ frappe.patches.v14_0.update_attachment_comment frappe.patches.v15_0.set_contact_full_name execute:frappe.delete_doc("Page", "activity", force=1) frappe.patches.v14_0.disable_email_accounts_with_oauth +frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings diff --git a/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py b/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py index 8c0ec4ca70..2c203784df 100644 --- a/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py +++ b/frappe/patches/v15_0/remove_prepared_report_settings_from_system_settings.py @@ -4,12 +4,6 @@ from frappe.utils import cint def execute(): expiry_period = ( - cint(frappe.db.get_single_value("System Settings", "prepared_report_expiry_period")) or 30 + cint(frappe.db.get_singles_dict("System Settings").get("prepared_report_expiry_period")) or 30 ) frappe.get_single("Log Settings").register_doctype("Prepared Report", expiry_period) - - singles = frappe.qb.DocType("Singles") - frappe.qb.from_(singles).delete().where( - (singles.doctype == "System Settings") - & (singles.field.isin(["enable_prepared_report_auto_deletion", "prepared_report_expiry_period"])) - ).run() From b812f73339dff2c6373d7f49d1bec4499d5bd69f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Fri, 27 Jan 2023 15:57:02 +0100 Subject: [PATCH 37/56] feat: better freeze message --- frappe/public/scss/desk/global.scss | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss index 0d7ca9ac06..5294779990 100644 --- a/frappe/public/scss/desk/global.scss +++ b/frappe/public/scss/desk/global.scss @@ -424,26 +424,19 @@ kbd { background-color: var(--bg-color); .freeze-message-container { + inset: 0; + padding: 3rem; + background-color: var(--bg-light-gray); + color: var(--text-on-light-gray); + font-size: larger; position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; display: grid; place-content: center; } - - .freeze-message { - color: var(--text-color) !important; - } -} - -#freeze.dark { - background-color: var(--gray-900); } #freeze.in { - opacity: 0.5; + opacity: 0.8; } .msg-box { From ee80d6a504a736380f6f9b1e5bf1da954cc3609c Mon Sep 17 00:00:00 2001 From: gavin Date: Fri, 27 Jan 2023 22:57:19 +0530 Subject: [PATCH 38/56] feat(cli): Pass extra args to DB console (#19809) --- frappe/commands/utils.py | 38 +++++++++++++++++++++++++------------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 2a1f7d3e5e..5ec0b54828 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -17,6 +17,7 @@ DATA_IMPORT_DEPRECATION = ( "[DEPRECATED] The `import-csv` command used 'Data Import Legacy' which has been deprecated.\n" "Use `data-import` command instead to import data via 'Data Import'." ) +EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True} @click.command("build") @@ -485,9 +486,10 @@ def bulk_rename(context, doctype, path): frappe.destroy() -@click.command("db-console") +@click.command("db-console", context_settings=EXTRA_ARGS_CTX) +@click.argument("extra_args", nargs=-1) @pass_context -def database(context): +def database(context, extra_args): """ Enter into the Database console for given site. """ @@ -496,14 +498,18 @@ def database(context): raise SiteNotSpecifiedError frappe.init(site=site) if not frappe.conf.db_type or frappe.conf.db_type == "mariadb": - _mariadb() + _mariadb(extra_args=extra_args) elif frappe.conf.db_type == "postgres": - _psql() + _psql(extra_args=extra_args) -@click.command("mariadb") +@click.command( + "mariadb", + context_settings=EXTRA_ARGS_CTX, +) +@click.argument("extra_args", nargs=-1) @pass_context -def mariadb(context): +def mariadb(context, extra_args): """ Enter into mariadb console for a given site. """ @@ -511,21 +517,22 @@ def mariadb(context): if not site: raise SiteNotSpecifiedError frappe.init(site=site) - _mariadb() + _mariadb(extra_args=extra_args) -@click.command("postgres") +@click.command("postgres", context_settings=EXTRA_ARGS_CTX) +@click.argument("extra_args", nargs=-1) @pass_context -def postgres(context): +def postgres(context, extra_args): """ Enter into postgres console for a given site. """ site = get_site(context) frappe.init(site=site) - _psql() + _psql(extra_args=extra_args) -def _mariadb(): +def _mariadb(extra_args=None): from frappe.database.mariadb.database import MariaDBDatabase mysql = which("mysql") @@ -543,10 +550,12 @@ def _mariadb(): "--safe-updates", "-A", ] + if extra_args: + command += list(extra_args) os.execv(mysql, command) -def _psql(): +def _psql(extra_args=None): psql = which("psql") host = frappe.conf.db_host or "127.0.0.1" @@ -554,7 +563,10 @@ def _psql(): env = os.environ.copy() env["PGPASSWORD"] = frappe.conf.db_password conn_string = f"postgresql://{frappe.conf.db_name}@{host}:{port}/{frappe.conf.db_name}" - subprocess.run([psql, conn_string], check=True, env=env) + psql_cmd = [psql, conn_string] + if extra_args: + psql_cmd = psql_cmd + list(extra_args) + subprocess.run(psql_cmd, check=True, env=env) @click.command("jupyter") From d3d76865f72c52e71661ef87dde5121a77d59e2c Mon Sep 17 00:00:00 2001 From: morehardik <93863349+morehardik@users.noreply.github.com> Date: Sun, 29 Jan 2023 03:01:29 +0530 Subject: [PATCH 39/56] fix: check permission before running onload hook #11774 (#19823) Added fix for issue wherein Document is loaded post permission check --- frappe/desk/form/load.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index d7dfbb90d7..81f5096f29 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -28,14 +28,10 @@ def getdoc(doctype, name, user=None): if not (doctype and name): raise Exception("doctype and name required!") - if not name: - name = doctype - if not is_virtual_doctype(doctype) and not frappe.db.exists(doctype, name): return [] doc = frappe.get_doc(doctype, name) - run_onload(doc) if not doc.has_permission("read"): frappe.flags.error_message = _("Insufficient Permission for {0}").format( @@ -43,6 +39,8 @@ def getdoc(doctype, name, user=None): ) raise frappe.PermissionError(("read", doctype, name)) + + run_onload(doc) doc.apply_fieldlevel_read_permissions() # add file list From 4d08a989f54a158a0765bcd7c7b134c6846b0f29 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 25 Jan 2023 15:03:59 +0530 Subject: [PATCH 40/56] feat: Audit hooks report --- frappe/custom/report/__init__.py | 0 .../report/audit_system_hooks/__init__.py | 0 .../audit_system_hooks/audit_system_hooks.js | 7 +++ .../audit_system_hooks.json | 27 +++++++++ .../audit_system_hooks/audit_system_hooks.py | 60 +++++++++++++++++++ .../test_audit_system_hooks.py | 17 ++++++ 6 files changed, 111 insertions(+) create mode 100644 frappe/custom/report/__init__.py create mode 100644 frappe/custom/report/audit_system_hooks/__init__.py create mode 100644 frappe/custom/report/audit_system_hooks/audit_system_hooks.js create mode 100644 frappe/custom/report/audit_system_hooks/audit_system_hooks.json create mode 100644 frappe/custom/report/audit_system_hooks/audit_system_hooks.py create mode 100644 frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py diff --git a/frappe/custom/report/__init__.py b/frappe/custom/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/report/audit_system_hooks/__init__.py b/frappe/custom/report/audit_system_hooks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.js b/frappe/custom/report/audit_system_hooks/audit_system_hooks.js new file mode 100644 index 0000000000..a78464f3da --- /dev/null +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.js @@ -0,0 +1,7 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Audit System Hooks"] = { + filters: [], +}; diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.json b/frappe/custom/report/audit_system_hooks/audit_system_hooks.json new file mode 100644 index 0000000000..d9ea86f07f --- /dev/null +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-01-25 15:02:21.896117", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "", + "modified": "2023-01-25 15:03:31.263337", + "modified_by": "Administrator", + "module": "Custom", + "name": "Audit System Hooks", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "System Settings", + "report_name": "Audit System Hooks", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py new file mode 100644 index 0000000000..8dab192a14 --- /dev/null +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py @@ -0,0 +1,60 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe + + +def execute(filters=None): + frappe.only_for("System Manager") + + return get_columns(), get_data() + + +def get_columns(): + values_field_type = "Data" # TODO: better text wrapping in reportview + columns = [ + {"label": "Hook name", "fieldname": "hook_name", "fieldtype": "Data", "width": 200}, + {"label": "Hook key (optional)", "fieldname": "key", "fieldtype": "Data", "width": 200}, + {"label": "Hook Values", "fieldname": "hook_values", "fieldtype": values_field_type}, + ] + + # Each app is shown in order as a column + installed_apps = frappe.get_installed_apps(_ensure_on_bench=True) + columns += [ + {"label": app, "fieldname": app, "fieldtype": values_field_type} for app in installed_apps + ] + + return columns + + +def get_data(): + hooks = frappe.get_hooks() + installed_apps = frappe.get_installed_apps(_ensure_on_bench=True) + + def fmt_hook_values(v): + """Improve readability by discarding falsy values and removing containers when only 1 + value is in container""" + if not v: + return "" + + if isinstance(v, list) and len(v) == 1: + return str(v[0]) + + if isinstance(v, (dict, list)): + try: + return frappe.as_json(v) + except Exception: + pass + + return str(v) + + data = [] + for hook, values in hooks.items(): + row = {"hook_name": hook, "hook_values": fmt_hook_values(values)} + + for app in installed_apps: + row[app] = fmt_hook_values(frappe.get_hooks(hook, app_name=app)) + + data.append(row) + + return data diff --git a/frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py new file mode 100644 index 0000000000..cd3edffc77 --- /dev/null +++ b/frappe/custom/report/audit_system_hooks/test_audit_system_hooks.py @@ -0,0 +1,17 @@ +# Copyright (c) 2022, Frappe Technologies and contributors +# For license information, please see license.txt + + +from frappe.custom.report.audit_system_hooks.audit_system_hooks import execute +from frappe.tests.utils import FrappeTestCase + + +class TestAuditSystemHooksReport(FrappeTestCase): + def test_basic_query(self): + _, data = execute() + for row in data: + if row.get("hook_name") == "app_name": + self.assertEqual(row.get("hook_values"), "frappe") + break + else: + self.fail("Failed to generate hooks report") From 80dcf0b1788088bb1d469766deeabcd85bc0d7dc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sun, 29 Jan 2023 20:09:45 +0530 Subject: [PATCH 41/56] feat: Split dict hooks to separate lines --- .../audit_system_hooks/audit_system_hooks.py | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py index 8dab192a14..a42c5c361a 100644 --- a/frappe/custom/report/audit_system_hooks/audit_system_hooks.py +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.py @@ -5,8 +5,6 @@ import frappe def execute(filters=None): - frappe.only_for("System Manager") - return get_columns(), get_data() @@ -14,8 +12,8 @@ def get_columns(): values_field_type = "Data" # TODO: better text wrapping in reportview columns = [ {"label": "Hook name", "fieldname": "hook_name", "fieldtype": "Data", "width": 200}, - {"label": "Hook key (optional)", "fieldname": "key", "fieldtype": "Data", "width": 200}, - {"label": "Hook Values", "fieldname": "hook_values", "fieldtype": values_field_type}, + {"label": "Hook key (optional)", "fieldname": "hook_key", "fieldtype": "Data", "width": 200}, + {"label": "Hook Values (resolved)", "fieldname": "hook_values", "fieldtype": values_field_type}, ] # Each app is shown in order as a column @@ -37,8 +35,7 @@ def get_data(): if not v: return "" - if isinstance(v, list) and len(v) == 1: - return str(v[0]) + v = delist(v) if isinstance(v, (dict, list)): try: @@ -50,11 +47,24 @@ def get_data(): data = [] for hook, values in hooks.items(): - row = {"hook_name": hook, "hook_values": fmt_hook_values(values)} + if isinstance(values, dict): + for k, v in values.items(): + row = {"hook_name": hook, "hook_key": fmt_hook_values(k), "hook_values": fmt_hook_values(v)} + for app in installed_apps: + if app_hooks := delist(frappe.get_hooks(hook, app_name=app)): + row[app] = fmt_hook_values(app_hooks.get(k)) + data.append(row) + else: + row = {"hook_name": hook, "hook_values": fmt_hook_values(values)} + for app in installed_apps: + row[app] = fmt_hook_values(frappe.get_hooks(hook, app_name=app)) - for app in installed_apps: - row[app] = fmt_hook_values(frappe.get_hooks(hook, app_name=app)) - - data.append(row) + data.append(row) return data + + +def delist(val): + if isinstance(val, list) and len(val) == 1: + return val[0] + return val From e18188ed3ed2a71997e756bf79999092762948e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BCrker=20Tunal=C4=B1?= Date: Mon, 30 Jan 2023 07:08:35 +0300 Subject: [PATCH 42/56] fix(i18n): Datepicker Turkish translations (#19777) * feat:Datepicker Turkish translations are added. * chore: format [skip ci] --------- --- .../frappe/form/controls/datepicker_i18n.js | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/datepicker_i18n.js b/frappe/public/js/frappe/form/controls/datepicker_i18n.js index eca5a61723..ff0c4ffe50 100644 --- a/frappe/public/js/frappe/form/controls/datepicker_i18n.js +++ b/frappe/public/js/frappe/form/controls/datepicker_i18n.js @@ -136,3 +136,44 @@ import "air-datepicker/dist/js/i18n/datepicker.zh.js"; firstDay: 1, }; })(jQuery); + +(function ($) { + $.fn.datepicker.language["tr"] = { + days: ["Pazar", "Pazartesi", "Salı", "Çarşamba", "Perşembe", "Cuma", "Cumartesi"], + daysShort: ["Paz", "Pzt", "Sal", "Çar", "Per", "Cum", "Cmt"], + daysMin: ["Pz", "Pt", "Sa", "Ça", "Pe", "Cu", "Ct"], + months: [ + "Ocak", + "Şubat", + "Mart", + "Nisan", + "Mayıs", + "Haziran", + "Temmuz", + "Ağustos", + "Eylül", + "Ekim", + "Kasım", + "Aralık", + ], + monthsShort: [ + "Oca", + "Şub", + "Mar", + "Nis", + "May", + "Haz", + "Tem", + "Ağu", + "Eyl", + "Eki", + "Kas", + "Ara", + ], + today: "Bugün", + clear: "Temizle", + dateFormat: "dd.mm.yyyy", + timeFormat: "hh:ii", + firstDay: 1, + }; +})(jQuery); From 1e4d28cc196cee68895fbc56a4f0407362c0bec2 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 27 Jan 2023 16:11:00 +0530 Subject: [PATCH 43/56] fix(test): Remove try-finally & ignore perms on test user's report insertion --- frappe/tests/test_boot.py | 83 +++++++++++++++++++-------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py index 7baebac140..ece2c181df 100644 --- a/frappe/tests/test_boot.py +++ b/frappe/tests/test_boot.py @@ -28,49 +28,48 @@ class TestBootData(FrappeTestCase): self.assertListEqual(unseen_notes, []) def test_get_user_pages_or_reports_with_permission_query(self): - try: - # Create a ToDo custom report with admin user - frappe.set_user("Administrator") - frappe.get_doc( - { - "doctype": "Report", - "ref_doctype": "ToDo", - "report_name": "Test Admin Report", - "report_type": "Report Builder", - "is_standard": "No", - } - ).insert() + # Create a ToDo custom report with admin user + frappe.set_user("Administrator") + frappe.get_doc( + { + "doctype": "Report", + "ref_doctype": "ToDo", + "report_name": "Test Admin Report", + "report_type": "Report Builder", + "is_standard": "No", + } + ).insert() - # Add permission query such that each user can only see their own custom reports - frappe.get_doc( - dict( - doctype="Server Script", - name="test_report_permission_query", - script_type="Permission Query", - reference_doctype="Report", - script="""conditions = f"(`tabReport`.is_standard = 'Yes' or `tabReport`.owner = '{frappe.session.user}')" - """, - ) - ).insert() + # Add permission query such that each user can only see their own custom reports + frappe.get_doc( + dict( + doctype="Server Script", + name="test_report_permission_query", + script_type="Permission Query", + reference_doctype="Report", + script="""conditions = f"(`tabReport`.is_standard = 'Yes' or `tabReport`.owner = '{frappe.session.user}')" + """, + ) + ).insert() - # Create a ToDo custom report with test user - frappe.set_user("test@example.com") - frappe.get_doc( - { - "doctype": "Report", - "ref_doctype": "ToDo", - "report_name": "Test User Report", - "report_type": "Report Builder", - "is_standard": "No", - } - ).insert() + # Create a ToDo custom report with test user + frappe.set_user("test@example.com") + frappe.get_doc( + { + "doctype": "Report", + "ref_doctype": "ToDo", + "report_name": "Test User Report", + "report_type": "Report Builder", + "is_standard": "No", + } + ).insert(ignore_permissions=True) - get_user_pages_or_reports("Report") - allowed_reports = frappe.cache().get_value("has_role:Report", user=frappe.session.user) + get_user_pages_or_reports("Report") + allowed_reports = frappe.cache().get_value("has_role:Report", user=frappe.session.user) - # Test user must not see admin user's report - self.assertNotIn("Test Admin Report", allowed_reports) - self.assertIn("Test User Report", allowed_reports) - finally: - frappe.db.rollback() - frappe.set_user("Administrator") + # Test user must not see admin user's report + self.assertNotIn("Test Admin Report", allowed_reports) + self.assertIn("Test User Report", allowed_reports) + + self.addCleanup(frappe.db.rollback) + self.addCleanup(frappe.set_user, "Administrator") From 13162d8fbdbdfbc531b7bfb35b911c5bf22a8444 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 Jan 2023 11:24:16 +0530 Subject: [PATCH 44/56] fix: Only apply perm query to non-admin users --- frappe/boot.py | 4 ++-- frappe/tests/test_boot.py | 3 --- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 8eed64b2dc..de3753f754 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -259,8 +259,8 @@ def _run_with_permission_query(query: "Query", doctype: str) -> list[dict]: Note: Works only if 'WHERE' is the last clause in the query """ permission_query = DatabaseQuery(doctype, frappe.session.user).get_permission_query_conditions() - if permission_query: - return frappe.db.sql(f"{query} AND {permission_query}", as_dict=True) # nosemgrep + if permission_query and frappe.session.user != "Administrator": + return frappe.db.sql(f"{query} AND {permission_query}", as_dict=True) return query.run(as_dict=True) diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py index ece2c181df..232c379e08 100644 --- a/frappe/tests/test_boot.py +++ b/frappe/tests/test_boot.py @@ -70,6 +70,3 @@ class TestBootData(FrappeTestCase): # Test user must not see admin user's report self.assertNotIn("Test Admin Report", allowed_reports) self.assertIn("Test User Report", allowed_reports) - - self.addCleanup(frappe.db.rollback) - self.addCleanup(frappe.set_user, "Administrator") From 1e115e5e50164fe6dbc8c29b61ee86de971de344 Mon Sep 17 00:00:00 2001 From: Sabu Siyad Date: Mon, 30 Jan 2023 12:09:05 +0530 Subject: [PATCH 45/56] chore!: Remove translation tool (page) (#19786) * refactor(page): remove translation tool files * refactor(patches): remove translation tool page --- frappe/desk/page/translation_tool/__init__.py | 0 .../translation_tool/translation_tool.css | 37 -- .../translation_tool/translation_tool.html | 20 - .../page/translation_tool/translation_tool.js | 473 ------------------ .../translation_tool/translation_tool.json | 26 - frappe/patches.txt | 1 + 6 files changed, 1 insertion(+), 556 deletions(-) delete mode 100644 frappe/desk/page/translation_tool/__init__.py delete mode 100644 frappe/desk/page/translation_tool/translation_tool.css delete mode 100644 frappe/desk/page/translation_tool/translation_tool.html delete mode 100644 frappe/desk/page/translation_tool/translation_tool.js delete mode 100644 frappe/desk/page/translation_tool/translation_tool.json diff --git a/frappe/desk/page/translation_tool/__init__.py b/frappe/desk/page/translation_tool/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/frappe/desk/page/translation_tool/translation_tool.css b/frappe/desk/page/translation_tool/translation_tool.css deleted file mode 100644 index 9603a4ce35..0000000000 --- a/frappe/desk/page/translation_tool/translation_tool.css +++ /dev/null @@ -1,37 +0,0 @@ -.translation-item { - font-size: 12px; - padding: 12px 15px; - min-height: 40px; - cursor: pointer; - overflow: hidden; -} - -.translation-item:hover { - background-color: #fafbfc; -} -.translation-item.active { - background-color: #fffce7; -} - -.translation-edit-section { - height: 100%; - overflow-y: scroll; - padding: 0px; -} - -.translation-tool { - display: flex; - width: 100%; - padding: 0; - height: 72vh; -} - -.left-side { - padding: 0px; - height: 100%; - overflow-y: scroll; -} - -.contributed-translation { - padding: 0.5rem 0; -} diff --git a/frappe/desk/page/translation_tool/translation_tool.html b/frappe/desk/page/translation_tool/translation_tool.html deleted file mode 100644 index a88f698584..0000000000 --- a/frappe/desk/page/translation_tool/translation_tool.html +++ /dev/null @@ -1,20 +0,0 @@ -
-
-
-
- {%= __("Contributed Translations") %} -
-
-
-
-
- {%= __("Source Text") %} -
-
-
-
-
-
-
-
-
diff --git a/frappe/desk/page/translation_tool/translation_tool.js b/frappe/desk/page/translation_tool/translation_tool.js deleted file mode 100644 index 5739eddfc7..0000000000 --- a/frappe/desk/page/translation_tool/translation_tool.js +++ /dev/null @@ -1,473 +0,0 @@ -frappe.pages["translation-tool"].on_page_load = function (wrapper) { - var page = frappe.ui.make_app_page({ - parent: wrapper, - title: __("Translation Tool"), - single_column: true, - card_layout: true, - }); - - frappe.translation_tool = new TranslationTool(page); -}; - -class TranslationTool { - constructor(page) { - this.page = page; - this.wrapper = $(page.body); - this.wrapper.append(frappe.render_template("translation_tool")); - frappe.utils.bind_actions_with_object(this.wrapper, this); - this.active_translation = null; - this.edited_translations = {}; - this.setup_search_box(); - this.setup_language_filter(); - this.page.set_primary_action( - __("Contribute Translations"), - this.show_confirmation_dialog.bind(this) - ); - this.page.set_secondary_action(__("Refresh"), this.fetch_messages_then_render.bind(this)); - this.update_header(); - } - - setup_language_filter() { - let languages = Object.keys(frappe.boot.lang_dict).map((language_label) => { - let value = frappe.boot.lang_dict[language_label]; - return { - label: `${language_label} (${value})`, - value: value, - }; - }); - - let language_selector = this.page.add_field({ - fieldname: "language", - fieldtype: "Select", - options: languages, - change: () => { - let language = language_selector.get_value(); - localStorage.setItem("translation_language", language); - this.language = language; - this.fetch_messages_then_render(); - }, - }); - let translation_language = localStorage.getItem("translation_language"); - if (translation_language || frappe.boot.lang !== "en") { - language_selector.set_value(translation_language || frappe.boot.lang); - } else { - frappe.prompt( - { - label: __("Please select target language for translation"), - fieldname: "language", - fieldtype: "Select", - options: languages, - reqd: 1, - }, - (values) => { - language_selector.set_value(values.language); - }, - __("Select Language") - ); - } - } - - setup_search_box() { - let search_box = this.page.add_field({ - fieldname: "search", - fieldtype: "Data", - label: __("Search Source Text"), - change: () => { - this.search_text = search_box.get_value(); - this.fetch_messages_then_render(); - }, - }); - } - - fetch_messages_then_render() { - this.fetch_messages().then((messages) => { - this.messages = messages; - this.render_messages(messages); - }); - this.setup_local_contributions(); - } - - fetch_messages() { - frappe.dom.freeze(__("Fetching...")); - return frappe - .xcall("frappe.translate.get_messages", { - language: this.language, - search_text: this.search_text, - }) - .then((messages) => { - return messages; - }) - .finally(() => { - frappe.dom.unfreeze(); - }); - } - - render_messages(messages) { - let template = (message) => ` -
-
- - ${frappe.utils.escape_html(message.source_text)} - -
-
- `; - - let html = messages.map(template).join(""); - this.wrapper.find(".translation-item-container").html(html); - } - - on_translation_click(e, $el) { - let message_id = decodeURIComponent($el.data("message-id")); - this.wrapper.find(".translation-item").removeClass("active"); - $el.addClass("active"); - this.active_translation = this.messages.find((m) => m.id === message_id); - this.edit_translation(this.active_translation); - } - - edit_translation(translation) { - if (this.form) { - this.form.set_values({}); - } - this.get_additional_info(translation.id).then((data) => { - this.make_edit_form(translation, data); - }); - } - - get_additional_info(source_id) { - frappe.dom.freeze("Fetching..."); - return frappe - .xcall("frappe.translate.get_source_additional_info", { - source: source_id, - language: this.page.fields_dict["language"].get_value(), - }) - .finally(frappe.dom.unfreeze); - } - - make_edit_form(translation, { contributions, positions }) { - if (!this.form) { - this.form = new frappe.ui.FieldGroup({ - fields: [ - { - fieldtype: "HTML", - fieldname: "header", - read_only: 1, - }, - { - fieldtype: "Data", - fieldname: "id", - hidden: 1, - }, - { - label: "Source Text", - fieldtype: "Code", - fieldname: "source_text", - read_only: 1, - enable_copy_button: 1, - }, - { - label: "Context", - fieldtype: "Code", - fieldname: "context", - read_only: 1, - }, - { - label: "DocType", - fieldtype: "Data", - fieldname: "doctype", - read_only: 1, - }, - { - label: "Translated Text", - fieldtype: "Small Text", - fieldname: "translated_text", - }, - { - label: "Suggest", - fieldtype: "Button", - click: () => { - let { id, translated_text, source_text } = this.form.get_values(); - let existing_value = this.form.translation_dict.translated_text; - if (is_null(translated_text) || existing_value === translated_text) { - delete this.edited_translations[id]; - } else if (existing_value !== translated_text) { - this.edited_translations[id] = { - id, - translated_text, - source_text, - }; - } - this.update_header(); - }, - }, - { - fieldtype: "Section Break", - fieldname: "contributed_translations_section", - label: "Contributed Translations", - }, - { - fieldtype: "HTML", - fieldname: "contributed_translations", - }, - { - fieldtype: "Section Break", - collapsible: 1, - label: "Occurences in source code", - }, - { - fieldtype: "HTML", - fieldname: "positions", - }, - ], - body: this.wrapper.find(".translation-edit-form"), - }); - - this.form.make(); - this.setup_header(); - } - - this.form.set_values(translation); - this.form.translation_dict = translation; - this.form.set_df_property("doctype", "hidden", !translation.doctype); - this.form.set_df_property("context", "hidden", !translation.context); - this.set_status(translation); - - this.setup_contributions(contributions); - this.setup_positions(positions); - } - - setup_header() { - this.form.get_field("header").$wrapper.html(`
- -
`); - } - - set_status(translation) { - this.form.get_field("header").$wrapper.find(".translation-status").html(` - - ${this.get_indicator_status_text(translation)} - - `); - } - - setup_positions(positions) { - let position_dom = ""; - if (positions && positions.length) { - position_dom = positions - .map((position) => { - if (position.path.startsWith("DocType: ")) { - return `
- ${position.path} -
`; - } else { - return ``; - } - }) - .join(""); - } - this.form.get_field("positions").$wrapper.html(position_dom); - } - - setup_contributions(contributions) { - const contributions_exists = contributions && contributions.length; - if (contributions_exists) { - let contributions_html = contributions.map((c) => { - return ` -
-
${c.translated}
-
- ${comment_when(c.creation)} -
-
- `; - }); - this.form.get_field("contributed_translations").html(contributions_html); - } - this.form.set_df_property( - "contributed_translations_section", - "hidden", - !contributions_exists - ); - } - show_confirmation_dialog() { - this.confirmation_dialog = new frappe.ui.Dialog({ - fields: [ - { - label: __("Language"), - fieldname: "language", - fieldtype: "Data", - read_only: 1, - bold: 1, - default: this.language, - }, - { - fieldtype: "HTML", - fieldname: "edited_translations", - }, - ], - title: __("Confirm Translations"), - no_submit_on_enter: true, - primary_action_label: __("Submit"), - primary_action: (values) => { - this.create_translations(values).then(this.confirmation_dialog.hide()); - }, - }); - this.confirmation_dialog.get_field("edited_translations").html(` - - - - - - ${Object.values(this.edited_translations) - .map( - (t) => ` - - - - - ` - ) - .join("")} -
${__("Source Text")}${__("Translated Text")}
${t.source_text}${t.translated_text}
- `); - this.confirmation_dialog.show(); - } - create_translations() { - frappe.dom.freeze(__("Submitting...")); - return frappe - .xcall("frappe.core.doctype.translation.translation.create_translations", { - translation_map: this.edited_translations, - language: this.language, - }) - .then(() => { - frappe.dom.unfreeze(); - frappe.show_alert({ - message: __("Successfully Submitted!"), - indicator: "success", - }); - this.edited_translations = {}; - this.update_header(); - this.fetch_messages_then_render(); - }) - .finally(() => frappe.dom.unfreeze()); - } - - setup_local_contributions() { - // TODO: Refactor - frappe - .xcall("frappe.translate.get_contributions", { - language: this.language, - }) - .then((messages) => { - let template = (message) => ` -
-
- - ${frappe.utils.escape_html(message.source_text)} - -
-
- `; - - let html = messages.map(template).join(""); - this.wrapper.find(".translation-item-tr").html(html); - }); - } - - show_translation_status_modal(e, $el) { - let message_id = decodeURIComponent($el.data("message-id")); - - frappe.xcall("frappe.translate.get_contribution_status", { message_id }).then((doc) => { - let d = new frappe.ui.Dialog({ - title: __("Contribution Status"), - fields: [ - { - fieldname: "source_message", - label: __("Source Message"), - fieldtype: "Data", - read_only: 1, - }, - { - fieldname: "translated", - label: __("Translated Message"), - fieldtype: "Data", - read_only: 1, - }, - { - fieldname: "contribution_status", - label: __("Contribution Status"), - fieldtype: "Data", - read_only: 1, - }, - { - fieldname: "modified_by", - label: __("Verified By"), - fieldtype: "Data", - read_only: 1, - depends_on: (doc) => { - return doc.contribution_status == "Verified"; - }, - }, - ], - }); - d.set_values(doc); - d.show(); - }); - } - - update_header() { - let edited_translations_count = Object.keys(this.edited_translations).length; - if (edited_translations_count) { - let message = ""; - if (edited_translations_count == 1) { - message = __("{0} translation pending", [edited_translations_count]); - } else { - message = __("{0} translations pending", [edited_translations_count]); - } - this.page.set_indicator(message, "orange"); - } else { - this.page.set_indicator(""); - } - this.page.btn_primary.prop("disabled", !edited_translations_count); - } - - get_indicator_color(message_obj) { - return !message_obj.translated - ? "red" - : message_obj.translated_by_google - ? "orange" - : "blue"; - } - - get_indicator_status_text(message_obj) { - if (!message_obj.translated) { - return __("Untranslated"); - } else if (message_obj.translated_by_google) { - return __("Google Translation"); - } else { - return __("Community Contribution"); - } - } - - get_contribution_indicator_color(message_obj) { - return message_obj.contribution_status == "Pending" ? "orange" : "green"; - } - - get_code_url(path, line_no, app) { - const code_path = path.substring(`apps/${app}`.length); - return `https://github.com/frappe/${app}/blob/develop/${code_path}#L${line_no}`; - } -} diff --git a/frappe/desk/page/translation_tool/translation_tool.json b/frappe/desk/page/translation_tool/translation_tool.json deleted file mode 100644 index a54b2a4724..0000000000 --- a/frappe/desk/page/translation_tool/translation_tool.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "content": null, - "creation": "2020-01-30 15:16:12.136323", - "docstatus": 0, - "doctype": "Page", - "idx": 0, - "modified": "2020-01-30 15:16:23.273733", - "modified_by": "Administrator", - "module": "Desk", - "name": "translation-tool", - "owner": "Administrator", - "page_name": "Translation Tool", - "roles": [ - { - "role": "System Manager" - }, - { - "role": "Translator" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 1, - "title": "Translation Tool" -} \ No newline at end of file diff --git a/frappe/patches.txt b/frappe/patches.txt index 49b43f0558..ec55c7fedd 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -220,4 +220,5 @@ frappe.patches.v14_0.update_attachment_comment frappe.patches.v15_0.set_contact_full_name execute:frappe.delete_doc("Page", "activity", force=1) frappe.patches.v14_0.disable_email_accounts_with_oauth +execute:frappe.delete_doc("Page", "translation-tool", force=1) frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings From e4f818b83827fcd82ff0a09401597d984c5cf9d4 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 30 Jan 2023 12:41:46 +0530 Subject: [PATCH 46/56] fix: remove row if search_row is not implemented --- frappe/public/js/frappe/form/grid.js | 2 ++ frappe/public/js/frappe/form/grid_row.js | 11 +++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 9e7a8b58aa..b767dac932 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -582,6 +582,8 @@ export default class Grid { get_filtered_data() { let all_data = this.frm ? this.frm.doc[this.df.fieldname] : this.df.data; + if (!all_data) return; + for (const field in this.filter) { all_data = all_data.filter((data) => { let { df, value } = this.filter[field]; diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 35caaa667f..ec235370fc 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -12,7 +12,8 @@ export default class GridRow { this.make(); } make() { - var me = this; + let me = this; + let render_row = true; this.wrapper = $('
'); this.row = $('
') @@ -36,9 +37,11 @@ export default class GridRow { if (this.grid.template && !this.grid.meta.editable_grid) { this.render_template(); } else { - this.render_row(); + render_row = this.render_row(); } + if (!this.render_row) return; + this.set_data(); this.wrapper.appendTo(this.parent); } @@ -312,6 +315,8 @@ export default class GridRow { if (this.frm && this.doc) { $(this.frm.wrapper).trigger("grid-row-render", [this]); } + + return true; } make_editable() { @@ -756,7 +761,9 @@ export default class GridRow { } show_search_row() { + // show or remove search columns based on grid rows this.show_search = this.show_search && this.grid.data.length >= 20; + !this.show_search && this.wrapper.remove(); return this.show_search; } From 1eab4e425340fabc2c5c25d91c0bd05de67df667 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 Jan 2023 14:11:38 +0530 Subject: [PATCH 47/56] fix: Convert doctype name to string (#19832) --- frappe/core/doctype/docshare/test_docshare.py | 14 ++++++++++++++ frappe/permissions.py | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py index f2ed8a32af..b874042d15 100644 --- a/frappe/core/doctype/docshare/test_docshare.py +++ b/frappe/core/doctype/docshare/test_docshare.py @@ -125,3 +125,17 @@ class TestDocShare(FrappeTestCase): ) frappe.share.remove(doctype, submittable_doc.name, self.user) + + def test_share_int_pk(self): + test_doc = frappe.new_doc("Console Log") + + test_doc.insert() + frappe.share.add("Console Log", test_doc.name, self.user) + + frappe.set_user(self.user) + self.assertIn( + str(test_doc.name), [str(name) for name in frappe.get_list("Console Log", pluck="name")] + ) + + test_doc.reload() + self.assertTrue(test_doc.has_permission("read")) diff --git a/frappe/permissions.py b/frappe/permissions.py index ef33c03875..2bee75d50c 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -637,7 +637,7 @@ def get_linked_doctypes(dt: str) -> list: def get_doc_name(doc): if not doc: return None - return doc if isinstance(doc, str) else doc.name + return doc if isinstance(doc, str) else str(doc.name) def allow_everything(): From 3e20e7df259b15840379f668790adcf3507b1065 Mon Sep 17 00:00:00 2001 From: Leonard Goertz <49870752+uepselon@users.noreply.github.com> Date: Mon, 30 Jan 2023 09:44:11 +0100 Subject: [PATCH 48/56] fix: add brackets for docshare or condition (#19650) Co-authored-by: Leonard Goertz --- frappe/model/db_query.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index efdad83f13..36bd325e26 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -779,7 +779,7 @@ class DatabaseQuery: # share is an OR condition, if there is a role permission if not only_if_shared and self.shared and conditions: - conditions = f"({conditions}) or ({self.get_share_condition()})" + conditions = f"(({conditions}) or ({self.get_share_condition()}))" return conditions From 8be98718f7406602464e8b17de4b06054ab6f855 Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Mon, 30 Jan 2023 10:14:53 +0100 Subject: [PATCH 49/56] fix: ldap with 2fa (#19753) Because we pop password 2fa fails when used with ldap Co-authored-by: Ankush Menat --- frappe/integrations/doctype/ldap_settings/ldap_settings.py | 4 +++- frappe/utils/error.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 21e5c5b312..5b8ad1c901 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -370,13 +370,15 @@ def login(): args = frappe.form_dict ldap: LDAPSettings = frappe.get_doc("LDAP Settings") - user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pop("pwd", None))) + user = ldap.authenticate(frappe.as_unicode(args.usr), frappe.as_unicode(args.pwd)) frappe.local.login_manager.user = user.name if should_run_2fa(user.name): authenticate_for_2factor(user.name) if not confirm_otp_token(frappe.local.login_manager): return False + + frappe.form_dict.pop("pwd", None) frappe.local.login_manager.post_login() # because of a GET request! diff --git a/frappe/utils/error.py b/frappe/utils/error.py index 432591175c..235a9b3e67 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -12,7 +12,7 @@ import pydoc import sys import traceback -from ldap3.core.exceptions import LDAPInvalidCredentialsResult +from ldap3.core.exceptions import LDAPException import frappe from frappe.utils import cstr, encode @@ -21,7 +21,7 @@ EXCLUDE_EXCEPTIONS = ( frappe.AuthenticationError, frappe.CSRFTokenError, # CSRF covers OAuth too frappe.SecurityException, - LDAPInvalidCredentialsResult, + LDAPException, ) From e82046ef00b719f3508fd9897d8e1c1aaf902105 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Mon, 30 Jan 2023 10:41:55 +0100 Subject: [PATCH 50/56] ci: bump isort to 5.12.0 (#19836) * ci: bump isort to 5.12.0 * style: remove trailing whitespace --- .pre-commit-config.yaml | 4 ++-- frappe/desk/form/load.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b3ea6d1ea..0c6bbe8ec9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,8 +48,8 @@ repos: )$ - - repo: https://github.com/timothycrosley/isort - rev: 5.9.1 + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 hooks: - id: isort diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 81f5096f29..3627f48109 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -39,7 +39,6 @@ def getdoc(doctype, name, user=None): ) raise frappe.PermissionError(("read", doctype, name)) - run_onload(doc) doc.apply_fieldlevel_read_permissions() From 338ccc5a2a11164854540492857a3f178d448d1d Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 30 Jan 2023 15:22:21 +0530 Subject: [PATCH 51/56] fix: sanitize form dict in error logs (#19835) [skip ci] --- .../error_snapshot/test_error_snapshot.py | 4 ++- frappe/utils/__init__.py | 4 +-- frappe/utils/logger.py | 25 ++++++++++++++++++- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/error_snapshot/test_error_snapshot.py b/frappe/core/doctype/error_snapshot/test_error_snapshot.py index 8ff48bc5c6..4779d56c7b 100644 --- a/frappe/core/doctype/error_snapshot/test_error_snapshot.py +++ b/frappe/core/doctype/error_snapshot/test_error_snapshot.py @@ -1,9 +1,11 @@ # 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): - pass + def test_form_dict_sanitization(self): + self.assertNotEqual(sanitized_dict({"pwd": "SECRET", "usr": "WHAT"}).get("pwd"), "SECRET") diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index d82c039b95..d37e8c201f 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -305,7 +305,7 @@ def get_traceback(with_context=False) -> str: return "" if with_context: - trace_list = iter_exc_lines(fmt=_get_sanitizer()) + trace_list = iter_exc_lines(fmt=_get_traceback_sanitizer()) tb = "\n".join(trace_list) else: trace_list = traceback.format_exception(exc_type, exc_value, exc_tb) @@ -316,7 +316,7 @@ def get_traceback(with_context=False) -> str: @functools.lru_cache(maxsize=1) -def _get_sanitizer(): +def _get_traceback_sanitizer(): from traceback_with_variables import Format blocklist = [ diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py index b642821c9d..ddb81f3d79 100755 --- a/frappe/utils/logger.py +++ b/frappe/utils/logger.py @@ -1,6 +1,7 @@ # imports - standard imports import logging import os +from copy import deepcopy from logging.handlers import RotatingFileHandler from typing import Literal @@ -91,7 +92,7 @@ class SiteContextFilter(logging.Filter): def filter(self, record) -> bool: if "Form Dict" not in str(record.msg): site = getattr(frappe.local, "site", None) - form_dict = getattr(frappe.local, "form_dict", None) + form_dict = sanitized_dict(getattr(frappe.local, "form_dict", None)) record.msg = str(record.msg) + f"\nSite: {site}\nForm Dict: {form_dict}" return True @@ -100,3 +101,25 @@ def set_log_level(level: Literal["ERROR", "WARNING", "WARN", "INFO", "DEBUG"]) - """Use this method to set log level to something other than the default DEBUG""" frappe.log_level = getattr(logging, (level or "").upper(), None) or default_log_level frappe.loggers = {} + + +def sanitized_dict(form_dict): + if not isinstance(form_dict, dict): + return form_dict + + sanitized_dict = deepcopy(form_dict) + + blocklist = [ + "password", + "passwd", + "secret", + "token", + "key", + "pwd", + ] + + for k in sanitized_dict: + for secret_kw in blocklist: + if secret_kw in k: + sanitized_dict[k] = "********" + return sanitized_dict From 45280d48013534c0f33098f1c67abb98e1b18e5b Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 31 Jan 2023 11:25:14 +0530 Subject: [PATCH 52/56] fix: website theme caching (#19848) --- .../website_theme/test_website_theme.py | 55 ++++++++++++------- .../doctype/website_theme/website_theme.py | 14 +---- 2 files changed, 36 insertions(+), 33 deletions(-) diff --git a/frappe/website/doctype/website_theme/test_website_theme.py b/frappe/website/doctype/website_theme/test_website_theme.py index 80456a71e4..045b063221 100644 --- a/frappe/website/doctype/website_theme/test_website_theme.py +++ b/frappe/website/doctype/website_theme/test_website_theme.py @@ -1,40 +1,53 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE -import os +from contextlib import contextmanager import frappe from frappe.tests.utils import FrappeTestCase +from frappe.website.doctype.website_theme.website_theme import ( + after_migrate, + get_active_theme, + get_scss_paths, +) -from .website_theme import get_scss_paths + +@contextmanager +def website_theme_fixture(**theme): + test_theme = "test-theme" + + frappe.delete_doc_if_exists("Website Theme", test_theme) + theme = frappe.get_doc(doctype="Website Theme", theme=test_theme, **theme) + theme.insert() + yield theme + theme.delete(force=True) class TestWebsiteTheme(FrappeTestCase): def test_website_theme(self): - frappe.delete_doc_if_exists("Website Theme", "test-theme") - theme = frappe.get_doc( - dict( - doctype="Website Theme", - theme="test-theme", - google_font="Inter", - custom_scss="body { font-size: 16.5px; }", # this will get minified! - ) - ).insert() + with website_theme_fixture( + google_font="Inter", + custom_scss="body { font-size: 16.5px; }", # this will get minified! + ) as theme: - theme_path = frappe.get_site_path("public", theme.theme_url[1:]) - with open(theme_path) as theme_file: - css = theme_file.read() + theme_path = frappe.get_site_path("public", theme.theme_url[1:]) + with open(theme_path) as theme_file: + css = theme_file.read() - self.assertTrue("body{font-size:16.5px}" in css) - self.assertTrue("fonts.googleapis.com" in css) + self.assertTrue("body{font-size:16.5px}" in css) + self.assertTrue("fonts.googleapis.com" in css) def test_get_scss_paths(self): self.assertIn("frappe/public/scss/website.bundle", get_scss_paths()) def test_imports_to_ignore(self): - frappe.delete_doc_if_exists("Website Theme", "test-theme") - theme = frappe.get_doc( - dict(doctype="Website Theme", theme="test-theme", ignored_apps=[{"app": "frappe"}]) - ).insert() + with website_theme_fixture(ignored_apps=[{"app": "frappe"}]) as theme: + self.assertTrue('@import "frappe/public/scss/website"' not in theme.theme_scss) - self.assertTrue('@import "frappe/public/scss/website"' not in theme.theme_scss) + def test_after_migrate_hook(self): + with website_theme_fixture(google_font="Inter") as theme: + theme.set_as_default() + before = get_active_theme().theme_url + after_migrate() + after = get_active_theme().theme_url + self.assertNotEqual(before, after) diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index 7abfab93e3..178e872c2f 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -99,18 +99,8 @@ class WebsiteTheme(Document): if fname.startswith(frappe.scrub(self.name) + "_") and fname.endswith(".css"): os.remove(os.path.join(folder_path, fname)) - def generate_theme_if_not_exist(self): - bench_path = frappe.utils.get_bench_path() - if self.theme_url: - theme_path = join_path(bench_path, "sites", self.theme_url[1:]) - if not path_exists(theme_path): - self.generate_bootstrap_theme() - else: - self.generate_bootstrap_theme() - @frappe.whitelist() def set_as_default(self): - self.generate_bootstrap_theme() self.save() website_settings = frappe.get_doc("Website Settings") website_settings.website_theme = self.name @@ -133,6 +123,7 @@ def get_active_theme() -> Optional["WebsiteTheme"]: try: return frappe.get_cached_doc("Website Theme", website_theme) except frappe.DoesNotExistError: + frappe.clear_last_message() pass @@ -187,5 +178,4 @@ def after_migrate(): return doc = frappe.get_doc("Website Theme", website_theme) - doc.generate_bootstrap_theme() - doc.save() + doc.save() # Just re-saving re-generates the theme. From b3b846472e8303e6cfff6d240d73accec2a1b771 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 31 Jan 2023 12:35:43 +0530 Subject: [PATCH 53/56] test: clear defaults after test --- frappe/website/doctype/website_theme/test_website_theme.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/website/doctype/website_theme/test_website_theme.py b/frappe/website/doctype/website_theme/test_website_theme.py index 045b063221..b99aa00043 100644 --- a/frappe/website/doctype/website_theme/test_website_theme.py +++ b/frappe/website/doctype/website_theme/test_website_theme.py @@ -20,7 +20,8 @@ def website_theme_fixture(**theme): theme = frappe.get_doc(doctype="Website Theme", theme=test_theme, **theme) theme.insert() yield theme - theme.delete(force=True) + frappe.db.set_single_value("Website Settings", "website_theme", "Standard") + theme.delete() class TestWebsiteTheme(FrappeTestCase): From 7f34d510f2ce5ee69cce2c4eabe4306d0118b54b Mon Sep 17 00:00:00 2001 From: gavin Date: Tue, 31 Jan 2023 14:43:26 +0530 Subject: [PATCH 54/56] fix(db_query): Allow link field to have 'tab' (#19820) * fix(db_query): Allow link field to have 'tab' Issue: Occurence of tab was used to check if the selected field is a table name and not a fieldname. This caused DocTypes with fields like `tablets` or `table_name` to break List Views. Change: Check if the field exists in meta to be sure that the selectable is a field. * fix: Split once to ensure at most 2 args --- frappe/model/db_query.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 36bd325e26..a5a4039223 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -317,13 +317,16 @@ class DatabaseQuery: # convert child_table.fieldname to `tabChild DocType`.`fieldname` for field in self.fields: - if "." in field and "tab" not in field: + if "." in field: original_field = field alias = None if " as " in field: - field, alias = field.split(" as ") - linked_fieldname, fieldname = field.split(".") + field, alias = field.split(" as ", 1) + linked_fieldname, fieldname = field.split(".", 1) linked_field = frappe.get_meta(self.doctype).get_field(linked_fieldname) + # this is not a link field + if not linked_field: + continue linked_doctype = linked_field.options if linked_field.fieldtype == "Link": self.append_link_table(linked_doctype, linked_fieldname) From 1fd74e94724c91a789a93d80db4ef4f0361d889a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 31 Jan 2023 14:53:50 +0530 Subject: [PATCH 55/56] chore: switch base doctype This is a dummy doctype and doesn't actually affect anything but needs to have "report" perm. [skip ci] --- .../custom/report/audit_system_hooks/audit_system_hooks.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/custom/report/audit_system_hooks/audit_system_hooks.json b/frappe/custom/report/audit_system_hooks/audit_system_hooks.json index d9ea86f07f..b13a43a0c5 100644 --- a/frappe/custom/report/audit_system_hooks/audit_system_hooks.json +++ b/frappe/custom/report/audit_system_hooks/audit_system_hooks.json @@ -9,14 +9,14 @@ "idx": 0, "is_standard": "Yes", "letter_head": "", - "modified": "2023-01-25 15:03:31.263337", + "modified": "2023-01-31 14:53:37.778576", "modified_by": "Administrator", "module": "Custom", "name": "Audit System Hooks", "owner": "Administrator", "prepared_report": 0, "query": "", - "ref_doctype": "System Settings", + "ref_doctype": "Property Setter", "report_name": "Audit System Hooks", "report_type": "Script Report", "roles": [ From 4d08f50a03dbf539b5835fcd0407542afe113b1a Mon Sep 17 00:00:00 2001 From: Samuel Danieli <23150094+scdanieli@users.noreply.github.com> Date: Tue, 31 Jan 2023 11:34:40 +0100 Subject: [PATCH 56/56] fix: PermissionError (#19856) * fix: PermissionError * fix: check perm on customize form --------- Co-authored-by: Ankush Menat --- frappe/printing/doctype/print_format/print_format.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 94f0ae5b1c..7b746241ac 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -39,7 +39,7 @@ frappe.ui.form.on("Print Format", { } else if (frm.doc.custom_format && !frm.doc.raw_printing) { frm.set_df_property("html", "reqd", 1); } - if (frappe.model.can_read(frm.doc.doc_type)) { + if (frappe.model.can_write("Customize Form")) { frappe.db.get_value("DocType", frm.doc.doc_type, "default_print_format", (r) => { if (r.default_print_format != frm.doc.name) { frm.add_custom_button(__("Set as Default"), function () {