diff --git a/README.md b/README.md index d3b76648a2..aefa0db1d2 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Full-stack web application framework that uses Python and MariaDB on the server ### Development * [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script) -* [Development installlation on bare metal](https://frappeframework.com/docs/user/en/installation) +* [Development installation on bare metal](https://frappeframework.com/docs/user/en/installation) ## Contributing diff --git a/cypress/integration/discussions.js b/cypress/integration/discussions.js index 55bcabce19..9caeddeb1f 100644 --- a/cypress/integration/discussions.js +++ b/cypress/integration/discussions.js @@ -24,9 +24,9 @@ context("Discussions", () => { .should("have.value", "Discussion from tests"); // Enter comment - cy.get(".modal .comment-field") - .type("This is a discussion from the cypress ui tests.") - .should("have.value", "This is a discussion from the cypress ui tests."); + cy.get(".modal .discussions-comment").type( + "This is a discussion from the cypress ui tests." + ); // Submit cy.get(".modal .submit-discussion").click(); @@ -38,21 +38,16 @@ context("Discussions", () => { "Discussion from tests" ); cy.get(".discussion-on-page:visible").should("have.class", "show"); - cy.get(".discussion-on-page:visible .reply-card .reply-text").should( + cy.get(".discussion-on-page:visible .reply-card .reply-text .ql-editor p").should( "have.text", - "This is a discussion from the cypress ui tests.\n" + "This is a discussion from the cypress ui tests." ); }; const reply_through_comment_box = () => { - cy.get(".discussion-form:visible .comment-field") - .type( - "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page." - ) - .should( - "have.value", - "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page." - ); + cy.get(".discussion-form:visible .discussions-comment").type( + "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page." + ); cy.get(".discussion-form:visible .submit-discussion").click(); cy.wait(3000); @@ -63,28 +58,18 @@ context("Discussions", () => { .find(".reply-text") .should( "have.text", - "This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n" + "This is a discussion from the cypress ui tests. This comment was entered through the commentbox on the page.\n" ); }; - const cancel_and_clear_comment_box = () => { - cy.get(".discussion-form:visible .comment-field") - .type("This is a discussion from the cypress ui tests.") - .should("have.value", "This is a discussion from the cypress ui tests."); - - cy.get(".discussion-form:visible .cancel-comment").click(); - cy.get(".discussion-form:visible .comment-field").should("have.value", ""); - }; - const single_thread_discussion = () => { cy.visit("/test-single-thread"); cy.get(".discussions-sidebar").should("have.length", 0); cy.get(".reply").should("have.length", 0); - cy.get(".discussion-form:visible .comment-field") - .type("This comment is being made on a single thread discussion.") - .should("have.value", "This comment is being made on a single thread discussion."); - + cy.get(".discussion-form:visible .discussions-comment").type( + "This comment is being made on a single thread discussion." + ); cy.get(".discussion-form:visible .submit-discussion").click(); cy.wait(3000); cy.get(".discussion-on-page") @@ -96,6 +81,5 @@ context("Discussions", () => { it("reply through modal", reply_through_modal); it("reply through comment box", reply_through_comment_box); - it("cancel and clear comment box", cancel_and_clear_comment_box); it("single thread discussion", single_thread_discussion); }); diff --git a/frappe/core/doctype/activity_log/activity_log.json b/frappe/core/doctype/activity_log/activity_log.json index b272bab180..c6c4b2102b 100644 --- a/frappe/core/doctype/activity_log/activity_log.json +++ b/frappe/core/doctype/activity_log/activity_log.json @@ -33,7 +33,6 @@ { "fieldname": "subject", "fieldtype": "Small Text", - "in_global_search": 1, "in_list_view": 1, "label": "Subject", "reqd": 1 diff --git a/frappe/core/doctype/comment/test_comment.py b/frappe/core/doctype/comment/test_comment.py index ee2d473210..9ee0e4dd00 100644 --- a/frappe/core/doctype/comment/test_comment.py +++ b/frappe/core/doctype/comment/test_comment.py @@ -10,15 +10,6 @@ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog class TestComment(FrappeTestCase): - def tearDown(self): - frappe.form_dict.comment = None - frappe.form_dict.comment_email = None - frappe.form_dict.comment_by = None - frappe.form_dict.reference_doctype = None - frappe.form_dict.reference_name = None - frappe.form_dict.route = None - frappe.local.request_ip = None - def test_comment_creation(self): test_doc = frappe.get_doc(dict(doctype="ToDo", description="test")) test_doc.insert() @@ -45,16 +36,15 @@ class TestComment(FrappeTestCase): test_blog = make_test_blog() frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) - - frappe.form_dict.comment = "Good comment with 10 chars" - frappe.form_dict.comment_email = "test@test.com" - frappe.form_dict.comment_by = "Good Tester" - frappe.form_dict.reference_doctype = "Blog Post" - frappe.form_dict.reference_name = test_blog.name - frappe.form_dict.route = test_blog.route - frappe.local.request_ip = "127.0.0.1" - - add_comment() + add_comment_args = { + "comment": "Good comment with 10 chars", + "comment_email": "test@test.com", + "comment_by": "Good Tester", + "reference_doctype": test_blog.doctype, + "reference_name": test_blog.name, + "route": test_blog.route, + } + add_comment(**add_comment_args) self.assertEqual( frappe.get_all( @@ -67,10 +57,10 @@ class TestComment(FrappeTestCase): frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) - frappe.form_dict.comment = "pleez vizits my site http://mysite.com" - frappe.form_dict.comment_by = "bad commentor" - - add_comment() + add_comment_args.update( + comment="pleez vizits my site http://mysite.com", comment_by="bad commentor" + ) + add_comment(**add_comment_args) self.assertEqual( len( @@ -86,11 +76,8 @@ class TestComment(FrappeTestCase): # test for filtering html and css injection elements frappe.db.delete("Comment", {"reference_doctype": "Blog Post"}) - frappe.form_dict.comment = "Comment" - frappe.form_dict.comment_by = "hacker" - - add_comment() - + add_comment_args.update(comment="Comment", comment_by="hacker") + add_comment(**add_comment_args) self.assertEqual( frappe.get_all( "Comment", @@ -106,27 +93,30 @@ class TestComment(FrappeTestCase): def test_guest_cannot_comment(self): test_blog = make_test_blog() with set_user("Guest"): - frappe.form_dict.comment = "Good comment with 10 chars" - frappe.form_dict.comment_email = "mail@example.org" - frappe.form_dict.comment_by = "Good Tester" - frappe.form_dict.reference_doctype = "Blog Post" - frappe.form_dict.reference_name = test_blog.name - frappe.form_dict.route = test_blog.route - frappe.local.request_ip = "127.0.0.1" - - self.assertEqual(add_comment(), None) + self.assertEqual( + add_comment( + comment="Good comment with 10 chars", + comment_email="mail@example.org", + comment_by="Good Tester", + reference_doctype="Blog Post", + reference_name=test_blog.name, + route=test_blog.route, + ), + None, + ) def test_user_not_logged_in(self): - some_system_user = frappe.db.get_value("User", {}) + some_system_user = frappe.db.get_value("User", {"name": ("not in", frappe.STANDARD_USERS)}) test_blog = make_test_blog() with set_user("Guest"): - frappe.form_dict.comment = "Good comment with 10 chars" - frappe.form_dict.comment_email = some_system_user - frappe.form_dict.comment_by = "Good Tester" - frappe.form_dict.reference_doctype = "Blog Post" - frappe.form_dict.reference_name = test_blog.name - frappe.form_dict.route = test_blog.route - frappe.local.request_ip = "127.0.0.1" - - self.assertRaises(frappe.ValidationError, add_comment) + self.assertRaises( + frappe.ValidationError, + add_comment, + comment="Good comment with 10 chars", + comment_email=some_system_user, + comment_by="Good Tester", + reference_doctype="Blog Post", + reference_name=test_blog.name, + route=test_blog.route, + ) diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index d5a8cc9ed8..b90ae39506 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -33,7 +33,7 @@ from frappe.model.meta import Meta from frappe.modules import get_doc_path, make_boilerplate from frappe.modules.import_file import get_file_path from frappe.query_builder.functions import Concat -from frappe.utils import cint, flt, random_string +from frappe.utils import cint, flt, get_table_name, random_string from frappe.website.utils import clear_cache if TYPE_CHECKING: @@ -198,6 +198,7 @@ class DocType(Document): self.set("can_change_name_type", validate_autoincrement_autoname(self)) self.validate_document_type() validate_fields(self) + self.check_indexing_for_dashboard_links() if not self.istable: validate_permissions(self) @@ -298,6 +299,23 @@ class DocType(Document): if d.translatable and not supports_translation(d.fieldtype): d.translatable = 0 + def check_indexing_for_dashboard_links(self): + """Enable indexing for outgoing links used in dashboard""" + for d in self.fields: + if d.fieldtype == "Link" and not (d.unique or d.search_index): + referred_as_link = frappe.db.exists( + "DocType Link", + {"parent": d.options, "link_doctype": self.name, "link_fieldname": d.fieldname}, + ) + if not referred_as_link: + continue + + frappe.msgprint( + _("{0} should be indexed because it's referred in dashboard connections").format(_(d.label)), + alert=True, + indicator="orange", + ) + def check_developer_mode(self): """Throw exception if not developer mode or via patch""" if frappe.flags.in_patch or frappe.flags.in_test: diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 7ee587942c..54d0e5fb7d 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -767,6 +767,7 @@ def new_doctype( unique: bool = False, depends_on: str = "", fields: list[dict] | None = None, + custom: bool = True, **kwargs, ): if not name: @@ -777,7 +778,7 @@ def new_doctype( { "doctype": "DocType", "module": "Core", - "custom": 1, + "custom": custom, "fields": [ { "label": "Some Field", diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js index 159cf1ce39..052772c54e 100644 --- a/frappe/core/doctype/file/file.js +++ b/frappe/core/doctype/file/file.js @@ -20,6 +20,9 @@ frappe.ui.form.on("File", { if (frm.doc.file_name && frm.doc.file_name.split(".").splice(-1)[0] === "zip") { frm.add_custom_button(__("Unzip"), () => frm.trigger("unzip")); } + if (frm.doc.file_url) { + frm.add_web_link(frm.doc.file_url, __("View file")); + } }, preview_file: function (frm) { diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index 6c64bfe274..01871af5a5 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -80,6 +80,7 @@ "in_list_view": 1, "label": "File Size", "length": 20, + "options": "File Size", "read_only": 1 }, { @@ -174,7 +175,7 @@ "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2023-05-02 15:42:14.274901", + "modified": "2023-08-02 09:43:51.178011", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index cc3c2c228e..d334eaad1e 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -734,6 +734,8 @@ class File(Document): continue if _file.is_folder: continue + if not has_permission(_file, "read"): + continue zf.writestr(_file.file_name, _file.get_content()) zf.close() return zip_file.getvalue() diff --git a/frappe/core/doctype/package/package.py b/frappe/core/doctype/package/package.py index a3be3ea7f4..812a589940 100644 --- a/frappe/core/doctype/package/package.py +++ b/frappe/core/doctype/package/package.py @@ -6,6 +6,12 @@ import os import frappe from frappe.model.document import Document +LICENSES = ( + "GNU Affero General Public License", + "GNU General Public License", + "MIT License", +) + class Package(Document): # begin: auto-generated types @@ -29,6 +35,7 @@ class Package(Document): @frappe.whitelist() -def get_license_text(license_type): - with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile: - return textfile.read() +def get_license_text(license_type: str) -> str | None: + if license_type in LICENSES: + with open(os.path.join(os.path.dirname(__file__), "licenses", license_type + ".md")) as textfile: + return textfile.read() diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index ef8ccce9c1..067cd728b2 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -211,9 +211,9 @@ def expire_stalled_report(): def delete_prepared_reports(reports): reports = frappe.parse_json(reports) for report in reports: - frappe.delete_doc( - "Prepared Report", report["name"], ignore_permissions=True, delete_permanently=True - ) + prepared_report = frappe.get_doc("Prepared Report", report["name"]) + if prepared_report.has_permission(): + prepared_report.delete(ignore_permissions=True, delete_permanently=True) def create_json_gz_file(data, dt, dn): diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index ca5b8d721b..b3193de714 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -68,7 +68,7 @@ else:

 # 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/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..6496752855 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1010,7 +1010,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 +1042,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")
 
