| ${key} | `) + .join("")} + |
|---|---|
| ${key} | `) + .join("")} + |
# generate dynamic conditions and set it in the conditions variable
tenant_id = frappe.db.get_value(...)
-conditions = 'tenant_id = {}'.format(tenant_id)
+conditions = f'tenant_id = {tenant_id}'
# resulting select query
select name from \`tabPerson\`
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index f5206872f9..1f288d7981 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -8,7 +8,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.rate_limiter import rate_limit
-from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, safe_exec
+from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, is_safe_exec_enabled, safe_exec
class ServerScript(Document):
@@ -277,3 +277,9 @@ def execute_api_server_script(script=None, *args, **kwargs):
_globals, _locals = safe_exec(script.script)
return _globals.frappe.flags
+
+
+@frappe.whitelist()
+def enabled() -> bool | None:
+ if frappe.has_permission("Server Script"):
+ return is_safe_exec_enabled()
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index af1352f02b..b83d1edda4 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -97,8 +97,9 @@ class TestServerScript(FrappeTestCase):
script_doc = frappe.get_doc(doctype="Server Script")
script_doc.update(script)
script_doc.insert()
-
+ cls.enable_safe_exec()
frappe.db.commit()
+ return super().setUpClass()
@classmethod
def tearDownClass(cls):
@@ -269,13 +270,13 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
site = frappe.utils.get_site_url(frappe.local.site)
client = FrappeClient(site)
- # Exhaust rate limti
+ # Exhaust rate limit
for _ in range(5):
client.get_api(script1.api_method)
self.assertRaises(FrappeException, client.get_api, script1.api_method)
- # Exhaust rate limti
+ # Exhaust rate limit
for _ in range(5):
client.get_api(script2.api_method)
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index baa870deab..e0c0c58fe7 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -56,6 +56,7 @@ class SystemSettings(Document):
]
float_precision: DF.Literal["", "2", "3", "4", "5", "6", "7", "8", "9"]
force_user_to_reset_password: DF.Int
+ force_web_capture_mode_for_uploads: DF.Check
hide_footer_in_auto_email_reports: DF.Check
language: DF.Link
lifespan_qrcode_image: DF.Int
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index b4d69d23d5..1ca0a56ec0 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -367,6 +367,9 @@ class TestUser(FrappeTestCase):
set_request(path="/random")
frappe.local.cookie_manager = CookieManager()
frappe.local.login_manager = LoginManager()
+ # used by rate limiter when calling reset_password
+ frappe.local.request_ip = "127.0.0.69"
+ frappe.db.set_single_value("System Settings", "password_reset_limit", 6)
frappe.set_user("testpassword@example.com")
test_user = frappe.get_doc("User", "testpassword@example.com")
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index b1f8777777..0b6b913234 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,8 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-from collections.abc import Sequence
+
+from collections.abc import Iterable
from datetime import timedelta
-from typing import Optional
import frappe
import frappe.defaults
@@ -54,7 +54,7 @@ class User(Document):
api_key: DF.Data | None
api_secret: DF.Password | None
banner_image: DF.AttachImage | None
- bio: DF.Text | None
+ bio: DF.SmallText | None
birth_date: DF.Date | None
block_modules: DF.Table[BlockModule]
bypass_restrict_ip_check_if_2fa_enabled: DF.Check
@@ -569,10 +569,7 @@ class User(Document):
tables = frappe.db.get_tables()
for tab in tables:
desc = frappe.db.get_table_columns_description(tab)
- has_fields = []
- for d in desc:
- if d.get("name") in ["owner", "modified_by"]:
- has_fields.append(d.get("name"))
+ has_fields = [d.get("name") for d in desc if d.get("name") in ["owner", "modified_by"]]
for field in has_fields:
frappe.db.sql(
"""UPDATE `%s`
@@ -1010,7 +1007,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
@frappe.whitelist(allow_guest=True)
-@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60, methods=["POST"])
+@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60)
def reset_password(user: str) -> str:
if user == "Administrator":
return "not allowed"
@@ -1042,7 +1039,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
conditions = []
user_type_condition = "and user_type != 'Website User'"
- if filters and filters.get("ignore_user_type"):
+ if filters and filters.get("ignore_user_type") and frappe.session.data.user_type == "System User":
user_type_condition = ""
filters.pop("ignore_user_type")
@@ -1090,29 +1087,24 @@ def get_total_users():
)
-def get_system_users(exclude_users=None, limit=None):
- if not exclude_users:
- exclude_users = []
- elif not isinstance(exclude_users, (list, tuple)):
- exclude_users = [exclude_users]
+def get_system_users(exclude_users: Iterable[str] | str | None = None, limit: int | None = None):
+ _excluded_users = list(STANDARD_USERS)
+ if isinstance(exclude_users, str):
+ _excluded_users.append(exclude_users)
+ elif isinstance(exclude_users, Iterable):
+ _excluded_users.extend(exclude_users)
- limit_cond = ""
- if limit:
- limit_cond = f"limit {limit}"
-
- exclude_users += list(STANDARD_USERS)
-
- system_users = frappe.db.sql_list(
- """select name from `tabUser`
- where enabled=1 and user_type != 'Website User'
- and name not in ({}) {}""".format(
- ", ".join(["%s"] * len(exclude_users)), limit_cond
- ),
- exclude_users,
+ return frappe.get_all(
+ "User",
+ filters={
+ "enabled": 1,
+ "user_type": ("!=", "Website User"),
+ "name": ("not in", _excluded_users),
+ },
+ pluck="name",
+ limit=limit,
)
- return system_users
-
def get_active_users():
"""Returns No. of system users who logged in, in the last 3 days"""
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index f52116ed75..ea00b604c1 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -144,13 +144,11 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
user_permissions = get_user_permissions(user).get(allow, [])
if not user_permissions:
return None
- has_same_user_permission = find(
+ return find(
user_permissions,
lambda perm: perm["doc"] == for_value and perm.get("applicable_for") == applicable_for,
)
- return has_same_user_permission
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -171,11 +169,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len,
linked_doctypes.sort()
- return_list = []
- for doctype in linked_doctypes[start:page_len]:
- return_list.append([doctype])
-
- return return_list
+ return [[doctype] for doctype in linked_doctypes[start:page_len]]
def get_permitted_documents(doctype):
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index f5c86b1ae5..355b390b3f 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -193,9 +193,7 @@ class UserType(Document):
doctypes.append("File")
for doctype in ["select_doctypes", "custom_select_doctypes"]:
- for dt in self.get(doctype):
- doctypes.append(dt.document_type)
-
+ doctypes.extend(dt.document_type for dt in self.get(doctype))
for perm in frappe.get_all(
"Custom DocPerm", filters={"role": self.role, "parent": ["not in", doctypes]}
):
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index fd879095c0..e0a7e3f8c2 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -43,9 +43,7 @@ def get_roles_and_doctypes():
restricted_roles = ["Administrator"]
if frappe.session.user != "Administrator":
custom_user_type_roles = frappe.get_all("User Type", filters={"is_standard": 0}, fields=["role"])
- for row in custom_user_type_roles:
- restricted_roles.append(row.role)
-
+ restricted_roles.extend(row.role for row in custom_user_type_roles)
restricted_roles.append("All")
roles = frappe.get_all(
diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js
deleted file mode 100644
index 1f004915fe..0000000000
--- a/frappe/core/page/recorder/recorder.js
+++ /dev/null
@@ -1,28 +0,0 @@
-frappe.pages["recorder"].on_page_load = function (wrapper) {
- frappe.ui.make_app_page({
- parent: wrapper,
- title: __("Recorder"),
- single_column: true,
- card_layout: true,
- });
-
- frappe.recorder = new Recorder(wrapper);
- $(wrapper).bind("show", function () {
- frappe.recorder.show();
- });
-
- frappe.require("recorder.bundle.js");
-};
-
-class Recorder {
- constructor(wrapper) {
- this.wrapper = $(wrapper);
- this.container = this.wrapper.find(".layout-main-section");
- this.container.append($(''));
- }
-
- show() {
- if (!this.route || this.route.name == "RecorderDetail") return;
- this.router?.replace({ name: "RecorderDetail" });
- }
-}
diff --git a/frappe/core/page/recorder/recorder.json b/frappe/core/page/recorder/recorder.json
deleted file mode 100644
index 43dfbc0e09..0000000000
--- a/frappe/core/page/recorder/recorder.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "content": null,
- "creation": "2019-02-08 08:17:45.392739",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2019-02-08 08:23:04.416426",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "recorder",
- "owner": "Administrator",
- "page_name": "Recorder",
- "roles": [
- {
- "role": "Administrator"
- }
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 0,
- "title": "Recorder"
-}
\ No newline at end of file
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index 4b455e0ab4..25657e17e8 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -56,11 +56,9 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})]
- out = []
- for dt in can_read:
- if txt.lower().replace("%", "") in dt.lower() and (
- include_single_doctypes or dt not in single_doctypes
- ):
- out.append([dt])
-
- return out
+ return [
+ [dt]
+ for dt in can_read
+ if txt.lower().replace("%", "") in dt.lower()
+ and (include_single_doctypes or dt not in single_doctypes)
+ ]
diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.py b/frappe/core/report/transaction_log_report/transaction_log_report.py
index 51a01ffc57..6928161046 100644
--- a/frappe/core/report/transaction_log_report/transaction_log_report.py
+++ b/frappe/core/report/transaction_log_report/transaction_log_report.py
@@ -77,7 +77,7 @@ def calculate_chain(transaction_hash, previous_hash):
def get_columns(filters=None):
- columns = [
+ return [
{
"label": _("Chain Integrity"),
"fieldname": "chain_integrity",
@@ -90,9 +90,28 @@ def get_columns(filters=None):
"fieldtype": "Data",
"width": 150,
},
- {"label": _("Reference Name"), "fieldname": "reference_name", "fieldtype": "Data", "width": 150},
- {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100},
- {"label": _("Modified By"), "fieldname": "modified_by", "fieldtype": "Data", "width": 100},
- {"label": _("Timestamp"), "fieldname": "timestamp", "fieldtype": "Data", "width": 100},
+ {
+ "label": _("Reference Name"),
+ "fieldname": "reference_name",
+ "fieldtype": "Data",
+ "width": 150,
+ },
+ {
+ "label": _("Owner"),
+ "fieldname": "owner",
+ "fieldtype": "Data",
+ "width": 100,
+ },
+ {
+ "label": _("Modified By"),
+ "fieldname": "modified_by",
+ "fieldtype": "Data",
+ "width": 100,
+ },
+ {
+ "label": _("Timestamp"),
+ "fieldname": "timestamp",
+ "fieldtype": "Data",
+ "width": 100,
+ },
]
- return columns
diff --git a/frappe/core/utils.py b/frappe/core/utils.py
index b445257b7d..5f388f5458 100644
--- a/frappe/core/utils.py
+++ b/frappe/core/utils.py
@@ -66,11 +66,7 @@ def find_all(list_of_dict, match_function):
red_shapes = find_all(colored_shapes, lambda d: d['color'] == 'red')
"""
- found = []
- for entry in list_of_dict:
- if match_function(entry):
- found.append(entry)
- return found
+ return [entry for entry in list_of_dict if match_function(entry)]
def ljust_list(_list, length, fill_word=None):
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index b73820a562..7342667668 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -293,7 +293,7 @@ def create_custom_field(doctype, df, ignore_validate=False, is_system_generated=
return custom_field
-def create_custom_fields(custom_fields, ignore_validate=False, update=True):
+def create_custom_fields(custom_fields: dict, ignore_validate=False, update=True):
"""Add / update multiple custom fields
:param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`"""
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 527f53ea71..730d3dfc6c 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -600,11 +600,8 @@ class CustomizeForm(Document):
),
as_dict=True,
)
- links = []
label = df.label
- for doc in docs:
- links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name))
- links_str = ", ".join(links)
+ links_str = ", ".join(frappe.utils.get_link_to_form(self.doc_type, doc.name) for doc in docs)
if docs:
frappe.throw(
@@ -710,7 +707,6 @@ doctype_properties = {
"naming_rule": "Data",
"autoname": "Data",
"show_title_field_in_link": "Check",
- "translate_link_fields": "Check",
"is_calendar_and_gantt": "Check",
"default_view": "Select",
"force_re_route_to_default_view": "Check",
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 61cabc0478..3c42b65e9b 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -29,6 +29,7 @@ from frappe.database.utils import (
is_query_type,
)
from frappe.exceptions import DoesNotExistError, ImplicitCommitError
+from frappe.monitor import get_trace_id
from frappe.query_builder.functions import Count
from frappe.utils import CallbackManager
from frappe.utils import cast as cast_fieldtype
@@ -113,6 +114,10 @@ class Database:
self.before_rollback = CallbackManager()
self.after_rollback = CallbackManager()
+ self._trace_comment = ""
+ if trace_id := get_trace_id():
+ self._trace_comment = f" /* FRAPPE_TRACE_ID: {trace_id} */"
+
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@@ -223,7 +228,9 @@ class Database:
values = None
elif not isinstance(values, (tuple, dict, list)):
values = (values,)
+
query, values = self._transform_query(query, values)
+ query += self._trace_comment
try:
self._cursor.execute(query, values)
diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py
index a1761b5995..bd696d00e3 100644
--- a/frappe/database/db_manager.py
+++ b/frappe/database/db_manager.py
@@ -78,4 +78,6 @@ class DbManager:
source=source,
port=frappe.conf.db_port,
)
+
os.system(command)
+ frappe.cache.delete_keys("") # Delete all keys associated with this site.
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index bbdd95d921..63cdca2736 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -70,29 +70,26 @@ class MariaDBTable(DBTable):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
- add_column_query = []
- modify_column_query = []
- add_index_query = []
- drop_index_query = []
-
- for col in self.add_column:
- add_column_query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}")
-
+ add_column_query = [
+ f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column
+ ]
columns_to_modify = set(self.change_type + self.set_default)
- for col in columns_to_modify:
- modify_column_query.append(
- f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
- )
-
- for col in self.add_unique:
- modify_column_query.append(
+ modify_column_query = [
+ f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}"
+ for col in columns_to_modify
+ ]
+ modify_column_query.extend(
+ [
f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)"
- )
-
- for col in self.add_index:
- # if index key does not exists
- 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.add_unique
+ ]
+ )
+ add_index_query = [
+ f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)"
+ for col in self.add_index
+ if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False)
+ ]
+ drop_index_query = []
for col in {*self.drop_index, *self.drop_unique}:
if col.fieldname == "name":
diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py
index 5e28c81455..b0a878fa8a 100644
--- a/frappe/database/postgres/schema.py
+++ b/frappe/database/postgres/schema.py
@@ -76,10 +76,7 @@ class PostgresTable(DBTable):
for col in self.columns.values():
col.build_for_alter_table(self.current_columns.get(col.fieldname.lower()))
- query = []
-
- for col in self.add_column:
- query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}")
+ query = [f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column]
for col in self.change_type:
using_clause = ""
@@ -88,7 +85,7 @@ class PostgresTable(DBTable):
# involving the old values of the row
# read more https://www.postgresql.org/docs/9.1/sql-altertable.html
using_clause = f"USING {col.fieldname}::timestamp without time zone"
- elif col.fieldtype in ("Check"):
+ elif col.fieldtype == "Check":
using_clause = f"USING {col.fieldname}::smallint"
query.append(
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index ed7d1d16fc..24eea24fa9 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -58,16 +58,16 @@ class DBTable:
return ret
def get_index_definitions(self):
- ret = []
- for key, col in self.columns.items():
+ return [
+ "index `" + key + "`(`" + key + "`)"
+ for key, col in self.columns.items()
if (
col.set_index
and not col.unique
and col.fieldtype in frappe.db.type_map
and frappe.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext")
- ):
- ret.append("index `" + key + "`(`" + key + "`)")
- return ret
+ )
+ ]
def get_columns_from_docfields(self):
"""
diff --git a/frappe/database/utils.py b/frappe/database/utils.py
index 61dd0016c5..7cdab76dda 100644
--- a/frappe/database/utils.py
+++ b/frappe/database/utils.py
@@ -39,8 +39,7 @@ def get_doctype_name(table_name: str) -> str:
if table_name.startswith(("tab", "`tab", '"tab')):
table_name = table_name.replace("tab", "", 1)
table_name = table_name.replace("`", "")
- table_name = table_name.replace('"', "")
- return table_name
+ return table_name.replace('"', "")
class LazyString:
diff --git a/frappe/defaults.py b/frappe/defaults.py
index 3bcfbec1ce..65b145f338 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -71,9 +71,7 @@ def get_user_default_as_list(key, user=None):
d = list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d))
# filter default values if not found in user permission
- values = [value for value in d if not not_in_user_permission(key, value)]
-
- return values
+ return [value for value in d if not not_in_user_permission(key, value)]
def is_a_user_permission_key(key):
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 141ac7d013..eac90cee94 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -493,11 +493,15 @@ def get_custom_doctype_list(module):
order_by="name",
)
- out = []
- for d in doctypes:
- out.append({"type": "Link", "link_type": "doctype", "link_to": d.name, "label": _(d.name)})
-
- return out
+ return [
+ {
+ "type": "Link",
+ "link_type": "doctype",
+ "link_to": d.name,
+ "label": _(d.name),
+ }
+ for d in doctypes
+ ]
def get_custom_report_list(module):
@@ -509,23 +513,20 @@ def get_custom_report_list(module):
order_by="name",
)
- out = []
- for r in reports:
- out.append(
- {
- "type": "Link",
- "link_type": "report",
- "doctype": r.ref_doctype,
- "dependencies": r.ref_doctype,
- "is_query_report": 1
- if r.report_type in ("Query Report", "Script Report", "Custom Report")
- else 0,
- "label": _(r.name),
- "link_to": r.name,
- }
- )
-
- return out
+ return [
+ {
+ "type": "Link",
+ "link_type": "report",
+ "doctype": r.ref_doctype,
+ "dependencies": r.ref_doctype,
+ "is_query_report": 1
+ if r.report_type in ("Query Report", "Script Report", "Custom Report")
+ else 0,
+ "label": _(r.name),
+ "link_to": r.name,
+ }
+ for r in reports
+ ]
def save_new_widget(doc, page, blocks, new_widgets):
diff --git a/frappe/desk/doctype/console_log/console_log.js b/frappe/desk/doctype/console_log/console_log.js
index 9a980667ac..bb9ab5272f 100644
--- a/frappe/desk/doctype/console_log/console_log.js
+++ b/frappe/desk/doctype/console_log/console_log.js
@@ -2,6 +2,11 @@
// For license information, please see license.txt
frappe.ui.form.on("Console Log", {
- // refresh: function(frm) {
- // }
+ refresh: function (frm) {
+ frm.add_custom_button(__("Re-Run in Console"), () => {
+ window.localStorage.setItem("system_console_code", frm.doc.script);
+ window.localStorage.setItem("system_console_type", frm.doc.type);
+ frappe.set_route("Form", "System Console");
+ });
+ },
});
diff --git a/frappe/desk/doctype/console_log/console_log.json b/frappe/desk/doctype/console_log/console_log.json
index b8ccf8c9b5..7531d97991 100644
--- a/frappe/desk/doctype/console_log/console_log.json
+++ b/frappe/desk/doctype/console_log/console_log.json
@@ -6,7 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "script"
+ "script",
+ "type"
],
"fields": [
{
@@ -15,11 +16,18 @@
"in_list_view": 1,
"label": "Script",
"read_only": 1
+ },
+ {
+ "fieldname": "type",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Type",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-07-05 22:16:02.823955",
+ "modified": "2023-07-27 22:52:37.239039",
"modified_by": "Administrator",
"module": "Desk",
"name": "Console Log",
diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py
index 9e243ee19a..bed829c5b8 100644
--- a/frappe/desk/doctype/console_log/console_log.py
+++ b/frappe/desk/doctype/console_log/console_log.py
@@ -15,5 +15,6 @@ class ConsoleLog(Document):
from frappe.types import DF
script: DF.Code | None
+ type: DF.Data | None
# end: auto-generated types
pass
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.py b/frappe/desk/doctype/custom_html_block/custom_html_block.py
index 3ce7966f6a..493b7ee4e4 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.py
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.py
@@ -30,7 +30,7 @@ def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filte
# return logged in users private blocks and all public blocks
customHTMLBlock = DocType("Custom HTML Block")
- condition_query = frappe.qb.get_query(customHTMLBlock)
+ condition_query = frappe.qb.from_(customHTMLBlock)
return (
condition_query.select(customHTMLBlock.name).where(
diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py
index 4b5e14e1bd..225a8d6435 100644
--- a/frappe/desk/doctype/dashboard/dashboard.py
+++ b/frappe/desk/doctype/dashboard/dashboard.py
@@ -82,14 +82,10 @@ def get_permission_query_conditions(user):
allowed_modules = [
frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user()
]
- module_condition = (
- "`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL".format(
- allowed_modules=",".join(allowed_modules)
- )
+ return "`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL".format(
+ allowed_modules=",".join(allowed_modules)
)
- return module_condition
-
@frappe.whitelist()
def get_permitted_charts(dashboard_name):
@@ -109,12 +105,8 @@ def get_permitted_charts(dashboard_name):
@frappe.whitelist()
def get_permitted_cards(dashboard_name):
- permitted_cards = []
dashboard = frappe.get_doc("Dashboard", dashboard_name)
- for card in dashboard.cards:
- if frappe.has_permission("Number Card", doc=card.card):
- permitted_cards.append(card)
- return permitted_cards
+ return [card for card in dashboard.cards if frappe.has_permission("Number Card", doc=card.card)]
def get_non_standard_charts_in_dashboard(dashboard):
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
index 6d23be79d7..5d16a6d6d1 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js
@@ -107,6 +107,8 @@ frappe.ui.form.on("Dashboard Chart", {
// set timeseries based on chart type
if (["Count", "Average", "Sum"].includes(frm.doc.chart_type)) {
frm.set_value("timeseries", 1);
+ } else if (frm.doc.chart_type == "Custom") {
+ return;
} else {
frm.set_value("timeseries", 0);
}
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
index a5aa6cc20a..d50f58c9af 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json
@@ -287,7 +287,7 @@
}
],
"links": [],
- "modified": "2022-07-27 11:09:09.203236",
+ "modified": "2023-08-14 16:33:30.172798",
"modified_by": "Administrator",
"module": "Desk",
"name": "Dashboard Chart",
@@ -319,7 +319,6 @@
"write": 1
},
{
- "create": 1,
"email": 1,
"export": 1,
"print": 1,
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 441dbc8d1a..9fe135d4e1 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -264,11 +264,10 @@ def get_heatmap_chart_config(chart, filters, heatmap_year):
)
)
- chart_config = {
+ return {
"labels": [],
"dataPoints": data,
}
- return chart_config
def get_group_by_chart_config(chart, filters):
@@ -292,12 +291,10 @@ def get_group_by_chart_config(chart, filters):
)
if data:
- chart_config = {
+ return {
"labels": [item["name"] if item["name"] else "Not Specified" for item in data],
"datasets": [{"name": chart.name, "values": [item["count"] for item in data]}],
}
-
- return chart_config
else:
return None
diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py
index 5485f3939a..e459a63ef8 100644
--- a/frappe/desk/doctype/notification_log/notification_log.py
+++ b/frappe/desk/doctype/notification_log/notification_log.py
@@ -60,8 +60,7 @@ def get_permission_query_conditions(for_user):
def get_title(doctype, docname, title_field=None):
if not title_field:
title_field = frappe.get_meta(doctype).get_title_field()
- title = docname if title_field == "name" else frappe.db.get_value(doctype, docname, title_field)
- return title
+ return docname if title_field == "name" else frappe.db.get_value(doctype, docname, title_field)
def get_title_html(title):
@@ -187,9 +186,12 @@ def mark_all_as_read():
@frappe.whitelist()
-def mark_as_read(docname):
+def mark_as_read(docname: str):
+ if frappe.flags.read_only:
+ return
+
if docname:
- frappe.db.set_value("Notification Log", docname, "read", 1, update_modified=False)
+ frappe.db.set_value("Notification Log", str(docname), "read", 1, update_modified=False)
@frappe.whitelist()
diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py
index bb5d002ed2..faa27a8ba4 100644
--- a/frappe/desk/doctype/notification_settings/notification_settings.py
+++ b/frappe/desk/doctype/notification_settings/notification_settings.py
@@ -26,7 +26,7 @@ class NotificationSettings(Document):
enabled: DF.Check
energy_points_system_notifications: DF.Check
seen: DF.Check
- subscribed_documents: DF.TableMultiSelect[NotificationSubscribedDocument] | None
+ subscribed_documents: DF.TableMultiSelect[NotificationSubscribedDocument]
user: DF.Link | None
# end: auto-generated types
def on_update(self):
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index a3af909c68..9fc12a1e0d 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -201,8 +201,7 @@ def calculate_previous_result(doc, filters):
else:
previous_date = add_to_date(current_date, years=-1)
- number = get_result(doc, filters, previous_date)
- return number
+ return get_result(doc, filters, previous_date)
@frappe.whitelist()
diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index dc73f33b67..e04ab59fdb 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -10,7 +10,6 @@ frappe.ui.form.on("System Console", {
description: __("Execute Console script"),
ignore_inputs: true,
});
- frm.set_value("type", "Python");
},
refresh: function (frm) {
@@ -22,6 +21,16 @@ frappe.ui.form.on("System Console", {
.then(() => frm.trigger("render_sql_output"))
.finally(() => $btn.text(__("Execute")));
});
+ if (
+ window.localStorage.getItem("system_console_code") &&
+ window.localStorage.getItem("system_console_type")
+ ) {
+ frm.set_value("type", localStorage.getItem("system_console_type"));
+ frm.set_value("console", localStorage.getItem("system_console_code"));
+ frm.set_value("output", "");
+ window.localStorage.removeItem("system_console_code");
+ window.localStorage.removeItem("system_console_type");
+ }
},
type: function (frm) {
diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py
index 540936581a..14576d3860 100644
--- a/frappe/desk/doctype/system_console/system_console.py
+++ b/frappe/desk/doctype/system_console/system_console.py
@@ -40,8 +40,7 @@ class SystemConsole(Document):
frappe.db.commit()
else:
frappe.db.rollback()
-
- frappe.get_doc(dict(doctype="Console Log", script=self.console)).insert()
+ frappe.get_doc(dict(doctype="Console Log", script=self.console, type=self.type)).insert()
frappe.db.commit()
diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py
index ade8704813..08a8b83708 100644
--- a/frappe/desk/doctype/system_console/test_system_console.py
+++ b/frappe/desk/doctype/system_console/test_system_console.py
@@ -5,6 +5,11 @@ from frappe.tests.utils import FrappeTestCase
class TestSystemConsole(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls) -> None:
+ cls.enable_safe_exec()
+ return super().setUpClass()
+
def test_system_console(self):
system_console = frappe.get_doc("System Console")
system_console.console = 'log("hello")'
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index b1d45ca8fc..05adaed926 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -188,16 +188,19 @@ def get_documents_for_tag(tag):
"""
# remove hastag `#` from tag
tag = tag[1:]
- results = []
result = frappe.get_list(
"Tag Link", filters={"tag": tag}, fields=["document_type", "document_name", "title", "tag"]
)
- for res in result:
- results.append({"doctype": res.document_type, "name": res.document_name, "content": res.title})
-
- return results
+ return [
+ {
+ "doctype": res.document_type,
+ "name": res.document_name,
+ "content": res.title,
+ }
+ for res in result
+ ]
@frappe.whitelist()
diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py
index 08fc61c727..e703e30002 100644
--- a/frappe/desk/doctype/todo/todo.py
+++ b/frappe/desk/doctype/todo/todo.py
@@ -96,7 +96,7 @@ class ToDo(Document):
filters={
"reference_type": self.reference_type,
"reference_name": self.reference_name,
- "status": ("!=", "Cancelled"),
+ "status": ("not in", ("Cancelled", "Closed")),
"allocated_to": ("is", "set"),
},
pluck="allocated_to",
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 9bcca88590..056f8b0768 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -64,6 +64,13 @@ class Workspace(Document):
except Exception:
frappe.throw(_("Content data shoud be a list"))
+ def clear_cache(self):
+ super().clear_cache()
+ if self.for_user:
+ frappe.cache.hdel("bootinfo", self.for_user)
+ else:
+ frappe.cache.delete_key("bootinfo")
+
def on_update(self):
if disable_saving_as_public():
return
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index ce8bb444a1..b1b14ee28b 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -32,7 +32,7 @@ def get(args=None):
filters={
"reference_type": args.get("doctype"),
"reference_name": args.get("name"),
- "status": ("!=", "Cancelled"),
+ "status": ("not in", ("Cancelled", "Closed")),
},
limit=5,
)
@@ -164,6 +164,14 @@ def remove(doctype, name, assign_to):
return set_status(doctype, name, "", assign_to, status="Cancelled")
+@frappe.whitelist()
+def close(doctype: str, name: str, assign_to: str):
+ if assign_to != frappe.session.user:
+ frappe.throw(_("Only the assignee can complete this to-do."))
+
+ return set_status(doctype, name, "", assign_to, status="Closed")
+
+
def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"):
"""remove from todo"""
try:
@@ -187,7 +195,7 @@ def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"):
pass
# clear assigned_to if field exists
- if frappe.get_meta(doctype).get_field("assigned_to") and status == "Cancelled":
+ if frappe.get_meta(doctype).get_field("assigned_to") and status in ("Cancelled", "Closed"):
frappe.db.set_value(doctype, name, "assigned_to", None)
return get({"doctype": doctype, "name": name})
@@ -233,11 +241,11 @@ def notify_assignment(
if action == "CLOSE":
subject = _("Your assignment on {0} {1} has been removed by {2}").format(
- frappe.bold(doc_type), get_title_html(title), frappe.bold(user_name)
+ frappe.bold(_(doc_type)), get_title_html(title), frappe.bold(user_name)
)
else:
user_name = frappe.bold(user_name)
- document_type = frappe.bold(doc_type)
+ document_type = frappe.bold(_(doc_type))
title = get_title_html(title)
subject = _("{0} assigned a new task {1} {2} to you").format(user_name, document_type, title)
diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py
index f12e44fe61..d698c647da 100644
--- a/frappe/desk/form/document_follow.py
+++ b/frappe/desk/form/document_follow.py
@@ -263,19 +263,17 @@ def get_row_changed(row_changed, time, doctype, doc_name, v):
def get_added_row(added, time, doctype, doc_name, v):
- items = []
- for d in added:
- items.append(
- {
- "time": v.modified,
- "data": {"to": d[0], "time": time},
- "doctype": doctype,
- "doc_name": doc_name,
- "type": "row added",
- "by": v.modified_by,
- }
- )
- return items
+ return [
+ {
+ "time": v.modified,
+ "data": {"to": d[0], "time": time},
+ "doctype": doctype,
+ "doc_name": doc_name,
+ "type": "row added",
+ "by": v.modified_by,
+ }
+ for d in added
+ ]
def get_field_changed(changed, time, doctype, doc_name, v):
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 1ec604c34d..8f569b5a9e 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -407,10 +407,7 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None):
def get_exempted_doctypes():
"""Get list of doctypes exempted from being auto-cancelled"""
- auto_cancel_exempt_doctypes = []
- for doctypes in frappe.get_hooks("auto_cancel_exempted_doctypes"):
- auto_cancel_exempt_doctypes.append(doctypes)
- return auto_cancel_exempt_doctypes
+ return list(frappe.get_hooks("auto_cancel_exempted_doctypes"))
def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> dict[str, list]:
diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py
index 56f39aacfb..e48157db11 100644
--- a/frappe/desk/form/load.py
+++ b/frappe/desk/form/load.py
@@ -7,7 +7,6 @@ from urllib.parse import quote
import frappe
import frappe.defaults
import frappe.desk.form.meta
-import frappe.share
import frappe.utils
from frappe import _, _dict
from frappe.desk.form.document_follow import is_document_followed
@@ -78,14 +77,18 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None):
def get_meta_bundle(doctype):
bundle = [frappe.desk.form.meta.get_meta(doctype)]
- for df in bundle[0].fields:
- if df.fieldtype in frappe.model.table_fields:
- bundle.append(frappe.desk.form.meta.get_meta(df.options))
+ bundle.extend(
+ frappe.desk.form.meta.get_meta(df.options)
+ for df in bundle[0].fields
+ if df.fieldtype in frappe.model.table_fields
+ )
return bundle
@frappe.whitelist()
def get_docinfo(doc=None, doctype=None, name=None):
+ from frappe.share import _get_users as get_docshares
+
if not doc:
doc = frappe.get_doc(doctype, name)
if not doc.has_permission("read"):
@@ -113,7 +116,7 @@ def get_docinfo(doc=None, doctype=None, name=None):
"versions": get_versions(doc),
"assignments": get_assignments(doc.doctype, doc.name),
"permissions": get_doc_permissions(doc),
- "shared": frappe.share.get_users(doc.doctype, doc.name),
+ "shared": get_docshares(doc),
"views": get_view_logs(doc.doctype, doc.name),
"energy_point_logs": get_point_logs(doc.doctype, doc.name),
"additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name),
@@ -320,7 +323,7 @@ def get_communication_data(
fields=fields, conditions=conditions
)
- communications = frappe.db.sql(
+ return frappe.db.sql(
"""
SELECT *
FROM (({part1}) UNION ({part2})) AS combined
@@ -331,12 +334,15 @@ def get_communication_data(
""".format(
part1=part1, part2=part2, group_by=(group_by or "")
),
- dict(doctype=doctype, name=name, start=frappe.utils.cint(start), limit=limit),
+ dict(
+ doctype=doctype,
+ name=name,
+ start=frappe.utils.cint(start),
+ limit=limit,
+ ),
as_dict=as_dict,
)
- return communications
-
def get_assignments(dt, dn):
return frappe.get_all(
@@ -345,24 +351,12 @@ def get_assignments(dt, dn):
filters={
"reference_type": dt,
"reference_name": dn,
- "status": ("!=", "Cancelled"),
+ "status": ("not in", ("Cancelled", "Closed")),
"allocated_to": ("is", "set"),
},
)
-@frappe.whitelist()
-def get_badge_info(doctypes, filters):
- filters = json.loads(filters)
- doctypes = json.loads(doctypes)
- filters["docstatus"] = ["!=", 2]
- out = {}
- for doctype in doctypes:
- out[doctype] = frappe.db.get_value(doctype, filters, "count(*)")
-
- return out
-
-
def run_onload(doc):
doc.set("__onload", frappe._dict())
doc.run_method("onload")
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index 6c338dbbbc..d0a6b2501c 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -242,9 +242,7 @@ class FormMeta(Meta):
workflow = frappe.get_doc("Workflow", workflow_name)
workflow_docs.append(workflow)
- for d in workflow.get("states"):
- workflow_docs.append(frappe.get_doc("Workflow State", d.state))
-
+ workflow_docs.extend(frappe.get_doc("Workflow State", d.state) for d in workflow.get("states"))
self.set("__workflow_docs", workflow_docs)
def load_templates(self):
diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
index afd0583cfb..0f936abae0 100644
--- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
+++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
@@ -8,7 +8,7 @@
"include_name_field": 0,
"is_standard": 1,
"list_name": "",
- "modified": "2023-05-24 12:43:43.741781",
+ "modified": "2023-08-24 11:01:18.688875",
"modified_by": "Administrator",
"module": "Desk",
"name": "Main Workspace Tour",
@@ -22,7 +22,7 @@
"steps": [
{
"description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.",
- "element_selector": "#navbar-search",
+ "element_selector": "#navbar-search[aria-expanded=\"true\"]",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 0,
diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py
index 65d6aaf785..ff41019aa1 100644
--- a/frappe/desk/leaderboard.py
+++ b/frappe/desk/leaderboard.py
@@ -3,7 +3,7 @@ from frappe.utils import get_fullname
def get_leaderboards():
- leaderboards = {
+ return {
"User": {
"fields": ["points"],
"method": "frappe.desk.leaderboard.get_energy_point_leaderboard",
@@ -11,7 +11,6 @@ def get_leaderboards():
"icon": "users",
}
}
- return leaderboards
@frappe.whitelist()
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index a1db82810e..cc32e4ab06 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import frappe
+from frappe.model import is_default_field
from frappe.query_builder import Order
from frappe.query_builder.functions import Count
from frappe.query_builder.terms import SubQuery
@@ -59,6 +60,9 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
.run(as_dict=True)
)
+ if not frappe.get_meta(doctype).has_field(field) and not is_default_field(field):
+ raise ValueError("Field does not belong to doctype")
+
return frappe.get_list(
doctype,
filters=current_filters,
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 6334b18d1c..3ae0619aab 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -235,20 +235,17 @@ def get_filters_for(doctype):
"""get open filters for doctype"""
config = get_notification_config()
doctype_config = config.get("for_doctype").get(doctype, {})
- filters = doctype_config if not isinstance(doctype_config, str) else None
-
- return filters
+ return None if isinstance(doctype_config, str) else doctype_config
@frappe.whitelist()
@frappe.read_only()
def get_open_count(doctype, name, items=None):
- """Get open count for given transactions and filters
+ """Get count for internal and external links for given transactions
:param doctype: Reference DocType
:param name: Reference Name
- :param transactions: List of transactions (json/dict)
- :param filters: optional filters (json/list)"""
+ :param items: Optional list of transactions (json/dict)"""
if frappe.flags.in_migrate or frappe.flags.in_install:
return {"count": []}
@@ -267,30 +264,26 @@ def get_open_count(doctype, name, items=None):
if not isinstance(items, list):
items = json.loads(items)
- out = []
+ out = {
+ "external_links_found": [],
+ "internal_links_found": [],
+ }
+
for d in items:
- if d in links.get("internal_links", {}):
- continue
-
- filters = get_filters_for(d)
- fieldname = links.get("non_standard_fieldnames", {}).get(d, links.get("fieldname"))
- data = {"name": d}
- if filters:
- # get the fieldname for the current document
- # we only need open documents related to the current document
- filters[fieldname] = name
- total = len(
- frappe.get_all(d, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True)
- )
- data["open_count"] = total
-
- total = len(
- frappe.get_all(
- d, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True
- )
- )
- data["count"] = total
- out.append(data)
+ internal_link_for_doctype = links.get("internal_links", {}).get(d)
+ if internal_link_for_doctype:
+ internal_links_data_for_d = get_internal_links(doc, internal_link_for_doctype, d)
+ if internal_links_data_for_d["count"]:
+ out["internal_links_found"].append(internal_links_data_for_d)
+ else:
+ try:
+ external_links_data_for_d = get_external_links(d, name, links)
+ out["external_links_found"].append(external_links_data_for_d)
+ except Exception as e:
+ out["external_links_found"].append({"doctype": d, "open_count": 0, "count": 0})
+ else:
+ external_links_data_for_d = get_external_links(d, name, links)
+ out["external_links_found"].append(external_links_data_for_d)
out = {
"count": out,
@@ -304,6 +297,58 @@ def get_open_count(doctype, name, items=None):
return out
+def get_internal_links(doc, link, link_doctype):
+ names = []
+ data = {"doctype": link_doctype}
+
+ if isinstance(link, str):
+ # get internal links in parent document
+ value = doc.get(link)
+ if value and value not in names:
+ names.append(value)
+ elif isinstance(link, list):
+ # get internal links in child documents
+ table_fieldname, link_fieldname = link
+ for row in doc.get(table_fieldname):
+ value = row.get(link_fieldname)
+ if value and value not in names:
+ names.append(value)
+
+ data["open_count"] = 0
+ data["count"] = len(names)
+ data["names"] = names
+
+ return data
+
+
+def get_external_links(doctype, name, links):
+ filters = get_filters_for(doctype)
+ fieldname = links.get("non_standard_fieldnames", {}).get(doctype, links.get("fieldname"))
+ data = {"doctype": doctype}
+
+ if filters:
+ # get the fieldname for the current document
+ # we only need open documents related to the current document
+ filters[fieldname] = name
+ total = len(
+ frappe.get_all(
+ doctype, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True
+ )
+ )
+ data["open_count"] = total
+ else:
+ data["open_count"] = 0
+
+ total = len(
+ frappe.get_all(
+ doctype, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True
+ )
+ )
+ data["count"] = total
+
+ return data
+
+
def notify_mentions(ref_doctype, ref_name, content):
if ref_doctype and ref_name and content:
mentions = extract_mentions(content)
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index 9554c7b9b7..ffc7d26317 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -82,6 +82,8 @@ def delete_downloadable_backups():
def schedule_files_backup(user_email):
from frappe.utils.background_jobs import enqueue, get_jobs
+ frappe.only_for("System Manager")
+
queued_jobs = get_jobs(site=frappe.local.site, queue="long")
method = "frappe.desk.page.backups.backups.backup_files_and_notify_user"
diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js
index 9f689b461e..a832e2cfb0 100644
--- a/frappe/desk/page/leaderboard/leaderboard.js
+++ b/frappe/desk/page/leaderboard/leaderboard.js
@@ -315,10 +315,9 @@ class Leaderboard {
})
.join("");
- const html = `
- ${filters}
- `;
- return html;
+ return `
+ ${filters}
+ `;
}
render_list_result(items) {
@@ -330,27 +329,24 @@ class Leaderboard {
})
.join("");
- let html = `
-
- ${_html}
-
- `;
-
- return html;
+ return `
+
+ ${_html}
+
+ `;
}
render_message() {
const display_class = this.message ? "" : "hide";
- let html = `
-
-
- ${this.message}
-
- `;
- return html;
+ return `
+
+
+ ${this.message}
+
+ `;
}
get_item_html(item, index) {
@@ -367,19 +363,17 @@ class Leaderboard {
const name_html = item.formatted_name
? `${item.formatted_name}`
: ` ${item.name} `;
- const html = `
-
- ${index}
-
-
- ${name_html}
-
-
- ${value}
-
- `;
-
- return html;
+ return `
+
+ ${index}
+
+
+ ${name_html}
+
+
+ ${value}
+
+ `;
}
get_sidebar_item(item, icon) {
diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py
index 91ea386948..2e16719bd1 100644
--- a/frappe/desk/page/setup_wizard/install_fixtures.py
+++ b/frappe/desk/page/setup_wizard/install_fixtures.py
@@ -17,7 +17,6 @@ def install():
add_unsubscribe()
-@frappe.whitelist()
def update_genders():
default_genders = [
"Male",
@@ -33,7 +32,6 @@ def update_genders():
frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True)
-@frappe.whitelist()
def update_salutations():
default_salutations = ["Mr", "Ms", "Mx", "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"]
records = [{"doctype": "Salutation", "salutation": d} for d in default_salutations]
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index 36658fe492..c237624fff 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -31,6 +31,9 @@ frappe.setup = {
};
frappe.pages["setup-wizard"].on_page_load = function (wrapper) {
+ if (frappe.boot.setup_complete) {
+ window.location.href = "/app";
+ }
let requires = frappe.boot.setup_wizard_requires || [];
frappe.require(requires, function () {
frappe.call({
@@ -399,9 +402,17 @@ frappe.setup.slides_settings = [
},
{
fieldname: "enable_telemetry",
- label: __("Allow Sending Usage Data for Improving Applications"),
+ label: __("Allow sending usage data for improving applications"),
fieldtype: "Check",
default: 1,
+ depends_on: "eval:frappe.telemetry.can_enable()",
+ },
+ {
+ fieldname: "allow_recording_first_session",
+ label: __("Allow recording my first session to improve user experience"),
+ fieldtype: "Check",
+ default: 0,
+ depends_on: "eval:frappe.telemetry.can_enable()",
},
],
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index d89a15ee8e..c320e74edc 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -6,7 +6,7 @@ import json
import frappe
from frappe.geo.country_info import get_country_info
from frappe.translate import get_messages_for_boot, send_translations, set_default_language
-from frappe.utils import cint, strip
+from frappe.utils import cint, now, strip
from frappe.utils.password import update_password
from . import install_fixtures
@@ -113,6 +113,9 @@ def run_post_setup_complete(args):
disable_future_access()
frappe.db.commit()
frappe.clear_cache()
+ # HACK: due to race condition sometimes old doc stays in cache.
+ # Remove this when we have reliable cache reset for docs
+ frappe.get_cached_doc("System Settings") and frappe.get_doc("System Settings")
def run_setup_success(args):
@@ -129,18 +132,20 @@ def get_stages_hooks(args):
def get_setup_complete_hooks(args):
- stages = []
- for method in frappe.get_hooks("setup_wizard_complete"):
- stages.append(
- {
- "status": "Executing method",
- "fail_msg": "Failed to execute method",
- "tasks": [
- {"fn": frappe.get_attr(method), "args": args, "fail_msg": "Failed to execute method"}
- ],
- }
- )
- return stages
+ return [
+ {
+ "status": "Executing method",
+ "fail_msg": "Failed to execute method",
+ "tasks": [
+ {
+ "fn": frappe.get_attr(method),
+ "args": args,
+ "fail_msg": "Failed to execute method",
+ }
+ ],
+ }
+ for method in frappe.get_hooks("setup_wizard_complete")
+ ]
def handle_setup_exception(args):
@@ -179,6 +184,8 @@ def update_system_settings(args):
}
)
system_settings.save()
+ if args.get("allow_recording_first_session"):
+ frappe.db.set_default("session_recording_start", now())
def update_user_name(args):
@@ -202,6 +209,8 @@ def update_user_name(args):
"last_name": last_name,
}
)
+
+ doc.append_roles(*_get_default_roles())
doc.flags.no_welcome_mail = True
doc.insert()
frappe.flags.mute_emails = _mute_emails
@@ -256,36 +265,29 @@ def parse_args(args):
def add_all_roles_to(name):
user = frappe.get_doc("User", name)
- for role in frappe.db.sql("""select name from tabRole"""):
- if role[0] not in [
- "Administrator",
- "Guest",
- "All",
- "Customer",
- "Supplier",
- "Partner",
- "Employee",
- ]:
- d = user.append("roles")
- d.role = role[0]
+ user.append_roles(*_get_default_roles())
user.save()
+def _get_default_roles() -> set[str]:
+ skip_roles = {
+ "Administrator",
+ "Guest",
+ "All",
+ "Customer",
+ "Supplier",
+ "Partner",
+ "Employee",
+ }
+ return set(frappe.get_all("Role", pluck="name")) - skip_roles
+
+
def disable_future_access():
frappe.db.set_default("desktop:home_page", "workspace")
- frappe.db.set_single_value("System Settings", "setup_complete", 1)
-
# Enable onboarding after install
frappe.db.set_single_value("System Settings", "enable_onboarding", 1)
- if not frappe.flags.in_test:
- # remove all roles and add 'Administrator' to prevent future access
- page = frappe.get_doc("Page", "setup-wizard")
- page.roles = []
- page.append("roles", {"role": "Administrator"})
- page.flags.do_not_update_json = True
- page.flags.ignore_permissions = True
- page.save()
+ frappe.db.set_single_value("System Settings", "setup_complete", 1)
@frappe.whitelist()
@@ -339,8 +341,7 @@ def prettify_args(args):
args[key] = f"Image Attached: '{filename}' of size {size} MB"
pretty_args = []
- for key in sorted(args):
- pretty_args.append(f"{key} = {args[key]}")
+ pretty_args.extend(f"{key} = {args[key]}" for key in sorted(args))
return pretty_args
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 3d54520356..20754ea665 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -201,7 +201,7 @@ def run(
if sbool(are_default_filters) and report.custom_filters:
filters = report.custom_filters
- if report.prepared_report and not ignore_prepared_report and not custom_columns:
+ if report.prepared_report and not sbool(ignore_prepared_report) and not custom_columns:
if filters:
if isinstance(filters, str):
filters = json.loads(filters)
@@ -459,14 +459,16 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None):
@frappe.whitelist()
-def get_data_for_custom_field(doctype, field):
+def get_data_for_custom_field(doctype, field, names=None):
if not frappe.has_permission(doctype, "read"):
frappe.throw(_("Not Permitted to read {0}").format(doctype), frappe.PermissionError)
- value_map = frappe._dict(frappe.get_all(doctype, fields=["name", field], as_list=1))
+ filters = {}
+ if names:
+ filters.update({"name": ["in", json.loads(names)]})
- return value_map
+ return frappe._dict(frappe.get_list(doctype, filters=filters, fields=["name", field], as_list=1))
def get_data_for_custom_report(columns):
diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py
index c6252250fb..102a708895 100644
--- a/frappe/desk/reportview.py
+++ b/frappe/desk/reportview.py
@@ -261,10 +261,7 @@ def compress(data, args=None):
values = []
keys = list(data[0])
for row in data:
- new_row = []
- for key in keys:
- new_row.append(row.get(key))
- values.append(new_row)
+ values.append([row.get(key) for key in keys])
# add user info for assignments (avatar)
if row.get("_assign", ""):
@@ -644,11 +641,7 @@ def scrub_user_tags(tagcount):
rdict[tag] += tagdict[t]
- rlist = []
- for tag in rdict:
- rlist.append([tag, rdict[tag]])
-
- return rlist
+ return [[tag, rdict[tag]] for tag in rdict]
# used in building query in queries.py
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index b4dd0efb63..fe43b7889f 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -109,7 +109,17 @@ def search_widget(
elif not query and doctype in standard_queries:
# from standard queries
search_widget(
- doctype, txt, standard_queries[doctype][0], searchfield, start, page_length, filters
+ doctype=doctype,
+ txt=txt,
+ query=standard_queries[doctype][0],
+ searchfield=searchfield,
+ start=start,
+ page_length=page_length,
+ filters=filters,
+ filter_fields=filter_fields,
+ as_dict=as_dict,
+ reference_doctype=reference_doctype,
+ ignore_user_permissions=ignore_user_permissions,
)
else:
meta = frappe.get_meta(doctype)
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index 666c726942..5cb9677f3e 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -94,11 +94,9 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter
d[0] for d in frappe.db.get_values("DocType", {"issingle": 0, "istable": 0, "hide_toolbar": 0})
]
- out = []
- for dt in com_doctypes:
- if txt.lower().replace("%", "") in dt.lower() and dt in can_read:
- out.append([dt])
- return out
+ return [
+ [dt] for dt in com_doctypes if txt.lower().replace("%", "") in dt.lower() and dt in can_read
+ ]
def get_cached_contacts(txt):
@@ -110,12 +108,11 @@ def get_cached_contacts(txt):
if not txt:
return contacts
- match = [
+ return [
d
for d in contacts
if (d.value and ((d.value and txt in d.value) or (d.description and txt in d.description)))
]
- return match
def update_contact_cache(contacts):
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.js b/frappe/email/doctype/auto_email_report/auto_email_report.js
index 62b562b97d..a6ceb08077 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.js
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.js
@@ -70,14 +70,18 @@ frappe.ui.form.on("Auto Email Report", {
frm.trigger("show_filters");
}
},
- show_filters: function (frm) {
+ show_filters: async function (frm) {
var wrapper = $(frm.get_field("filters_display").wrapper);
wrapper.empty();
+ let reference_report = frappe.query_reports[frm.doc.report];
+ if (!reference_report || !reference_report.filters) {
+ reference_report = await frappe.model.with_doc("Report", frm.doc.report);
+ }
if (
frm.doc.report_type === "Custom Report" ||
(frm.doc.report_type !== "Report Builder" &&
- frappe.query_reports[frm.doc.report] &&
- frappe.query_reports[frm.doc.report].filters)
+ reference_report &&
+ reference_report.filters)
) {
// make a table to show filters
var table = $(
@@ -99,8 +103,8 @@ frappe.ui.form.on("Auto Email Report", {
if (
frm.doc.report_type === "Custom Report" &&
- frappe.query_reports[frm.doc.reference_report] &&
- frappe.query_reports[frm.doc.reference_report].filters
+ reference_report &&
+ reference_report.filters
) {
if (frm.doc.filters) {
filters = JSON.parse(frm.doc.filters);
@@ -115,7 +119,7 @@ frappe.ui.form.on("Auto Email Report", {
report_filters = frappe.query_reports[frm.doc.reference_report].filters;
} else {
filters = JSON.parse(frm.doc.filters || "{}");
- report_filters = frappe.query_reports[frm.doc.report].filters;
+ report_filters = reference_report.filters;
}
if (report_filters && report_filters.length > 0) {
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 0a11de21f3..38e627539d 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -115,10 +115,9 @@ class AutoEmailReport(Document):
# Check if all Mandatory Report Filters are filled by the User
filters = frappe.parse_json(self.filters) if self.filters else {}
filter_meta = frappe.parse_json(self.filter_meta) if self.filter_meta else {}
- throw_list = []
- for meta in filter_meta:
- if meta.get("reqd") and not filters.get(meta["fieldname"]):
- throw_list.append(meta["label"])
+ throw_list = [
+ meta["label"] for meta in filter_meta if meta.get("reqd") and not filters.get(meta["fieldname"])
+ ]
if throw_list:
frappe.throw(
title=_("Missing Filters Required"),
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 75463ebc41..bdae8d1d3c 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -417,8 +417,7 @@ class EmailAccount(Document):
@classmethod
def find_default_incoming(cls):
- doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1)
- return doc
+ return cls.find_one_by_filters(enable_incoming=1, default_incoming=1)
@classmethod
def get_account_details_from_site_config(cls):
@@ -628,8 +627,7 @@ class EmailAccount(Document):
def get_unreplied_notification_emails(self):
"""Return list of emails listed"""
self.send_notification_to = self.send_notification_to.replace(",", "\n")
- out = [e.strip() for e in self.send_notification_to.split("\n") if e.strip()]
- return out
+ return [e.strip() for e in self.send_notification_to.split("\n") if e.strip()]
def on_trash(self):
"""Clear communications where email account is linked"""
@@ -736,22 +734,22 @@ def get_append_to(
doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None
):
txt = txt if txt else ""
- email_append_to_list = []
- # Set Email Append To DocTypes via DocType
filters = {"istable": 0, "issingle": 0, "email_append_to": 1}
- for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]):
- email_append_to_list.append(dt.name)
-
+ # Set Email Append To DocTypes via DocType
+ email_append_to_list = [
+ dt.name for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"])
+ ]
# Set Email Append To DocTypes set via Customize Form
- for dt in frappe.get_list(
- "Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"]
- ):
- email_append_to_list.append(dt.doc_type)
-
- email_append_to = [[d] for d in set(email_append_to_list) if txt in d]
-
- return email_append_to
+ email_append_to_list.extend(
+ dt.doc_type
+ for dt in frappe.get_list(
+ "Property Setter",
+ filters={"property": "email_append_to", "value": 1},
+ fields=["doc_type"],
+ )
+ )
+ return [[d] for d in set(email_append_to_list) if txt in d]
def test_internet(host="8.8.8.8", port=53, timeout=3):
@@ -889,8 +887,7 @@ def get_max_email_uid(email_account):
if not result:
return 1
else:
- max_uid = cint(result[0].get("uid", 0)) + 1
- return max_uid
+ return cint(result[0].get("uid", 0)) + 1
def setup_user_email_inbox(
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 8c939a5e76..633d5463af 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -517,9 +517,7 @@ def get_assignees(doc):
fields=["allocated_to"],
)
- recipients = [d.allocated_to for d in assignees]
-
- return recipients
+ return [d.allocated_to for d in assignees]
def get_emails_from_template(template, context):
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index cae5f76b3d..b481fd21cd 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -88,11 +88,6 @@ def get_unsubcribed_url(
if unsubscribe_params:
params.update(unsubscribe_params)
- query_string = get_signed_params(params)
-
- # for test
- frappe.local.flags.signed_query_string = query_string
-
return get_url(unsubscribe_method + "?" + get_signed_params(params))
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 5ddd71a4f6..6af6c3cebe 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -18,7 +18,8 @@ from email_reply_parser import EmailReplyParser
import frappe
from frappe import _, safe_decode, safe_encode
-from frappe.core.doctype.file import MaxFileSizeReachedError, get_random_filename
+from frappe.core.doctype.file.exceptions import MaxFileSizeReachedError
+from frappe.core.doctype.file.utils import get_random_filename
from frappe.email.oauth import Oauth
from frappe.utils import (
add_days,
diff --git a/frappe/exceptions.py b/frappe/exceptions.py
index 8dbd778a7d..f4bcb661f1 100644
--- a/frappe/exceptions.py
+++ b/frappe/exceptions.py
@@ -252,6 +252,10 @@ class SessionBootFailed(ValidationError):
http_status_code = 500
+class PrintFormatError(ValidationError):
+ pass
+
+
class TooManyWritesError(Exception):
pass
diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py
index d89e2a16cd..662e058d68 100644
--- a/frappe/geo/utils.py
+++ b/frappe/geo/utils.py
@@ -15,8 +15,7 @@ def get_coords(doctype, filters, type):
elif type == "coordinates":
coords = return_coordinates(doctype, filters_sql)
- out = convert_to_geojson(type, coords)
- return out
+ return convert_to_geojson(type, coords)
def convert_to_geojson(type, coords):
diff --git a/frappe/handler.py b/frappe/handler.py
index f4b03271eb..275c9867a9 100644
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -10,7 +10,7 @@ from werkzeug.wrappers import Response
import frappe
import frappe.sessions
import frappe.utils
-from frappe import _, is_whitelisted
+from frappe import _, is_whitelisted, ping
from frappe.core.doctype.server_script.server_script_utils import get_server_script_map
from frappe.monitor import add_data_to_monitor
from frappe.utils import cint
@@ -260,11 +260,6 @@ def get_attr(cmd):
return method
-@frappe.whitelist(allow_guest=True)
-def ping():
- return "pong"
-
-
def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
"""run a whitelisted controller method"""
from inspect import signature
diff --git a/frappe/hooks.py b/frappe/hooks.py
index bb464b193b..d4f0ad9980 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -146,8 +146,8 @@ doc_events = {
"on_update": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
- "frappe.automation.doctype.assignment_rule.assignment_rule.apply",
"frappe.core.doctype.file.utils.attach_files_to_document",
+ "frappe.automation.doctype.assignment_rule.assignment_rule.apply",
"frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date",
"frappe.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type",
],
@@ -155,13 +155,16 @@ doc_events = {
"on_cancel": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
+ "frappe.automation.doctype.assignment_rule.assignment_rule.apply",
],
"on_trash": [
"frappe.desk.notifications.clear_doctype_notifications",
"frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
],
"on_update_after_submit": [
- "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions"
+ "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions",
+ "frappe.automation.doctype.assignment_rule.assignment_rule.apply",
+ "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date",
],
"on_change": [
"frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points",
@@ -423,6 +426,7 @@ before_job = [
after_job = [
"frappe.monitor.stop",
"frappe.utils.file_lock.release_document_locks",
+ "frappe.utils.telemetry.flush",
]
extend_bootinfo = [
diff --git a/frappe/installer.py b/frappe/installer.py
index a8646f480b..f2ce450187 100644
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -265,7 +265,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False):
if app_hooks.required_apps:
for app in app_hooks.required_apps:
required_app = parse_app_name(app)
- install_app(required_app, verbose=verbose, force=force)
+ install_app(required_app, verbose=verbose)
frappe.flags.in_install = name
frappe.clear_cache()
diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
index 21920e8235..216b7defec 100644
--- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
+++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py
@@ -310,10 +310,7 @@ def get_dropbox_settings(redirect_uri=False):
def delete_older_backups(dropbox_client, folder_path, to_keep):
res = dropbox_client.files_list_folder(path=folder_path)
- files = []
- for f in res.entries:
- if isinstance(f, dropbox.files.FileMetadata) and "sql" in f.name:
- files.append(f)
+ files = [f for f in res.entries if isinstance(f, dropbox.files.FileMetadata) and "sql" in f.name]
if len(files) <= to_keep:
return
diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py
index 14a5cb5e7c..695ae7db15 100644
--- a/frappe/integrations/doctype/google_calendar/google_calendar.py
+++ b/frappe/integrations/doctype/google_calendar/google_calendar.py
@@ -299,9 +299,7 @@ def sync_events_from_google_calendar(g_calendar, method=None):
else:
frappe.throw(msg)
- for event in events.get("items", []):
- results.append(event)
-
+ results.extend(event for event in events.get("items", []))
if not events.get("nextPageToken"):
if events.get("nextSyncToken"):
account.next_sync_token = events.get("nextSyncToken")
diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py
index 4edaaf6a8d..cee04a92a1 100644
--- a/frappe/integrations/doctype/google_contacts/google_contacts.py
+++ b/frappe/integrations/doctype/google_contacts/google_contacts.py
@@ -140,9 +140,7 @@ def sync_contacts_from_google_contacts(g_contact):
).format(account.name, err.resp.status)
)
- for contact in contacts.get("connections", []):
- results.append(contact)
-
+ results.extend(contact for contact in contacts.get("connections", []))
if not contacts.get("nextPageToken"):
if contacts.get("nextSyncToken"):
frappe.db.set_value(
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index 84ea3cc8ab..93d970d95d 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -300,10 +300,7 @@ class LDAPSettings(Document):
) # Build search query
if len(conn.entries) >= 1:
- fetch_ldap_groups = []
- for group in conn.entries:
- fetch_ldap_groups.append(group["cn"].value)
-
+ fetch_ldap_groups = [group["cn"].value for group in conn.entries]
return fetch_ldap_groups
def authenticate(self, username: str, password: str):
diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
index 0958786cbb..63eadd7f4a 100644
--- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
+++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py
@@ -22,12 +22,10 @@ class OAuthProviderSettings(Document):
def get_oauth_settings():
"""Returns oauth settings"""
- out = frappe._dict(
+ return frappe._dict(
{
"skip_authorization": frappe.db.get_single_value(
"OAuth Provider Settings", "skip_authorization"
)
}
)
-
- return out
diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
index c8eedf23da..90003da3f3 100644
--- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
@@ -57,8 +57,7 @@ def make_social_login_key(**kwargs):
kwargs["doctype"] = "Social Login Key"
if not "provider_name" in kwargs:
kwargs["provider_name"] = "Test OAuth2 Provider"
- doc = frappe.get_doc(kwargs)
- return doc
+ return frappe.get_doc(kwargs)
def create_or_update_social_login_key():
diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py
index 4c566352f5..da72335413 100644
--- a/frappe/integrations/doctype/token_cache/token_cache.py
+++ b/frappe/integrations/doctype/token_cache/token_cache.py
@@ -34,9 +34,7 @@ class TokenCache(Document):
# end: auto-generated types
def get_auth_header(self):
if self.access_token:
- headers = {"Authorization": "Bearer " + self.get_password("access_token")}
- return headers
-
+ return {"Authorization": "Bearer " + self.get_password("access_token")}
raise frappe.exceptions.DoesNotExistError
def update_data(self, data):
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index b724f18f7e..3ecfe6cd61 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -104,10 +104,7 @@ class Webhook(Document):
def validate_repeating_fields(self):
"""Error when Same Field is entered multiple times in webhook_data"""
- webhook_data = []
- for entry in self.webhook_data:
- webhook_data.append(entry.fieldname)
-
+ webhook_data = [entry.fieldname for entry in self.webhook_data]
if len(webhook_data) != len(set(webhook_data)):
frappe.throw(_("Same Field is entered more than once"))
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index ff6ad36c42..32e46c83c2 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -225,3 +225,7 @@ def get_permitted_fields(
return meta_fields + permitted_fields + optional_meta_fields
return []
+
+
+def is_default_field(fieldname: str) -> bool:
+ return fieldname in default_fields
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 609bfa4b9e..59cdea8031 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import datetime
import json
+from typing import TYPE_CHECKING, TypeVar
import frappe
from frappe import _, _dict
@@ -18,9 +19,25 @@ from frappe.model.docstatus import DocStatus
from frappe.model.naming import set_new_name
from frappe.model.utils.link_count import notify_link_count
from frappe.modules import load_doctype_module
-from frappe.utils import cast_fieldtype, cint, compare, cstr, flt, now, sanitize_html, strip_html
+from frappe.utils import (
+ cast_fieldtype,
+ cint,
+ compare,
+ cstr,
+ flt,
+ is_a_property,
+ now,
+ sanitize_html,
+ strip_html,
+)
from frappe.utils.html_utils import unescape_html
+if TYPE_CHECKING:
+ from frappe.model.document import Document
+
+D = TypeVar("D", bound="Document")
+
+
max_positive_value = {"smallint": 2**15 - 1, "int": 2**31 - 1, "bigint": 2**63 - 1}
DOCTYPE_TABLE_FIELDS = [
@@ -91,19 +108,22 @@ def import_controller(doctype):
class BaseDocument:
- _reserved_keywords = {
- "doctype",
- "meta",
- "_meta",
- "flags",
- "parent_doc",
- "_table_fields",
- "_valid_columns",
- "_doc_before_save",
- "_table_fieldnames",
- "_reserved_keywords",
- "dont_update_if_missing",
- }
+ _reserved_keywords = frozenset(
+ (
+ "doctype",
+ "meta",
+ "_meta",
+ "flags",
+ "parent_doc",
+ "_table_fields",
+ "_valid_columns",
+ "_doc_before_save",
+ "_table_fieldnames",
+ "_reserved_keywords",
+ "_permitted_fieldnames",
+ "dont_update_if_missing",
+ )
+ )
def __init__(self, d):
if d.get("doctype"):
@@ -118,11 +138,22 @@ class BaseDocument:
@property
def meta(self):
- if not (meta := getattr(self, "_meta", None)):
+ meta = getattr(self, "_meta", None)
+ if meta is None:
self._meta = meta = frappe.get_meta(self.doctype)
return meta
+ @property
+ def permitted_fieldnames(self):
+ permitted_fieldnames = getattr(self, "_permitted_fieldnames", None)
+ if permitted_fieldnames is None:
+ self._permitted_fieldnames = permitted_fieldnames = get_permitted_fields(
+ doctype=self.doctype, parenttype=getattr(self, "parenttype", None)
+ )
+
+ return permitted_fieldnames
+
def __getstate__(self):
"""
Called when pickling.
@@ -141,6 +172,7 @@ class BaseDocument:
"""Remove unpicklable values before pickling"""
state.pop("_meta", None)
+ state.pop("_permitted_fieldnames", None)
def update(self, d):
"""Update multiple fields of a doctype using a dictionary of key-value pairs.
@@ -220,7 +252,7 @@ class BaseDocument:
if key in self.__dict__:
del self.__dict__[key]
- def append(self, key, value=None):
+ def append(self, key: str, value: D | dict | None = None) -> D:
"""Append an item to a child table.
Example:
@@ -236,13 +268,13 @@ class BaseDocument:
if (table := self.__dict__.get(key)) is None:
self.__dict__[key] = table = []
- value = self._init_child(value, key)
- table.append(value)
+ ret_value = self._init_child(value, key)
+ table.append(ret_value)
# reference parent document
- value.parent_doc = self
+ ret_value.parent_doc = self
- return value
+ return ret_value
def extend(self, key, value):
try:
@@ -302,20 +334,16 @@ class BaseDocument:
def get_valid_dict(
self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False
- ) -> dict:
+ ) -> _dict:
d = _dict()
- permitted_fields = get_permitted_fields(
- doctype=self.doctype, parenttype=getattr(self, "parenttype", None)
- )
+ field_values = self.__dict__
for fieldname in self.meta.get_valid_columns():
- field_value = getattr(self, fieldname, None)
-
- # column is valid, we can use getattr
- d[fieldname] = field_value
+ value = field_values.get(fieldname)
# if no need for sanitization and value is None, continue
- if not sanitize and d[fieldname] is None:
+ if not sanitize and value is None:
+ d[fieldname] = None
continue
df = self.meta.get_field(fieldname)
@@ -323,46 +351,51 @@ class BaseDocument:
if df:
if is_virtual_field:
- if ignore_virtual or fieldname not in permitted_fields:
- del d[fieldname]
+ if ignore_virtual or fieldname not in self.permitted_fieldnames:
continue
- if d[fieldname] is None and (options := getattr(df, "options", None)):
- from frappe.utils.safe_exec import get_safe_globals
+ if value is None:
+ if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop):
+ value = getattr(self, fieldname)
- d[fieldname] = frappe.safe_eval(
- code=options,
- eval_globals=get_safe_globals(),
- eval_locals={"doc": self},
- )
+ elif options := getattr(df, "options", None):
+ from frappe.utils.safe_exec import get_safe_globals
- if isinstance(d[fieldname], list) and df.fieldtype not in table_fields:
+ value = frappe.safe_eval(
+ code=options,
+ eval_globals=get_safe_globals(),
+ eval_locals={"doc": self},
+ )
+
+ if isinstance(value, list) and df.fieldtype not in table_fields:
frappe.throw(_("Value for {0} cannot be a list").format(_(df.label)))
if df.fieldtype == "Check":
- d[fieldname] = 1 if cint(d[fieldname]) else 0
+ value = 1 if cint(value) else 0
- elif df.fieldtype == "Int" and not isinstance(d[fieldname], int):
- d[fieldname] = cint(d[fieldname])
+ elif df.fieldtype == "Int" and not isinstance(value, int):
+ value = cint(value)
- elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict):
- d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": "))
+ elif df.fieldtype == "JSON" and isinstance(value, dict):
+ value = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": "))
- elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float):
- d[fieldname] = flt(d[fieldname])
+ elif df.fieldtype in float_like_fields and not isinstance(value, float):
+ value = flt(value)
- elif (df.fieldtype in datetime_fields and d[fieldname] == "") or (
- getattr(df, "unique", False) and cstr(d[fieldname]).strip() == ""
+ elif (df.fieldtype in datetime_fields and value == "") or (
+ getattr(df, "unique", False) and cstr(value).strip() == ""
):
- d[fieldname] = None
+ value = None
if convert_dates_to_str and isinstance(
- d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
+ value, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta)
):
- d[fieldname] = str(d[fieldname])
+ value = str(value)
- if ignore_nulls and not is_virtual_field and d[fieldname] is None:
- del d[fieldname]
+ if ignore_nulls and not is_virtual_field and value is None:
+ continue
+
+ d[fieldname] = value
return d
@@ -1182,15 +1215,15 @@ class BaseDocument:
def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields):
"""If the user does not have permissions at permlevel > 0, then reset the values to original / default"""
- to_reset = []
-
- for df in high_permlevel_fields:
+ to_reset = [
+ df
+ for df in high_permlevel_fields
if (
df.permlevel not in has_access_to
and df.fieldtype not in display_fieldtypes
and df.fieldname not in self.flags.get("ignore_permlevel_for_fields", [])
- ):
- to_reset.append(df)
+ )
+ ]
if to_reset:
if self.is_new():
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 85c42b94b2..f54886a4ef 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -376,9 +376,7 @@ class DatabaseQuery:
if isinstance(filters, dict):
fdict = filters
- filters = []
- for key, value in fdict.items():
- filters.append(make_filter_tuple(self.doctype, key, value))
+ filters = [make_filter_tuple(self.doctype, key, value) for key, value in fdict.items()]
setattr(self, filter_name, filters)
def sanitize_fields(self):
@@ -564,10 +562,7 @@ class DatabaseQuery:
# remove from fields
to_remove = []
for fld in self.fields:
- for f in optional_fields:
- if f in fld and not f in self.columns:
- to_remove.append(fld)
-
+ to_remove.extend(fld for f in optional_fields if f in fld and f not in self.columns)
for fld in to_remove:
del self.fields[self.fields.index(fld)]
@@ -577,10 +572,9 @@ class DatabaseQuery:
if isinstance(each, str):
each = [each]
- for element in each:
- if element in optional_fields and element not in self.columns:
- to_remove.append(each)
-
+ to_remove.extend(
+ each for element in each if element in optional_fields and element not in self.columns
+ )
for each in to_remove:
if isinstance(self.filters, dict):
del self.filters[each]
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 9768200164..4f966c88b2 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -4,7 +4,7 @@ import hashlib
import json
import time
from collections.abc import Generator, Iterable
-from typing import Any
+from typing import TYPE_CHECKING, Any, Optional
from werkzeug.exceptions import NotFound
@@ -24,6 +24,9 @@ from frappe.utils import compare, cstr, date_diff, file_lock, flt, get_datetime_
from frappe.utils.data import get_absolute_url
from frappe.utils.global_search import update_global_search
+if TYPE_CHECKING:
+ from frappe.core.doctype.docfield.docfield import DocField
+
def get_doc(*args, **kwargs):
"""returns a frappe.model.Document object.
@@ -409,13 +412,13 @@ class Document(BaseDocument):
for df in self.meta.get_table_fields():
self.update_child_table(df.fieldname, df)
- def update_child_table(self, fieldname, df=None):
+ def update_child_table(self, fieldname: str, df: Optional["DocField"] = None):
"""sync child table for given fieldname"""
rows = []
- if not df:
- df = self.meta.get_field(fieldname)
+ df: "DocField" = df or self.meta.get_field(fieldname)
for d in self.get(df.fieldname):
+ d: Document
d.db_update()
rows.append(d.name)
@@ -427,25 +430,20 @@ class Document(BaseDocument):
# hack for docperm :(
return
- if rows:
- # select rows that do not match the ones in the document
- deleted_rows = frappe.db.sql(
- """select name from `tab{}` where parent=%s
- and parenttype=%s and parentfield=%s
- and name not in ({})""".format(
- df.options, ",".join(["%s"] * len(rows))
- ),
- [self.name, self.doctype, fieldname] + rows,
- )
- if len(deleted_rows) > 0:
- # delete rows that do not match the ones in the document
- frappe.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))})
+ # delete rows that do not match the ones in the document
+ tbl = frappe.qb.DocType(df.options)
+ qry = (
+ frappe.qb.from_(tbl)
+ .where(tbl.parent == self.name)
+ .where(tbl.parenttype == self.doctype)
+ .where(tbl.parentfield == fieldname)
+ .delete()
+ )
- else:
- # no rows found, delete all rows
- frappe.db.delete(
- df.options, {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname}
- )
+ if rows:
+ qry = qry.where(tbl.name.notin(rows))
+
+ qry.run()
def get_doc_before_save(self) -> "Document":
return getattr(self, "_doc_before_save", None)
@@ -1039,7 +1037,7 @@ class Document(BaseDocument):
"""Rename the document to `name`. This transforms the current object."""
return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename)
- def delete(self, ignore_permissions=False, force=False):
+ def delete(self, ignore_permissions=False, force=False, *, delete_permanently=False):
"""Delete document."""
return frappe.delete_doc(
self.doctype,
@@ -1047,6 +1045,7 @@ class Document(BaseDocument):
ignore_permissions=ignore_permissions,
flags=self.flags,
force=force,
+ delete_permanently=delete_permanently,
)
def run_before_save_methods(self):
@@ -1382,7 +1381,7 @@ class Document(BaseDocument):
:param comment_type: e.g. `Comment`. See Communication for more info."""
- out = frappe.get_doc(
+ return frappe.get_doc(
{
"doctype": "Comment",
"comment_type": comment_type,
@@ -1393,7 +1392,6 @@ class Document(BaseDocument):
"content": text or comment_type,
}
).insert(ignore_permissions=True)
- return out
def add_seen(self, user=None):
"""add the given/current user to list of users who have seen this document (_seen)"""
@@ -1568,8 +1566,7 @@ class Document(BaseDocument):
pluck="allocated_to",
)
- users = set(assigned_users)
- return users
+ return set(assigned_users)
def add_tag(self, tag):
"""Add a Tag to this document"""
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 83c21f8502..b26abef775 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -94,15 +94,17 @@ def load_doctype_from_file(doctype):
class Meta(Document):
_metaclass = True
default_fields = list(default_fields)[1:]
- special_doctypes = {
- "DocField",
- "DocPerm",
- "DocType",
- "Module Def",
- "DocType Action",
- "DocType Link",
- "DocType State",
- }
+ special_doctypes = frozenset(
+ (
+ "DocField",
+ "DocPerm",
+ "DocType",
+ "Module Def",
+ "DocType Action",
+ "DocType Link",
+ "DocType State",
+ )
+ )
standard_set_once_fields = [
frappe._dict(fieldname="creation", fieldtype="Datetime"),
frappe._dict(fieldname="owner", fieldtype="Data"),
@@ -421,11 +423,7 @@ class Meta(Document):
order = json.loads(self.get(f"{fieldname}_order") or "[]")
if order:
name_map = {d.name: d for d in self.get(fieldname)}
- new_list = []
- for name in order:
- if name in name_map:
- new_list.append(name_map[name])
-
+ new_list = [name_map[name] for name in order if name in name_map]
# add the missing items that have not be added
# maybe these items were added to the standard product
# after the customization was done
@@ -564,11 +562,7 @@ class Meta(Document):
def get_high_permlevel_fields(self):
"""Build list of fields with high perm level and all the higher perm levels defined."""
if not hasattr(self, "high_permlevel_fields"):
- self.high_permlevel_fields = []
- for df in self.fields:
- if df.permlevel > 0:
- self.high_permlevel_fields.append(df)
-
+ self.high_permlevel_fields = [df for df in self.fields if df.permlevel > 0]
return self.high_permlevel_fields
def get_permitted_fieldnames(self, parenttype=None, *, user=None, permission_type="read"):
@@ -594,10 +588,11 @@ class Meta(Document):
self.get_permlevel_access(permission_type=permission_type, parenttype=parenttype, user=user)
)
- for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True):
- if df.permlevel in permlevel_access:
- permitted_fieldnames.append(df.fieldname)
-
+ permitted_fieldnames.extend(
+ df.fieldname
+ for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True)
+ if df.permlevel in permlevel_access
+ )
return permitted_fieldnames
def get_permlevel_access(self, permission_type="read", parenttype=None, *, user=None):
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index a202cba11f..c90b7f517b 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -12,17 +12,13 @@ from frappe import _
from frappe.model import log_types
from frappe.query_builder import DocType
from frappe.utils import cint, cstr, now_datetime
+from frappe.utils.caching import redis_cache
if TYPE_CHECKING:
from frappe.model.document import Document
from frappe.model.meta import Meta
-# NOTE: This is used to keep track of status of sites
-# whether `log_types` have autoincremented naming set for the site or not.
-# Structure: {"sitename": {"doctype": 1}}
-autoincremented_site_status_map = defaultdict(dict)
-
NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE)
BRACED_PARAMS_PATTERN = re.compile(r"(\{[\w | #]+\})")
@@ -182,16 +178,7 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
"""Checks if the doctype has autoincrement autoname set"""
if doctype in log_types:
- site_map = autoincremented_site_status_map[frappe.local.site]
- if site_map.get(doctype) is None:
- query = f"""select data_type FROM information_schema.columns where column_name = 'name' and table_name = 'tab{doctype}'"""
- values = ()
- if frappe.db.db_type == "mariadb":
- query += " and table_schema = %s"
- values = (frappe.db.db_name,)
- site_map[doctype] = frappe.db.sql(query, values)[0][0] == "bigint"
-
- return bool(site_map[doctype])
+ return _implicitly_auto_incremented(doctype)
else:
if not meta:
meta = frappe.get_meta(doctype)
@@ -202,6 +189,16 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
return False
+@redis_cache
+def _implicitly_auto_incremented(doctype) -> bool:
+ query = f"""select data_type FROM information_schema.columns where column_name = 'name' and table_name = 'tab{doctype}'"""
+ values = ()
+ if frappe.db.db_type == "mariadb":
+ query += " and table_schema = %s"
+ values = (frappe.db.db_name,)
+ return frappe.db.sql(query, values)[0][0] == "bigint"
+
+
def set_name_from_naming_options(autoname, doc):
"""
Get a name based on the autoname field option
@@ -538,8 +535,7 @@ def _field_autoname(autoname, doc, skip_slicing=None):
`autoname` field starts with 'field:'
"""
fieldname = autoname if skip_slicing else autoname[6:]
- name = (cstr(doc.get(fieldname)) or "").strip()
- return name
+ return (cstr(doc.get(fieldname)) or "").strip()
def _prompt_autoname(autoname, doc):
@@ -552,7 +548,7 @@ def _prompt_autoname(autoname, doc):
frappe.throw(_("Please set the document name"))
-def _format_autoname(autoname, doc):
+def _format_autoname(autoname: str, doc):
"""
Generate autoname by replacing all instances of braced params (fields, date params ('DD', 'MM', 'YY'), series)
Independent of remaining string or separators.
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index e8f5626af4..7554755d2b 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -65,6 +65,8 @@ def update_document_title(
)
name_updated = updated_name and (updated_name != doc.name)
+ queue = kwargs.get("queue") or "default"
+
if name_updated:
if action_enqueued:
current_name = doc.name
@@ -86,7 +88,7 @@ def update_document_title(
save_point=True,
)
- doc.queue_action("rename", name=transformed_name, merge=merge)
+ doc.queue_action("rename", name=transformed_name, merge=merge, queue=queue)
else:
doc.rename(updated_name, merge=merge)
diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py
index 65b5092d46..532a7807bd 100644
--- a/frappe/model/utils/link_count.py
+++ b/frappe/model/utils/link_count.py
@@ -5,11 +5,37 @@ from collections import defaultdict
import frappe
-ignore_doctypes = ("DocType", "Print Format", "Role", "Module Def", "Communication", "ToDo")
+ignore_doctypes = {
+ "DocType",
+ "Print Format",
+ "Role",
+ "Module Def",
+ "Communication",
+ "ToDo",
+ "Version",
+ "Error Log",
+ "Scheduled Job Log",
+ "Event Sync Log",
+ "Event Update Log",
+ "Access Log",
+ "View Log",
+ "Activity Log",
+ "Energy Point Log",
+ "Notification Log",
+ "Email Queue",
+ "DocShare",
+ "Document Follow",
+ "Console Log",
+ "User",
+}
def notify_link_count(doctype, name):
"""updates link count for given document"""
+
+ if doctype in ignore_doctypes or not frappe.request:
+ return
+
if not hasattr(frappe.local, "_link_count"):
frappe.local._link_count = defaultdict(int)
frappe.db.after_commit.add(flush_local_link_count)
@@ -41,13 +67,12 @@ def update_link_count():
if link_count:
for (doctype, name), count in link_count.items():
- if doctype not in ignore_doctypes:
- try:
- table = frappe.qb.DocType(doctype)
- frappe.qb.update(table).set(table.idx, table.idx + count).where(table.name == name).run()
- frappe.db.commit()
- except Exception as e:
- if not frappe.db.is_table_missing(e): # table not found, single
- raise e
+ try:
+ table = frappe.qb.DocType(doctype)
+ frappe.qb.update(table).set(table.idx, table.idx + count).where(table.name == name).run()
+ frappe.db.commit()
+ except Exception as e:
+ if not frappe.db.is_table_missing(e): # table not found, single
+ raise e
# reset the count
frappe.cache.delete_value("_link_count")
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index 8c9a209501..0295fbaaf2 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -42,14 +42,17 @@ ignore_doctypes = [""]
def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False):
if type(module) is list:
- out = []
- for m in module:
- out.append(
- import_file(
- m[0], m[1], m[2], force=force, pre_process=pre_process, reset_permissions=reset_permissions
- )
+ return [
+ import_file(
+ m[0],
+ m[1],
+ m[2],
+ force=force,
+ pre_process=pre_process,
+ reset_permissions=reset_permissions,
)
- return out
+ for m in module
+ ]
else:
return import_file(
module, dt, dn, force=force, pre_process=pre_process, reset_permissions=reset_permissions
@@ -59,10 +62,9 @@ def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_
def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False):
"""Sync a file from txt if modifed, return false if not updated"""
path = get_file_path(module, dt, dn)
- ret = import_file_by_path(
+ return import_file_by_path(
path, force, pre_process=pre_process, reset_permissions=reset_permissions
)
- return ret
def get_file_path(module, dt, dn):
diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py
index bf0bd3d869..c9bf443248 100644
--- a/frappe/modules/patch_handler.py
+++ b/frappe/modules/patch_handler.py
@@ -100,8 +100,7 @@ def get_patches_from_app(app: str, patch_type: PatchType | None = None) -> list[
1. ini like file with section for different patch_type
2. plain text file with each line representing a patch.
"""
-
- patches_file = frappe.get_pymodule_path(app, "patches.txt")
+ patches_file = frappe.get_app_path(app, "patches.txt")
try:
return parse_as_configfile(patches_file, patch_type)
diff --git a/frappe/monitor.py b/frappe/monitor.py
index da2deb859e..9b8f500358 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -32,6 +32,12 @@ def add_data_to_monitor(**kwargs) -> None:
frappe.local.monitor.add_custom_data(**kwargs)
+def get_trace_id() -> str | None:
+ """Get unique ID for current transaction."""
+ if monitor := getattr(frappe.local, "monitor", None):
+ return monitor.data.uuid
+
+
def log_file():
return os.path.join(frappe.utils.get_bench_path(), "logs", "monitor.json.log")
@@ -66,14 +72,16 @@ class Monitor:
}
)
+ if request_id := frappe.request.headers.get("X-Frappe-Request-Id"):
+ self.data.uuid = request_id
+
def collect_job_meta(self, method, kwargs):
self.data.job = frappe._dict({"method": method, "scheduled": False, "wait": 0})
if "run_scheduled_job" in method:
self.data.job.method = kwargs["job_type"]
self.data.job.scheduled = True
- job = rq.get_current_job()
- if job:
+ if job := rq.get_current_job():
self.data.uuid = job.id
waitdiff = self.data.timestamp - job.enqueued_at
self.data.job.wait = int(waitdiff.total_seconds() * 1000000)
diff --git a/frappe/oauth.py b/frappe/oauth.py
index aa486fe8ba..b338651dab 100644
--- a/frappe/oauth.py
+++ b/frappe/oauth.py
@@ -43,8 +43,7 @@ class OAuthWebRequestValidator(RequestValidator):
# The redirect used if none has been supplied.
# Prefer your clients to pre register a redirect uri rather than
# supplying one on each authorization request.
- redirect_uri = frappe.db.get_value("OAuth Client", client_id, "default_redirect_uri")
- return redirect_uri
+ return frappe.db.get_value("OAuth Client", client_id, "default_redirect_uri")
def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs):
# Is the client allowed to access the requested scopes?
@@ -150,11 +149,7 @@ class OAuthWebRequestValidator(RequestValidator):
filters={"client": client_id, "validity": "Valid"},
)
- checkcodes = []
- for vcode in validcodes:
- checkcodes.append(vcode["name"])
-
- if code in checkcodes:
+ if code in [vcode["name"] for vcode in validcodes]:
request.scopes = frappe.db.get_value("OAuth Authorization Code", code, "scopes").split(
get_url_delimiter()
)
@@ -231,10 +226,7 @@ class OAuthWebRequestValidator(RequestValidator):
otoken.save(ignore_permissions=True)
frappe.db.commit()
- default_redirect_uri = frappe.db.get_value(
- "OAuth Client", request.client["name"], "default_redirect_uri"
- )
- return default_redirect_uri
+ return frappe.db.get_value("OAuth Client", request.client["name"], "default_redirect_uri")
def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs):
# Authorization codes are use once, invalidate it when a Bearer token
@@ -375,8 +367,7 @@ class OAuthWebRequestValidator(RequestValidator):
def get_userinfo_claims(self, request):
user = frappe.get_doc("User", frappe.session.user)
- userinfo = get_userinfo(user)
- return userinfo
+ return get_userinfo(user)
def validate_id_token(self, token, scopes, request):
try:
@@ -580,7 +571,7 @@ def get_userinfo(user):
else:
picture = urljoin(frappe_server_url, user.user_image)
- userinfo = frappe._dict(
+ return frappe._dict(
{
"sub": frappe.db.get_value(
"User Social Login",
@@ -597,8 +588,6 @@ def get_userinfo(user):
}
)
- return userinfo
-
def get_url_delimiter(separator_character=" "):
return separator_character
diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py
index b7c3966df1..250e8bec76 100644
--- a/frappe/parallel_test_runner.py
+++ b/frappe/parallel_test_runner.py
@@ -97,7 +97,7 @@ class ParallelTestRunner:
make_test_records(doctype, commit=True)
def get_module(self, path, filename):
- app_path = frappe.get_pymodule_path(self.app)
+ app_path = frappe.get_app_path(self.app)
relative_path = os.path.relpath(path, app_path)
if relative_path == ".":
module_name = self.app
@@ -217,7 +217,7 @@ class ParallelTestResult(unittest.TextTestResult):
def get_all_tests(app):
test_file_list = []
- for path, folders, files in os.walk(frappe.get_pymodule_path(app)):
+ for path, folders, files in os.walk(frappe.get_app_path(app)):
for dontwalk in ("locals", ".git", "public", "__pycache__"):
if dontwalk in folders:
folders.remove(dontwalk)
@@ -230,10 +230,11 @@ def get_all_tests(app):
# in /doctype/doctype/boilerplate/
continue
- for filename in files:
- if filename.startswith("test_") and filename.endswith(".py") and filename != "test_runner.py":
- test_file_list.append([path, filename])
-
+ test_file_list.extend(
+ [path, filename]
+ for filename in files
+ if filename.startswith("test_") and filename.endswith(".py") and filename != "test_runner.py"
+ )
return test_file_list
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 054fe9b946..120a2b07d8 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -193,6 +193,7 @@ frappe.patches.v14_0.delete_payment_gateways
frappe.patches.v15_0.remove_event_streaming
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
execute:frappe.reload_doc("desk", "doctype", "Form Tour")
+execute:frappe.delete_doc('Page', 'recorder', ignore_missing=True, force=True)
[post_model_sync]
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
@@ -227,3 +228,4 @@ execute:frappe.delete_doc_if_exists("Workspace", "Customization")
execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter")
execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot")
frappe.patches.v15_0.move_event_cancelled_to_status
+frappe.patches.v15_0.set_file_type
diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py
index b3471ca4e8..ce0e43302a 100644
--- a/frappe/patches/v11_0/remove_skip_for_doctype.py
+++ b/frappe/patches/v11_0/remove_skip_for_doctype.py
@@ -55,21 +55,20 @@ def execute():
user_permissions_to_delete.append(user_permission.name)
user_permission.name = None
user_permission.skip_for_doctype = None
- for doctype in applicable_for_doctypes:
- if doctype:
- # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes, creation, modified)
- new_user_permissions_list.append(
- (
- frappe.generate_hash(length=10),
- user_permission.user,
- user_permission.allow,
- user_permission.for_value,
- doctype,
- 0,
- user_permission.creation,
- user_permission.modified,
- )
- )
+ new_user_permissions_list.extend(
+ (
+ frappe.generate_hash(length=10),
+ user_permission.user,
+ user_permission.allow,
+ user_permission.for_value,
+ doctype,
+ 0,
+ user_permission.creation,
+ user_permission.modified,
+ )
+ for doctype in applicable_for_doctypes
+ if doctype
+ )
else:
# No skip_for_doctype found! Just update apply_to_all_doctypes.
frappe.db.set_value("User Permission", user_permission.name, "apply_to_all_doctypes", 1)
diff --git a/frappe/patches/v11_0/replicate_old_user_permissions.py b/frappe/patches/v11_0/replicate_old_user_permissions.py
index 999a5d7698..b66818d252 100644
--- a/frappe/patches/v11_0/replicate_old_user_permissions.py
+++ b/frappe/patches/v11_0/replicate_old_user_permissions.py
@@ -67,10 +67,8 @@ def get_doctypes_to_skip(doctype, user):
else:
doctypes_to_skip.append(parent_doctype)
- # to remove possible duplicates
- doctypes_to_skip = list(set(doctypes_to_skip))
-
- return doctypes_to_skip
+ # remove possible duplicates
+ return list(set(doctypes_to_skip))
# store user's valid perms to avoid repeated query
diff --git a/frappe/patches/v13_0/remove_duplicate_navbar_items.py b/frappe/patches/v13_0/remove_duplicate_navbar_items.py
index 593a529efc..88ab8e399e 100644
--- a/frappe/patches/v13_0/remove_duplicate_navbar_items.py
+++ b/frappe/patches/v13_0/remove_duplicate_navbar_items.py
@@ -3,11 +3,11 @@ import frappe
def execute():
navbar_settings = frappe.get_single("Navbar Settings")
- duplicate_items = []
-
- for navbar_item in navbar_settings.settings_dropdown:
- if navbar_item.item_label == "Toggle Full Width":
- duplicate_items.append(navbar_item)
+ duplicate_items = [
+ navbar_item
+ for navbar_item in navbar_settings.settings_dropdown
+ if navbar_item.item_label == "Toggle Full Width"
+ ]
if len(duplicate_items) > 1:
navbar_settings.remove(duplicate_items[0])
diff --git a/frappe/patches/v15_0/set_file_type.py b/frappe/patches/v15_0/set_file_type.py
new file mode 100644
index 0000000000..2c90b216e5
--- /dev/null
+++ b/frappe/patches/v15_0/set_file_type.py
@@ -0,0 +1,32 @@
+import mimetypes
+
+import frappe
+
+
+def execute():
+ """Set 'File Type' for all files based on file extension."""
+ files = frappe.db.get_all(
+ "File",
+ fields=["name", "file_name", "file_url"],
+ filters={"is_folder": 0, "file_type": ("is", "not set")},
+ )
+
+ frappe.db.auto_commit_on_many_writes = 1
+
+ for file in files:
+ file_extension = get_file_extension(file.file_name or file.file_url)
+ if file_extension:
+ frappe.db.set_value("File", file.name, "file_type", file_extension, update_modified=False)
+
+ frappe.db.auto_commit_on_many_writes = 0
+
+
+def get_file_extension(file_name):
+ if not file_name:
+ return None
+ file_type = mimetypes.guess_type(file_name)[0]
+ if not file_type:
+ return None
+
+ file_extension = mimetypes.guess_extension(file_type)
+ return file_extension.lstrip(".").upper() if file_extension else None
diff --git a/frappe/permissions.py b/frappe/permissions.py
index e71e2be20f..0b44f1e791 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -630,8 +630,7 @@ def allow_everything():
returns a dict with access to everything
eg. {"read": 1, "write": 1, ...}
"""
- perm = {ptype: 1 for ptype in rights}
- return perm
+ return {ptype: 1 for ptype in rights}
def get_allowed_docs_for_doctype(user_permissions, doctype):
diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
index 273ad9c4d1..7bae0996eb 100644
--- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
+++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py
@@ -37,9 +37,10 @@ class NetworkPrinterSettings(Document):
cups.setPort(self.port)
conn = cups.Connection()
printers = conn.getPrinters()
- for printer_id, printer in printers.items():
- printer_list.append({"value": printer_id, "label": printer["printer-make-and-model"]})
-
+ printer_list.extend(
+ {"value": printer_id, "label": printer["printer-make-and-model"]}
+ for printer_id, printer in printers.items()
+ )
except RuntimeError:
frappe.throw(_("Failed to connect to server"))
except frappe.ValidationError:
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 468172007c..11c3b8cd19 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -713,8 +713,7 @@ frappe.ui.form.PrintView = class {
get_print_format_printer_map() {
// returns the whole object "print_format_printer_map" stored in the localStorage.
try {
- let print_format_printer_map = JSON.parse(localStorage.print_format_printer_map);
- return print_format_printer_map;
+ return JSON.parse(localStorage.print_format_printer_map);
} catch (e) {
return {};
}
diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js
index 5b79cd6bc3..1f20a009b5 100644
--- a/frappe/public/js/form_builder/store.js
+++ b/frappe/public/js/form_builder/store.js
@@ -71,7 +71,7 @@ export const useStore = defineStore("form-builder-store", () => {
async function fetch() {
doc.value = frm.value.doc;
- if (doctype.value.startsWith("new-doctype-")) {
+ if (doctype.value.startsWith("new-doctype-") && !doc.value.fields) {
doc.value.fields = [get_df("Data", "", __("Title"))];
}
@@ -91,9 +91,11 @@ export const useStore = defineStore("form-builder-store", () => {
form.value.selected_field = null;
nextTick(() => {
- dirty.value = false;
- frm.value.doc.__unsaved = 0;
- frm.value.page.clear_indicator();
+ if (!doctype.value.startsWith("new-doctype-")) {
+ dirty.value = false;
+ frm.value.doc.__unsaved = 0;
+ frm.value.page.clear_indicator();
+ }
read_only.value =
!is_customize_form.value && !frappe.boot.developer_mode && !doc.value.custom;
preview.value = false;
diff --git a/frappe/public/js/frappe/defaults.js b/frappe/public/js/frappe/defaults.js
index bca2b0dad4..09ea90d047 100644
--- a/frappe/public/js/frappe/defaults.js
+++ b/frappe/public/js/frappe/defaults.js
@@ -107,10 +107,9 @@ frappe.defaults = {
let user_permission = this.get_user_permissions()[frappe.model.unscrub(key)];
if (user_permission && user_permission.length) {
- let doc_found = user_permission.some((perm) => {
+ return user_permission.some((perm) => {
return perm.doc === value;
});
- return doc_found;
} else {
// there is no user permission for this doctype
// so we can allow this doc i.e., value
diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js
index a12e56d0d7..bb67cef187 100644
--- a/frappe/public/js/frappe/dom.js
+++ b/frappe/public/js/frappe/dom.js
@@ -405,6 +405,7 @@ frappe.create_shadow_element = function (wrapper, html, css, js) {
// bind online/offline events
$(window).on("online", function () {
+ if (document.hidden) return;
frappe.show_alert({
indicator: "green",
message: __("You are connected to internet."),
@@ -412,6 +413,7 @@ $(window).on("online", function () {
});
$(window).on("offline", function () {
+ if (document.hidden) return;
frappe.show_alert({
indicator: "orange",
message: __("Connection lost. Some features might not work."),
diff --git a/frappe/public/js/frappe/form/controls/multiselect.js b/frappe/public/js/frappe/form/controls/multiselect.js
index 0e91d6fc39..995847afbf 100644
--- a/frappe/public/js/frappe/form/controls/multiselect.js
+++ b/frappe/public/js/frappe/form/controls/multiselect.js
@@ -70,9 +70,7 @@ frappe.ui.form.ControlMultiSelect = class ControlMultiSelect extends (
get_values() {
const value = this.get_value() || "";
- const values = value.split(/\s*,\s*/).filter((d) => d);
-
- return values;
+ return value.split(/\s*,\s*/).filter((d) => d);
}
get_data() {
diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js
index fd0e878567..15e11cd9e4 100644
--- a/frappe/public/js/frappe/form/controls/text_editor.js
+++ b/frappe/public/js/frappe/form/controls/text_editor.js
@@ -198,14 +198,18 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for
get_quill_options() {
return {
modules: {
- toolbar: this.get_toolbar_options(),
+ toolbar: Object.keys(this.df).includes("get_toolbar_options")
+ ? this.df.get_toolbar_options()
+ : this.get_toolbar_options(),
table: true,
imageResize: {},
magicUrl: true,
mention: this.get_mention_options(),
},
- theme: "snow",
+ theme: this.df.theme || "snow",
readOnly: this.disabled,
+ bounds: this.quill_container[0],
+ placeholder: this.df.placeholder || "",
};
}
diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index 0f3083ed69..f80ee0897b 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -175,10 +175,9 @@ frappe.ui.form.Dashboard = class FormDashboard {
make_progress_chart(title) {
this.progress_area.show();
- let progress_chart = $(
- ''
- ).appendTo(this.progress_area.body);
- return progress_chart;
+ return $('').appendTo(
+ this.progress_area.body
+ );
}
refresh() {
@@ -369,7 +368,10 @@ frappe.ui.form.Dashboard = class FormDashboard {
let doctype = $link.attr("data-doctype"),
names = $link.attr("data-names") || [];
- if (this.data.internal_links[doctype]) {
+ if (
+ this.internal_links_found &&
+ this.internal_links_found.find((d) => d.doctype === doctype)
+ ) {
if (names.length) {
frappe.route_options = { name: ["in", names] };
} else {
@@ -437,32 +439,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
me.update_heatmap(r.message.timeline_data);
}
- // update badges
- $.each(r.message.count, function (i, d) {
- me.frm.dashboard.set_badge_count(d.name, cint(d.open_count), cint(d.count));
- });
-
- // update from internal links
- $.each(me.data.internal_links, (doctype, link) => {
- let names = [];
- if (typeof link === "string" || link instanceof String) {
- // get internal links in parent document
- let value = me.frm.doc[link];
- if (value && !names.includes(value)) {
- names.push(value);
- }
- } else if (Array.isArray(link)) {
- // get internal links in child documents
- let [table_fieldname, link_fieldname] = link;
- (me.frm.doc[table_fieldname] || []).forEach((d) => {
- let value = d[link_fieldname];
- if (value && !names.includes(value)) {
- names.push(value);
- }
- });
- }
- me.frm.dashboard.set_badge_count(doctype, 0, names.length, names);
- });
+ me.update_badges(r.message.count);
me.frm.dashboard_data = r.message;
me._fetched_counts = true;
@@ -471,11 +448,52 @@ frappe.ui.form.Dashboard = class FormDashboard {
});
}
- set_badge_count(doctype, open_count, count, names) {
+ update_badges(count) {
+ let me = this;
+
+ this.internal_links_found = count.internal_links_found;
+
+ $.each(count.internal_links_found, function (i, d) {
+ me.frm.dashboard.set_badge_count_for_internal_link(
+ d.doctype,
+ cint(d.open_count),
+ cint(d.count),
+ d.names
+ );
+ });
+
+ $.each(count.external_links_found, function (i, d) {
+ me.frm.dashboard.set_badge_count_for_external_link(
+ d.doctype,
+ cint(d.open_count),
+ cint(d.count)
+ );
+ });
+ }
+
+ set_badge_count_for_external_link(doctype, open_count, count) {
let $link = $(this.transactions_area).find(
'.document-link[data-doctype="' + doctype + '"]'
);
+ this.set_badge_count_common(open_count, count, $link);
+ }
+
+ set_badge_count_for_internal_link(doctype, open_count, count, names) {
+ let $link = $(this.transactions_area).find(
+ '.document-link[data-doctype="' + doctype + '"]'
+ );
+
+ this.set_badge_count_common(open_count, count, $link);
+
+ if (names && names.length) {
+ $link.attr("data-names", names ? names.join(",") : "");
+ } else {
+ $link.find("a").attr("disabled", true);
+ }
+ }
+
+ set_badge_count_common(open_count, count, $link) {
if (open_count) {
$link
.find(".open-notification")
@@ -489,14 +507,6 @@ frappe.ui.form.Dashboard = class FormDashboard {
.removeClass("hidden")
.text(count > 99 ? "99+" : count);
}
-
- if (this.data.internal_links[doctype]) {
- if (names && names.length) {
- $link.attr("data-names", names ? names.join(",") : "");
- } else {
- $link.find("a").attr("disabled", true);
- }
- }
}
update_heatmap(data) {
@@ -551,7 +561,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
.addClass("indicator-column");
}
- let indicator = $(
+ return $(
'"
).appendTo(this.stats_area_row);
-
- return indicator;
}
// graphs
diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js
index 2f2df4e4c1..e42b4af681 100644
--- a/frappe/public/js/frappe/form/footer/base_timeline.js
+++ b/frappe/public/js/frappe/form/footer/base_timeline.js
@@ -138,7 +138,7 @@ class BaseTimeline {
let timeline_content = timeline_item.find(".timeline-content");
timeline_content.append(item.content);
if (!item.hide_timestamp && !item.is_card) {
- timeline_content.append(` - ${comment_when(item.creation)}`);
+ timeline_content.append(` · ${comment_when(item.creation)}`);
}
if (item.id) {
timeline_content.attr("id", item.id);
diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js
index b3258e8506..71423e4433 100644
--- a/frappe/public/js/frappe/form/footer/form_timeline.js
+++ b/frappe/public/js/frappe/form/footer/form_timeline.js
@@ -118,7 +118,7 @@ class FormTimeline extends BaseTimeline {
if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) {
frappe.utils.get_page_view_count(this.frm.doc.route).then((res) => {
this.add_timeline_item({
- content: __("{0} Web page views", [res.message], "Form timeline"),
+ content: __("{0} Web page views", [res.message]),
hide_timestamp: true,
});
});
@@ -126,27 +126,23 @@ class FormTimeline extends BaseTimeline {
}
get_creation_message() {
- const user_link = get_user_link(this.frm.doc.owner);
-
return {
creation: this.frm.doc.creation,
content: get_user_message(
this.frm.doc.owner,
- __("You created this", null, "Form timeline"),
- __("{0} created this", [user_link], "Form timeline")
+ __("You created this"),
+ __("{0} created this", [get_user_link(this.frm.doc.owner)])
),
};
}
get_modified_message() {
- const user_link = get_user_link(this.frm.doc.modified_by);
-
return {
creation: this.frm.doc.modified,
content: get_user_message(
this.frm.doc.modified_by,
- __("You last edited this", null, "Form timeline"),
- __("{0} last edited this", [user_link], "Form timeline")
+ __("You last edited this"),
+ __("{0} last edited this", [get_user_link(this.frm.doc.modified_by)])
),
};
}
@@ -174,18 +170,13 @@ class FormTimeline extends BaseTimeline {
get_view_timeline_contents() {
let view_timeline_contents = [];
(this.doc_info.views || []).forEach((view) => {
- const view_time = comment_when(view.creation);
- const user_link = get_user_link(view.owner);
- const timeline_content = get_user_message(
- view.owner,
- __("You viewed this {0}", [view_time], "Form timeline"),
- __("{0} viewed this {1}", [user_link, view_time], "Form timeline")
- );
-
view_timeline_contents.push({
creation: view.creation,
- content: timeline_content,
- hide_timestamp: true,
+ content: get_user_message(
+ view.owner,
+ __("You viewed this"),
+ __("{0} viewed this", [get_user_link(view.owner)])
+ ),
});
});
@@ -463,8 +454,8 @@ class FormTimeline extends BaseTimeline {
(this.doc_info.like_logs || []).forEach((like_log) => {
const timeline_content = get_user_message(
like_log.owner,
- __("You Liked", null, "Form timeline"),
- __("{0} Liked", [get_user_link(like_log.owner)], "Form timeline")
+ __("You Liked"),
+ __("{0} Liked", [get_user_link(like_log.owner)])
);
like_timeline_contents.push({
diff --git a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js
index 84ee4fd67d..77e6cacd66 100644
--- a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js
+++ b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js
@@ -284,7 +284,7 @@ function format_content_for_timeline(content) {
}
function get_user_link(user) {
- const user_display_text = (frappe.user_info(user).fullname || "").bold();
+ const user_display_text = frappe.user_info(user).fullname || "";
return frappe.utils.get_form_link("User", user, true, user_display_text);
}
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 6fb84709ee..d9b07b6252 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -338,7 +338,7 @@ export default class GridRow {
this.open_form_button = $(`
`)
.appendTo(this.open_form_button)
diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js
index 3313834dc1..b7f0770a72 100644
--- a/frappe/public/js/frappe/form/layout.js
+++ b/frappe/public/js/frappe/form/layout.js
@@ -102,6 +102,7 @@ frappe.ui.form.Layout = class Layout {
// remove previous color
this.message.removeClass(this.message_color);
}
+ let close_message = $(``);
this.message_color =
color && ["yellow", "blue", "red", "green", "orange"].includes(color) ? color : "blue";
if (html) {
@@ -111,6 +112,8 @@ frappe.ui.form.Layout = class Layout {
}
this.message.removeClass("hidden").addClass(this.message_color);
$(html).appendTo(this.message);
+ close_message.appendTo(this.message);
+ close_message.on("click", () => this.message.empty().addClass("hidden"));
} else {
this.message.empty().addClass("hidden");
}
diff --git a/frappe/public/js/frappe/form/link_selector.js b/frappe/public/js/frappe/form/link_selector.js
index e90ed3e394..1040233b61 100644
--- a/frappe/public/js/frappe/form/link_selector.js
+++ b/frappe/public/js/frappe/form/link_selector.js
@@ -19,6 +19,7 @@ frappe.ui.form.LinkSelector = class LinkSelector {
var me = this;
this.start = 0;
+ this.page_length = 10;
this.dialog = new frappe.ui.Dialog({
title: __("Select {0}", [this.doctype == "[Select]" ? __("value") : __(this.doctype)]),
fields: [
@@ -37,7 +38,7 @@ frappe.ui.form.LinkSelector = class LinkSelector {
fieldname: "more",
label: __("More"),
click: () => {
- me.start += 20;
+ me.start += me.page_length;
me.search();
},
},
@@ -65,6 +66,7 @@ frappe.ui.form.LinkSelector = class LinkSelector {
txt: this.dialog.fields_dict.txt.get_value(),
searchfield: "name",
start: this.start,
+ page_length: this.page_length,
};
var me = this;
@@ -91,7 +93,7 @@ frappe.ui.form.LinkSelector = class LinkSelector {
}
if (r.values.length) {
- $.each(r.values, function (i, v) {
+ for (const v of r.values) {
var row = $(
repl(
'\
@@ -126,7 +128,7 @@ frappe.ui.form.LinkSelector = class LinkSelector {
}
return false;
});
- });
+ }
} else {
$(
'
' +
@@ -146,9 +148,11 @@ frappe.ui.form.LinkSelector = class LinkSelector {
});
}
- if (r.values.length < 20) {
- var more_btn = me.dialog.fields_dict.more.$wrapper;
+ var more_btn = me.dialog.fields_dict.more.$wrapper;
+ if (r.values.length < me.page_length) {
more_btn.hide();
+ } else {
+ more_btn.show();
}
},
this.dialog.get_primary_btn()
diff --git a/frappe/public/js/frappe/form/sidebar/assign_to.js b/frappe/public/js/frappe/form/sidebar/assign_to.js
index cd83705f19..a5741522b7 100644
--- a/frappe/public/js/frappe/form/sidebar/assign_to.js
+++ b/frappe/public/js/frappe/form/sidebar/assign_to.js
@@ -288,6 +288,13 @@ frappe.ui.form.AssignmentDialog = class {
assign_to: assignment,
});
}
+ close_assignment(assignment) {
+ return frappe.xcall("frappe.desk.form.assign_to.close", {
+ doctype: this.frm.doctype,
+ name: this.frm.docname,
+ assign_to: assignment,
+ });
+ }
update_assignment(assignment) {
const in_the_list = this.assignment_list.find(`[data-user="${assignment}"]`).length;
if (!in_the_list) {
@@ -295,22 +302,40 @@ frappe.ui.form.AssignmentDialog = class {
}
}
get_assignment_row(assignment) {
- let row = $(`
+ const row = $(`
-
+
${frappe.avatar(assignment)}
${frappe.user.full_name(assignment)}
-
+
+
+
`);
- if (assignment === frappe.session.user || this.frm.perm[0].write) {
- row.append(`
-
- ${frappe.utils.icon("close")}
-
+ const btn_group = row.find(".btn-group");
+
+ if (assignment === frappe.session.user) {
+ btn_group.append(`
+
`);
- row.find(".remove-btn").click(() => {
+ btn_group.find(".complete-btn").click(() => {
+ this.close_assignment(assignment).then((assignments) => {
+ row.remove();
+ this.render(assignments);
+ });
+ });
+ }
+
+ if (assignment === frappe.session.user || this.frm.perm[0].write) {
+ btn_group.append(`
+
+ `);
+ btn_group.find(".remove-btn").click(() => {
this.remove_assignment(assignment).then((assignments) => {
row.remove();
this.render(assignments);
diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js
index b7763c397d..c8785a97c8 100644
--- a/frappe/public/js/frappe/form/sidebar/attachments.js
+++ b/frappe/public/js/frappe/form/sidebar/attachments.js
@@ -1,9 +1,12 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-
frappe.ui.form.Attachments = class Attachments {
constructor(opts) {
$.extend(this, opts);
+
+ this.attachments_page_length = 10; // show n attachments initially
+ this.show_all_attachments = false;
+
this.make();
}
make() {
@@ -11,7 +14,16 @@ frappe.ui.form.Attachments = class Attachments {
this.parent.find(".add-attachment-btn").click(function () {
me.new_attachment();
});
- this.add_attachment_wrapper = this.parent.find(".add-attachment-btn");
+
+ this.parent.find(".explore-btn").click(() => {
+ frappe.open_in_new_tab = true;
+ frappe.set_route("List", "File", {
+ attached_to_doctype: this.frm.doctype,
+ attached_to_name: this.frm.docname,
+ });
+ });
+
+ this.add_attachment_wrapper = this.parent.find(".attachments-actions");
this.attachments_label = this.parent.find(".attachments-label");
}
max_reached(raise_exception = false) {
@@ -31,8 +43,6 @@ frappe.ui.form.Attachments = class Attachments {
return false;
}
refresh() {
- var me = this;
-
if (this.frm.doc.__islocal) {
this.parent.toggle(false);
return;
@@ -42,12 +52,66 @@ frappe.ui.form.Attachments = class Attachments {
var max_reached = this.max_reached();
this.add_attachment_wrapper.toggle(!max_reached);
+ this.setup_expanded_explore_button(max_reached);
// add attachment objects
var attachments = this.get_attachments();
- if (attachments.length) {
+ this.render_attachments(attachments);
+ this.setup_show_all_button(attachments);
+ }
+
+ setup_expanded_explore_button(max_reached) {
+ if (!max_reached) {
+ this.parent.find(".explore-full-btn").addClass("hidden");
+ return;
+ }
+
+ this.parent.find(".explore-full-btn").removeClass("hidden");
+ this.parent.find(".explore-full-btn").click(() => {
+ frappe.set_route("List", "File", {
+ attached_to_doctype: this.frm.doctype,
+ attached_to_name: this.frm.docname,
+ });
+ });
+ }
+
+ setup_show_all_button(attachments) {
+ // show button if there is more to show and user has not clicked on "Show All"
+ let is_slicable = attachments.length > this.attachments_page_length;
+ let show = !this.show_all_attachments && is_slicable;
+
+ let show_all_btn = this.parent.find(".show-all-btn");
+ if (!show) {
+ show_all_btn.addClass("hidden");
+ return;
+ }
+
+ show_all_btn.removeClass("hidden");
+ show_all_btn.click(() => {
+ show_all_btn.addClass("hidden");
+ this.show_all_attachments = true;
+ this.refresh();
+ });
+ }
+
+ get_attachments() {
+ return this.frm.get_docinfo().attachments || [];
+ }
+
+ render_attachments(attachments) {
+ var me = this;
+ let attachments_to_render = attachments;
+
+ let is_slicable = attachments.length > this.attachments_page_length;
+ if (!this.show_all_attachments && is_slicable) {
+ // render last n attachments as they are at the top
+ let start = attachments.length - this.attachments_page_length;
+ attachments_to_render = attachments.slice(start, attachments.length);
+ }
+
+ if (attachments_to_render.length) {
let exists = {};
- let unique_attachments = attachments.filter((attachment) => {
+ let unique_attachments = attachments_to_render.filter((attachment) => {
return Object.prototype.hasOwnProperty.call(exists, attachment.file_name)
? false
: (exists[attachment.file_name] = true);
@@ -55,13 +119,15 @@ frappe.ui.form.Attachments = class Attachments {
unique_attachments.forEach((attachment) => {
me.add_attachment(attachment);
});
- } else {
+ }
+
+ if (!attachments.length) {
+ // If no attachments in totality
this.attachments_label.removeClass("has-attachments");
+ this.parent.find(".explore-btn").toggle(false); // hide explore icon button
}
}
- get_attachments() {
- return this.frm.get_docinfo().attachments || [];
- }
+
add_attachment(attachment) {
var file_name = attachment.file_name;
var file_url = this.get_file_url(attachment);
@@ -101,8 +167,11 @@ frappe.ui.form.Attachments = class Attachments {
$(``)
.append(frappe.get_data_pill(file_label, fileid, remove_action, icon))
- .insertAfter(this.attachments_label.addClass("has-attachments"));
+ .insertAfter(this.add_attachment_wrapper);
+
+ this.parent.find(".explore-btn").toggle(true); // show explore icon button if hidden
}
+
get_file_url(attachment) {
var file_url = attachment.file_url;
if (!file_url) {
diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js
index 4859fa5c02..4a98f5056a 100644
--- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js
+++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js
@@ -5,6 +5,7 @@ import "./review";
import "./document_follow";
import "./user_image";
import "./form_sidebar_users";
+import { get_user_link, get_user_message } from "../footer/version_timeline_content_builder";
frappe.ui.form.Sidebar = class {
constructor(opts) {
@@ -79,33 +80,31 @@ frappe.ui.form.Sidebar = class {
frappe.utils.get_page_view_count(route).then((res) => {
this.sidebar
.find(".pageview-count")
- .html(__("{0} Page Views", [String(res.message).bold()]));
+ .html(__("{0} Web page views", [String(res.message).bold()]));
});
}
this.sidebar
.find(".modified-by")
.html(
- __(
- "{0} edited this {1}",
- [
- frappe.user.full_name(this.frm.doc.modified_by).bold(),
- "
" + comment_when(this.frm.doc.modified),
- ],
- "For example, 'Jon Doe edited this 5 minutes ago'."
- )
+ get_user_message(
+ this.frm.doc.modified_by,
+ __("You last edited this", null),
+ __("{0} last edited this", [get_user_link(this.frm.doc.modified_by)])
+ ) +
+ " · " +
+ comment_when(this.frm.doc.modified)
);
this.sidebar
.find(".created-by")
.html(
- __(
- "{0} created this {1}",
- [
- frappe.user.full_name(this.frm.doc.owner).bold(),
- "
" + comment_when(this.frm.doc.creation),
- ],
- "For example, 'Jon Doe created this 5 minutes ago'."
- )
+ get_user_message(
+ this.frm.doc.owner,
+ __("You created this", null),
+ __("{0} created this", [get_user_link(this.frm.doc.owner)])
+ ) +
+ " · " +
+ comment_when(this.frm.doc.creation)
);
this.refresh_like();
@@ -130,7 +129,7 @@ frappe.ui.form.Sidebar = class {
callback: function (res) {
me.sidebar
.find(".auto-repeat-status")
- .html(__("Repeats {0}", [res.message.frequency]));
+ .html(__("Repeats {0}", [__(res.message.frequency)]));
me.sidebar.find(".auto-repeat-status").on("click", function () {
frappe.set_route("Form", "Auto Repeat", me.frm.doc.auto_repeat);
});
diff --git a/frappe/public/js/frappe/form/templates/form_sidebar.html b/frappe/public/js/frappe/form/templates/form_sidebar.html
index 9b15dae476..7d553c4f89 100644
--- a/frappe/public/js/frappe/form/templates/form_sidebar.html
+++ b/frappe/public/js/frappe/form/templates/form_sidebar.html
@@ -53,6 +53,41 @@