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 @@
-
+