diff --git a/frappe/desk/doctype/console_log/console_log.js b/frappe/desk/doctype/console_log/console_log.js
index 9a980667ac..822fbb466e 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"), () => {
+			localStorage.setItem("system_console_code", frm.doc.script);
+			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_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/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js
index dc73f33b67..bd993824ce 100644
--- a/frappe/desk/doctype/system_console/system_console.js
+++ b/frappe/desk/doctype/system_console/system_console.js
@@ -10,7 +10,15 @@ frappe.ui.form.on("System Console", {
 			description: __("Execute Console script"),
 			ignore_inputs: true,
 		});
-		frm.set_value("type", "Python");
+		if (
+			localStorage.getItem("system_console_code") &&
+			localStorage.getItem("system_console_type")
+		) {
+			frm.set_value("type", localStorage.getItem("system_console_type"));
+			frm.set_value("console", localStorage.getItem("system_console_code"));
+			localStorage.removeItem("system_console_code");
+			localStorage.removeItem("system_console_type");
+		}
 	},
 
 	refresh: 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/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/load.py b/frappe/desk/form/load.py
index 56f39aacfb..2351bb13b5 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
@@ -86,6 +85,8 @@ def get_meta_bundle(doctype):
 
 @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 +114,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),
@@ -351,18 +352,6 @@ def get_assignments(dt, dn):
 	)
 
 
