diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js
index 04a72a9436..f14c991c7c 100644
--- a/cypress/integration/kanban.js
+++ b/cypress/integration/kanban.js
@@ -98,15 +98,17 @@ context("Kanban Board", () => {
});
it("Checks if Kanban Board edits are blocked for non-System Manager and non-owner of the Board", () => {
- // create admin kanban board
- cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
-
cy.switch_to_user("Administrator");
- cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
- // remove sys manager
- cy.remove_role("frappe@example.com", "System Manager");
- cy.switch_to_user("frappe@example.com");
+ const noSystemManager = "nosysmanager@example.com";
+ cy.call("frappe.tests.ui_test_helpers.create_test_user", {
+ username: noSystemManager,
+ });
+ cy.remove_role(noSystemManager, "System Manager");
+ cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Frappe User ToDo" });
+ cy.call("frappe.tests.ui_test_helpers.create_admin_kanban");
+
+ cy.switch_to_user(noSystemManager);
cy.visit("/app/todo/view/kanban/Admin Kanban");
@@ -122,7 +124,8 @@ context("Kanban Board", () => {
// Column actions should be hidden (dropdown for 'Archive' and indicators)
cy.get(".kanban .column-options").should("have.length", 0);
- cy.add_role("frappe@example.com", "System Manager");
+ cy.switch_to_user("Administrator");
+ cy.call("frappe.client.delete", { doctype: "User", name: noSystemManager });
});
after(() => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 332224f989..9d7befe2d1 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1274,7 +1274,7 @@ def reload_doc(
return frappe.modules.reload_doc(module, dt, dn, force=force, reset_permissions=reset_permissions)
-@whitelist()
+@whitelist(methods=["POST", "PUT"])
def rename_doc(
doctype: str,
old: str,
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index f41cca3c57..03374986d4 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -24,7 +24,11 @@ EXTRA_ARGS_CTX = {"ignore_unknown_options": True, "allow_extra_args": True}
@click.option("--app", help="Build assets for app")
@click.option("--apps", help="Build assets for specific apps")
@click.option(
- "--hard-link", is_flag=True, default=False, help="Copy the files instead of symlinking"
+ "--hard-link",
+ is_flag=True,
+ default=False,
+ help="Copy the files instead of symlinking",
+ envvar="FRAPPE_HARD_LINK_ASSETS",
)
@click.option(
"--make-copy",
@@ -908,7 +912,7 @@ def run_ui_tests(
os.chdir(app_base_path)
- node_bin = subprocess.getoutput("yarn bin")
+ node_bin = subprocess.getoutput("(cd ../frappe && yarn bin)")
cypress_path = f"{node_bin}/cypress"
drag_drop_plugin_path = f"{node_bin}/../@4tw/cypress-drag-drop"
real_events_plugin_path = f"{node_bin}/../cypress-real-events"
@@ -935,7 +939,7 @@ def run_ui_tests(
"@cypress/code-coverage@^3",
]
)
- frappe.commands.popen(f"yarn add {packages} --no-lockfile")
+ frappe.commands.popen(f"(cd ../frappe && yarn add {packages} --no-lockfile)")
# run for headless mode
run_or_open = "run --browser chrome --record" if headless else "open"
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index 5fe22eb7f2..965425019c 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -254,19 +254,23 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
"""select
`tabAddress`.name, `tabAddress`.city, `tabAddress`.country
from
- `tabAddress`, `tabDynamic Link`
+ `tabAddress`
+ join `tabDynamic Link`
+ on (`tabDynamic Link`.parent = `tabAddress`.name and `tabDynamic Link`.parenttype = 'Address')
where
- `tabDynamic Link`.parent = `tabAddress`.name and
- `tabDynamic Link`.parenttype = 'Address' and
`tabDynamic Link`.link_doctype = %(link_doctype)s and
`tabDynamic Link`.link_name = %(link_name)s and
ifnull(`tabAddress`.disabled, 0) = 0 and
({search_condition})
{mcond} {condition}
order by
- if(locate(%(_txt)s, `tabAddress`.name), locate(%(_txt)s, `tabAddress`.name), 99999),
+ case
+ when locate(%(_txt)s, `tabAddress`.name) != 0
+ then locate(%(_txt)s, `tabAddress`.name)
+ else 99999
+ end,
`tabAddress`.idx desc, `tabAddress`.name
- limit %(start)s, %(page_len)s """.format(
+ limit %(page_len)s offset %(start)s""".format(
mcond=get_match_cond(doctype),
search_condition=search_condition,
condition=condition or "",
diff --git a/frappe/contacts/doctype/address/test_address.py b/frappe/contacts/doctype/address/test_address.py
index 1d11c5efef..ecb95f9e0c 100644
--- a/frappe/contacts/doctype/address/test_address.py
+++ b/frappe/contacts/doctype/address/test_address.py
@@ -1,7 +1,9 @@
# Copyright (c) 2015, Frappe Technologies and Contributors
# License: MIT. See LICENSE
+from functools import partial
+
import frappe
-from frappe.contacts.doctype.address.address import get_address_display
+from frappe.contacts.doctype.address.address import address_query, get_address_display
from frappe.tests.utils import FrappeTestCase
@@ -28,3 +30,29 @@ class TestAddress(FrappeTestCase):
address = frappe.get_list("Address")[0].name
display = get_address_display(frappe.get_doc("Address", address).as_dict())
self.assertTrue(display)
+
+ def test_address_query(self):
+ def query(doctype="Address", txt="", searchfield="name", start=0, page_len=20, filters=None):
+ if filters is None:
+ filters = {"link_doctype": "User", "link_name": "Administrator"}
+ return address_query(doctype, txt, searchfield, start, page_len, filters)
+
+ frappe.get_doc(
+ {
+ "address_type": "Billing",
+ "address_line1": "1",
+ "city": "Mumbai",
+ "state": "Maharashtra",
+ "country": "India",
+ "doctype": "Address",
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_name": "Administrator",
+ }
+ ],
+ }
+ ).insert()
+
+ self.assertGreaterEqual(len(query(txt="Admin")), 1)
+ self.assertEqual(len(query(txt="what_zyx")), 0)
diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py
index 2e199e014d..1733b7b716 100755
--- a/frappe/core/doctype/communication/email.py
+++ b/frappe/core/doctype/communication/email.py
@@ -165,7 +165,7 @@ def _make(
if not comm.get_outgoing_email_account():
frappe.throw(
_(
- "Unable to send mail because of a missing email account. Please setup default Email Account from Setup > Email > Email Account"
+ "Unable to send mail because of a missing email account. Please setup default Email Account from Settings > Email Account"
),
exc=frappe.OutgoingEmailError,
)
diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py
index 7b6427d1c2..24b6a8fafb 100644
--- a/frappe/core/doctype/communication/mixins.py
+++ b/frappe/core/doctype/communication/mixins.py
@@ -70,7 +70,7 @@ class CommunicationEmailMixin:
if include_sender:
cc.append(self.sender_mailid)
if is_inbound_mail_communcation:
- if (doc_owner := self.get_owner()) not in frappe.STANDARD_USERS:
+ if (doc_owner := self.get_owner()) and (doc_owner not in frappe.STANDARD_USERS):
cc.append(doc_owner)
cc = set(cc) - {self.sender_mailid}
cc.update(self.get_assignees())
@@ -216,7 +216,11 @@ class CommunicationEmailMixin:
"reference_name": self.reference_name,
"reference_type": self.reference_doctype,
}
- return ToDo.get_owners(filters)
+
+ if self.reference_doctype and self.reference_name:
+ return ToDo.get_owners(filters)
+ else:
+ return []
@staticmethod
def filter_thread_notification_disbled_users(emails):
diff --git a/frappe/core/doctype/docshare/test_docshare.py b/frappe/core/doctype/docshare/test_docshare.py
index b874042d15..e080b0d4ff 100644
--- a/frappe/core/doctype/docshare/test_docshare.py
+++ b/frappe/core/doctype/docshare/test_docshare.py
@@ -4,7 +4,7 @@
import frappe
import frappe.share
from frappe.automation.doctype.auto_repeat.test_auto_repeat import create_submittable_doctype
-from frappe.tests.utils import FrappeTestCase
+from frappe.tests.utils import FrappeTestCase, change_settings
test_dependencies = ["User"]
@@ -139,3 +139,61 @@ class TestDocShare(FrappeTestCase):
test_doc.reload()
self.assertTrue(test_doc.has_permission("read"))
+
+ @change_settings("System Settings", {"disable_document_sharing": 1})
+ def test_share_disabled_add(self):
+ "Test if user loses share access on disabling share globally."
+ frappe.share.add("Event", self.event.name, self.user, share=1) # Share as admin
+ frappe.set_user(self.user)
+
+ # User does not have share access although given to them
+ self.assertFalse(self.event.has_permission("share"))
+ self.assertRaises(
+ frappe.PermissionError, frappe.share.add, "Event", self.event.name, "test1@example.com"
+ )
+
+ @change_settings("System Settings", {"disable_document_sharing": 1})
+ def test_share_disabled_add_with_ignore_permissions(self):
+ frappe.share.add("Event", self.event.name, self.user, share=1)
+ frappe.set_user(self.user)
+
+ # User does not have share access although given to them
+ self.assertFalse(self.event.has_permission("share"))
+
+ # Test if behaviour is consistent for developer overrides
+ frappe.share.add_docshare(
+ "Event", self.event.name, "test1@example.com", flags={"ignore_share_permission": True}
+ )
+
+ @change_settings("System Settings", {"disable_document_sharing": 1})
+ def test_share_disabled_set_permission(self):
+ frappe.share.add("Event", self.event.name, self.user, share=1)
+ frappe.set_user(self.user)
+
+ # User does not have share access although given to them
+ self.assertFalse(self.event.has_permission("share"))
+ self.assertRaises(
+ frappe.PermissionError,
+ frappe.share.set_permission,
+ "Event",
+ self.event.name,
+ "test1@example.com",
+ "read",
+ )
+
+ @change_settings("System Settings", {"disable_document_sharing": 1})
+ def test_share_disabled_assign_to(self):
+ """
+ Assigning a document to a user without access must not share the document,
+ if sharing disabled.
+ """
+ from frappe.desk.form.assign_to import add
+
+ frappe.share.add("Event", self.event.name, self.user, share=1)
+ frappe.set_user(self.user)
+
+ self.assertRaises(
+ frappe.ValidationError,
+ add,
+ {"doctype": "Event", "name": self.event.name, "assign_to": ["test1@example.com"]},
+ )
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index dc502e4683..17cddee1e8 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -169,7 +169,6 @@ class ServerScript(Document):
return items
-@frappe.whitelist()
def setup_scheduler_events(script_name, frequency):
"""Creates or Updates Scheduled Job Type documents based on the specified script name and frequency
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 8ebcb493de..102a0a76c2 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -13,6 +13,7 @@
"time_zone",
"enable_onboarding",
"setup_complete",
+ "disable_document_sharing",
"date_and_number_format",
"date_format",
"time_format",
@@ -528,12 +529,18 @@
"fieldtype": "Select",
"label": "Rounding Method",
"options": "Banker's Rounding (legacy)\nBanker's Rounding\nCommercial Rounding"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_document_sharing",
+ "fieldtype": "Check",
+ "label": "Disable Document Sharing"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2023-03-10 12:23:45.248125",
+ "modified": "2023-03-14 11:30:56.465653",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@@ -552,4 +559,4 @@
"sort_order": "ASC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 413dd07dc4..918a9ee37c 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -219,7 +219,10 @@ frappe.ui.form.on("User", {
});
}
- if (frappe.session.user == doc.name || frappe.user.has_role("System Manager")) {
+ if (
+ cint(frappe.boot.sysdefaults.enable_two_factor_auth) &&
+ (frappe.session.user == doc.name || frappe.user.has_role("System Manager"))
+ ) {
frm.add_custom_button(
__("Reset OTP Secret"),
function () {
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 7f9846d6c4..43540956e0 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -200,7 +200,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
return db_size[0].get("database_size")
def log_query(self, query, values, debug, explain):
- self.last_query = query = self._cursor._last_executed
+ self.last_query = query = self._cursor._executed
self._log_query(query, debug, explain)
return self.last_query
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.js b/frappe/desk/doctype/bulk_update/bulk_update.js
index 017eee1480..d8a2b89cf3 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.js
+++ b/frappe/desk/doctype/bulk_update/bulk_update.js
@@ -16,38 +16,27 @@ frappe.ui.form.on("Bulk Update", {
if (!frm.doc.update_value) {
frappe.throw(__('Field "value" is mandatory. Please specify value to be updated'));
} else {
- frappe
- .call({
- method: "frappe.desk.doctype.bulk_update.bulk_update.update",
- args: {
- doctype: frm.doc.document_type,
- field: frm.doc.field,
- value: frm.doc.update_value,
- condition: frm.doc.condition,
- limit: frm.doc.limit,
- },
- })
- .then((r) => {
- let failed = r.message;
- if (!failed) failed = [];
+ frm.call("bulk_update").then((r) => {
+ let failed = r.message;
+ if (!failed) failed = [];
- if (failed.length && !r._server_messages) {
- frappe.throw(
- __("Cannot update {0}", [
- failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
- ])
- );
- } else {
- frappe.msgprint({
- title: __("Success"),
- message: __("Updated Successfully"),
- indicator: "green",
- });
- }
+ if (failed.length && !r._server_messages) {
+ frappe.throw(
+ __("Cannot update {0}", [
+ failed.map((f) => (f.bold ? f.bold() : f)).join(", "),
+ ])
+ );
+ } else {
+ frappe.msgprint({
+ title: __("Success"),
+ message: __("Updated Successfully"),
+ indicator: "green",
+ });
+ }
- frappe.hide_progress();
- frm.save();
- });
+ frappe.hide_progress();
+ frm.save();
+ });
}
});
},
diff --git a/frappe/desk/doctype/bulk_update/bulk_update.py b/frappe/desk/doctype/bulk_update/bulk_update.py
index 5521d9583f..535be8155f 100644
--- a/frappe/desk/doctype/bulk_update/bulk_update.py
+++ b/frappe/desk/doctype/bulk_update/bulk_update.py
@@ -10,26 +10,24 @@ from frappe.utils.scheduler import is_scheduler_inactive
class BulkUpdate(Document):
- pass
+ @frappe.whitelist()
+ def bulk_update(self):
+ self.check_permission("write")
+ limit = self.limit if self.limit and cint(self.limit) < 500 else 500
+ condition = ""
+ if self.condition:
+ if ";" in self.condition:
+ frappe.throw(_("; not allowed in condition"))
-@frappe.whitelist()
-def update(doctype, field, value, condition="", limit=500):
- if not limit or cint(limit) > 500:
- limit = 500
+ condition = f" where {self.condition}"
- if condition:
- condition = " where " + condition
-
- if ";" in condition:
- frappe.throw(_("; not allowed in condition"))
-
- docnames = frappe.db.sql_list(
- f"""select name from `tab{doctype}`{condition} limit {limit} offset 0"""
- )
- data = {}
- data[field] = value
- return submit_cancel_or_update_docs(doctype, docnames, "update", data)
+ docnames = frappe.db.sql_list(
+ f"""select name from `tab{self.document_type}`{condition} limit {limit} offset 0"""
+ )
+ return submit_cancel_or_update_docs(
+ self.document_type, docnames, "update", {self.field: self.update_value}
+ )
@frappe.whitelist()
diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py
index 72265dce1f..2b17d38371 100644
--- a/frappe/desk/form/assign_to.py
+++ b/frappe/desk/form/assign_to.py
@@ -93,10 +93,17 @@ def add(args=None):
doc = frappe.get_doc(args["doctype"], args["name"])
- # if assignee does not have permissions, share
+ # if assignee does not have permissions, share or inform
if not frappe.has_permission(doc=doc, user=assign_to):
- frappe.share.add(doc.doctype, doc.name, assign_to)
- shared_with_users.append(assign_to)
+ if frappe.get_system_settings("disable_document_sharing"):
+ msg = _("User {0} is not permitted to access this document.").format(frappe.bold(assign_to))
+ msg += "
" + _(
+ "As document sharing is disabled, please give them the required permissions before assigning."
+ )
+ frappe.throw(msg, title=_("Missing Permission"))
+ else:
+ frappe.share.add(doc.doctype, doc.name, assign_to)
+ shared_with_users.append(assign_to)
# make this document followed by assigned user
if frappe.get_cached_value("User", assign_to, "follow_assigned_documents"):
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index 1f6af4a3e7..486db2a784 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -11,7 +11,7 @@ def sendmail_to_system_managers(subject, content):
@frappe.whitelist()
def get_contact_list(txt, page_length=20) -> list[dict]:
- """Returns contacts (from autosuggest)"""
+ """Return email ids for a multiselect field."""
if cached_contacts := get_cached_contacts(txt):
return cached_contacts[:page_length]
@@ -19,11 +19,14 @@ def get_contact_list(txt, page_length=20) -> list[dict]:
reportview_conditions = build_match_conditions("Contact")
match_conditions = f"and {reportview_conditions}" if reportview_conditions else ""
+ # The multiselect field will store the `label` as the selected value.
+ # The `value` is just used as a unique key to distinguish between the options.
+ # https://github.com/frappe/frappe/blob/6c6a89bcdd9454060a1333e23b855d0505c9ebc2/frappe/public/js/frappe/form/controls/autocomplete.js#L29-L35
out = frappe.db.sql(
- f"""select email_id as value,
+ f"""select name as value, email_id as label,
concat(first_name, ifnull(concat(' ',last_name), '' )) as description
from tabContact
- where name like %(txt)s or email_id like %(txt)s
+ where (name like %(txt)s or email_id like %(txt)s) and email_id != ''
{match_conditions}
limit %(page_length)s""",
{"txt": f"%{txt}%", "page_length": page_length},
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 2e5dbe2e24..faf28afdb3 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -325,7 +325,7 @@ class EmailAccount(Document):
if _raise_error:
frappe.throw(
- _("Please setup default Email Account from Setup > Email > Email Account"),
+ _("Please setup default Email Account from Settings > Email Account"),
frappe.OutgoingEmailError,
)
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index 028b21b0ae..3b22bc4ce4 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -69,9 +69,7 @@ class SMTPServer:
if not self.server:
frappe.msgprint(
- _(
- "Email Account not setup. Please create a new Email Account from Setup > Email > Email Account"
- ),
+ _("Email Account not setup. Please create a new Email Account from Settings > Email Account"),
raise_exception=frappe.OutgoingEmailError,
)
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index 420cbee091..3908365291 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -488,6 +488,9 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
if frappe.conf.developer_mode:
for name in frappe.get_all("DocField", filters={"options": old}, pluck="parent"):
+ if name in (old, new):
+ continue
+
doctype = frappe.get_doc("DocType", name)
save = False
for f in doctype.fields:
@@ -496,11 +499,11 @@ def update_options_for_fieldtype(fieldtype: str, old: str, new: str) -> None:
save = True
if save:
doctype.save()
- else:
- DocField = frappe.qb.DocType("DocField")
- frappe.qb.update(DocField).set(DocField.options, new).where(
- (DocField.fieldtype == fieldtype) & (DocField.options == old)
- ).run()
+
+ DocField = frappe.qb.DocType("DocField")
+ frappe.qb.update(DocField).set(DocField.options, new).where(
+ (DocField.fieldtype == fieldtype) & (DocField.options == old)
+ ).run()
frappe.qb.update(CustomField).set(CustomField.options, new).where(
(CustomField.fieldtype == fieldtype) & (CustomField.options == old)
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 75a940233e..97badea500 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -77,6 +77,9 @@ def has_permission(
if user == "Administrator":
return True
+ if ptype == "share" and frappe.get_system_settings("disable_document_sharing"):
+ return False
+
if not doc and hasattr(doctype, "doctype"):
# first argument can be doc or doctype
doc = doctype
diff --git a/frappe/public/js/frappe/form/controls/barcode.js b/frappe/public/js/frappe/form/controls/barcode.js
index c130ecc039..a819384773 100644
--- a/frappe/public/js/frappe/form/controls/barcode.js
+++ b/frappe/public/js/frappe/form/controls/barcode.js
@@ -27,6 +27,7 @@ frappe.ui.form.ControlBarcode = class ControlBarcode extends frappe.ui.form.Cont
let svg = value;
let barcode_value = "";
+ this.set_empty_description();
if (value && value.startsWith("
Your OTP secret on {0} has been reset. If you did not perform this reset and did not request it, please contact your System Administrator immediately.
" - ).format(otp_issuer or "Frappe Framework"), + ).format(otp_issuer), "delayed": False, "retry": 3, } diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 5d1aed259a..ef32ff5653 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -1031,8 +1031,13 @@ def groupby_metric(iterable: dict[str, list], key: str): return records -def get_table_name(table_name: str) -> str: - return f"tab{table_name}" if not table_name.startswith("__") else table_name +def get_table_name(table_name: str, wrap_in_backticks: bool = False) -> str: + name = f"tab{table_name}" if not table_name.startswith("__") else table_name + + if wrap_in_backticks: + return f"`{name}`" + + return name def squashify(what): diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index 1cd57f4695..2e8a5088ed 100644 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -150,7 +150,7 @@ def _create_app_boilerplate(dest, hooks, no_git=False): f.write(frappe.as_unicode(gitignore_template.format(app_name=hooks.app_name))) # initialize git repository - app_repo = git.Repo.init(app_directory) + app_repo = git.Repo.init(app_directory, initial_branch="develop") app_repo.git.add(A=True) app_repo.index.commit("feat: Initialize App") diff --git a/frappe/utils/caching.py b/frappe/utils/caching.py index a2c9496098..007582f25f 100644 --- a/frappe/utils/caching.py +++ b/frappe/utils/caching.py @@ -128,3 +128,39 @@ def site_cache(ttl: int | None = None, maxsize: int | None = None) -> Callable: return time_cache_wrapper(ttl) return time_cache_wrapper + + +def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Callable: + """Decorator to cache method calls and its return values in Redis + + args: + ttl: time to expiry in seconds, defaults to 1 hour + user: `true` should cache be specific to session user. + """ + + def wrapper(func: Callable = None) -> Callable: + + func_key = f"{func.__module__}.{func.__qualname__}" + + def clear_cache(): + frappe.cache().delete_keys(func_key) + + func.clear_cache = clear_cache + func.ttl = ttl if not callable(ttl) else 3600 + + @wraps(func) + def redis_cache_wrapper(*args, **kwargs): + func_call_key = func_key + str(__generate_request_cache_key(args, kwargs)) + if frappe.cache().exists(func_call_key): + return frappe.cache().get_value(func_call_key, user=user) + else: + val = func(*args, **kwargs) + ttl = getattr(func, "ttl", 3600) + frappe.cache().set_value(func_call_key, val, expires_in_sec=ttl, user=user) + return val + + return redis_cache_wrapper + + if callable(ttl): + return wrapper(ttl) + return wrapper diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py index ea91299cfc..3b335b2c1d 100644 --- a/frappe/utils/redis_wrapper.py +++ b/frappe/utils/redis_wrapper.py @@ -195,6 +195,10 @@ class RedisWrapper(redis.Redis): except redis.exceptions.ConnectionError: return False + def exists(self, *names: str, user=None, shared=None) -> int: + names = [self.make_key(n, user=user, shared=shared) for n in names] + return super().exists(*names) + def hgetall(self, name): value = super().hgetall(self.make_key(name)) return {key: pickle.loads(value) for key, value in value.items()} diff --git a/frappe/www/printview.py b/frappe/www/printview.py index 9fdc77a2ba..38a0409e5f 100644 --- a/frappe/www/printview.py +++ b/frappe/www/printview.py @@ -11,7 +11,7 @@ import frappe from frappe import _, get_module_path from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.document_share_key.document_share_key import is_expired -from frappe.utils import cint, sanitize_html, strip_html +from frappe.utils import cint, escape_html, strip_html from frappe.utils.jinja_globals import is_rtl if TYPE_CHECKING: @@ -27,12 +27,11 @@ def get_context(context): """Build context for print""" if not ((frappe.form_dict.doctype and frappe.form_dict.name) or frappe.form_dict.doc): return { - "body": sanitize_html( - """Parameters doctype and name required
-%s""" - % repr(frappe.form_dict) - ) +
{escape_html(frappe.as_json(frappe.form_dict, indent=2))}
+ """
}
if frappe.form_dict.doc:
diff --git a/pyproject.toml b/pyproject.toml
index 837ea4624a..daa0748e5f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ dependencies = [
"Jinja2~=3.1.2",
"Pillow~=9.3.0",
"PyJWT~=2.4.0",
- "PyMySQL~=1.0.2",
+ "PyMySQL==1.0.3",
"PyPDF2~=2.1.0",
"PyPika~=0.48.9",
"PyQRCode~=1.2.1",
@@ -57,7 +57,7 @@ dependencies = [
"python-dateutil~=2.8.1",
"pytz==2022.1",
"rauth~=0.7.3",
- "redis~=4.3.4",
+ "redis~=4.5.4",
"hiredis~=2.0.0",
"requests-oauthlib~=1.3.0",
"requests~=2.27.1",