-@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/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..b6068d1a3e 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -243,12 +243,11 @@ def get_filters_for(doctype):
 @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 +266,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 +299,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/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/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/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/document.py b/frappe/model/document.py
index 3343a5dab8..cedbe9ad71 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -399,6 +399,7 @@ class Document(BaseDocument):
 					"attached_to_name": self.name,
 					"attached_to_doctype": self.doctype,
 					"folder": "Home/Attachments",
+					"is_private": attach_item.is_private,
 				}
 			)
 			_file.save()
@@ -1038,7 +1039,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,
@@ -1046,6 +1047,7 @@ class Document(BaseDocument):
 			ignore_permissions=ignore_permissions,
 			flags=self.flags,
 			force=force,
+			delete_permanently=delete_permanently,
 		)
 
 	def run_before_save_methods(self):
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index 3d8845382b..a202cba11f 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -3,6 +3,7 @@
 
 import datetime
 import re
+from collections import defaultdict
 from collections.abc import Callable
 from typing import TYPE_CHECKING, Optional
 
@@ -19,7 +20,8 @@ if TYPE_CHECKING:
 
 # NOTE: This is used to keep track of status of sites
 # whether `log_types` have autoincremented naming set for the site or not.
-autoincremented_site_status_map = {}
+# 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 | #]+\})")
@@ -180,22 +182,16 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool:
 	"""Checks if the doctype has autoincrement autoname set"""
 
 	if doctype in log_types:
-		if autoincremented_site_status_map.get(frappe.local.site) is None:
-			if (
-				frappe.db.sql(
-					f"""select data_type FROM information_schema.columns
-				where column_name = 'name' and table_name = 'tab{doctype}'"""
-				)[0][0]
-				== "bigint"
-			):
-				autoincremented_site_status_map[frappe.local.site] = 1
-				return True
-			else:
-				autoincremented_site_status_map[frappe.local.site] = 0
-
-		elif autoincremented_site_status_map[frappe.local.site]:
-			return True
+		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])
 	else:
 		if not meta:
 			meta = frappe.get_meta(doctype)
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/public/js/frappe-web.bundle.js b/frappe/public/js/frappe-web.bundle.js
index 36064767fb..30cf552c82 100644
--- a/frappe/public/js/frappe-web.bundle.js
+++ b/frappe/public/js/frappe-web.bundle.js
@@ -24,3 +24,4 @@ import "./bootstrap-4-web.bundle";
 
 import "../../website/js/website.js";
 import "./frappe/socketio_client.js";
+import "./frappe/form/controls/control.js";
diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue
index 6d082b640e..e1628c2b09 100644
--- a/frappe/public/js/frappe/file_uploader/FileUploader.vue
+++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue
@@ -70,7 +70,7 @@
 						
{{ __('Google Drive') }}
-
+
{{ upload_notes }}
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..19ced24802 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -369,7 +369,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 +440,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 +449,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 +508,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) { diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index c312af389e..30ba5eaad9 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -41,6 +41,7 @@ frappe.ui.form.Form = class FrappeForm { this.doctype_layout = frappe.get_doc("DocType Layout", doctype_layout_name); this.undo_manager = new UndoManager({ frm: this }); this.setup_meta(doctype); + this.debounced_reload_doc = frappe.utils.debounce(this.reload_doc.bind(this), 1000); this.beforeUnloadListener = (event) => { event.preventDefault(); @@ -543,7 +544,7 @@ frappe.ui.form.Form = class FrappeForm { this.doc.__last_sync_on && new Date() - this.doc.__last_sync_on > this.refresh_if_stale_for * 1000 ) { - this.reload_doc(); + this.debounced_reload_doc(); return true; } } @@ -1132,7 +1133,7 @@ frappe.ui.form.Form = class FrappeForm { "alert-warning" ); } else { - this.reload_doc(); + this.debounced_reload_doc(); } } } diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 4c7eb4cf94..3fba6eb5fa 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -73,6 +73,9 @@ frappe.form.formatters = { } }, Int: function (value, docfield, options) { + if (cstr(docfield.options).trim() === "File Size") { + return frappe.form.formatters.FileSize(value); + } return frappe.form.formatters._right(value == null ? "" : cint(value), options); }, Percent: function (value, docfield, options) { @@ -339,10 +342,11 @@ frappe.form.formatters = { return $("
").text(value).html(); }, FileSize: function (value) { + value = cint(value); if (value > 1048576) { - value = flt(flt(value) / 1048576, 1) + "M"; + return (value / 1048576).toFixed(2) + "M"; } else if (value > 1024) { - value = flt(flt(value) / 1024, 1) + "K"; + return (value / 1024).toFixed(2) + "K"; } return value; }, diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index 4859fa5c02..430ff487e7 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -130,7 +130,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/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index e6c24e304c..4d6f96caca 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -98,6 +98,10 @@ frappe.ui.form.Toolbar = class Toolbar { const docname = this.frm.doc.name; const title_field = this.frm.meta.title_field || ""; const doctype = this.frm.doctype; + let queue; + if (this.frm.__rename_queue) { + queue = this.frm.__rename_queue; + } if (input_name) { const warning = __("This cannot be undone"); @@ -120,6 +124,7 @@ frappe.ui.form.Toolbar = class Toolbar { merge, freeze: true, freeze_message: __("Updating related fields..."), + queue, }) .then((new_docname) => { const reload_form = (input_name) => { diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index e2a7d2091c..e3532fddc8 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -36,8 +36,9 @@ frappe.ui.form.States = class FormStates { ).join(", ") || __("None: End of Workflow").bold(); const document_editable_by = frappe.workflow - .get_document_state(me.frm.doctype, state) - .allow_edit.bold(); + .get_document_state_roles(me.frm.doctype, state) + .map((role) => role.bold()) + .join(", "); $(d.body) .html( diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index a4a12af54e..715f3c7533 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -151,7 +151,7 @@ $.extend(frappe.model, { ) { if (data.modified !== cur_frm.doc.modified && !frappe.ui.form.is_saving) { if (!cur_frm.is_dirty()) { - cur_frm.reload_doc(); + cur_frm.debounced_reload_doc(); } else { doc.__needs_refresh = true; cur_frm.show_conflict_message(); diff --git a/frappe/public/js/frappe/scanner/index.js b/frappe/public/js/frappe/scanner/index.js index 6e641677fd..067e073094 100644 --- a/frappe/public/js/frappe/scanner/index.js +++ b/frappe/public/js/frappe/scanner/index.js @@ -94,6 +94,6 @@ frappe.ui.Scanner = class Scanner { } load_lib() { - return frappe.require("/assets/frappe/node_modules/html5-qrcode/dist/html5-qrcode.min.js"); + return frappe.require("/assets/frappe/node_modules/html5-qrcode/html5-qrcode.min.js"); } }; diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 273c327b8c..a88d7d01f2 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -52,6 +52,7 @@ frappe.ui.Filter = class { "Markdown Editor": ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"], Password: ["Between", "Timespan", ">", "<", ">=", "<=", "in", "not in"], Rating: ["like", "not like", "Between", "in", "not in", "Timespan"], + Float: ["like", "not like", "Between", "in", "not in", "Timespan"], }; } diff --git a/frappe/public/js/frappe/ui/toolbar/search.js b/frappe/public/js/frappe/ui/toolbar/search.js index cfbfa72f7e..3f9edeb1f5 100644 --- a/frappe/public/js/frappe/ui/toolbar/search.js +++ b/frappe/public/js/frappe/ui/toolbar/search.js @@ -51,7 +51,7 @@ frappe.search.SearchDialog = class { no_results_status: () => __("No Results found"), get_results: (keywords, callback) => { let start = 0, - limit = 1000; + limit = 100; let results = frappe.search.utils.get_nav_results(keywords); frappe.search.utils.get_global_results(keywords, start, limit).then( (global_results) => { diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index 2efe2314f5..c7fd865795 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1345,15 +1345,81 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { get_filters_html_for_print() { const filters = this.filter_area.get(); - return filters - .map((f) => { - const [doctype, fieldname, condition, value] = f; - if (condition !== "=") return ""; - const label = frappe.meta.get_label(doctype, fieldname); - const docfield = frappe.meta.get_docfield(doctype, fieldname); - return `
${__(label)}: ${frappe.format(value, docfield)}
`; - }) - .join(""); + return ( + `
${__("Filters:")}
` + + filters + .map((f) => { + const [doctype, fieldname, condition, value] = f; + const docfield = frappe.meta.get_docfield(doctype, fieldname); + const label = `${__(frappe.meta.get_label(doctype, fieldname))}`; + switch (condition) { + case "=": + return __("{0} is equal to {1}", [ + label, + frappe.format(value, docfield), + ]); + case "!=": + return __("{0} is not equal to {1}", [ + __(label), + frappe.format(value, docfield), + ]); + case ">": + return __("{0} is greater than {1}", [ + __(label), + frappe.format(value, docfield), + ]); + case "<": + return __("{0} is less than {1}", [ + __(label), + frappe.format(value, docfield), + ]); + case ">=": + return __("{0} is greater than or equal to {1}", [ + __(label), + frappe.format(value, docfield), + ]); + case "<=": + return __("{0} is less than or equal to {1}", [ + __(label), + frappe.format(value, docfield), + ]); + case "Between": + return __("{0} is between {1} and {2}", [ + __(label), + frappe.format(value[0], docfield), + frappe.format(value[1], docfield), + ]); + case "Timespan": + return __("{0} is within {1}", [__(label), __(value)]); + case "in": + return __("{0} is one of {1}", [ + __(label), + frappe.utils.comma_or( + value.map((v) => frappe.format(v, docfield)) + ), + ]); + case "not in": + return __("{0} is not one of {1}", [ + __(label), + frappe.utils.comma_or( + value.map((v) => frappe.format(v, docfield)) + ), + ]); + case "like": + return __("{0} is like {1}", [__(label), value]); + case "not like": + return __("{0} is not like {1}", [__(label), value]); + case "is": + return value === "set" + ? __("{0} is set", [__(label)]) + : __("{0} is not set", [__(label)]); + default: + return null; + } + }) + .filter(Boolean) + .join("
") + ); } get_columns_totals(data) { diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index f13541e4f7..c940d284fb 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -103,7 +103,7 @@ export default class ChartWidget extends Widget { this.action_area.empty(); this.prepare_chart_actions(); - if (this.chart_doc.timeseries && this.chart_doc.chart_type !== "Custom") { + if (this.chart_doc.timeseries) { this.render_time_series_filters(); } } diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py index 887f102d6f..75eb6922f9 100644 --- a/frappe/rate_limiter.py +++ b/frappe/rate_limiter.py @@ -107,17 +107,14 @@ def rate_limit( :returns: a decorator function that limit the number of requests per endpoint """ - def ratelimit_decorator(fun): - @wraps(fun) + def ratelimit_decorator(fn): + @wraps(fn) def wrapper(*args, **kwargs): # Do not apply rate limits if method is not opted to check - if ( - methods != "ALL" - and frappe.request - and frappe.request.method - and frappe.request.method.upper() not in methods + if not frappe.request or ( + methods != "ALL" and frappe.request.method and frappe.request.method.upper() not in methods ): - return frappe.call(fun, **frappe.form_dict or kwargs) + return fn(*args, **kwargs) _limit = limit() if callable(limit) else limit @@ -147,7 +144,7 @@ def rate_limit( _("You hit the rate limit because of too many requests. Please try after sometime.") ) - return frappe.call(fun, **frappe.form_dict or kwargs) + return fn(*args, **kwargs) return wrapper diff --git a/frappe/recorder.py b/frappe/recorder.py index 402175aa50..6c88b2007f 100644 --- a/frappe/recorder.py +++ b/frappe/recorder.py @@ -45,7 +45,7 @@ def get_current_stack_frames(): current = inspect.currentframe() frames = inspect.getouterframes(current, context=10) for frame, filename, lineno, function, context, index in list(reversed(frames))[:-2]: - if "/apps/" in filename: + if "/apps/" in filename or "" in filename: yield { "filename": TRACEBACK_PATH_PATTERN.sub("", filename), "lineno": lineno, diff --git a/frappe/share.py b/frappe/share.py index c068e063b2..55d235789b 100644 --- a/frappe/share.py +++ b/frappe/share.py @@ -1,6 +1,8 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +from typing import TYPE_CHECKING + import frappe from frappe import _ from frappe.desk.doctype.notification_log.notification_log import ( @@ -11,6 +13,9 @@ from frappe.desk.doctype.notification_log.notification_log import ( from frappe.desk.form.document_follow import follow_document from frappe.utils import cint +if TYPE_CHECKING: + from frappe.model.document import Document + @frappe.whitelist() def add(doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0, notify=0): @@ -122,8 +127,18 @@ def set_docshare_permission(doctype, name, user, permission_to, value=1, everyon @frappe.whitelist() -def get_users(doctype, name): +def get_users(doctype: str, name: str) -> list: """Get list of users with which this document is shared""" + doc = frappe.get_doc(doctype, name) + return _get_users(doc) + + +def _get_users(doc: "Document") -> list: + from frappe.permissions import has_permission + + if not has_permission(doc.doctype, "read", doc, raise_exception=False): + return [] + return frappe.get_all( "DocShare", fields=[ @@ -137,7 +152,7 @@ def get_users(doctype, name): "owner", "creation", ], - filters=dict(share_doctype=doctype, share_name=name), + filters=dict(share_doctype=doc.doctype, share_name=doc.name), ) diff --git a/frappe/templates/discussions/button.html b/frappe/templates/discussions/button.html index 746227aa0b..dfc206b0b9 100644 --- a/frappe/templates/discussions/button.html +++ b/frappe/templates/discussions/button.html @@ -1,6 +1,6 @@ {% if frappe.session.user != "Guest" and (condition is not defined or (condition is defined and condition )) %} - + {{ _(cta_title) }} {% endif %} diff --git a/frappe/templates/discussions/comment_box.html b/frappe/templates/discussions/comment_box.html index 23c1bbecf1..eb27f6623a 100644 --- a/frappe/templates/discussions/comment_box.html +++ b/frappe/templates/discussions/comment_box.html @@ -15,19 +15,16 @@
- +