diff --git a/.github/stale.yml b/.github/stale.yml index 2d776759e4..38b81af4e9 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -1,7 +1,7 @@ # Configuration for probot-stale - https://github.com/probot/stale # Number of days of inactivity before an Issue or Pull Request becomes stale -daysUntilStale: 7 +daysUntilStale: 14 # Number of days of inactivity before a stale Issue or Pull Request is closed. # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index 87cd530538..b3283cde3b 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -157,3 +157,13 @@ jobs: echo "Printing log: $f"; cat $f done + + faux-test: + name: Patch + runs-on: ubuntu-latest + needs: checkrun + if: ${{ needs.checkrun.outputs.build != 'strawberry' }} + + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml index 3f51769b2a..0916313d20 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -147,6 +147,22 @@ jobs: name: coverage-${{ matrix.db }}-${{ matrix.container }} path: /home/runner/frappe-bench/sites/coverage.xml + # This is required because github still doesn't understand knowingly skipped tests + faux-test: + name: Unit Tests + runs-on: ubuntu-latest + needs: checkrun + if: ${{ needs.checkrun.outputs.build != 'strawberry' }} + + strategy: + matrix: + db: ["mariadb", "postgres"] + container: [1, 2] + + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" + coverage: name: Coverage Wrap Up needs: [test, checkrun] diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 6c7c2716ca..6237829cef 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -167,6 +167,18 @@ jobs: if: ${{ always() }} run: cat ~/frappe-bench/bench_start.log || true + faux-test: + runs-on: ubuntu-latest + needs: checkrun + if: ${{ needs.checkrun.outputs.build != 'strawberry' && github.repository_owner == 'frappe' }} + name: UI Tests (Cypress) + strategy: + matrix: + container: [1, 2, 3] + + steps: + - name: Pass skipped tests unconditionally + run: "echo Skipped" coverage: name: Coverage Wrap Up 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/fixtures/sample_attachments/attachment-1.jpg b/cypress/fixtures/sample_attachments/attachment-1.jpg new file mode 100644 index 0000000000..be6e4f0991 Binary files /dev/null and b/cypress/fixtures/sample_attachments/attachment-1.jpg differ diff --git a/cypress/fixtures/sample_attachments/attachment-10.txt b/cypress/fixtures/sample_attachments/attachment-10.txt new file mode 100644 index 0000000000..9a037142aa --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-10.txt @@ -0,0 +1 @@ +10 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-11.txt b/cypress/fixtures/sample_attachments/attachment-11.txt new file mode 100644 index 0000000000..9d607966b7 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-11.txt @@ -0,0 +1 @@ +11 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-2.txt b/cypress/fixtures/sample_attachments/attachment-2.txt new file mode 100644 index 0000000000..d8263ee986 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-2.txt @@ -0,0 +1 @@ +2 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-3.txt b/cypress/fixtures/sample_attachments/attachment-3.txt new file mode 100644 index 0000000000..e440e5c842 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-3.txt @@ -0,0 +1 @@ +3 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-4.txt b/cypress/fixtures/sample_attachments/attachment-4.txt new file mode 100644 index 0000000000..bf0d87ab1b --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-4.txt @@ -0,0 +1 @@ +4 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-5.txt b/cypress/fixtures/sample_attachments/attachment-5.txt new file mode 100644 index 0000000000..7813681f5b --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-5.txt @@ -0,0 +1 @@ +5 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-6.txt b/cypress/fixtures/sample_attachments/attachment-6.txt new file mode 100644 index 0000000000..62f9457511 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-6.txt @@ -0,0 +1 @@ +6 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-7.txt b/cypress/fixtures/sample_attachments/attachment-7.txt new file mode 100644 index 0000000000..c7930257df --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-7.txt @@ -0,0 +1 @@ +7 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-8.txt b/cypress/fixtures/sample_attachments/attachment-8.txt new file mode 100644 index 0000000000..301160a930 --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-8.txt @@ -0,0 +1 @@ +8 \ No newline at end of file diff --git a/cypress/fixtures/sample_attachments/attachment-9.txt b/cypress/fixtures/sample_attachments/attachment-9.txt new file mode 100644 index 0000000000..f11c82a4cb --- /dev/null +++ b/cypress/fixtures/sample_attachments/attachment-9.txt @@ -0,0 +1 @@ +9 \ No newline at end of file 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/cypress/integration/sidebar.js b/cypress/integration/sidebar.js index 91c38ef6ce..320403bcfa 100644 --- a/cypress/integration/sidebar.js +++ b/cypress/integration/sidebar.js @@ -13,6 +13,27 @@ const verify_attachment_visibility = (document, is_private) => { cy.get_open_dialog().findByRole("checkbox", { name: "Private" }).should(assertion); }; +const attach_file = (file, no_of_files = 1) => { + let files = []; + if (file) { + files = [file]; + } else if (no_of_files > 1) { + // attach n files + files = [...Array(no_of_files)].map( + (el, idx) => + "cypress/fixtures/sample_attachments/attachment-" + + (idx + 1) + + (idx == 0 ? ".jpg" : ".txt") + ); + } + + cy.findByRole("button", { name: "Attach File" }).click(); + cy.get_open_dialog().find(".file-upload-area").selectFile(files, { + action: "drag-drop", + }); + cy.get_open_dialog().findByRole("button", { name: "Upload" }).click(); +}; + context("Sidebar", () => { before(() => { cy.visit("/login"); @@ -35,6 +56,36 @@ context("Sidebar", () => { verify_attachment_visibility("blog-post/test-blog-attachment-post", false); }); + it("Verify attachment accessibility UX", () => { + cy.call("frappe.tests.ui_test_helpers.create_todo_with_attachment_limit", { + description: "Sidebar Attachment Access Test ToDo", + }).then((todo) => { + cy.visit(`/app/todo/${todo.message.name}`); + + // explore icon btn should be hidden as there are no attachments + cy.get(".explore-btn").should("be.hidden"); + + attach_file("cypress/fixtures/sample_image.jpg"); + cy.get(".explore-btn").should("be.visible"); + cy.get(".show-all-btn").should("be.hidden"); + + // attach 10 images + attach_file(null, 10); + cy.get(".show-all-btn").should("be.visible"); + + // attach 1 more image to reach attachment limit + attach_file("cypress/fixtures/sample_attachments/attachment-11.txt"); + cy.get(".explore-full-btn").should("be.visible"); + cy.get(".attachments-actions").should("be.hidden"); + cy.get(".explore-btn").should("be.hidden"); + + // test "Show All" button + cy.get(".attachment-row").should("have.length", 10); + cy.get(".show-all-btn").click(); + cy.get(".attachment-row").should("have.length", 12); + }); + }); + it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { cy.call("frappe.tests.ui_test_helpers.create_todo", { description: "Sidebar Attachment ToDo", diff --git a/frappe/__init__.py b/frappe/__init__.py index 9a83443b02..62292c9718 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -32,7 +32,7 @@ from frappe.query_builder import ( patch_query_execute, ) from frappe.utils.caching import request_cache -from frappe.utils.data import cstr, sbool +from frappe.utils.data import cint, cstr, sbool # Local application imports from .exceptions import * @@ -302,19 +302,13 @@ def connect_replica() -> bool: def get_site_config(sites_path: str | None = None, site_path: str | None = None) -> dict[str, Any]: """Returns `site_config.json` combined with `sites/common_site_config.json`. `site_config` is a set of site wide settings like database name, password, email etc.""" - config = {} + config = _dict() sites_path = sites_path or getattr(local, "sites_path", None) site_path = site_path or getattr(local, "site_path", None) if sites_path: - common_site_config = os.path.join(sites_path, "common_site_config.json") - if os.path.exists(common_site_config): - try: - config.update(get_file_json(common_site_config)) - except Exception as error: - click.secho("common_site_config.json is invalid", fg="red") - print(error) + config.update(get_common_site_config(sites_path)) if site_path: site_config = os.path.join(site_path, "site_config.json") @@ -348,7 +342,26 @@ def get_site_config(sites_path: str | None = None, site_path: str | None = None) os.environ.get("FRAPPE_DB_PORT") or config.get("db_port") or db_default_ports(config["db_type"]) ) - return _dict(config) + return config + + +def get_common_site_config(sites_path: str | None = None) -> dict[str, Any]: + """Returns common site config as dictionary. + + This is useful for: + - checking configuration which should only be allowed in common site config + - When no site context is present and fallback is required. + """ + sites_path = sites_path or getattr(local, "sites_path", None) + + common_site_config = os.path.join(sites_path, "common_site_config.json") + if os.path.exists(common_site_config): + try: + return _dict(get_file_json(common_site_config)) + except Exception as error: + click.secho("common_site_config.json is invalid", fg="red") + print(error) + return _dict() def get_conf(site: str | None = None) -> dict[str, Any]: @@ -523,11 +536,7 @@ def clear_messages(): def get_message_log(): - log = [] - for msg_out in local.message_log: - log.append(json.loads(msg_out)) - - return log + return [json.loads(msg_out) for msg_out in local.message_log] def clear_last_message(): @@ -918,11 +927,8 @@ def clear_cache(user: str | None = None, doctype: str | None = None): elif user: frappe.cache_manager.clear_user_cache(user) else: # everything - from frappe import translate - - frappe.cache_manager.clear_user_cache() - frappe.cache_manager.clear_domain_cache() - translate.clear_cache() + # Delete ALL keys associated with this site. + frappe.cache.delete_keys("") reset_metadata_version() local.cache = {} local.new_doc_templates = {} @@ -989,7 +995,9 @@ def has_permission( if throw and not out: # mimics frappe.throw - document_label = f"{_(doc.doctype)} {doc.name}" if doc else _(doctype) + document_label = ( + f"{_(doctype)} {doc if isinstance(doc, str) else doc.name}" if doc else _(doctype) + ) msgprint( _("No permission for {0}").format(document_label), raise_exception=ValidationError, @@ -1418,11 +1426,21 @@ def get_app_path(app_name, *joins): return get_pymodule_path(app_name, *joins) +def get_app_source_path(app_name, *joins): + """Return source path of given app. + + :param app: App name. + :param *joins: Join additional path elements using `os.path.join`.""" + return get_app_path(app_name, "..", *joins) + + def get_site_path(*joins): """Return path of current site. :param *joins: Join additional path elements using `os.path.join`.""" - return os.path.join(local.site_path, *joins) + from os.path import abspath, join + + return abspath(join(local.site_path, *joins)) def get_pymodule_path(modulename, *joins): @@ -1430,14 +1448,17 @@ def get_pymodule_path(modulename, *joins): :param modulename: Python module name. :param *joins: Join additional path elements using `os.path.join`.""" - if not "public" in joins: + from os.path import abspath, dirname, join + + if "public" not in joins: joins = [scrub(part) for part in joins] - return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ""), *joins) + + return abspath(join(dirname(get_module(scrub(modulename)).__file__ or ""), *joins)) def get_module_list(app_name): """Get list of modules for given all via `app/modules.txt`.""" - return get_file_items(os.path.join(os.path.dirname(get_module(app_name).__file__), "modules.txt")) + return get_file_items(get_app_path(app_name, "modules.txt")) def get_all_apps(with_internal_apps=True, sites_path=None): @@ -1984,8 +2005,6 @@ def as_json(obj: dict | list, indent=1, separators=None, ensure_ascii=True) -> s def are_emails_muted(): - from frappe.utils import cint - return flags.mute_emails or cint(conf.get("mute_emails") or 0) or False @@ -2074,53 +2093,46 @@ def attach_print( lang=None, print_letterhead=True, password=None, + letterhead=None, ): + from frappe.translate import print_language from frappe.utils import scrub_urls from frappe.utils.pdf import get_pdf - if not file_name: - file_name = name - file_name = cstr(file_name).replace(" ", "").replace("/", "-") - print_settings = db.get_singles_dict("Print Settings") - _lang = local.lang - - # set lang as specified in print format attachment - if lang: - local.lang = lang - local.flags.ignore_print_permissions = True - - no_letterhead = not print_letterhead - kwargs = dict( print_format=print_format, style=style, doc=doc, - no_letterhead=no_letterhead, + no_letterhead=not print_letterhead, + letterhead=letterhead, password=password, ) - content = "" - if int(print_settings.send_print_as_pdf or 0): - ext = ".pdf" - kwargs["as_pdf"] = True - content = ( - get_pdf(html, options={"password": password} if password else None) - if html - else get_print(doctype, name, **kwargs) - ) - else: - ext = ".html" - content = html or scrub_urls(get_print(doctype, name, **kwargs)).encode("utf-8") + local.flags.ignore_print_permissions = True - out = {"fname": file_name + ext, "fcontent": content} + with print_language(lang or local.lang): + content = "" + if cint(print_settings.send_print_as_pdf): + ext = ".pdf" + kwargs["as_pdf"] = True + content = ( + get_pdf(html, options={"password": password} if password else None) + if html + else get_print(doctype, name, **kwargs) + ) + else: + ext = ".html" + content = html or scrub_urls(get_print(doctype, name, **kwargs)).encode("utf-8") local.flags.ignore_print_permissions = False - # reset lang to original local lang - local.lang = _lang - return out + if not file_name: + file_name = name + file_name = cstr(file_name).replace(" ", "").replace("/", "-") + ext + + return {"fname": file_name, "fcontent": content} def publish_progress(*args, **kwargs): @@ -2256,41 +2268,10 @@ def bold(text): def safe_eval(code, eval_globals=None, eval_locals=None): """A safer `eval`""" - whitelisted_globals = {"int": int, "float": float, "long": int, "round": round} - code = unicodedata.normalize("NFKC", code) - UNSAFE_ATTRIBUTES = { - # Generator Attributes - "gi_frame", - "gi_code", - # Coroutine Attributes - "cr_frame", - "cr_code", - "cr_origin", - # Async Generator Attributes - "ag_code", - "ag_frame", - # Traceback Attributes - "tb_frame", - "tb_next", - # Format Attributes - "format", - "format_map", - } + from frappe.utils.safe_exec import safe_eval - for attribute in UNSAFE_ATTRIBUTES: - if attribute in code: - throw(f'Illegal rule {bold(code)}. Cannot use "{attribute}"') - - if "__" in code: - throw(f'Illegal rule {bold(code)}. Cannot use "__"') - - if not eval_globals: - eval_globals = {} - - eval_globals["__builtins__"] = {} - eval_globals.update(whitelisted_globals) - return eval(code, eval_globals, eval_locals) + return safe_eval(code, eval_globals, eval_locals) def get_website_settings(key): @@ -2419,7 +2400,6 @@ def validate_and_sanitize_search_inputs(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): from frappe.desk.search import sanitize_searchfield - from frappe.utils import cint kwargs.update(dict(zip(fn.__code__.co_varnames, args))) sanitize_searchfield(kwargs["searchfield"]) diff --git a/frappe/app.py b/frappe/app.py index 137165c1e9..ca4907486b 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -401,11 +401,7 @@ def serve( application = ProfilerMiddleware(application, sort_by=("cumtime", "calls")) if not os.environ.get("NO_STATICS"): - application = SharedDataMiddleware( - application, {"/assets": str(os.path.join(sites_path, "assets"))} - ) - - application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(sites_path))}) + application = application_with_statics() application.debug = True application.config = {"SERVER_NAME": "localhost:8000"} @@ -429,6 +425,18 @@ def serve( ) +def application_with_statics(): + global application, _sites_path + + application = SharedDataMiddleware( + application, {"/assets": str(os.path.join(_sites_path, "assets"))} + ) + + application = StaticDataMiddleware(application, {"/files": str(os.path.abspath(_sites_path))}) + + return application + + # Remove references to pattern that are pre-compiled and loaded to global scopes. re.purge() diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index dfff7626ff..24116a0571 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -36,7 +36,7 @@ class AssignmentRule(Document): priority: DF.Int rule: DF.Literal["Round Robin", "Load Balancing", "Based on Field"] unassign_condition: DF.Code | None - users: DF.TableMultiSelect[AssignmentRuleUser] | None + users: DF.TableMultiSelect[AssignmentRuleUser] # end: auto-generated types def validate(self): self.validate_document_types() @@ -141,17 +141,20 @@ class AssignmentRule(Document): def get_user_load_balancing(self): """Assign to the user with least number of open assignments""" - counts = [] - for d in self.users: - counts.append( - dict( - user=d.user, - count=frappe.db.count( - "ToDo", dict(reference_type=self.document_type, allocated_to=d.user, status="Open") + counts = [ + dict( + user=d.user, + count=frappe.db.count( + "ToDo", + dict( + reference_type=self.document_type, + allocated_to=d.user, + status="Open", ), - ) + ), ) - + for d in self.users + ] # sort by dict value sorted_counts = sorted(counts, key=lambda k: k["count"]) diff --git a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py index 2460c40e8a..aadd28fbea 100644 --- a/frappe/automation/doctype/assignment_rule/test_assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/test_assignment_rule.py @@ -274,6 +274,55 @@ class TestAutoAssign(FrappeTestCase): assignment_rule.delete() frappe.db.commit() # undo changes commited by DDL + def test_submittable_assignment(self): + # create a submittable doctype + submittable_doctype = "Assignment Test Submittable" + create_test_doctype(submittable_doctype) + dt = frappe.get_doc("DocType", submittable_doctype) + dt.is_submittable = 1 + dt.save() + + # create a rule for the submittable doctype + assignment_rule = frappe.new_doc("Assignment Rule") + assignment_rule.name = f"For {submittable_doctype}" + assignment_rule.document_type = submittable_doctype + assignment_rule.rule = "Round Robin" + assignment_rule.extend("assignment_days", self.days) + assignment_rule.append("users", {"user": "test@example.com"}) + assignment_rule.assign_condition = "docstatus == 1" + assignment_rule.unassign_condition = "docstatus == 2" + assignment_rule.save() + + # create a submittable doc + doc = frappe.new_doc(submittable_doctype) + doc.save() + doc.submit() + + # check if todo is created + todos = frappe.get_all( + "ToDo", + filters={ + "reference_type": submittable_doctype, + "reference_name": doc.name, + "status": "Open", + "allocated_to": "test@example.com", + }, + ) + self.assertEqual(len(todos), 1) + + # check if todo is closed on cancel + doc.cancel() + todos = frappe.get_all( + "ToDo", + filters={ + "reference_type": submittable_doctype, + "reference_name": doc.name, + "status": "Cancelled", + "allocated_to": "test@example.com", + }, + ) + self.assertEqual(len(todos), 1) + def clear_assignments(): frappe.db.delete("ToDo", {"reference_type": TEST_DOCTYPE}) @@ -335,7 +384,7 @@ def _make_test_record(**kwargs): def create_test_doctype(doctype: str): """Create custom doctype.""" - frappe.db.delete("DocType", doctype) + frappe.delete_doc("DocType", doctype) frappe.get_doc( { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 049ca8422d..dab10dcc0a 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -569,6 +569,7 @@ def update_reference(docname, reference): def generate_message_preview(reference_dt, reference_doc, message=None, subject=None): frappe.has_permission("Auto Repeat", "write", throw=True) doc = frappe.get_doc(reference_dt, reference_doc) + doc.check_permission() subject_preview = _("Please add a subject to your email") msg_preview = frappe.render_template(message, {"doc": doc}) if subject: diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index 969c68fbb8..740b329851 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -242,7 +242,7 @@ class TestAutoRepeat(FrappeTestCase): def make_auto_repeat(**args): args = frappe._dict(args) - doc = frappe.get_doc( + return frappe.get_doc( { "doctype": "Auto Repeat", "reference_doctype": args.reference_doctype or "ToDo", @@ -259,8 +259,6 @@ def make_auto_repeat(**args): } ).insert(ignore_permissions=True) - return doc - def create_submittable_doctype(doctype, submit_perms=1): if frappe.db.exists("DocType", doctype): diff --git a/frappe/build.py b/frappe/build.py index 5a9855ef16..7f111b9a69 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -5,7 +5,7 @@ import re import shutil import subprocess from subprocess import getoutput -from tempfile import mkdtemp, mktemp +from tempfile import mkdtemp from urllib.parse import urlparse import click @@ -183,7 +183,7 @@ def symlink(target, link_name, overwrite=False): # Create link to target with temporary filename while True: - temp_link_name = mktemp(dir=link_dir) + temp_link_name = f"tmp{frappe.generate_hash()}" # os.* functions mimic as closely as possible system functions # The POSIX symlink() returns EEXIST if link_name already exists @@ -253,7 +253,7 @@ def bundle( command += " --save-metafiles" check_node_executable() - frappe_app_path = frappe.get_app_path("frappe", "..") + frappe_app_path = frappe.get_app_source_path("frappe") frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True) @@ -271,7 +271,7 @@ def watch(apps=None): command += " --live-reload" check_node_executable() - frappe_app_path = frappe.get_app_path("frappe", "..") + frappe_app_path = frappe.get_app_source_path("frappe") frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env()) @@ -286,8 +286,7 @@ def check_node_executable(): def get_node_env(): - node_env = {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"} - return node_env + return {"NODE_OPTIONS": f"--max_old_space_size={get_safe_max_old_space_size()}"} def get_safe_max_old_space_size(): diff --git a/frappe/client.py b/frappe/client.py index b09f9168f4..ed853f2307 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -208,11 +208,7 @@ def insert_many(docs=None): if len(docs) > 200: frappe.throw(_("Only 200 inserts allowed in one request")) - out = [] - for doc in docs: - out.append(insert_doc(doc).name) - - return out + return [insert_doc(doc).name for doc in docs] @frappe.whitelist(methods=["POST", "PUT"]) diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index 05c3593175..8a89d07b35 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -52,8 +52,7 @@ def pass_context(f): def get_site(context, raise_err=True): try: - site = context.sites[0] - return site + return context.sites[0] except (IndexError, TypeError): if raise_err: raise frappe.SiteNotSpecifiedError diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 01b3be9590..1a6999d8b0 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -931,7 +931,7 @@ def _drop_site( drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password) archived_sites_path = archived_sites_path or os.path.join( - frappe.get_app_path("frappe"), "..", "..", "..", "archived", "sites" + frappe.utils.get_bench_path(), "archived", "sites" ) archived_sites_path = os.path.realpath(archived_sites_path) diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index bdddad8cf6..f7d0a8f16d 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -254,9 +254,8 @@ def execute(context, method, args=None, kwargs=None, profile=False): try: ret = frappe.get_attr(method)(*args, **kwargs) except Exception: - ret = frappe.safe_eval( - method + "(*args, **kwargs)", eval_globals=globals(), eval_locals=locals() - ) + # eval is safe here because input is from console + ret = eval(method + "(*args, **kwargs)", globals(), locals()) # nosemgrep if profile: import pstats @@ -864,7 +863,7 @@ def run_ui_tests( ): "Run UI tests" site = get_site(context) - app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), "..")) + app_base_path = frappe.get_app_source_path(app) site_url = frappe.utils.get_site_url(site) admin_password = frappe.get_conf(site).admin_password @@ -1076,7 +1075,7 @@ def get_version(output): app_info = frappe._dict() try: - app_info.commit = Repo(frappe.get_app_path(app, "..")).head.object.hexsha[:7] + app_info.commit = Repo(frappe.get_app_source_path(app)).head.object.hexsha[:7] except InvalidGitRepositoryError: app_info.commit = "" diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 74cbcfa6b7..9f700f4e39 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -51,21 +51,17 @@ def get_permission_query_conditions(doctype): return "" elif not links.get("permitted_links"): - conditions = [] - # when everything is not permitted - for df in links.get("not_permitted_links"): - # like ifnull(customer, '')='' and ifnull(supplier, '')='' - conditions.append(f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')=''") + conditions = [ + f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')=''" for df in links.get("not_permitted_links") + ] return "( " + " and ".join(conditions) + " )" else: - conditions = [] - - for df in links.get("permitted_links"): - # like ifnull(customer, '')!='' or ifnull(supplier, '')!='' - conditions.append(f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')!=''") + conditions = [ + f"ifnull(`tab{doctype}`.`{df.fieldname}`, '')!=''" for df in links.get("permitted_links") + ] return "( " + " or ".join(conditions) + " )" diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index a3407e9c63..e78f1b006c 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -217,18 +217,15 @@ def invite_user(contact): @frappe.whitelist() def get_contact_details(contact): contact = frappe.get_doc("Contact", contact) - out = { + return { "contact_person": contact.get("name"), - "contact_display": " ".join( - filter(None, [contact.get("salutation"), contact.get("first_name"), contact.get("last_name")]) - ), + "contact_display": contact.get("full_name"), "contact_email": contact.get("email_id"), "contact_mobile": contact.get("mobile_no"), "contact_phone": contact.get("phone"), "contact_designation": contact.get("designation"), "contact_department": contact.get("department"), } - return out def update_contact(doc, method): diff --git a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py index 9848e81b63..70ac273e57 100644 --- a/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/addresses_and_contacts.py @@ -115,10 +115,7 @@ def get_reference_details(reference_doctype, doctype, reference_list, reference_ fields = ["`tabDynamic Link`.link_name"] + field_map.get(doctype, []) records = frappe.get_list(doctype, filters=filters, fields=fields, as_list=True) - temp_records = list() - - for d in records: - temp_records.append(d[1:]) + temp_records = [d[1:] for d in records] if not reference_list: frappe.throw(_("No records present in {0}").format(reference_doctype)) diff --git a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py index 74c797ca65..bbb1b03e79 100644 --- a/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py +++ b/frappe/contacts/report/addresses_and_contacts/test_addresses_and_contacts.py @@ -27,13 +27,12 @@ def get_custom_linked_doctype(): def get_custom_doc_for_address_and_contacts(): get_custom_linked_doctype() - linked_doc = frappe.get_doc( + return frappe.get_doc( { "doctype": "Test Custom Doctype", "test_field": "Hello", } ).insert() - return linked_doc def create_linked_address(link_list): diff --git a/frappe/core/api/file.py b/frappe/core/api/file.py index e3e6a9de08..1a616c3134 100644 --- a/frappe/core/api/file.py +++ b/frappe/core/api/file.py @@ -1,7 +1,8 @@ import json import frappe -from frappe.core.doctype.file.file import File, setup_folder_path +from frappe.core.doctype.file.file import File +from frappe.core.doctype.file.utils import setup_folder_path from frappe.utils import cint, cstr 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/activity_log/test_activity_log.py b/frappe/core/doctype/activity_log/test_activity_log.py index df3f113a85..32644d9630 100644 --- a/frappe/core/doctype/activity_log/test_activity_log.py +++ b/frappe/core/doctype/activity_log/test_activity_log.py @@ -51,8 +51,7 @@ class TestActivityLog(FrappeTestCase): ) name = names[0] - auth_log = frappe.get_doc("Activity Log", name) - return auth_log + return frappe.get_doc("Activity Log", name) def test_brute_security(self): update_system_settings({"allow_consecutive_login_attempts": 3, "allow_login_after_fail": 5}) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 03557c303a..946f9833e1 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -45,8 +45,6 @@ class Comment(Document): ] content: DF.HTMLEditor | None ip_address: DF.Data | None - link_doctype: DF.Link | None - link_name: DF.DynamicLink | None published: DF.Check reference_doctype: DF.Link | None reference_name: DF.DynamicLink | None 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/communication/communication.py b/frappe/core/doctype/communication/communication.py index 9818b12e99..0b3812486c 100644 --- a/frappe/core/doctype/communication/communication.py +++ b/frappe/core/doctype/communication/communication.py @@ -309,12 +309,14 @@ class Communication(Document, CommunicationEmailMixin): return self._get_emails_list(self.bcc, exclude_displayname=exclude_displayname) def get_attachments(self): - attachments = frappe.get_all( + return frappe.get_all( "File", fields=["name", "file_name", "file_url", "is_private"], - filters={"attached_to_name": self.name, "attached_to_doctype": self.DOCTYPE}, + filters={ + "attached_to_name": self.name, + "attached_to_doctype": self.DOCTYPE, + }, ) - return attachments def notify_change(self, action): frappe.publish_realtime( @@ -551,9 +553,7 @@ def get_emails(email_strings: list[str]) -> list[str]: for email_string in email_strings: if email_string: result = getaddresses([email_string]) - for email in result: - email_addrs.append(email[1]) - + email_addrs.extend(email[1] for email in result) return email_addrs diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 1733b7b716..219568f7a6 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import json +from collections.abc import Iterable from typing import TYPE_CHECKING import frappe @@ -37,7 +38,7 @@ def make( send_email=False, print_html=None, print_format=None, - attachments="[]", + attachments=None, send_me_a_copy=False, cc=None, bcc=None, @@ -60,7 +61,7 @@ def make( :param send_email: Send via email (default **False**). :param print_html: HTML Print format to be sent as attachment. :param print_format: Print Format name of parent document to be sent as attachment. - :param attachments: List of attachments as list of files or JSON string. + :param attachments: List of File names or dicts with keys "fname" and "fcontent" :param send_me_a_copy: Send a copy to the sender (default **False**). :param email_template: Template which is used to compose mail . """ @@ -114,7 +115,7 @@ def _make( send_email=False, print_html=None, print_format=None, - attachments="[]", + attachments=None, send_me_a_copy=False, cc=None, bcc=None, @@ -218,26 +219,41 @@ def set_incoming_outgoing_accounts(doc): doc.db_set("email_account", doc.outgoing_email_account.name) -def add_attachments(name, attachments): - """Add attachments to the given Communication""" +def add_attachments(name: str, attachments: Iterable[str | dict]) -> None: + """Add attachments to the given Communication + + :param name: Communication name + :param attachments: File names or dicts with keys "fname" and "fcontent" + """ # loop through attachments for a in attachments: if isinstance(a, str): - attach = frappe.db.get_value( - "File", {"name": a}, ["file_name", "file_url", "is_private"], as_dict=1 - ) - # save attachments to new doc - _file = frappe.get_doc( - { - "doctype": "File", - "file_url": attach.file_url, - "attached_to_doctype": "Communication", - "attached_to_name": name, - "folder": "Home/Attachments", - "is_private": attach.is_private, - } - ) - _file.save(ignore_permissions=True) + attach = frappe.db.get_value("File", {"name": a}, ["file_url", "is_private"], as_dict=1) + file_args = { + "file_url": attach.file_url, + "is_private": attach.is_private, + } + elif isinstance(a, dict) and "fcontent" in a and "fname" in a: + # dict returned by frappe.attach_print() + file_args = { + "file_name": a["fname"], + "content": a["fcontent"], + "is_private": 1, + } + else: + continue + + file_args.update( + { + "attached_to_doctype": "Communication", + "attached_to_name": name, + "folder": "Home/Attachments", + } + ) + + _file = frappe.new_doc("File") + _file.update(file_args) + _file.save(ignore_permissions=True) @frappe.whitelist(allow_guest=True, methods=("GET",)) diff --git a/frappe/core/doctype/communication/mixins.py b/frappe/core/doctype/communication/mixins.py index 73e94fad09..d6f134127f 100644 --- a/frappe/core/doctype/communication/mixins.py +++ b/frappe/core/doctype/communication/mixins.py @@ -189,9 +189,7 @@ class CommunicationEmailMixin: } final_attachments.append(d) - for a in self.get_attachments() or []: - final_attachments.append({"fid": a["name"]}) - + final_attachments.extend({"fid": a["name"]} for a in self.get_attachments() or []) return final_attachments def get_unsubscribe_message(self): diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index 7f2d36d60a..90b2c624ec 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -5,6 +5,7 @@ from urllib.parse import quote import frappe from frappe.core.doctype.communication.communication import Communication, get_emails +from frappe.core.doctype.communication.email import add_attachments from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.tests.utils import FrappeTestCase @@ -374,6 +375,39 @@ class TestCommunicationEmailMixin(FrappeTestCase): doc.delete() comm.delete() + def test_add_attachments_by_filename(self): + to_list = ["to "] + comm = self.new_communication(recipients=to_list) + + file = frappe.new_doc("File") + file.file_name = "test_add_attachments_by_filename.txt" + file.content = "test_add_attachments_by_filename" + file.insert(ignore_permissions=True) + + add_attachments(comm.name, [file.name]) + + attached_file_name, attached_content_hash = frappe.db.get_value( + "File", + {"attached_to_name": comm.name, "attached_to_doctype": comm.doctype}, + ["file_name", "content_hash"], + ) + self.assertEqual(attached_content_hash, file.content_hash) + self.assertEqual(attached_file_name, file.file_name) + + def test_add_attachments_by_file_content(self): + to_list = ["to "] + comm = self.new_communication(recipients=to_list) + file_name = "test_add_attachments_by_file_content.txt" + file_content = "test_add_attachments_by_file_content" + add_attachments(comm.name, [{"fcontent": file_content, "fname": file_name}]) + attached_file_name = frappe.db.get_value( + "File", + {"attached_to_name": comm.name, "attached_to_doctype": comm.doctype}, + ) + attached_file = frappe.get_doc("File", attached_file_name) + self.assertEqual(attached_file.file_name, file_name) + self.assertEqual(attached_file.get_content(), file_content) + def create_email_account() -> "EmailAccount": frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1") diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index bc67087151..6190034308 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -120,9 +120,10 @@ class DataExporter: self.column_start_end = {} if self.all_doctypes: - self.child_doctypes = [] - for df in frappe.get_meta(self.doctype).get_table_fields(): - self.child_doctypes.append(dict(doctype=df.options, parentfield=df.fieldname)) + self.child_doctypes = [ + dict(doctype=df.options, parentfield=df.fieldname) + for df in frappe.get_meta(self.doctype).get_table_fields() + ] def build_response(self): self.writer = UnicodeWriter() diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 6d6e34d97d..60e7ee7f9f 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -606,8 +606,6 @@ class ImportFile: class Row: - link_values_exist_map = {} - def __init__(self, index, row, doctype, header, import_type): self.index = index self.row_number = index + 1 @@ -640,8 +638,7 @@ class Row: return None columns = self.header.get_columns(col_indexes) - doc = self._parse_doc(doctype, columns, values, parent_doc, table_df) - return doc + return self._parse_doc(doctype, columns, values, parent_doc, table_df) def _parse_doc(self, doctype, columns, values, parent_doc=None, table_df=None): doc = frappe._dict() @@ -749,10 +746,7 @@ class Row: return value def link_exists(self, value, df): - key = df.options + "::" + cstr(value) - if Row.link_values_exist_map.get(key) is None: - Row.link_values_exist_map[key] = frappe.db.exists(df.options, value) - return Row.link_values_exist_map.get(key) + return bool(frappe.db.exists(df.options, value, cache=True)) def parse_value(self, value, col): df = col.df @@ -848,9 +842,6 @@ class Header(Row): class Column: - seen = [] - fields_column_map = {} - def __init__(self, index, header, doctype, column_values, map_to_field=None, seen=None): if seen is None: seen = [] diff --git a/frappe/core/doctype/docfield/docfield.py b/frappe/core/doctype/docfield/docfield.py index 5c65d416a1..c4067646e6 100644 --- a/frappe/core/doctype/docfield/docfield.py +++ b/frappe/core/doctype/docfield/docfield.py @@ -126,14 +126,17 @@ class DocField(Document): if self.fieldtype == "Table MultiSelect": table_doctype = self.options - link_doctype = frappe.db.get_value( + return frappe.db.get_value( "DocField", - {"fieldtype": "Link", "parenttype": "DocType", "parent": table_doctype, "in_list_view": 1}, + { + "fieldtype": "Link", + "parenttype": "DocType", + "parent": table_doctype, + "in_list_view": 1, + }, "options", ) - return link_doctype - def get_select_options(self): if self.fieldtype == "Select": options = self.options or "" diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index d42fa62802..85a79ba42c 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -91,7 +91,8 @@ "column_break_51", "email_append_to", "sender_field", - "subject_field" + "subject_field", + "connections_tab" ], "fields": [ { @@ -231,6 +232,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.istable", "fieldname": "form_settings_section", "fieldtype": "Section Break", "label": "Form Settings" @@ -304,6 +306,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.istable", "fieldname": "view_settings", "fieldtype": "Section Break", "label": "View Settings" @@ -414,7 +417,7 @@ "oldfieldtype": "Check" }, { - "depends_on": "eval:doc.custom===0", + "depends_on": "eval:doc.custom===0 && !doc.istable", "fieldname": "web_view", "fieldtype": "Section Break", "label": "Web View" @@ -482,6 +485,7 @@ { "collapsible": 1, "collapsible_depends_on": "actions", + "depends_on": "eval:!doc.istable", "fieldname": "actions_section", "fieldtype": "Section Break", "label": "Actions" @@ -495,6 +499,7 @@ { "collapsible": 1, "collapsible_depends_on": "links", + "depends_on": "eval:!doc.istable", "fieldname": "links_section", "fieldtype": "Section Break", "label": "Linked Documents" @@ -526,6 +531,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.istable", "fieldname": "email_settings_sb", "fieldtype": "Section Break", "label": "Email Settings" @@ -538,7 +544,6 @@ }, { "default": "0", - "depends_on": "eval:!doc.istable", "fieldname": "is_virtual", "fieldtype": "Check", "label": "Is Virtual" @@ -579,6 +584,7 @@ }, { "collapsible": 1, + "depends_on": "eval:!doc.istable", "fieldname": "document_states_section", "fieldtype": "Section Break", "label": "Document States" @@ -648,6 +654,12 @@ "fieldname": "fields_section", "fieldtype": "Section Break", "label": "Fields" + }, + { + "fieldname": "connections_tab", + "fieldtype": "Tab Break", + "label": "Connections", + "show_dashboard": 1 } ], "icon": "fa fa-bolt", @@ -730,7 +742,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2023-07-12 13:56:26.185637", + "modified": "2023-08-23 15:09:08.789467", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index e6a25f81b4..d428373c05 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, get_table_name, random_string +from frappe.utils import cint, flt, is_a_property, random_string from frappe.website.utils import clear_cache if TYPE_CHECKING: @@ -1542,9 +1542,9 @@ def validate_fields(meta): return doctype = docfield.options - meta = frappe.get_meta(doctype) + child_doctype_meta = frappe.get_meta(doctype) - if not meta.istable: + if not child_doctype_meta.istable: frappe.throw( _("Option {0} for field {1} is not a child table").format( frappe.bold(doctype), frappe.bold(docfield.fieldname) @@ -1552,6 +1552,15 @@ def validate_fields(meta): title=_("Invalid Option"), ) + if not (meta.is_virtual == child_doctype_meta.is_virtual): + error_msg = " should be virtual." if meta.is_virtual else " cannot be virtual." + frappe.throw( + _("Child Table {0} for field {1}" + error_msg).format( + frappe.bold(doctype), frappe.bold(docfield.fieldname) + ), + title=_("Invalid Option"), + ) + def check_max_height(docfield): if getattr(docfield, "max_height", None) and (docfield.max_height[-2:] not in ("px", "em")): frappe.throw(f"Max for {frappe.bold(docfield.fieldname)} height must be in px, em, rem") @@ -1811,13 +1820,6 @@ def make_module_and_roles(doc, perm_fieldname="permissions"): raise -def is_a_property(x) -> bool: - """Get properties (@property, @cached_property) in a controller class""" - from functools import cached_property - - return isinstance(x, (property, cached_property)) - - def check_fieldname_conflicts(docfield): """Checks if fieldname conflicts with methods or properties""" doc = frappe.get_doc({"doctype": docfield.dt}) diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 40c55c594e..54d0e5fb7d 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -567,20 +567,10 @@ class TestDocType(FrappeTestCase): "options": "Test Virtual DocType as Child Table", }, ) + self.assertRaises(frappe.exceptions.ValidationError, parent_doc.insert) + parent_doc.is_virtual = 1 parent_doc.insert(ignore_permissions=True) - - # create entry for parent doctype - parent_doc_entry = frappe.get_doc( - {"doctype": "Test Parent Virtual DocType", "some_fieldname": "Test"} - ) - parent_doc_entry.insert(ignore_permissions=True) - - # update the parent doc (should not abort because of any DB query to a virtual child table, as there is none) - parent_doc_entry.some_fieldname = "Test update" - parent_doc_entry.save(ignore_permissions=True) - - # delete the parent doc (should not abort because of any DB query to a virtual child table, as there is none) - parent_doc_entry.delete() + self.assertFalse(frappe.db.table_exists("Test Parent Virtual DocType")) def test_default_fieldname(self): fields = [ @@ -777,6 +767,7 @@ def new_doctype( unique: bool = False, depends_on: str = "", fields: list[dict] | None = None, + custom: bool = True, **kwargs, ): if not name: @@ -787,7 +778,7 @@ def new_doctype( { "doctype": "DocType", "module": "Core", - "custom": 1, + "custom": custom, "fields": [ { "label": "Some Field", diff --git a/frappe/core/doctype/error_log/error_log.json b/frappe/core/doctype/error_log/error_log.json index 2ee86bd118..a8bb7a57d0 100644 --- a/frappe/core/doctype/error_log/error_log.json +++ b/frappe/core/doctype/error_log/error_log.json @@ -11,7 +11,8 @@ "reference_name", "section_break_5", "method", - "error" + "error", + "trace_id" ], "fields": [ { @@ -57,13 +58,19 @@ { "fieldname": "section_break_5", "fieldtype": "Section Break" + }, + { + "fieldname": "trace_id", + "fieldtype": "Data", + "label": "Trace ID", + "read_only": 1 } ], "icon": "fa fa-warning-sign", "idx": 1, "in_create": 1, "links": [], - "modified": "2022-06-13 06:34:05.158606", + "modified": "2023-08-23 14:20:15.343339", "modified_by": "Administrator", "module": "Core", "name": "Error Log", diff --git a/frappe/core/doctype/error_log/error_log.py b/frappe/core/doctype/error_log/error_log.py index 90f00a1531..09a671638c 100644 --- a/frappe/core/doctype/error_log/error_log.py +++ b/frappe/core/doctype/error_log/error_log.py @@ -21,6 +21,7 @@ class ErrorLog(Document): reference_doctype: DF.Link | None reference_name: DF.Data | None seen: DF.Check + trace_id: DF.Data | None # end: auto-generated types def onload(self): if not self.seen and not frappe.flags.read_only: diff --git a/frappe/core/doctype/file/file.js b/frappe/core/doctype/file/file.js index 052772c54e..35d0e3e51c 100644 --- a/frappe/core/doctype/file/file.js +++ b/frappe/core/doctype/file/file.js @@ -27,8 +27,7 @@ frappe.ui.form.on("File", { preview_file: function (frm) { let $preview = ""; - let file_name = frm.doc.file_name.split("?")[0]; - let file_extension = file_name.split(".").pop()?.toLowerCase(); + let file_extension = frm.doc.file_type.toLowerCase(); if (frappe.utils.is_image_file(frm.doc.file_url)) { $preview = $(`
diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index 01871af5a5..0477d82383 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -8,6 +8,8 @@ "field_order": [ "file_name", "is_private", + "column_break_7jmm", + "file_type", "preview", "preview_html", "section_break_5", @@ -169,13 +171,25 @@ "fieldtype": "Check", "label": "Uploaded To Google Drive", "read_only": 1 + }, + { + "fieldname": "column_break_7jmm", + "fieldtype": "Column Break" + }, + { + "fieldname": "file_type", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "File Type", + "read_only": 1 } ], "force_re_route_to_default_view": 1, "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2023-08-02 09:43:51.178011", + "modified": "2023-08-02 09:43:51.178012", "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..985e5f50ba 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -44,6 +44,7 @@ class File(Document): content_hash: DF.Data | None file_name: DF.Data | None file_size: DF.Int + file_type: DF.Data | None file_url: DF.Code | None folder: DF.Link | None is_attachments_folder: DF.Check @@ -86,6 +87,7 @@ class File(Document): self.set_folder_name() self.set_file_name() self.validate_attachment_limit() + self.set_file_type() if self.is_folder: return @@ -330,6 +332,17 @@ class File(Document): elif not self.is_home_folder: self.folder = "Home" + def set_file_type(self): + if self.is_folder: + return + + file_type = mimetypes.guess_type(self.file_name)[0] + if not file_type: + return + + file_extension = mimetypes.guess_extension(file_type) + self.file_type = file_extension.lstrip(".").upper() if file_extension else None + def validate_file_on_disk(self): """Validates existence file""" full_path = self.get_full_path() @@ -734,6 +747,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/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 9acc9953c6..6b4b08c600 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -23,7 +23,6 @@ class ModuleDef(Document): module_name: DF.Data package: DF.Link | None restrict_to_domain: DF.Link | None - # end: auto-generated types def on_update(self): """If in `developer_mode`, create folder for module and diff --git a/frappe/core/doctype/navbar_settings/navbar_settings.py b/frappe/core/doctype/navbar_settings/navbar_settings.py index 1f670975af..5b0f813d56 100644 --- a/frappe/core/doctype/navbar_settings/navbar_settings.py +++ b/frappe/core/doctype/navbar_settings/navbar_settings.py @@ -53,5 +53,4 @@ def get_app_logo(): def get_navbar_settings(): - navbar_settings = frappe.get_single("Navbar Settings") - return navbar_settings + return frappe.get_single("Navbar Settings") 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/patch_log/patch_log.js b/frappe/core/doctype/patch_log/patch_log.js index 78580a0cb0..171a1d3a0f 100644 --- a/frappe/core/doctype/patch_log/patch_log.js +++ b/frappe/core/doctype/patch_log/patch_log.js @@ -4,9 +4,5 @@ frappe.ui.form.on("Patch Log", { refresh: function (frm) { frm.disable_save(); - - frm.add_custom_button(__("Re-Run Patch"), () => { - frm.call("rerun_patch"); - }); }, }); diff --git a/frappe/core/doctype/patch_log/patch_log.py b/frappe/core/doctype/patch_log/patch_log.py index f5d7caca70..12582c1199 100644 --- a/frappe/core/doctype/patch_log/patch_log.py +++ b/frappe/core/doctype/patch_log/patch_log.py @@ -4,7 +4,6 @@ # License: MIT. See LICENSE import frappe -from frappe import _ from frappe.model.document import Document @@ -21,15 +20,7 @@ class PatchLog(Document): skipped: DF.Check traceback: DF.Code | None # end: auto-generated types - @frappe.whitelist() - def rerun_patch(self): - from frappe.modules.patch_handler import run_single - - if not frappe.conf.developer_mode: - frappe.throw(_("Re-running patch is only allowed in developer mode.")) - - run_single(self.patch, force=True) - frappe.msgprint(_("Successfully re-ran patch: {0}").format(self.patch), alert=True) + pass def before_migrate(): diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index ef8ccce9c1..37023a238d 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -61,13 +61,13 @@ class PreparedReport(Document): self.status = "Queued" def on_trash(self): - # If job is running then send stop signal. - if self.status != "Started": + """Remove pending job from queue, if already running then kill the job.""" + if self.status not in ("Started", "Queued"): return with suppress(Exception): job = frappe.get_doc("RQ Job", self.job_id) - job.stop_job() + job.stop_job() if self.status == "Started" else job.delete() def after_insert(self): enqueue( @@ -168,7 +168,7 @@ def process_filters_for_prepared_report(filters: dict[str, Any] | str) -> str: @frappe.whitelist() def get_reports_in_queued_state(report_name, filters): - reports = frappe.get_all( + return frappe.get_all( "Prepared Report", filters={ "report_name": report_name, @@ -177,7 +177,6 @@ def get_reports_in_queued_state(report_name, filters): "owner": frappe.session.user, }, ) - return reports def get_completed_prepared_report(filters, user, report_name): @@ -211,9 +210,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/page/recorder/__init__.py b/frappe/core/doctype/recorder/__init__.py similarity index 100% rename from frappe/core/page/recorder/__init__.py rename to frappe/core/doctype/recorder/__init__.py diff --git a/frappe/core/doctype/recorder/recorder.js b/frappe/core/doctype/recorder/recorder.js new file mode 100644 index 0000000000..7900c2d494 --- /dev/null +++ b/frappe/core/doctype/recorder/recorder.js @@ -0,0 +1,58 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on("Recorder Query", "form_render", function (frm, cdt, cdn) { + let row = frappe.get_doc(cdt, cdn); + let stack = JSON.parse(row.stack); + render_html_field(stack, "stack_html", __("Stack Trace")); + + let explain_result = JSON.parse(row.explain_result); + render_html_field(explain_result, "sql_explain_html", __("SQL Explain")); + + function render_html_field(parsed_json, fieldname, label) { + let html = + "
"; + if (parsed_json.length == 0) { + html += ""; + } else { + html = create_html_table(parsed_json, html); + } + + let field_wrapper = + frm.fields_dict[row.parentfield].grid.grid_rows_by_docname[cdn].grid_form.fields_dict[ + fieldname + ].wrapper; + $(html).appendTo(field_wrapper); + } + + function create_html_table(table_content, html) { + html += ` + + `; + return html; + } +}); diff --git a/frappe/core/doctype/recorder/recorder.json b/frappe/core/doctype/recorder/recorder.json new file mode 100644 index 0000000000..aa0d782811 --- /dev/null +++ b/frappe/core/doctype/recorder/recorder.json @@ -0,0 +1,126 @@ +{ + "actions": [], + "creation": "2023-08-01 12:06:49.630877", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "path", + "number_of_queries", + "time_in_queries", + "method", + "column_break_qo53", + "cmd", + "time", + "duration", + "section_break_1skt", + "request_headers", + "section_break_sgro", + "form_dict", + "section_break_9jhm", + "sql_queries" + ], + "fields": [ + { + "fieldname": "path", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Path" + }, + { + "fieldname": "cmd", + "fieldtype": "Data", + "in_standard_filter": 1, + "label": "CMD" + }, + { + "fieldname": "duration", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Duration" + }, + { + "fieldname": "time", + "fieldtype": "Datetime", + "in_list_view": 1, + "label": "Time" + }, + { + "fieldname": "number_of_queries", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Number of Queries" + }, + { + "fieldname": "time_in_queries", + "fieldtype": "Float", + "label": "Time in Queries" + }, + { + "fieldname": "column_break_qo53", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_1skt", + "fieldtype": "Section Break" + }, + { + "fieldname": "request_headers", + "fieldtype": "Code", + "label": "Request Headers" + }, + { + "fieldname": "section_break_sgro", + "fieldtype": "Section Break" + }, + { + "fieldname": "form_dict", + "fieldtype": "Code", + "label": "Form Dict" + }, + { + "fieldname": "method", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Method", + "options": "GET\nPOST\nPUT\nDELETE\nPATCH\nHEAD\nOPTIONS" + }, + { + "fieldname": "sql_queries", + "fieldtype": "Table", + "label": "SQL Queries", + "options": "Recorder Query" + }, + { + "fieldname": "section_break_9jhm", + "fieldtype": "Section Break" + } + ], + "hide_toolbar": 1, + "in_create": 1, + "index_web_pages_for_search": 1, + "is_virtual": 1, + "links": [], + "modified": "2023-08-10 12:01:03.456643", + "modified_by": "Administrator", + "module": "Core", + "name": "Recorder", + "owner": "Administrator", + "permissions": [ + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1 + } + ], + "sort_field": "duration", + "sort_order": "DESC", + "states": [], + "title_field": "path" +} \ No newline at end of file diff --git a/frappe/core/doctype/recorder/recorder.py b/frappe/core/doctype/recorder/recorder.py new file mode 100644 index 0000000000..c8ca1cc798 --- /dev/null +++ b/frappe/core/doctype/recorder/recorder.py @@ -0,0 +1,102 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe.recorder import get as get_recorder_data +from frappe.utils import cint, evaluate_filters, make_filter_dict + + +class Recorder(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.core.doctype.recorder_query.recorder_query import RecorderQuery + from frappe.types import DF + + cmd: DF.Data | None + duration: DF.Float + form_dict: DF.Code | None + method: DF.Literal["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"] + number_of_queries: DF.Int + path: DF.Data | None + request_headers: DF.Code | None + sql_queries: DF.Table[RecorderQuery] + time: DF.Datetime | None + time_in_queries: DF.Float + # end: auto-generated types + + def load_from_db(self): + request_data = get_recorder_data(self.name) + if not request_data: + raise frappe.DoesNotExistError + request = serialize_request(request_data) + super(Document, self).__init__(request) + + @staticmethod + def get_list(args): + start = cint(args.get("start")) or 0 + page_length = cint(args.get("page_length")) or 20 + requests = Recorder.get_filtered_requests(args)[start : start + page_length] + + if order_by_statment := args.get("order_by"): + if "." in order_by_statment: + order_by_statment = order_by_statment.split(".")[1] + + if " " in order_by_statment: + sort_key, sort_order = order_by_statment.split(" ", 1) + else: + sort_key = order_by_statment + sort_order = "desc" + + sort_key = sort_key.replace("`", "") + return sorted(requests, key=lambda r: r.get(sort_key) or 0, reverse=bool(sort_order == "desc")) + + return sorted(requests, key=lambda r: r.duration, reverse=1) + + @staticmethod + def get_count(args): + return len(Recorder.get_filtered_requests(args)) + + @staticmethod + def get_filtered_requests(args): + filters = args.get("filters") + requests = [serialize_request(request) for request in get_recorder_data()] + return [req for req in requests if evaluate_filters(req, filters)] + + @staticmethod + def get_stats(args): + pass + + @staticmethod + def delete(self): + pass + + def db_insert(self, *args, **kwargs): + pass + + def db_update(self): + pass + + +def serialize_request(request): + request = frappe._dict(request) + if request.get("calls"): + for i in request.calls: + i["stack"] = frappe.as_json(i["stack"]) + i["explain_result"] = frappe.as_json(i["explain_result"]) + request.update( + name=request.get("uuid"), + number_of_queries=request.get("queries"), + time_in_queries=request.get("time_queries"), + request_headers=frappe.as_json(request.get("headers"), indent=4), + form_dict=frappe.as_json(request.get("form_dict"), indent=4), + sql_queries=request.get("calls"), + modified=request.get("time"), + creation=request.get("time"), + ) + + return request diff --git a/frappe/core/doctype/recorder/recorder_list.js b/frappe/core/doctype/recorder/recorder_list.js new file mode 100644 index 0000000000..a0eadae260 --- /dev/null +++ b/frappe/core/doctype/recorder/recorder_list.js @@ -0,0 +1,110 @@ +frappe.listview_settings["Recorder"] = { + hide_name_column: true, + + onload(listview) { + listview.page.sidebar.remove(); + if (!has_common(frappe.user_roles, ["Administrator", "System Manager"])) return; + + if (listview.list_view_settings) { + listview.list_view_settings.disable_comment_count = true; + } + + listview.page.add_button(__("Clear"), () => { + frappe.call({ + method: "frappe.recorder.delete", + callback: function () { + listview.refresh(); + }, + }); + }); + + listview.page.add_menu_item(__("Import"), () => { + new frappe.ui.FileUploader({ + folder: this.current_folder, + on_success: (file) => { + if (cur_list.data.length > 0) { + // don't replace existing capture + return; + } + frappe.call({ + method: "frappe.recorder.import_data", + args: { + file: file.file_url, + }, + callback: function () { + listview.refresh(); + }, + }); + }, + }); + }); + + listview.page.add_menu_item(__("Export"), () => { + frappe.call({ + method: "frappe.recorder.export_data", + callback: function (r) { + const data = r.message; + const filename = `${data[0]["uuid"]}..${data[data.length - 1]["uuid"]}.json`; + + const el = document.createElement("a"); + el.setAttribute( + "href", + "data:application/json," + encodeURIComponent(JSON.stringify(data)) + ); + el.setAttribute("download", filename); + el.click(); + }, + }); + }); + + setInterval(() => { + if (listview.list_view_settings.disable_auto_refresh) { + return; + } + if (!listview.enabled) return; + + const route = frappe.get_route() || []; + if (route[0] != "List" || "Recorder" != route[1]) { + return; + } + + listview.refresh(); + }, 5000); + }, + + refresh(listview) { + this.fetch_recorder_status(listview).then(() => this.refresh_controls(listview)); + }, + + refresh_controls(listview) { + this.setup_recorder_controls(listview); + this.update_indicators(listview); + }, + + fetch_recorder_status(listview) { + return frappe.xcall("frappe.recorder.status").then((status) => { + listview.enabled = Boolean(status); + }); + }, + + setup_recorder_controls(listview) { + listview.page.set_primary_action(listview.enabled ? __("Stop") : __("Start"), () => { + frappe.call({ + method: listview.enabled ? "frappe.recorder.stop" : "frappe.recorder.start", + callback: function () { + listview.refresh(); + }, + }); + listview.enabled = !listview.enabled; + this.refresh_controls(listview); + }); + }, + + update_indicators(listview) { + if (listview.enabled) { + listview.page.set_indicator(__("Active"), "green"); + } else { + listview.page.set_indicator(__("Inactive"), "red"); + } + }, +}; diff --git a/frappe/core/doctype/recorder/test_recorder.py b/frappe/core/doctype/recorder/test_recorder.py new file mode 100644 index 0000000000..aad47cadf5 --- /dev/null +++ b/frappe/core/doctype/recorder/test_recorder.py @@ -0,0 +1,77 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +import re + +import frappe +import frappe.recorder +from frappe.core.doctype.recorder.recorder import serialize_request +from frappe.recorder import get as get_recorder_data +from frappe.tests.utils import FrappeTestCase +from frappe.utils import set_request + + +class TestRecorder(FrappeTestCase): + def setUp(self): + self.start_recoder() + + def tearDown(self) -> None: + frappe.recorder.stop() + + def start_recoder(self): + frappe.recorder.stop() + frappe.recorder.delete() + set_request(path="/api/method/ping") + frappe.recorder.start() + frappe.recorder.record() + + def stop_recorder(self): + frappe.recorder.dump() + + def test_recorder_list(self): + frappe.get_all("User") # trigger one query + self.stop_recorder() + requests = frappe.get_all("Recorder") + self.assertGreaterEqual(len(requests), 1) + request = frappe.get_doc("Recorder", requests[0].name) + self.assertGreaterEqual(len(request.sql_queries), 1) + queries = [sql_query.query for sql_query in request.sql_queries] + match_flag = 0 + for query in queries: + if bool(re.match("^[select.*from `tabUser`]", query, flags=re.IGNORECASE)): + match_flag = 1 + break + self.assertEqual(match_flag, 1) + + def test_recorder_list_filters(self): + user = frappe.qb.DocType("User") + frappe.qb.from_(user).select("name").run() + self.stop_recorder() + + set_request(path="/api/method/abc") + frappe.recorder.start() + frappe.recorder.record() + frappe.get_all("User") + self.stop_recorder() + + requests = frappe.get_list( + "Recorder", filters={"path": ("like", "/api/method/ping"), "number_of_queries": 1} + ) + self.assertGreaterEqual(len(requests), 1) + requests = frappe.get_list("Recorder", filters={"path": ("like", "/api/method/test")}) + self.assertEqual(len(requests), 0) + + requests = frappe.get_list("Recorder", filters={"method": "GET"}) + self.assertGreaterEqual(len(requests), 1) + requests = frappe.get_list("Recorder", filters={"method": "POST"}) + self.assertEqual(len(requests), 0) + + requests = frappe.get_list("Recorder", order_by="path desc") + self.assertEqual(requests[0].path, "/api/method/ping") + + def test_recorder_serialization(self): + frappe.get_all("User") # trigger one query + self.stop_recorder() + requests = frappe.get_all("Recorder") + request_doc = get_recorder_data(requests[0].name) + self.assertIsInstance(serialize_request(request_doc), dict) diff --git a/frappe/core/doctype/recorder_query/__init__.py b/frappe/core/doctype/recorder_query/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/recorder_query/recorder_query.js b/frappe/core/doctype/recorder_query/recorder_query.js new file mode 100644 index 0000000000..6cfeb48944 --- /dev/null +++ b/frappe/core/doctype/recorder_query/recorder_query.js @@ -0,0 +1,8 @@ +// Copyright (c) 2023, Frappe Technologies and contributors +// For license information, please see license.txt + +// frappe.ui.form.on("Recorder Query", { +// refresh(frm) { + +// }, +// }); diff --git a/frappe/core/doctype/recorder_query/recorder_query.json b/frappe/core/doctype/recorder_query/recorder_query.json new file mode 100644 index 0000000000..4a529adb5b --- /dev/null +++ b/frappe/core/doctype/recorder_query/recorder_query.json @@ -0,0 +1,106 @@ +{ + "actions": [], + "creation": "2023-08-01 17:04:12.173774", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "index", + "query", + "duration", + "column_break_qmju", + "exact_copies", + "normalized_query", + "normalized_copies", + "section_break_dygy", + "stack_html", + "stack", + "section_break_kvkb", + "sql_explain_html", + "explain_result" + ], + "fields": [ + { + "fieldname": "query", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Query", + "length": 2 + }, + { + "fieldname": "normalized_query", + "fieldtype": "Data", + "label": "Normalized Query" + }, + { + "fieldname": "duration", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Duration" + }, + { + "fieldname": "exact_copies", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Exact Copies" + }, + { + "fieldname": "normalized_copies", + "fieldtype": "Int", + "label": "Normalized Copies" + }, + { + "fieldname": "column_break_qmju", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_dygy", + "fieldtype": "Section Break" + }, + { + "fieldname": "stack", + "fieldtype": "Text", + "hidden": 1, + "print_hide": 1 + }, + { + "fieldname": "stack_html", + "fieldtype": "HTML", + "label": "Stack Trace" + }, + { + "fieldname": "section_break_kvkb", + "fieldtype": "Section Break" + }, + { + "fieldname": "explain_result", + "fieldtype": "Text", + "hidden": 1, + "print_hide": 1 + }, + { + "fieldname": "sql_explain_html", + "fieldtype": "HTML", + "label": "SQL Explain" + }, + { + "fieldname": "index", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Index" + } + ], + "index_web_pages_for_search": 1, + "is_virtual": 1, + "istable": 1, + "links": [], + "modified": "2023-08-07 13:12:23.496002", + "modified_by": "Administrator", + "module": "Core", + "name": "Recorder Query", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/frappe/core/doctype/recorder_query/recorder_query.py b/frappe/core/doctype/recorder_query/recorder_query.py new file mode 100644 index 0000000000..185c927dbe --- /dev/null +++ b/frappe/core/doctype/recorder_query/recorder_query.py @@ -0,0 +1,53 @@ +# Copyright (c) 2023, Frappe Technologies and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class RecorderQuery(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + duration: DF.Float + exact_copies: DF.Int + explain_result: DF.Text | None + index: DF.Int + normalized_copies: DF.Int + normalized_query: DF.Data | None + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + query: DF.Data + stack: DF.Text | None + # end: auto-generated types + pass + + def db_insert(self, *args, **kwargs): + pass + + def load_from_db(self): + pass + + def db_update(self): + pass + + @staticmethod + def get_list(args): + pass + + @staticmethod + def get_count(args): + pass + + @staticmethod + def get_stats(args): + pass + + def delete(self): + pass diff --git a/frappe/core/doctype/recorder_query/test_recorder_query.py b/frappe/core/doctype/recorder_query/test_recorder_query.py new file mode 100644 index 0000000000..a21fdcef08 --- /dev/null +++ b/frappe/core/doctype/recorder_query/test_recorder_query.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, Frappe Technologies and Contributors +# See license.txt + +# import frappe +from frappe.tests.utils import FrappeTestCase + + +class TestRecorderQuery(FrappeTestCase): + pass diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 64f125eae2..697aa9a300 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -290,10 +290,11 @@ class Report(Document): columns = params.get("fields") else: columns = [["name", self.ref_doctype]] - for df in frappe.get_meta(self.ref_doctype).fields: - if df.in_list_view: - columns.append([df.fieldname, self.ref_doctype]) - + columns.extend( + [df.fieldname, self.ref_doctype] + for df in frappe.get_meta(self.ref_doctype).fields + if df.in_list_view + ) return columns def get_standard_report_filters(self, params, filters): diff --git a/frappe/core/doctype/report/test_report.py b/frappe/core/doctype/report/test_report.py index 670b6b7410..4f9c229ab8 100644 --- a/frappe/core/doctype/report/test_report.py +++ b/frappe/core/doctype/report/test_report.py @@ -18,6 +18,11 @@ test_dependencies = ["User"] class TestReport(FrappeTestCase): + @classmethod + def setUpClass(cls) -> None: + cls.enable_safe_exec() + return super().setUpClass() + def test_report_builder(self): if frappe.db.exists("Report", "User Activity Report"): frappe.delete_doc("Report", "User Activity Report") diff --git a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py index e8b975f638..ebc2fc9900 100644 --- a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py +++ b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py @@ -95,11 +95,9 @@ class RolePermissionforPageandReport(Document): return {check_for_field: name} def get_roles(self): - roles = [] - for data in self.roles: - if data.role != "All": - roles.append({"role": data.role, "parenttype": "Custom Role"}) - return roles + return [ + {"role": data.role, "parenttype": "Custom Role"} for data in self.roles if data.role != "All" + ] def update_status(self): return frappe.render_template diff --git a/frappe/core/doctype/rq_job/rq_job.js b/frappe/core/doctype/rq_job/rq_job.js index 3f7a1a15b7..0f1f88af9d 100644 --- a/frappe/core/doctype/rq_job/rq_job.js +++ b/frappe/core/doctype/rq_job/rq_job.js @@ -6,20 +6,22 @@ frappe.ui.form.on("RQ Job", { // Nothing in this form is supposed to be editable. frm.disable_form(); frm.dashboard.set_headline_alert( - "This is a virtual doctype and data is cleared periodically." + __("This is a virtual doctype and data is cleared periodically.") ); if (["started", "queued"].includes(frm.doc.status)) { frm.add_custom_button(__("Force Stop job"), () => { frappe.confirm( - "This will terminate the job immediately and might be dangerous, are you sure? ", + __( + "This will terminate the job immediately and might be dangerous, are you sure? " + ), () => { frappe .xcall("frappe.core.doctype.rq_job.rq_job.stop_job", { job_id: frm.doc.name, }) .then((r) => { - frappe.show_alert("Job Stopped Succefully"); + frappe.show_alert(__("Job Stopped Successfully")); frm.reload_doc(); }); } diff --git a/frappe/core/doctype/rq_job/rq_job_list.js b/frappe/core/doctype/rq_job/rq_job_list.js index 7d140d668f..bfdd23377d 100644 --- a/frappe/core/doctype/rq_job/rq_job_list.js +++ b/frappe/core/doctype/rq_job/rq_job_list.js @@ -14,10 +14,6 @@ frappe.listview_settings["RQ Job"] = { __("Actions") ); - if (listview.list_view_settings) { - listview.list_view_settings.disable_sidebar_stats = 1; - } - frappe.xcall("frappe.utils.scheduler.get_scheduler_status").then(({ status }) => { if (status === "active") { listview.page.set_indicator(__("Scheduler: Active"), "green"); diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py index f5d5f89ed4..315a9dfd90 100644 --- a/frappe/core/doctype/rq_job/test_rq_job.py +++ b/frappe/core/doctype/rq_job/test_rq_job.py @@ -9,13 +9,13 @@ from rq.job import Job import frappe from frappe.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs, stop_job +from frappe.installer import update_site_config from frappe.tests.utils import FrappeTestCase, timeout from frappe.utils import cstr, execute_in_shell from frappe.utils.background_jobs import get_job_status, is_job_enqueued class TestRQJob(FrappeTestCase): - BG_JOB = "frappe.core.doctype.rq_job.test_rq_job.test_func" @timeout(seconds=20) @@ -163,6 +163,17 @@ class TestRQJob(FrappeTestCase): LAST_MEASURED_USAGE = 40 self.assertLessEqual(rss, LAST_MEASURED_USAGE * 1.05, msg) + @timeout(20) + def test_clear_failed_jobs(self): + limit = 10 + update_site_config("rq_failed_jobs_limit", limit) + + jobs = [frappe.enqueue(method=self.BG_JOB, queue="short", fail=True) for _ in range(limit * 2)] + self.check_status(jobs[-1], "failed") + self.assertLessEqual( + RQJob.get_count({"filters": [["RQ Job", "status", "=", "failed"]]}), limit * 1.1 + ) + def test_func(fail=False, sleep=0): if fail: diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index ca5b8d721b..6663e6a545 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -21,6 +21,20 @@ frappe.ui.form.on("Server Script", { .then((items) => { frm.set_df_property("script", "autocompletions", items); }); + + frm.trigger("check_safe_exec"); + }, + + check_safe_exec(frm) { + frappe.xcall("frappe.core.doctype.server_script.server_script.enabled").then((enabled) => { + if (enabled === false) { + frm.dashboard.clear_comment(); + let msg = __("Server Scripts feature is not available on this site.") + " "; + msg += __("Please contact your system administrator to enable this feature."); + frm.dashboard.add_comment(msg, "yellow", true); + frm.disable_form(); + } + }); }, setup_help(frm) { @@ -68,7 +82,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/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index f5206872f9..1f288d7981 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -8,7 +8,7 @@ import frappe
 from frappe import _
 from frappe.model.document import Document
 from frappe.rate_limiter import rate_limit
-from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, safe_exec
+from frappe.utils.safe_exec import NamespaceDict, get_safe_globals, is_safe_exec_enabled, safe_exec
 
 
 class ServerScript(Document):
@@ -277,3 +277,9 @@ def execute_api_server_script(script=None, *args, **kwargs):
 	_globals, _locals = safe_exec(script.script)
 
 	return _globals.frappe.flags
+
+
+@frappe.whitelist()
+def enabled() -> bool | None:
+	if frappe.has_permission("Server Script"):
+		return is_safe_exec_enabled()
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index af1352f02b..b83d1edda4 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -97,8 +97,9 @@ class TestServerScript(FrappeTestCase):
 			script_doc = frappe.get_doc(doctype="Server Script")
 			script_doc.update(script)
 			script_doc.insert()
-
+		cls.enable_safe_exec()
 		frappe.db.commit()
+		return super().setUpClass()
 
 	@classmethod
 	def tearDownClass(cls):
@@ -269,13 +270,13 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
 		site = frappe.utils.get_site_url(frappe.local.site)
 		client = FrappeClient(site)
 
-		# Exhaust rate limti
+		# Exhaust rate limit
 		for _ in range(5):
 			client.get_api(script1.api_method)
 
 		self.assertRaises(FrappeException, client.get_api, script1.api_method)
 
-		# Exhaust rate limti
+		# Exhaust rate limit
 		for _ in range(5):
 			client.get_api(script2.api_method)
 
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index baa870deab..e0c0c58fe7 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -56,6 +56,7 @@ class SystemSettings(Document):
 		]
 		float_precision: DF.Literal["", "2", "3", "4", "5", "6", "7", "8", "9"]
 		force_user_to_reset_password: DF.Int
+		force_web_capture_mode_for_uploads: DF.Check
 		hide_footer_in_auto_email_reports: DF.Check
 		language: DF.Link
 		lifespan_qrcode_image: DF.Int
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index b4d69d23d5..1ca0a56ec0 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -367,6 +367,9 @@ class TestUser(FrappeTestCase):
 		set_request(path="/random")
 		frappe.local.cookie_manager = CookieManager()
 		frappe.local.login_manager = LoginManager()
+		# used by rate limiter when calling reset_password
+		frappe.local.request_ip = "127.0.0.69"
+		frappe.db.set_single_value("System Settings", "password_reset_limit", 6)
 
 		frappe.set_user("testpassword@example.com")
 		test_user = frappe.get_doc("User", "testpassword@example.com")
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index b1f8777777..0b6b913234 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -1,8 +1,8 @@
 # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 # License: MIT. See LICENSE
-from collections.abc import Sequence
+
+from collections.abc import Iterable
 from datetime import timedelta
-from typing import Optional
 
 import frappe
 import frappe.defaults
@@ -54,7 +54,7 @@ class User(Document):
 		api_key: DF.Data | None
 		api_secret: DF.Password | None
 		banner_image: DF.AttachImage | None
-		bio: DF.Text | None
+		bio: DF.SmallText | None
 		birth_date: DF.Date | None
 		block_modules: DF.Table[BlockModule]
 		bypass_restrict_ip_check_if_2fa_enabled: DF.Check
@@ -569,10 +569,7 @@ class User(Document):
 		tables = frappe.db.get_tables()
 		for tab in tables:
 			desc = frappe.db.get_table_columns_description(tab)
-			has_fields = []
-			for d in desc:
-				if d.get("name") in ["owner", "modified_by"]:
-					has_fields.append(d.get("name"))
+			has_fields = [d.get("name") for d in desc if d.get("name") in ["owner", "modified_by"]]
 			for field in has_fields:
 				frappe.db.sql(
 					"""UPDATE `%s`
@@ -1010,7 +1007,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
 
 
 @frappe.whitelist(allow_guest=True)
-@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60, methods=["POST"])
+@rate_limit(limit=get_password_reset_limit, seconds=24 * 60 * 60)
 def reset_password(user: str) -> str:
 	if user == "Administrator":
 		return "not allowed"
@@ -1042,7 +1039,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
 	conditions = []
 
 	user_type_condition = "and user_type != 'Website User'"
-	if filters and filters.get("ignore_user_type"):
+	if filters and filters.get("ignore_user_type") and frappe.session.data.user_type == "System User":
 		user_type_condition = ""
 		filters.pop("ignore_user_type")
 
@@ -1090,29 +1087,24 @@ def get_total_users():
 	)
 
 
-def get_system_users(exclude_users=None, limit=None):
-	if not exclude_users:
-		exclude_users = []
-	elif not isinstance(exclude_users, (list, tuple)):
-		exclude_users = [exclude_users]
+def get_system_users(exclude_users: Iterable[str] | str | None = None, limit: int | None = None):
+	_excluded_users = list(STANDARD_USERS)
+	if isinstance(exclude_users, str):
+		_excluded_users.append(exclude_users)
+	elif isinstance(exclude_users, Iterable):
+		_excluded_users.extend(exclude_users)
 
-	limit_cond = ""
-	if limit:
-		limit_cond = f"limit {limit}"
-
-	exclude_users += list(STANDARD_USERS)
-
-	system_users = frappe.db.sql_list(
-		"""select name from `tabUser`
-		where enabled=1 and user_type != 'Website User'
-		and name not in ({}) {}""".format(
-			", ".join(["%s"] * len(exclude_users)), limit_cond
-		),
-		exclude_users,
+	return frappe.get_all(
+		"User",
+		filters={
+			"enabled": 1,
+			"user_type": ("!=", "Website User"),
+			"name": ("not in", _excluded_users),
+		},
+		pluck="name",
+		limit=limit,
 	)
 
-	return system_users
-
 
 def get_active_users():
 	"""Returns No. of system users who logged in, in the last 3 days"""
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index f52116ed75..ea00b604c1 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -144,13 +144,11 @@ def user_permission_exists(user, allow, for_value, applicable_for=None):
 	user_permissions = get_user_permissions(user).get(allow, [])
 	if not user_permissions:
 		return None
-	has_same_user_permission = find(
+	return find(
 		user_permissions,
 		lambda perm: perm["doc"] == for_value and perm.get("applicable_for") == applicable_for,
 	)
 
-	return has_same_user_permission
-
 
 @frappe.whitelist()
 @frappe.validate_and_sanitize_search_inputs
@@ -171,11 +169,7 @@ def get_applicable_for_doctype_list(doctype, txt, searchfield, start, page_len,
 
 	linked_doctypes.sort()
 
-	return_list = []
-	for doctype in linked_doctypes[start:page_len]:
-		return_list.append([doctype])
-
-	return return_list
+	return [[doctype] for doctype in linked_doctypes[start:page_len]]
 
 
 def get_permitted_documents(doctype):
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index f5c86b1ae5..355b390b3f 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -193,9 +193,7 @@ class UserType(Document):
 		doctypes.append("File")
 
 		for doctype in ["select_doctypes", "custom_select_doctypes"]:
-			for dt in self.get(doctype):
-				doctypes.append(dt.document_type)
-
+			doctypes.extend(dt.document_type for dt in self.get(doctype))
 		for perm in frappe.get_all(
 			"Custom DocPerm", filters={"role": self.role, "parent": ["not in", doctypes]}
 		):
diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py
index fd879095c0..e0a7e3f8c2 100644
--- a/frappe/core/page/permission_manager/permission_manager.py
+++ b/frappe/core/page/permission_manager/permission_manager.py
@@ -43,9 +43,7 @@ def get_roles_and_doctypes():
 	restricted_roles = ["Administrator"]
 	if frappe.session.user != "Administrator":
 		custom_user_type_roles = frappe.get_all("User Type", filters={"is_standard": 0}, fields=["role"])
-		for row in custom_user_type_roles:
-			restricted_roles.append(row.role)
-
+		restricted_roles.extend(row.role for row in custom_user_type_roles)
 		restricted_roles.append("All")
 
 	roles = frappe.get_all(
diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js
deleted file mode 100644
index 1f004915fe..0000000000
--- a/frappe/core/page/recorder/recorder.js
+++ /dev/null
@@ -1,28 +0,0 @@
-frappe.pages["recorder"].on_page_load = function (wrapper) {
-	frappe.ui.make_app_page({
-		parent: wrapper,
-		title: __("Recorder"),
-		single_column: true,
-		card_layout: true,
-	});
-
-	frappe.recorder = new Recorder(wrapper);
-	$(wrapper).bind("show", function () {
-		frappe.recorder.show();
-	});
-
-	frappe.require("recorder.bundle.js");
-};
-
-class Recorder {
-	constructor(wrapper) {
-		this.wrapper = $(wrapper);
-		this.container = this.wrapper.find(".layout-main-section");
-		this.container.append($('
')); - } - - show() { - if (!this.route || this.route.name == "RecorderDetail") return; - this.router?.replace({ name: "RecorderDetail" }); - } -} diff --git a/frappe/core/page/recorder/recorder.json b/frappe/core/page/recorder/recorder.json deleted file mode 100644 index 43dfbc0e09..0000000000 --- a/frappe/core/page/recorder/recorder.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "content": null, - "creation": "2019-02-08 08:17:45.392739", - "docstatus": 0, - "doctype": "Page", - "idx": 0, - "modified": "2019-02-08 08:23:04.416426", - "modified_by": "Administrator", - "module": "Core", - "name": "recorder", - "owner": "Administrator", - "page_name": "Recorder", - "roles": [ - { - "role": "Administrator" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0, - "title": "Recorder" -} \ No newline at end of file diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index 4b455e0ab4..25657e17e8 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -56,11 +56,9 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters): single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})] - out = [] - for dt in can_read: - if txt.lower().replace("%", "") in dt.lower() and ( - include_single_doctypes or dt not in single_doctypes - ): - out.append([dt]) - - return out + return [ + [dt] + for dt in can_read + if txt.lower().replace("%", "") in dt.lower() + and (include_single_doctypes or dt not in single_doctypes) + ] diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.py b/frappe/core/report/transaction_log_report/transaction_log_report.py index 51a01ffc57..6928161046 100644 --- a/frappe/core/report/transaction_log_report/transaction_log_report.py +++ b/frappe/core/report/transaction_log_report/transaction_log_report.py @@ -77,7 +77,7 @@ def calculate_chain(transaction_hash, previous_hash): def get_columns(filters=None): - columns = [ + return [ { "label": _("Chain Integrity"), "fieldname": "chain_integrity", @@ -90,9 +90,28 @@ def get_columns(filters=None): "fieldtype": "Data", "width": 150, }, - {"label": _("Reference Name"), "fieldname": "reference_name", "fieldtype": "Data", "width": 150}, - {"label": _("Owner"), "fieldname": "owner", "fieldtype": "Data", "width": 100}, - {"label": _("Modified By"), "fieldname": "modified_by", "fieldtype": "Data", "width": 100}, - {"label": _("Timestamp"), "fieldname": "timestamp", "fieldtype": "Data", "width": 100}, + { + "label": _("Reference Name"), + "fieldname": "reference_name", + "fieldtype": "Data", + "width": 150, + }, + { + "label": _("Owner"), + "fieldname": "owner", + "fieldtype": "Data", + "width": 100, + }, + { + "label": _("Modified By"), + "fieldname": "modified_by", + "fieldtype": "Data", + "width": 100, + }, + { + "label": _("Timestamp"), + "fieldname": "timestamp", + "fieldtype": "Data", + "width": 100, + }, ] - return columns diff --git a/frappe/core/utils.py b/frappe/core/utils.py index b445257b7d..5f388f5458 100644 --- a/frappe/core/utils.py +++ b/frappe/core/utils.py @@ -66,11 +66,7 @@ def find_all(list_of_dict, match_function): red_shapes = find_all(colored_shapes, lambda d: d['color'] == 'red') """ - found = [] - for entry in list_of_dict: - if match_function(entry): - found.append(entry) - return found + return [entry for entry in list_of_dict if match_function(entry)] def ljust_list(_list, length, fill_word=None): diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index b73820a562..7342667668 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -293,7 +293,7 @@ def create_custom_field(doctype, df, ignore_validate=False, is_system_generated= return custom_field -def create_custom_fields(custom_fields, ignore_validate=False, update=True): +def create_custom_fields(custom_fields: dict, ignore_validate=False, update=True): """Add / update multiple custom fields :param custom_fields: example `{'Sales Invoice': [dict(fieldname='test')]}`""" diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 527f53ea71..730d3dfc6c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -600,11 +600,8 @@ class CustomizeForm(Document): ), as_dict=True, ) - links = [] label = df.label - for doc in docs: - links.append(frappe.utils.get_link_to_form(self.doc_type, doc.name)) - links_str = ", ".join(links) + links_str = ", ".join(frappe.utils.get_link_to_form(self.doc_type, doc.name) for doc in docs) if docs: frappe.throw( @@ -710,7 +707,6 @@ doctype_properties = { "naming_rule": "Data", "autoname": "Data", "show_title_field_in_link": "Check", - "translate_link_fields": "Check", "is_calendar_and_gantt": "Check", "default_view": "Select", "force_re_route_to_default_view": "Check", diff --git a/frappe/database/database.py b/frappe/database/database.py index 61cabc0478..3c42b65e9b 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -29,6 +29,7 @@ from frappe.database.utils import ( is_query_type, ) from frappe.exceptions import DoesNotExistError, ImplicitCommitError +from frappe.monitor import get_trace_id from frappe.query_builder.functions import Count from frappe.utils import CallbackManager from frappe.utils import cast as cast_fieldtype @@ -113,6 +114,10 @@ class Database: self.before_rollback = CallbackManager() self.after_rollback = CallbackManager() + self._trace_comment = "" + if trace_id := get_trace_id(): + self._trace_comment = f" /* FRAPPE_TRACE_ID: {trace_id} */" + # self.db_type: str # self.last_query (lazy) attribute of last sql query executed @@ -223,7 +228,9 @@ class Database: values = None elif not isinstance(values, (tuple, dict, list)): values = (values,) + query, values = self._transform_query(query, values) + query += self._trace_comment try: self._cursor.execute(query, values) diff --git a/frappe/database/db_manager.py b/frappe/database/db_manager.py index a1761b5995..bd696d00e3 100644 --- a/frappe/database/db_manager.py +++ b/frappe/database/db_manager.py @@ -78,4 +78,6 @@ class DbManager: source=source, port=frappe.conf.db_port, ) + os.system(command) + frappe.cache.delete_keys("") # Delete all keys associated with this site. diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py index bbdd95d921..63cdca2736 100644 --- a/frappe/database/mariadb/schema.py +++ b/frappe/database/mariadb/schema.py @@ -70,29 +70,26 @@ class MariaDBTable(DBTable): for col in self.columns.values(): col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) - add_column_query = [] - modify_column_query = [] - add_index_query = [] - drop_index_query = [] - - for col in self.add_column: - add_column_query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}") - + add_column_query = [ + f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column + ] columns_to_modify = set(self.change_type + self.set_default) - for col in columns_to_modify: - modify_column_query.append( - f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}" - ) - - for col in self.add_unique: - modify_column_query.append( + modify_column_query = [ + f"MODIFY `{col.fieldname}` {col.get_definition(for_modification=True)}" + for col in columns_to_modify + ] + modify_column_query.extend( + [ f"ADD UNIQUE INDEX IF NOT EXISTS {col.fieldname} (`{col.fieldname}`)" - ) - - for col in self.add_index: - # if index key does not exists - if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False): - add_index_query.append(f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)") + for col in self.add_unique + ] + ) + add_index_query = [ + f"ADD INDEX `{col.fieldname}_index`(`{col.fieldname}`)" + for col in self.add_index + if not frappe.db.get_column_index(self.table_name, col.fieldname, unique=False) + ] + drop_index_query = [] for col in {*self.drop_index, *self.drop_unique}: if col.fieldname == "name": diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py index 5e28c81455..b0a878fa8a 100644 --- a/frappe/database/postgres/schema.py +++ b/frappe/database/postgres/schema.py @@ -76,10 +76,7 @@ class PostgresTable(DBTable): for col in self.columns.values(): col.build_for_alter_table(self.current_columns.get(col.fieldname.lower())) - query = [] - - for col in self.add_column: - query.append(f"ADD COLUMN `{col.fieldname}` {col.get_definition()}") + query = [f"ADD COLUMN `{col.fieldname}` {col.get_definition()}" for col in self.add_column] for col in self.change_type: using_clause = "" @@ -88,7 +85,7 @@ class PostgresTable(DBTable): # involving the old values of the row # read more https://www.postgresql.org/docs/9.1/sql-altertable.html using_clause = f"USING {col.fieldname}::timestamp without time zone" - elif col.fieldtype in ("Check"): + elif col.fieldtype == "Check": using_clause = f"USING {col.fieldname}::smallint" query.append( diff --git a/frappe/database/schema.py b/frappe/database/schema.py index ed7d1d16fc..24eea24fa9 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -58,16 +58,16 @@ class DBTable: return ret def get_index_definitions(self): - ret = [] - for key, col in self.columns.items(): + return [ + "index `" + key + "`(`" + key + "`)" + for key, col in self.columns.items() if ( col.set_index and not col.unique and col.fieldtype in frappe.db.type_map and frappe.db.type_map.get(col.fieldtype)[0] not in ("text", "longtext") - ): - ret.append("index `" + key + "`(`" + key + "`)") - return ret + ) + ] def get_columns_from_docfields(self): """ diff --git a/frappe/database/utils.py b/frappe/database/utils.py index 61dd0016c5..7cdab76dda 100644 --- a/frappe/database/utils.py +++ b/frappe/database/utils.py @@ -39,8 +39,7 @@ def get_doctype_name(table_name: str) -> str: if table_name.startswith(("tab", "`tab", '"tab')): table_name = table_name.replace("tab", "", 1) table_name = table_name.replace("`", "") - table_name = table_name.replace('"', "") - return table_name + return table_name.replace('"', "") class LazyString: diff --git a/frappe/defaults.py b/frappe/defaults.py index 3bcfbec1ce..65b145f338 100644 --- a/frappe/defaults.py +++ b/frappe/defaults.py @@ -71,9 +71,7 @@ def get_user_default_as_list(key, user=None): d = list(filter(None, (not isinstance(d, (list, tuple))) and [d] or d)) # filter default values if not found in user permission - values = [value for value in d if not not_in_user_permission(key, value)] - - return values + return [value for value in d if not not_in_user_permission(key, value)] def is_a_user_permission_key(key): diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 141ac7d013..eac90cee94 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -493,11 +493,15 @@ def get_custom_doctype_list(module): order_by="name", ) - out = [] - for d in doctypes: - out.append({"type": "Link", "link_type": "doctype", "link_to": d.name, "label": _(d.name)}) - - return out + return [ + { + "type": "Link", + "link_type": "doctype", + "link_to": d.name, + "label": _(d.name), + } + for d in doctypes + ] def get_custom_report_list(module): @@ -509,23 +513,20 @@ def get_custom_report_list(module): order_by="name", ) - out = [] - for r in reports: - out.append( - { - "type": "Link", - "link_type": "report", - "doctype": r.ref_doctype, - "dependencies": r.ref_doctype, - "is_query_report": 1 - if r.report_type in ("Query Report", "Script Report", "Custom Report") - else 0, - "label": _(r.name), - "link_to": r.name, - } - ) - - return out + return [ + { + "type": "Link", + "link_type": "report", + "doctype": r.ref_doctype, + "dependencies": r.ref_doctype, + "is_query_report": 1 + if r.report_type in ("Query Report", "Script Report", "Custom Report") + else 0, + "label": _(r.name), + "link_to": r.name, + } + for r in reports + ] def save_new_widget(doc, page, blocks, new_widgets): diff --git a/frappe/desk/doctype/console_log/console_log.js b/frappe/desk/doctype/console_log/console_log.js index 9a980667ac..bb9ab5272f 100644 --- a/frappe/desk/doctype/console_log/console_log.js +++ b/frappe/desk/doctype/console_log/console_log.js @@ -2,6 +2,11 @@ // For license information, please see license.txt frappe.ui.form.on("Console Log", { - // refresh: function(frm) { - // } + refresh: function (frm) { + frm.add_custom_button(__("Re-Run in Console"), () => { + window.localStorage.setItem("system_console_code", frm.doc.script); + window.localStorage.setItem("system_console_type", frm.doc.type); + frappe.set_route("Form", "System Console"); + }); + }, }); diff --git a/frappe/desk/doctype/console_log/console_log.json b/frappe/desk/doctype/console_log/console_log.json index b8ccf8c9b5..7531d97991 100644 --- a/frappe/desk/doctype/console_log/console_log.json +++ b/frappe/desk/doctype/console_log/console_log.json @@ -6,7 +6,8 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "script" + "script", + "type" ], "fields": [ { @@ -15,11 +16,18 @@ "in_list_view": 1, "label": "Script", "read_only": 1 + }, + { + "fieldname": "type", + "fieldtype": "Data", + "hidden": 1, + "label": "Type", + "read_only": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2023-07-05 22:16:02.823955", + "modified": "2023-07-27 22:52:37.239039", "modified_by": "Administrator", "module": "Desk", "name": "Console Log", diff --git a/frappe/desk/doctype/console_log/console_log.py b/frappe/desk/doctype/console_log/console_log.py index 9e243ee19a..bed829c5b8 100644 --- a/frappe/desk/doctype/console_log/console_log.py +++ b/frappe/desk/doctype/console_log/console_log.py @@ -15,5 +15,6 @@ class ConsoleLog(Document): from frappe.types import DF script: DF.Code | None + type: DF.Data | None # end: auto-generated types pass diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.py b/frappe/desk/doctype/custom_html_block/custom_html_block.py index 3ce7966f6a..493b7ee4e4 100644 --- a/frappe/desk/doctype/custom_html_block/custom_html_block.py +++ b/frappe/desk/doctype/custom_html_block/custom_html_block.py @@ -30,7 +30,7 @@ def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filte # return logged in users private blocks and all public blocks customHTMLBlock = DocType("Custom HTML Block") - condition_query = frappe.qb.get_query(customHTMLBlock) + condition_query = frappe.qb.from_(customHTMLBlock) return ( condition_query.select(customHTMLBlock.name).where( diff --git a/frappe/desk/doctype/dashboard/dashboard.py b/frappe/desk/doctype/dashboard/dashboard.py index 4b5e14e1bd..225a8d6435 100644 --- a/frappe/desk/doctype/dashboard/dashboard.py +++ b/frappe/desk/doctype/dashboard/dashboard.py @@ -82,14 +82,10 @@ def get_permission_query_conditions(user): allowed_modules = [ frappe.db.escape(module.get("module_name")) for module in get_modules_from_all_apps_for_user() ] - module_condition = ( - "`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL".format( - allowed_modules=",".join(allowed_modules) - ) + return "`tabDashboard`.`module` in ({allowed_modules}) or `tabDashboard`.`module` is NULL".format( + allowed_modules=",".join(allowed_modules) ) - return module_condition - @frappe.whitelist() def get_permitted_charts(dashboard_name): @@ -109,12 +105,8 @@ def get_permitted_charts(dashboard_name): @frappe.whitelist() def get_permitted_cards(dashboard_name): - permitted_cards = [] dashboard = frappe.get_doc("Dashboard", dashboard_name) - for card in dashboard.cards: - if frappe.has_permission("Number Card", doc=card.card): - permitted_cards.append(card) - return permitted_cards + return [card for card in dashboard.cards if frappe.has_permission("Number Card", doc=card.card)] def get_non_standard_charts_in_dashboard(dashboard): diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js index 6d23be79d7..5d16a6d6d1 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.js +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.js @@ -107,6 +107,8 @@ frappe.ui.form.on("Dashboard Chart", { // set timeseries based on chart type if (["Count", "Average", "Sum"].includes(frm.doc.chart_type)) { frm.set_value("timeseries", 1); + } else if (frm.doc.chart_type == "Custom") { + return; } else { frm.set_value("timeseries", 0); } diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json index a5aa6cc20a..d50f58c9af 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.json +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.json @@ -287,7 +287,7 @@ } ], "links": [], - "modified": "2022-07-27 11:09:09.203236", + "modified": "2023-08-14 16:33:30.172798", "modified_by": "Administrator", "module": "Desk", "name": "Dashboard Chart", @@ -319,7 +319,6 @@ "write": 1 }, { - "create": 1, "email": 1, "export": 1, "print": 1, diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 441dbc8d1a..9fe135d4e1 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -264,11 +264,10 @@ def get_heatmap_chart_config(chart, filters, heatmap_year): ) ) - chart_config = { + return { "labels": [], "dataPoints": data, } - return chart_config def get_group_by_chart_config(chart, filters): @@ -292,12 +291,10 @@ def get_group_by_chart_config(chart, filters): ) if data: - chart_config = { + return { "labels": [item["name"] if item["name"] else "Not Specified" for item in data], "datasets": [{"name": chart.name, "values": [item["count"] for item in data]}], } - - return chart_config else: return None diff --git a/frappe/desk/doctype/notification_log/notification_log.py b/frappe/desk/doctype/notification_log/notification_log.py index 5485f3939a..e459a63ef8 100644 --- a/frappe/desk/doctype/notification_log/notification_log.py +++ b/frappe/desk/doctype/notification_log/notification_log.py @@ -60,8 +60,7 @@ def get_permission_query_conditions(for_user): def get_title(doctype, docname, title_field=None): if not title_field: title_field = frappe.get_meta(doctype).get_title_field() - title = docname if title_field == "name" else frappe.db.get_value(doctype, docname, title_field) - return title + return docname if title_field == "name" else frappe.db.get_value(doctype, docname, title_field) def get_title_html(title): @@ -187,9 +186,12 @@ def mark_all_as_read(): @frappe.whitelist() -def mark_as_read(docname): +def mark_as_read(docname: str): + if frappe.flags.read_only: + return + if docname: - frappe.db.set_value("Notification Log", docname, "read", 1, update_modified=False) + frappe.db.set_value("Notification Log", str(docname), "read", 1, update_modified=False) @frappe.whitelist() diff --git a/frappe/desk/doctype/notification_settings/notification_settings.py b/frappe/desk/doctype/notification_settings/notification_settings.py index bb5d002ed2..faa27a8ba4 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.py +++ b/frappe/desk/doctype/notification_settings/notification_settings.py @@ -26,7 +26,7 @@ class NotificationSettings(Document): enabled: DF.Check energy_points_system_notifications: DF.Check seen: DF.Check - subscribed_documents: DF.TableMultiSelect[NotificationSubscribedDocument] | None + subscribed_documents: DF.TableMultiSelect[NotificationSubscribedDocument] user: DF.Link | None # end: auto-generated types def on_update(self): diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index a3af909c68..9fc12a1e0d 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -201,8 +201,7 @@ def calculate_previous_result(doc, filters): else: previous_date = add_to_date(current_date, years=-1) - number = get_result(doc, filters, previous_date) - return number + return get_result(doc, filters, previous_date) @frappe.whitelist() diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js index dc73f33b67..e04ab59fdb 100644 --- a/frappe/desk/doctype/system_console/system_console.js +++ b/frappe/desk/doctype/system_console/system_console.js @@ -10,7 +10,6 @@ frappe.ui.form.on("System Console", { description: __("Execute Console script"), ignore_inputs: true, }); - frm.set_value("type", "Python"); }, refresh: function (frm) { @@ -22,6 +21,16 @@ frappe.ui.form.on("System Console", { .then(() => frm.trigger("render_sql_output")) .finally(() => $btn.text(__("Execute"))); }); + if ( + window.localStorage.getItem("system_console_code") && + window.localStorage.getItem("system_console_type") + ) { + frm.set_value("type", localStorage.getItem("system_console_type")); + frm.set_value("console", localStorage.getItem("system_console_code")); + frm.set_value("output", ""); + window.localStorage.removeItem("system_console_code"); + window.localStorage.removeItem("system_console_type"); + } }, type: function (frm) { diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index 540936581a..14576d3860 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -40,8 +40,7 @@ class SystemConsole(Document): frappe.db.commit() else: frappe.db.rollback() - - frappe.get_doc(dict(doctype="Console Log", script=self.console)).insert() + frappe.get_doc(dict(doctype="Console Log", script=self.console, type=self.type)).insert() frappe.db.commit() diff --git a/frappe/desk/doctype/system_console/test_system_console.py b/frappe/desk/doctype/system_console/test_system_console.py index ade8704813..08a8b83708 100644 --- a/frappe/desk/doctype/system_console/test_system_console.py +++ b/frappe/desk/doctype/system_console/test_system_console.py @@ -5,6 +5,11 @@ from frappe.tests.utils import FrappeTestCase class TestSystemConsole(FrappeTestCase): + @classmethod + def setUpClass(cls) -> None: + cls.enable_safe_exec() + return super().setUpClass() + def test_system_console(self): system_console = frappe.get_doc("System Console") system_console.console = 'log("hello")' diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index b1d45ca8fc..05adaed926 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -188,16 +188,19 @@ def get_documents_for_tag(tag): """ # remove hastag `#` from tag tag = tag[1:] - results = [] result = frappe.get_list( "Tag Link", filters={"tag": tag}, fields=["document_type", "document_name", "title", "tag"] ) - for res in result: - results.append({"doctype": res.document_type, "name": res.document_name, "content": res.title}) - - return results + return [ + { + "doctype": res.document_type, + "name": res.document_name, + "content": res.title, + } + for res in result + ] @frappe.whitelist() diff --git a/frappe/desk/doctype/todo/todo.py b/frappe/desk/doctype/todo/todo.py index 08fc61c727..e703e30002 100644 --- a/frappe/desk/doctype/todo/todo.py +++ b/frappe/desk/doctype/todo/todo.py @@ -96,7 +96,7 @@ class ToDo(Document): filters={ "reference_type": self.reference_type, "reference_name": self.reference_name, - "status": ("!=", "Cancelled"), + "status": ("not in", ("Cancelled", "Closed")), "allocated_to": ("is", "set"), }, pluck="allocated_to", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 9bcca88590..056f8b0768 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -64,6 +64,13 @@ class Workspace(Document): except Exception: frappe.throw(_("Content data shoud be a list")) + def clear_cache(self): + super().clear_cache() + if self.for_user: + frappe.cache.hdel("bootinfo", self.for_user) + else: + frappe.cache.delete_key("bootinfo") + def on_update(self): if disable_saving_as_public(): return diff --git a/frappe/desk/form/assign_to.py b/frappe/desk/form/assign_to.py index ce8bb444a1..b1b14ee28b 100644 --- a/frappe/desk/form/assign_to.py +++ b/frappe/desk/form/assign_to.py @@ -32,7 +32,7 @@ def get(args=None): filters={ "reference_type": args.get("doctype"), "reference_name": args.get("name"), - "status": ("!=", "Cancelled"), + "status": ("not in", ("Cancelled", "Closed")), }, limit=5, ) @@ -164,6 +164,14 @@ def remove(doctype, name, assign_to): return set_status(doctype, name, "", assign_to, status="Cancelled") +@frappe.whitelist() +def close(doctype: str, name: str, assign_to: str): + if assign_to != frappe.session.user: + frappe.throw(_("Only the assignee can complete this to-do.")) + + return set_status(doctype, name, "", assign_to, status="Closed") + + def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"): """remove from todo""" try: @@ -187,7 +195,7 @@ def set_status(doctype, name, todo=None, assign_to=None, status="Cancelled"): pass # clear assigned_to if field exists - if frappe.get_meta(doctype).get_field("assigned_to") and status == "Cancelled": + if frappe.get_meta(doctype).get_field("assigned_to") and status in ("Cancelled", "Closed"): frappe.db.set_value(doctype, name, "assigned_to", None) return get({"doctype": doctype, "name": name}) @@ -233,11 +241,11 @@ def notify_assignment( if action == "CLOSE": subject = _("Your assignment on {0} {1} has been removed by {2}").format( - frappe.bold(doc_type), get_title_html(title), frappe.bold(user_name) + frappe.bold(_(doc_type)), get_title_html(title), frappe.bold(user_name) ) else: user_name = frappe.bold(user_name) - document_type = frappe.bold(doc_type) + document_type = frappe.bold(_(doc_type)) title = get_title_html(title) subject = _("{0} assigned a new task {1} {2} to you").format(user_name, document_type, title) diff --git a/frappe/desk/form/document_follow.py b/frappe/desk/form/document_follow.py index f12e44fe61..d698c647da 100644 --- a/frappe/desk/form/document_follow.py +++ b/frappe/desk/form/document_follow.py @@ -263,19 +263,17 @@ def get_row_changed(row_changed, time, doctype, doc_name, v): def get_added_row(added, time, doctype, doc_name, v): - items = [] - for d in added: - items.append( - { - "time": v.modified, - "data": {"to": d[0], "time": time}, - "doctype": doctype, - "doc_name": doc_name, - "type": "row added", - "by": v.modified_by, - } - ) - return items + return [ + { + "time": v.modified, + "data": {"to": d[0], "time": time}, + "doctype": doctype, + "doc_name": doc_name, + "type": "row added", + "by": v.modified_by, + } + for d in added + ] def get_field_changed(changed, time, doctype, doc_name, v): diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 1ec604c34d..8f569b5a9e 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -407,10 +407,7 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=None): def get_exempted_doctypes(): """Get list of doctypes exempted from being auto-cancelled""" - auto_cancel_exempt_doctypes = [] - for doctypes in frappe.get_hooks("auto_cancel_exempted_doctypes"): - auto_cancel_exempt_doctypes.append(doctypes) - return auto_cancel_exempt_doctypes + return list(frappe.get_hooks("auto_cancel_exempted_doctypes")) def get_linked_docs(doctype: str, name: str, linkinfo: dict | None = None) -> dict[str, list]: diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index 56f39aacfb..e48157db11 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -7,7 +7,6 @@ from urllib.parse import quote import frappe import frappe.defaults import frappe.desk.form.meta -import frappe.share import frappe.utils from frappe import _, _dict from frappe.desk.form.document_follow import is_document_followed @@ -78,14 +77,18 @@ def getdoctype(doctype, with_parent=False, cached_timestamp=None): def get_meta_bundle(doctype): bundle = [frappe.desk.form.meta.get_meta(doctype)] - for df in bundle[0].fields: - if df.fieldtype in frappe.model.table_fields: - bundle.append(frappe.desk.form.meta.get_meta(df.options)) + bundle.extend( + frappe.desk.form.meta.get_meta(df.options) + for df in bundle[0].fields + if df.fieldtype in frappe.model.table_fields + ) return bundle @frappe.whitelist() def get_docinfo(doc=None, doctype=None, name=None): + from frappe.share import _get_users as get_docshares + if not doc: doc = frappe.get_doc(doctype, name) if not doc.has_permission("read"): @@ -113,7 +116,7 @@ def get_docinfo(doc=None, doctype=None, name=None): "versions": get_versions(doc), "assignments": get_assignments(doc.doctype, doc.name), "permissions": get_doc_permissions(doc), - "shared": frappe.share.get_users(doc.doctype, doc.name), + "shared": get_docshares(doc), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), @@ -320,7 +323,7 @@ def get_communication_data( fields=fields, conditions=conditions ) - communications = frappe.db.sql( + return frappe.db.sql( """ SELECT * FROM (({part1}) UNION ({part2})) AS combined @@ -331,12 +334,15 @@ def get_communication_data( """.format( part1=part1, part2=part2, group_by=(group_by or "") ), - dict(doctype=doctype, name=name, start=frappe.utils.cint(start), limit=limit), + dict( + doctype=doctype, + name=name, + start=frappe.utils.cint(start), + limit=limit, + ), as_dict=as_dict, ) - return communications - def get_assignments(dt, dn): return frappe.get_all( @@ -345,24 +351,12 @@ def get_assignments(dt, dn): filters={ "reference_type": dt, "reference_name": dn, - "status": ("!=", "Cancelled"), + "status": ("not in", ("Cancelled", "Closed")), "allocated_to": ("is", "set"), }, ) -@frappe.whitelist() -def get_badge_info(doctypes, filters): - filters = json.loads(filters) - doctypes = json.loads(doctypes) - filters["docstatus"] = ["!=", 2] - out = {} - for doctype in doctypes: - out[doctype] = frappe.db.get_value(doctype, filters, "count(*)") - - return out - - def run_onload(doc): doc.set("__onload", frappe._dict()) doc.run_method("onload") diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index 6c338dbbbc..d0a6b2501c 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -242,9 +242,7 @@ class FormMeta(Meta): workflow = frappe.get_doc("Workflow", workflow_name) workflow_docs.append(workflow) - for d in workflow.get("states"): - workflow_docs.append(frappe.get_doc("Workflow State", d.state)) - + workflow_docs.extend(frappe.get_doc("Workflow State", d.state) for d in workflow.get("states")) self.set("__workflow_docs", workflow_docs) def load_templates(self): diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json index afd0583cfb..0f936abae0 100644 --- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json +++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json @@ -8,7 +8,7 @@ "include_name_field": 0, "is_standard": 1, "list_name": "", - "modified": "2023-05-24 12:43:43.741781", + "modified": "2023-08-24 11:01:18.688875", "modified_by": "Administrator", "module": "Desk", "name": "Main Workspace Tour", @@ -22,7 +22,7 @@ "steps": [ { "description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.", - "element_selector": "#navbar-search", + "element_selector": "#navbar-search[aria-expanded=\"true\"]", "fieldtype": "0", "has_next_condition": 0, "hide_buttons": 0, diff --git a/frappe/desk/leaderboard.py b/frappe/desk/leaderboard.py index 65d6aaf785..ff41019aa1 100644 --- a/frappe/desk/leaderboard.py +++ b/frappe/desk/leaderboard.py @@ -3,7 +3,7 @@ from frappe.utils import get_fullname def get_leaderboards(): - leaderboards = { + return { "User": { "fields": ["points"], "method": "frappe.desk.leaderboard.get_energy_point_leaderboard", @@ -11,7 +11,6 @@ def get_leaderboards(): "icon": "users", } } - return leaderboards @frappe.whitelist() diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py index a1db82810e..cc32e4ab06 100644 --- a/frappe/desk/listview.py +++ b/frappe/desk/listview.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import frappe +from frappe.model import is_default_field from frappe.query_builder import Order from frappe.query_builder.functions import Count from frappe.query_builder.terms import SubQuery @@ -59,6 +60,9 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d .run(as_dict=True) ) + if not frappe.get_meta(doctype).has_field(field) and not is_default_field(field): + raise ValueError("Field does not belong to doctype") + return frappe.get_list( doctype, filters=current_filters, diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py index 6334b18d1c..3ae0619aab 100644 --- a/frappe/desk/notifications.py +++ b/frappe/desk/notifications.py @@ -235,20 +235,17 @@ def get_filters_for(doctype): """get open filters for doctype""" config = get_notification_config() doctype_config = config.get("for_doctype").get(doctype, {}) - filters = doctype_config if not isinstance(doctype_config, str) else None - - return filters + return None if isinstance(doctype_config, str) else doctype_config @frappe.whitelist() @frappe.read_only() def get_open_count(doctype, name, items=None): - """Get open count for given transactions and filters + """Get count for internal and external links for given transactions :param doctype: Reference DocType :param name: Reference Name - :param transactions: List of transactions (json/dict) - :param filters: optional filters (json/list)""" + :param items: Optional list of transactions (json/dict)""" if frappe.flags.in_migrate or frappe.flags.in_install: return {"count": []} @@ -267,30 +264,26 @@ def get_open_count(doctype, name, items=None): if not isinstance(items, list): items = json.loads(items) - out = [] + out = { + "external_links_found": [], + "internal_links_found": [], + } + for d in items: - if d in links.get("internal_links", {}): - continue - - filters = get_filters_for(d) - fieldname = links.get("non_standard_fieldnames", {}).get(d, links.get("fieldname")) - data = {"name": d} - if filters: - # get the fieldname for the current document - # we only need open documents related to the current document - filters[fieldname] = name - total = len( - frappe.get_all(d, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True) - ) - data["open_count"] = total - - total = len( - frappe.get_all( - d, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True - ) - ) - data["count"] = total - out.append(data) + internal_link_for_doctype = links.get("internal_links", {}).get(d) + if internal_link_for_doctype: + internal_links_data_for_d = get_internal_links(doc, internal_link_for_doctype, d) + if internal_links_data_for_d["count"]: + out["internal_links_found"].append(internal_links_data_for_d) + else: + try: + external_links_data_for_d = get_external_links(d, name, links) + out["external_links_found"].append(external_links_data_for_d) + except Exception as e: + out["external_links_found"].append({"doctype": d, "open_count": 0, "count": 0}) + else: + external_links_data_for_d = get_external_links(d, name, links) + out["external_links_found"].append(external_links_data_for_d) out = { "count": out, @@ -304,6 +297,58 @@ def get_open_count(doctype, name, items=None): return out +def get_internal_links(doc, link, link_doctype): + names = [] + data = {"doctype": link_doctype} + + if isinstance(link, str): + # get internal links in parent document + value = doc.get(link) + if value and value not in names: + names.append(value) + elif isinstance(link, list): + # get internal links in child documents + table_fieldname, link_fieldname = link + for row in doc.get(table_fieldname): + value = row.get(link_fieldname) + if value and value not in names: + names.append(value) + + data["open_count"] = 0 + data["count"] = len(names) + data["names"] = names + + return data + + +def get_external_links(doctype, name, links): + filters = get_filters_for(doctype) + fieldname = links.get("non_standard_fieldnames", {}).get(doctype, links.get("fieldname")) + data = {"doctype": doctype} + + if filters: + # get the fieldname for the current document + # we only need open documents related to the current document + filters[fieldname] = name + total = len( + frappe.get_all( + doctype, fields="name", filters=filters, limit=100, distinct=True, ignore_ifnull=True + ) + ) + data["open_count"] = total + else: + data["open_count"] = 0 + + total = len( + frappe.get_all( + doctype, fields="name", filters={fieldname: name}, limit=100, distinct=True, ignore_ifnull=True + ) + ) + data["count"] = total + + return data + + def notify_mentions(ref_doctype, ref_name, content): if ref_doctype and ref_name and content: mentions = extract_mentions(content) diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py index 9554c7b9b7..ffc7d26317 100644 --- a/frappe/desk/page/backups/backups.py +++ b/frappe/desk/page/backups/backups.py @@ -82,6 +82,8 @@ def delete_downloadable_backups(): def schedule_files_backup(user_email): from frappe.utils.background_jobs import enqueue, get_jobs + frappe.only_for("System Manager") + queued_jobs = get_jobs(site=frappe.local.site, queue="long") method = "frappe.desk.page.backups.backups.backup_files_and_notify_user" diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index 9f689b461e..a832e2cfb0 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -315,10 +315,9 @@ class Leaderboard { }) .join(""); - const html = `
-
${filters}
-
`; - return html; + return `
+
${filters}
+
`; } render_list_result(items) { @@ -330,27 +329,24 @@ class Leaderboard { }) .join(""); - let html = `
-
- ${_html} -
-
`; - - return html; + return `
+
+ ${_html} +
+
`; } render_message() { const display_class = this.message ? "" : "hide"; - let html = `
-
- Empty State -
${this.message}
-
-
`; - return html; + return `
+
+ Empty State +
${this.message}
+
+
`; } get_item_html(item, index) { @@ -367,19 +363,17 @@ class Leaderboard { const name_html = item.formatted_name ? `${item.formatted_name}` : ` ${item.name} `; - const html = `
-
- ${index} -
-
- ${name_html} -
-
- ${value} -
-
`; - - return html; + return `
+
+ ${index} +
+
+ ${name_html} +
+
+ ${value} +
+
`; } get_sidebar_item(item, icon) { diff --git a/frappe/desk/page/setup_wizard/install_fixtures.py b/frappe/desk/page/setup_wizard/install_fixtures.py index 91ea386948..2e16719bd1 100644 --- a/frappe/desk/page/setup_wizard/install_fixtures.py +++ b/frappe/desk/page/setup_wizard/install_fixtures.py @@ -17,7 +17,6 @@ def install(): add_unsubscribe() -@frappe.whitelist() def update_genders(): default_genders = [ "Male", @@ -33,7 +32,6 @@ def update_genders(): frappe.get_doc(record).insert(ignore_permissions=True, ignore_if_duplicate=True) -@frappe.whitelist() def update_salutations(): default_salutations = ["Mr", "Ms", "Mx", "Dr", "Mrs", "Madam", "Miss", "Master", "Prof"] records = [{"doctype": "Salutation", "salutation": d} for d in default_salutations] diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js index 36658fe492..c237624fff 100644 --- a/frappe/desk/page/setup_wizard/setup_wizard.js +++ b/frappe/desk/page/setup_wizard/setup_wizard.js @@ -31,6 +31,9 @@ frappe.setup = { }; frappe.pages["setup-wizard"].on_page_load = function (wrapper) { + if (frappe.boot.setup_complete) { + window.location.href = "/app"; + } let requires = frappe.boot.setup_wizard_requires || []; frappe.require(requires, function () { frappe.call({ @@ -399,9 +402,17 @@ frappe.setup.slides_settings = [ }, { fieldname: "enable_telemetry", - label: __("Allow Sending Usage Data for Improving Applications"), + label: __("Allow sending usage data for improving applications"), fieldtype: "Check", default: 1, + depends_on: "eval:frappe.telemetry.can_enable()", + }, + { + fieldname: "allow_recording_first_session", + label: __("Allow recording my first session to improve user experience"), + fieldtype: "Check", + default: 0, + depends_on: "eval:frappe.telemetry.can_enable()", }, ], diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py index d89a15ee8e..c320e74edc 100755 --- a/frappe/desk/page/setup_wizard/setup_wizard.py +++ b/frappe/desk/page/setup_wizard/setup_wizard.py @@ -6,7 +6,7 @@ import json import frappe from frappe.geo.country_info import get_country_info from frappe.translate import get_messages_for_boot, send_translations, set_default_language -from frappe.utils import cint, strip +from frappe.utils import cint, now, strip from frappe.utils.password import update_password from . import install_fixtures @@ -113,6 +113,9 @@ def run_post_setup_complete(args): disable_future_access() frappe.db.commit() frappe.clear_cache() + # HACK: due to race condition sometimes old doc stays in cache. + # Remove this when we have reliable cache reset for docs + frappe.get_cached_doc("System Settings") and frappe.get_doc("System Settings") def run_setup_success(args): @@ -129,18 +132,20 @@ def get_stages_hooks(args): def get_setup_complete_hooks(args): - stages = [] - for method in frappe.get_hooks("setup_wizard_complete"): - stages.append( - { - "status": "Executing method", - "fail_msg": "Failed to execute method", - "tasks": [ - {"fn": frappe.get_attr(method), "args": args, "fail_msg": "Failed to execute method"} - ], - } - ) - return stages + return [ + { + "status": "Executing method", + "fail_msg": "Failed to execute method", + "tasks": [ + { + "fn": frappe.get_attr(method), + "args": args, + "fail_msg": "Failed to execute method", + } + ], + } + for method in frappe.get_hooks("setup_wizard_complete") + ] def handle_setup_exception(args): @@ -179,6 +184,8 @@ def update_system_settings(args): } ) system_settings.save() + if args.get("allow_recording_first_session"): + frappe.db.set_default("session_recording_start", now()) def update_user_name(args): @@ -202,6 +209,8 @@ def update_user_name(args): "last_name": last_name, } ) + + doc.append_roles(*_get_default_roles()) doc.flags.no_welcome_mail = True doc.insert() frappe.flags.mute_emails = _mute_emails @@ -256,36 +265,29 @@ def parse_args(args): def add_all_roles_to(name): user = frappe.get_doc("User", name) - for role in frappe.db.sql("""select name from tabRole"""): - if role[0] not in [ - "Administrator", - "Guest", - "All", - "Customer", - "Supplier", - "Partner", - "Employee", - ]: - d = user.append("roles") - d.role = role[0] + user.append_roles(*_get_default_roles()) user.save() +def _get_default_roles() -> set[str]: + skip_roles = { + "Administrator", + "Guest", + "All", + "Customer", + "Supplier", + "Partner", + "Employee", + } + return set(frappe.get_all("Role", pluck="name")) - skip_roles + + def disable_future_access(): frappe.db.set_default("desktop:home_page", "workspace") - frappe.db.set_single_value("System Settings", "setup_complete", 1) - # Enable onboarding after install frappe.db.set_single_value("System Settings", "enable_onboarding", 1) - if not frappe.flags.in_test: - # remove all roles and add 'Administrator' to prevent future access - page = frappe.get_doc("Page", "setup-wizard") - page.roles = [] - page.append("roles", {"role": "Administrator"}) - page.flags.do_not_update_json = True - page.flags.ignore_permissions = True - page.save() + frappe.db.set_single_value("System Settings", "setup_complete", 1) @frappe.whitelist() @@ -339,8 +341,7 @@ def prettify_args(args): args[key] = f"Image Attached: '{filename}' of size {size} MB" pretty_args = [] - for key in sorted(args): - pretty_args.append(f"{key} = {args[key]}") + pretty_args.extend(f"{key} = {args[key]}" for key in sorted(args)) return pretty_args diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 3d54520356..20754ea665 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -201,7 +201,7 @@ def run( if sbool(are_default_filters) and report.custom_filters: filters = report.custom_filters - if report.prepared_report and not ignore_prepared_report and not custom_columns: + if report.prepared_report and not sbool(ignore_prepared_report) and not custom_columns: if filters: if isinstance(filters, str): filters = json.loads(filters) @@ -459,14 +459,16 @@ def add_total_row(result, columns, meta=None, is_tree=False, parent_field=None): @frappe.whitelist() -def get_data_for_custom_field(doctype, field): +def get_data_for_custom_field(doctype, field, names=None): if not frappe.has_permission(doctype, "read"): frappe.throw(_("Not Permitted to read {0}").format(doctype), frappe.PermissionError) - value_map = frappe._dict(frappe.get_all(doctype, fields=["name", field], as_list=1)) + filters = {} + if names: + filters.update({"name": ["in", json.loads(names)]}) - return value_map + return frappe._dict(frappe.get_list(doctype, filters=filters, fields=["name", field], as_list=1)) def get_data_for_custom_report(columns): diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index c6252250fb..102a708895 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -261,10 +261,7 @@ def compress(data, args=None): values = [] keys = list(data[0]) for row in data: - new_row = [] - for key in keys: - new_row.append(row.get(key)) - values.append(new_row) + values.append([row.get(key) for key in keys]) # add user info for assignments (avatar) if row.get("_assign", ""): @@ -644,11 +641,7 @@ def scrub_user_tags(tagcount): rdict[tag] += tagdict[t] - rlist = [] - for tag in rdict: - rlist.append([tag, rdict[tag]]) - - return rlist + return [[tag, rdict[tag]] for tag in rdict] # used in building query in queries.py diff --git a/frappe/desk/search.py b/frappe/desk/search.py index b4dd0efb63..fe43b7889f 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -109,7 +109,17 @@ def search_widget( elif not query and doctype in standard_queries: # from standard queries search_widget( - doctype, txt, standard_queries[doctype][0], searchfield, start, page_length, filters + doctype=doctype, + txt=txt, + query=standard_queries[doctype][0], + searchfield=searchfield, + start=start, + page_length=page_length, + filters=filters, + filter_fields=filter_fields, + as_dict=as_dict, + reference_doctype=reference_doctype, + ignore_user_permissions=ignore_user_permissions, ) else: meta = frappe.get_meta(doctype) diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py index 666c726942..5cb9677f3e 100644 --- a/frappe/email/__init__.py +++ b/frappe/email/__init__.py @@ -94,11 +94,9 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter d[0] for d in frappe.db.get_values("DocType", {"issingle": 0, "istable": 0, "hide_toolbar": 0}) ] - out = [] - for dt in com_doctypes: - if txt.lower().replace("%", "") in dt.lower() and dt in can_read: - out.append([dt]) - return out + return [ + [dt] for dt in com_doctypes if txt.lower().replace("%", "") in dt.lower() and dt in can_read + ] def get_cached_contacts(txt): @@ -110,12 +108,11 @@ def get_cached_contacts(txt): if not txt: return contacts - match = [ + return [ d for d in contacts if (d.value and ((d.value and txt in d.value) or (d.description and txt in d.description))) ] - return match def update_contact_cache(contacts): diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.js b/frappe/email/doctype/auto_email_report/auto_email_report.js index 62b562b97d..a6ceb08077 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.js +++ b/frappe/email/doctype/auto_email_report/auto_email_report.js @@ -70,14 +70,18 @@ frappe.ui.form.on("Auto Email Report", { frm.trigger("show_filters"); } }, - show_filters: function (frm) { + show_filters: async function (frm) { var wrapper = $(frm.get_field("filters_display").wrapper); wrapper.empty(); + let reference_report = frappe.query_reports[frm.doc.report]; + if (!reference_report || !reference_report.filters) { + reference_report = await frappe.model.with_doc("Report", frm.doc.report); + } if ( frm.doc.report_type === "Custom Report" || (frm.doc.report_type !== "Report Builder" && - frappe.query_reports[frm.doc.report] && - frappe.query_reports[frm.doc.report].filters) + reference_report && + reference_report.filters) ) { // make a table to show filters var table = $( @@ -99,8 +103,8 @@ frappe.ui.form.on("Auto Email Report", { if ( frm.doc.report_type === "Custom Report" && - frappe.query_reports[frm.doc.reference_report] && - frappe.query_reports[frm.doc.reference_report].filters + reference_report && + reference_report.filters ) { if (frm.doc.filters) { filters = JSON.parse(frm.doc.filters); @@ -115,7 +119,7 @@ frappe.ui.form.on("Auto Email Report", { report_filters = frappe.query_reports[frm.doc.reference_report].filters; } else { filters = JSON.parse(frm.doc.filters || "{}"); - report_filters = frappe.query_reports[frm.doc.report].filters; + report_filters = reference_report.filters; } if (report_filters && report_filters.length > 0) { diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 0a11de21f3..38e627539d 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -115,10 +115,9 @@ class AutoEmailReport(Document): # Check if all Mandatory Report Filters are filled by the User filters = frappe.parse_json(self.filters) if self.filters else {} filter_meta = frappe.parse_json(self.filter_meta) if self.filter_meta else {} - throw_list = [] - for meta in filter_meta: - if meta.get("reqd") and not filters.get(meta["fieldname"]): - throw_list.append(meta["label"]) + throw_list = [ + meta["label"] for meta in filter_meta if meta.get("reqd") and not filters.get(meta["fieldname"]) + ] if throw_list: frappe.throw( title=_("Missing Filters Required"), diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 75463ebc41..bdae8d1d3c 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -417,8 +417,7 @@ class EmailAccount(Document): @classmethod def find_default_incoming(cls): - doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1) - return doc + return cls.find_one_by_filters(enable_incoming=1, default_incoming=1) @classmethod def get_account_details_from_site_config(cls): @@ -628,8 +627,7 @@ class EmailAccount(Document): def get_unreplied_notification_emails(self): """Return list of emails listed""" self.send_notification_to = self.send_notification_to.replace(",", "\n") - out = [e.strip() for e in self.send_notification_to.split("\n") if e.strip()] - return out + return [e.strip() for e in self.send_notification_to.split("\n") if e.strip()] def on_trash(self): """Clear communications where email account is linked""" @@ -736,22 +734,22 @@ def get_append_to( doctype=None, txt=None, searchfield=None, start=None, page_len=None, filters=None ): txt = txt if txt else "" - email_append_to_list = [] - # Set Email Append To DocTypes via DocType filters = {"istable": 0, "issingle": 0, "email_append_to": 1} - for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]): - email_append_to_list.append(dt.name) - + # Set Email Append To DocTypes via DocType + email_append_to_list = [ + dt.name for dt in frappe.get_all("DocType", filters=filters, fields=["name", "email_append_to"]) + ] # Set Email Append To DocTypes set via Customize Form - for dt in frappe.get_list( - "Property Setter", filters={"property": "email_append_to", "value": 1}, fields=["doc_type"] - ): - email_append_to_list.append(dt.doc_type) - - email_append_to = [[d] for d in set(email_append_to_list) if txt in d] - - return email_append_to + email_append_to_list.extend( + dt.doc_type + for dt in frappe.get_list( + "Property Setter", + filters={"property": "email_append_to", "value": 1}, + fields=["doc_type"], + ) + ) + return [[d] for d in set(email_append_to_list) if txt in d] def test_internet(host="8.8.8.8", port=53, timeout=3): @@ -889,8 +887,7 @@ def get_max_email_uid(email_account): if not result: return 1 else: - max_uid = cint(result[0].get("uid", 0)) + 1 - return max_uid + return cint(result[0].get("uid", 0)) + 1 def setup_user_email_inbox( diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index 8c939a5e76..633d5463af 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -517,9 +517,7 @@ def get_assignees(doc): fields=["allocated_to"], ) - recipients = [d.allocated_to for d in assignees] - - return recipients + return [d.allocated_to for d in assignees] def get_emails_from_template(template, context): diff --git a/frappe/email/queue.py b/frappe/email/queue.py index cae5f76b3d..b481fd21cd 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -88,11 +88,6 @@ def get_unsubcribed_url( if unsubscribe_params: params.update(unsubscribe_params) - query_string = get_signed_params(params) - - # for test - frappe.local.flags.signed_query_string = query_string - return get_url(unsubscribe_method + "?" + get_signed_params(params)) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index 5ddd71a4f6..6af6c3cebe 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -18,7 +18,8 @@ from email_reply_parser import EmailReplyParser import frappe from frappe import _, safe_decode, safe_encode -from frappe.core.doctype.file import MaxFileSizeReachedError, get_random_filename +from frappe.core.doctype.file.exceptions import MaxFileSizeReachedError +from frappe.core.doctype.file.utils import get_random_filename from frappe.email.oauth import Oauth from frappe.utils import ( add_days, diff --git a/frappe/exceptions.py b/frappe/exceptions.py index 8dbd778a7d..f4bcb661f1 100644 --- a/frappe/exceptions.py +++ b/frappe/exceptions.py @@ -252,6 +252,10 @@ class SessionBootFailed(ValidationError): http_status_code = 500 +class PrintFormatError(ValidationError): + pass + + class TooManyWritesError(Exception): pass diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py index d89e2a16cd..662e058d68 100644 --- a/frappe/geo/utils.py +++ b/frappe/geo/utils.py @@ -15,8 +15,7 @@ def get_coords(doctype, filters, type): elif type == "coordinates": coords = return_coordinates(doctype, filters_sql) - out = convert_to_geojson(type, coords) - return out + return convert_to_geojson(type, coords) def convert_to_geojson(type, coords): diff --git a/frappe/handler.py b/frappe/handler.py index f4b03271eb..275c9867a9 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -10,7 +10,7 @@ from werkzeug.wrappers import Response import frappe import frappe.sessions import frappe.utils -from frappe import _, is_whitelisted +from frappe import _, is_whitelisted, ping from frappe.core.doctype.server_script.server_script_utils import get_server_script_map from frappe.monitor import add_data_to_monitor from frappe.utils import cint @@ -260,11 +260,6 @@ def get_attr(cmd): return method -@frappe.whitelist(allow_guest=True) -def ping(): - return "pong" - - def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): """run a whitelisted controller method""" from inspect import signature diff --git a/frappe/hooks.py b/frappe/hooks.py index bb464b193b..d4f0ad9980 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -146,8 +146,8 @@ doc_events = { "on_update": [ "frappe.desk.notifications.clear_doctype_notifications", "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", - "frappe.automation.doctype.assignment_rule.assignment_rule.apply", "frappe.core.doctype.file.utils.attach_files_to_document", + "frappe.automation.doctype.assignment_rule.assignment_rule.apply", "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date", "frappe.core.doctype.user_type.user_type.apply_permissions_for_non_standard_user_type", ], @@ -155,13 +155,16 @@ doc_events = { "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "frappe.automation.doctype.assignment_rule.assignment_rule.apply", ], "on_trash": [ "frappe.desk.notifications.clear_doctype_notifications", "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", ], "on_update_after_submit": [ - "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" + "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "frappe.automation.doctype.assignment_rule.assignment_rule.apply", + "frappe.automation.doctype.assignment_rule.assignment_rule.update_due_date", ], "on_change": [ "frappe.social.doctype.energy_point_rule.energy_point_rule.process_energy_points", @@ -423,6 +426,7 @@ before_job = [ after_job = [ "frappe.monitor.stop", "frappe.utils.file_lock.release_document_locks", + "frappe.utils.telemetry.flush", ] extend_bootinfo = [ diff --git a/frappe/installer.py b/frappe/installer.py index a8646f480b..f2ce450187 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -265,7 +265,7 @@ def install_app(name, verbose=False, set_as_patched=True, force=False): if app_hooks.required_apps: for app in app_hooks.required_apps: required_app = parse_app_name(app) - install_app(required_app, verbose=verbose, force=force) + install_app(required_app, verbose=verbose) frappe.flags.in_install = name frappe.clear_cache() diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 21920e8235..216b7defec 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -310,10 +310,7 @@ def get_dropbox_settings(redirect_uri=False): def delete_older_backups(dropbox_client, folder_path, to_keep): res = dropbox_client.files_list_folder(path=folder_path) - files = [] - for f in res.entries: - if isinstance(f, dropbox.files.FileMetadata) and "sql" in f.name: - files.append(f) + files = [f for f in res.entries if isinstance(f, dropbox.files.FileMetadata) and "sql" in f.name] if len(files) <= to_keep: return diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 14a5cb5e7c..695ae7db15 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -299,9 +299,7 @@ def sync_events_from_google_calendar(g_calendar, method=None): else: frappe.throw(msg) - for event in events.get("items", []): - results.append(event) - + results.extend(event for event in events.get("items", [])) if not events.get("nextPageToken"): if events.get("nextSyncToken"): account.next_sync_token = events.get("nextSyncToken") diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 4edaaf6a8d..cee04a92a1 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -140,9 +140,7 @@ def sync_contacts_from_google_contacts(g_contact): ).format(account.name, err.resp.status) ) - for contact in contacts.get("connections", []): - results.append(contact) - + results.extend(contact for contact in contacts.get("connections", [])) if not contacts.get("nextPageToken"): if contacts.get("nextSyncToken"): frappe.db.set_value( diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py index 84ea3cc8ab..93d970d95d 100644 --- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py +++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py @@ -300,10 +300,7 @@ class LDAPSettings(Document): ) # Build search query if len(conn.entries) >= 1: - fetch_ldap_groups = [] - for group in conn.entries: - fetch_ldap_groups.append(group["cn"].value) - + fetch_ldap_groups = [group["cn"].value for group in conn.entries] return fetch_ldap_groups def authenticate(self, username: str, password: str): diff --git a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py index 0958786cbb..63eadd7f4a 100644 --- a/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py +++ b/frappe/integrations/doctype/oauth_provider_settings/oauth_provider_settings.py @@ -22,12 +22,10 @@ class OAuthProviderSettings(Document): def get_oauth_settings(): """Returns oauth settings""" - out = frappe._dict( + return frappe._dict( { "skip_authorization": frappe.db.get_single_value( "OAuth Provider Settings", "skip_authorization" ) } ) - - return out diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index c8eedf23da..90003da3f3 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -57,8 +57,7 @@ def make_social_login_key(**kwargs): kwargs["doctype"] = "Social Login Key" if not "provider_name" in kwargs: kwargs["provider_name"] = "Test OAuth2 Provider" - doc = frappe.get_doc(kwargs) - return doc + return frappe.get_doc(kwargs) def create_or_update_social_login_key(): diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py index 4c566352f5..da72335413 100644 --- a/frappe/integrations/doctype/token_cache/token_cache.py +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -34,9 +34,7 @@ class TokenCache(Document): # end: auto-generated types def get_auth_header(self): if self.access_token: - headers = {"Authorization": "Bearer " + self.get_password("access_token")} - return headers - + return {"Authorization": "Bearer " + self.get_password("access_token")} raise frappe.exceptions.DoesNotExistError def update_data(self, data): diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index b724f18f7e..3ecfe6cd61 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -104,10 +104,7 @@ class Webhook(Document): def validate_repeating_fields(self): """Error when Same Field is entered multiple times in webhook_data""" - webhook_data = [] - for entry in self.webhook_data: - webhook_data.append(entry.fieldname) - + webhook_data = [entry.fieldname for entry in self.webhook_data] if len(webhook_data) != len(set(webhook_data)): frappe.throw(_("Same Field is entered more than once")) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index ff6ad36c42..32e46c83c2 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -225,3 +225,7 @@ def get_permitted_fields( return meta_fields + permitted_fields + optional_meta_fields return [] + + +def is_default_field(fieldname: str) -> bool: + return fieldname in default_fields diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 609bfa4b9e..59cdea8031 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -2,6 +2,7 @@ # License: MIT. See LICENSE import datetime import json +from typing import TYPE_CHECKING, TypeVar import frappe from frappe import _, _dict @@ -18,9 +19,25 @@ from frappe.model.docstatus import DocStatus from frappe.model.naming import set_new_name from frappe.model.utils.link_count import notify_link_count from frappe.modules import load_doctype_module -from frappe.utils import cast_fieldtype, cint, compare, cstr, flt, now, sanitize_html, strip_html +from frappe.utils import ( + cast_fieldtype, + cint, + compare, + cstr, + flt, + is_a_property, + now, + sanitize_html, + strip_html, +) from frappe.utils.html_utils import unescape_html +if TYPE_CHECKING: + from frappe.model.document import Document + +D = TypeVar("D", bound="Document") + + max_positive_value = {"smallint": 2**15 - 1, "int": 2**31 - 1, "bigint": 2**63 - 1} DOCTYPE_TABLE_FIELDS = [ @@ -91,19 +108,22 @@ def import_controller(doctype): class BaseDocument: - _reserved_keywords = { - "doctype", - "meta", - "_meta", - "flags", - "parent_doc", - "_table_fields", - "_valid_columns", - "_doc_before_save", - "_table_fieldnames", - "_reserved_keywords", - "dont_update_if_missing", - } + _reserved_keywords = frozenset( + ( + "doctype", + "meta", + "_meta", + "flags", + "parent_doc", + "_table_fields", + "_valid_columns", + "_doc_before_save", + "_table_fieldnames", + "_reserved_keywords", + "_permitted_fieldnames", + "dont_update_if_missing", + ) + ) def __init__(self, d): if d.get("doctype"): @@ -118,11 +138,22 @@ class BaseDocument: @property def meta(self): - if not (meta := getattr(self, "_meta", None)): + meta = getattr(self, "_meta", None) + if meta is None: self._meta = meta = frappe.get_meta(self.doctype) return meta + @property + def permitted_fieldnames(self): + permitted_fieldnames = getattr(self, "_permitted_fieldnames", None) + if permitted_fieldnames is None: + self._permitted_fieldnames = permitted_fieldnames = get_permitted_fields( + doctype=self.doctype, parenttype=getattr(self, "parenttype", None) + ) + + return permitted_fieldnames + def __getstate__(self): """ Called when pickling. @@ -141,6 +172,7 @@ class BaseDocument: """Remove unpicklable values before pickling""" state.pop("_meta", None) + state.pop("_permitted_fieldnames", None) def update(self, d): """Update multiple fields of a doctype using a dictionary of key-value pairs. @@ -220,7 +252,7 @@ class BaseDocument: if key in self.__dict__: del self.__dict__[key] - def append(self, key, value=None): + def append(self, key: str, value: D | dict | None = None) -> D: """Append an item to a child table. Example: @@ -236,13 +268,13 @@ class BaseDocument: if (table := self.__dict__.get(key)) is None: self.__dict__[key] = table = [] - value = self._init_child(value, key) - table.append(value) + ret_value = self._init_child(value, key) + table.append(ret_value) # reference parent document - value.parent_doc = self + ret_value.parent_doc = self - return value + return ret_value def extend(self, key, value): try: @@ -302,20 +334,16 @@ class BaseDocument: def get_valid_dict( self, sanitize=True, convert_dates_to_str=False, ignore_nulls=False, ignore_virtual=False - ) -> dict: + ) -> _dict: d = _dict() - permitted_fields = get_permitted_fields( - doctype=self.doctype, parenttype=getattr(self, "parenttype", None) - ) + field_values = self.__dict__ for fieldname in self.meta.get_valid_columns(): - field_value = getattr(self, fieldname, None) - - # column is valid, we can use getattr - d[fieldname] = field_value + value = field_values.get(fieldname) # if no need for sanitization and value is None, continue - if not sanitize and d[fieldname] is None: + if not sanitize and value is None: + d[fieldname] = None continue df = self.meta.get_field(fieldname) @@ -323,46 +351,51 @@ class BaseDocument: if df: if is_virtual_field: - if ignore_virtual or fieldname not in permitted_fields: - del d[fieldname] + if ignore_virtual or fieldname not in self.permitted_fieldnames: continue - if d[fieldname] is None and (options := getattr(df, "options", None)): - from frappe.utils.safe_exec import get_safe_globals + if value is None: + if (prop := getattr(type(self), fieldname, None)) and is_a_property(prop): + value = getattr(self, fieldname) - d[fieldname] = frappe.safe_eval( - code=options, - eval_globals=get_safe_globals(), - eval_locals={"doc": self}, - ) + elif options := getattr(df, "options", None): + from frappe.utils.safe_exec import get_safe_globals - if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: + value = frappe.safe_eval( + code=options, + eval_globals=get_safe_globals(), + eval_locals={"doc": self}, + ) + + if isinstance(value, list) and df.fieldtype not in table_fields: frappe.throw(_("Value for {0} cannot be a list").format(_(df.label))) if df.fieldtype == "Check": - d[fieldname] = 1 if cint(d[fieldname]) else 0 + value = 1 if cint(value) else 0 - elif df.fieldtype == "Int" and not isinstance(d[fieldname], int): - d[fieldname] = cint(d[fieldname]) + elif df.fieldtype == "Int" and not isinstance(value, int): + value = cint(value) - elif df.fieldtype == "JSON" and isinstance(d[fieldname], dict): - d[fieldname] = json.dumps(d[fieldname], sort_keys=True, indent=4, separators=(",", ": ")) + elif df.fieldtype == "JSON" and isinstance(value, dict): + value = json.dumps(value, sort_keys=True, indent=4, separators=(",", ": ")) - elif df.fieldtype in float_like_fields and not isinstance(d[fieldname], float): - d[fieldname] = flt(d[fieldname]) + elif df.fieldtype in float_like_fields and not isinstance(value, float): + value = flt(value) - elif (df.fieldtype in datetime_fields and d[fieldname] == "") or ( - getattr(df, "unique", False) and cstr(d[fieldname]).strip() == "" + elif (df.fieldtype in datetime_fields and value == "") or ( + getattr(df, "unique", False) and cstr(value).strip() == "" ): - d[fieldname] = None + value = None if convert_dates_to_str and isinstance( - d[fieldname], (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) + value, (datetime.datetime, datetime.date, datetime.time, datetime.timedelta) ): - d[fieldname] = str(d[fieldname]) + value = str(value) - if ignore_nulls and not is_virtual_field and d[fieldname] is None: - del d[fieldname] + if ignore_nulls and not is_virtual_field and value is None: + continue + + d[fieldname] = value return d @@ -1182,15 +1215,15 @@ class BaseDocument: def reset_values_if_no_permlevel_access(self, has_access_to, high_permlevel_fields): """If the user does not have permissions at permlevel > 0, then reset the values to original / default""" - to_reset = [] - - for df in high_permlevel_fields: + to_reset = [ + df + for df in high_permlevel_fields if ( df.permlevel not in has_access_to and df.fieldtype not in display_fieldtypes and df.fieldname not in self.flags.get("ignore_permlevel_for_fields", []) - ): - to_reset.append(df) + ) + ] if to_reset: if self.is_new(): diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index 85c42b94b2..f54886a4ef 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -376,9 +376,7 @@ class DatabaseQuery: if isinstance(filters, dict): fdict = filters - filters = [] - for key, value in fdict.items(): - filters.append(make_filter_tuple(self.doctype, key, value)) + filters = [make_filter_tuple(self.doctype, key, value) for key, value in fdict.items()] setattr(self, filter_name, filters) def sanitize_fields(self): @@ -564,10 +562,7 @@ class DatabaseQuery: # remove from fields to_remove = [] for fld in self.fields: - for f in optional_fields: - if f in fld and not f in self.columns: - to_remove.append(fld) - + to_remove.extend(fld for f in optional_fields if f in fld and f not in self.columns) for fld in to_remove: del self.fields[self.fields.index(fld)] @@ -577,10 +572,9 @@ class DatabaseQuery: if isinstance(each, str): each = [each] - for element in each: - if element in optional_fields and element not in self.columns: - to_remove.append(each) - + to_remove.extend( + each for element in each if element in optional_fields and element not in self.columns + ) for each in to_remove: if isinstance(self.filters, dict): del self.filters[each] diff --git a/frappe/model/document.py b/frappe/model/document.py index 9768200164..4f966c88b2 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -4,7 +4,7 @@ import hashlib import json import time from collections.abc import Generator, Iterable -from typing import Any +from typing import TYPE_CHECKING, Any, Optional from werkzeug.exceptions import NotFound @@ -24,6 +24,9 @@ from frappe.utils import compare, cstr, date_diff, file_lock, flt, get_datetime_ from frappe.utils.data import get_absolute_url from frappe.utils.global_search import update_global_search +if TYPE_CHECKING: + from frappe.core.doctype.docfield.docfield import DocField + def get_doc(*args, **kwargs): """returns a frappe.model.Document object. @@ -409,13 +412,13 @@ class Document(BaseDocument): for df in self.meta.get_table_fields(): self.update_child_table(df.fieldname, df) - def update_child_table(self, fieldname, df=None): + def update_child_table(self, fieldname: str, df: Optional["DocField"] = None): """sync child table for given fieldname""" rows = [] - if not df: - df = self.meta.get_field(fieldname) + df: "DocField" = df or self.meta.get_field(fieldname) for d in self.get(df.fieldname): + d: Document d.db_update() rows.append(d.name) @@ -427,25 +430,20 @@ class Document(BaseDocument): # hack for docperm :( return - if rows: - # select rows that do not match the ones in the document - deleted_rows = frappe.db.sql( - """select name from `tab{}` where parent=%s - and parenttype=%s and parentfield=%s - and name not in ({})""".format( - df.options, ",".join(["%s"] * len(rows)) - ), - [self.name, self.doctype, fieldname] + rows, - ) - if len(deleted_rows) > 0: - # delete rows that do not match the ones in the document - frappe.db.delete(df.options, {"name": ("in", tuple(row[0] for row in deleted_rows))}) + # delete rows that do not match the ones in the document + tbl = frappe.qb.DocType(df.options) + qry = ( + frappe.qb.from_(tbl) + .where(tbl.parent == self.name) + .where(tbl.parenttype == self.doctype) + .where(tbl.parentfield == fieldname) + .delete() + ) - else: - # no rows found, delete all rows - frappe.db.delete( - df.options, {"parent": self.name, "parenttype": self.doctype, "parentfield": fieldname} - ) + if rows: + qry = qry.where(tbl.name.notin(rows)) + + qry.run() def get_doc_before_save(self) -> "Document": return getattr(self, "_doc_before_save", None) @@ -1039,7 +1037,7 @@ class Document(BaseDocument): """Rename the document to `name`. This transforms the current object.""" return self._rename(name=name, merge=merge, force=force, validate_rename=validate_rename) - def delete(self, ignore_permissions=False, force=False): + def delete(self, ignore_permissions=False, force=False, *, delete_permanently=False): """Delete document.""" return frappe.delete_doc( self.doctype, @@ -1047,6 +1045,7 @@ class Document(BaseDocument): ignore_permissions=ignore_permissions, flags=self.flags, force=force, + delete_permanently=delete_permanently, ) def run_before_save_methods(self): @@ -1382,7 +1381,7 @@ class Document(BaseDocument): :param comment_type: e.g. `Comment`. See Communication for more info.""" - out = frappe.get_doc( + return frappe.get_doc( { "doctype": "Comment", "comment_type": comment_type, @@ -1393,7 +1392,6 @@ class Document(BaseDocument): "content": text or comment_type, } ).insert(ignore_permissions=True) - return out def add_seen(self, user=None): """add the given/current user to list of users who have seen this document (_seen)""" @@ -1568,8 +1566,7 @@ class Document(BaseDocument): pluck="allocated_to", ) - users = set(assigned_users) - return users + return set(assigned_users) def add_tag(self, tag): """Add a Tag to this document""" diff --git a/frappe/model/meta.py b/frappe/model/meta.py index 83c21f8502..b26abef775 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -94,15 +94,17 @@ def load_doctype_from_file(doctype): class Meta(Document): _metaclass = True default_fields = list(default_fields)[1:] - special_doctypes = { - "DocField", - "DocPerm", - "DocType", - "Module Def", - "DocType Action", - "DocType Link", - "DocType State", - } + special_doctypes = frozenset( + ( + "DocField", + "DocPerm", + "DocType", + "Module Def", + "DocType Action", + "DocType Link", + "DocType State", + ) + ) standard_set_once_fields = [ frappe._dict(fieldname="creation", fieldtype="Datetime"), frappe._dict(fieldname="owner", fieldtype="Data"), @@ -421,11 +423,7 @@ class Meta(Document): order = json.loads(self.get(f"{fieldname}_order") or "[]") if order: name_map = {d.name: d for d in self.get(fieldname)} - new_list = [] - for name in order: - if name in name_map: - new_list.append(name_map[name]) - + new_list = [name_map[name] for name in order if name in name_map] # add the missing items that have not be added # maybe these items were added to the standard product # after the customization was done @@ -564,11 +562,7 @@ class Meta(Document): def get_high_permlevel_fields(self): """Build list of fields with high perm level and all the higher perm levels defined.""" if not hasattr(self, "high_permlevel_fields"): - self.high_permlevel_fields = [] - for df in self.fields: - if df.permlevel > 0: - self.high_permlevel_fields.append(df) - + self.high_permlevel_fields = [df for df in self.fields if df.permlevel > 0] return self.high_permlevel_fields def get_permitted_fieldnames(self, parenttype=None, *, user=None, permission_type="read"): @@ -594,10 +588,11 @@ class Meta(Document): self.get_permlevel_access(permission_type=permission_type, parenttype=parenttype, user=user) ) - for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True): - if df.permlevel in permlevel_access: - permitted_fieldnames.append(df.fieldname) - + permitted_fieldnames.extend( + df.fieldname + for df in self.get_fieldnames_with_value(with_field_meta=True, with_virtual_fields=True) + if df.permlevel in permlevel_access + ) return permitted_fieldnames def get_permlevel_access(self, permission_type="read", parenttype=None, *, user=None): diff --git a/frappe/model/naming.py b/frappe/model/naming.py index a202cba11f..c90b7f517b 100644 --- a/frappe/model/naming.py +++ b/frappe/model/naming.py @@ -12,17 +12,13 @@ from frappe import _ from frappe.model import log_types from frappe.query_builder import DocType from frappe.utils import cint, cstr, now_datetime +from frappe.utils.caching import redis_cache if TYPE_CHECKING: from frappe.model.document import Document from frappe.model.meta import Meta -# NOTE: This is used to keep track of status of sites -# whether `log_types` have autoincremented naming set for the site or not. -# Structure: {"sitename": {"doctype": 1}} -autoincremented_site_status_map = defaultdict(dict) - NAMING_SERIES_PATTERN = re.compile(r"^[\w\- \/.#{}]+$", re.UNICODE) BRACED_PARAMS_PATTERN = re.compile(r"(\{[\w | #]+\})") @@ -182,16 +178,7 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool: """Checks if the doctype has autoincrement autoname set""" if doctype in log_types: - site_map = autoincremented_site_status_map[frappe.local.site] - if site_map.get(doctype) is None: - query = f"""select data_type FROM information_schema.columns where column_name = 'name' and table_name = 'tab{doctype}'""" - values = () - if frappe.db.db_type == "mariadb": - query += " and table_schema = %s" - values = (frappe.db.db_name,) - site_map[doctype] = frappe.db.sql(query, values)[0][0] == "bigint" - - return bool(site_map[doctype]) + return _implicitly_auto_incremented(doctype) else: if not meta: meta = frappe.get_meta(doctype) @@ -202,6 +189,16 @@ def is_autoincremented(doctype: str, meta: Optional["Meta"] = None) -> bool: return False +@redis_cache +def _implicitly_auto_incremented(doctype) -> bool: + query = f"""select data_type FROM information_schema.columns where column_name = 'name' and table_name = 'tab{doctype}'""" + values = () + if frappe.db.db_type == "mariadb": + query += " and table_schema = %s" + values = (frappe.db.db_name,) + return frappe.db.sql(query, values)[0][0] == "bigint" + + def set_name_from_naming_options(autoname, doc): """ Get a name based on the autoname field option @@ -538,8 +535,7 @@ def _field_autoname(autoname, doc, skip_slicing=None): `autoname` field starts with 'field:' """ fieldname = autoname if skip_slicing else autoname[6:] - name = (cstr(doc.get(fieldname)) or "").strip() - return name + return (cstr(doc.get(fieldname)) or "").strip() def _prompt_autoname(autoname, doc): @@ -552,7 +548,7 @@ def _prompt_autoname(autoname, doc): frappe.throw(_("Please set the document name")) -def _format_autoname(autoname, doc): +def _format_autoname(autoname: str, doc): """ Generate autoname by replacing all instances of braced params (fields, date params ('DD', 'MM', 'YY'), series) Independent of remaining string or separators. diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index e8f5626af4..7554755d2b 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -65,6 +65,8 @@ def update_document_title( ) name_updated = updated_name and (updated_name != doc.name) + queue = kwargs.get("queue") or "default" + if name_updated: if action_enqueued: current_name = doc.name @@ -86,7 +88,7 @@ def update_document_title( save_point=True, ) - doc.queue_action("rename", name=transformed_name, merge=merge) + doc.queue_action("rename", name=transformed_name, merge=merge, queue=queue) else: doc.rename(updated_name, merge=merge) diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py index 65b5092d46..532a7807bd 100644 --- a/frappe/model/utils/link_count.py +++ b/frappe/model/utils/link_count.py @@ -5,11 +5,37 @@ from collections import defaultdict import frappe -ignore_doctypes = ("DocType", "Print Format", "Role", "Module Def", "Communication", "ToDo") +ignore_doctypes = { + "DocType", + "Print Format", + "Role", + "Module Def", + "Communication", + "ToDo", + "Version", + "Error Log", + "Scheduled Job Log", + "Event Sync Log", + "Event Update Log", + "Access Log", + "View Log", + "Activity Log", + "Energy Point Log", + "Notification Log", + "Email Queue", + "DocShare", + "Document Follow", + "Console Log", + "User", +} def notify_link_count(doctype, name): """updates link count for given document""" + + if doctype in ignore_doctypes or not frappe.request: + return + if not hasattr(frappe.local, "_link_count"): frappe.local._link_count = defaultdict(int) frappe.db.after_commit.add(flush_local_link_count) @@ -41,13 +67,12 @@ def update_link_count(): if link_count: for (doctype, name), count in link_count.items(): - if doctype not in ignore_doctypes: - try: - table = frappe.qb.DocType(doctype) - frappe.qb.update(table).set(table.idx, table.idx + count).where(table.name == name).run() - frappe.db.commit() - except Exception as e: - if not frappe.db.is_table_missing(e): # table not found, single - raise e + try: + table = frappe.qb.DocType(doctype) + frappe.qb.update(table).set(table.idx, table.idx + count).where(table.name == name).run() + frappe.db.commit() + except Exception as e: + if not frappe.db.is_table_missing(e): # table not found, single + raise e # reset the count frappe.cache.delete_value("_link_count") diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py index 8c9a209501..0295fbaaf2 100644 --- a/frappe/modules/import_file.py +++ b/frappe/modules/import_file.py @@ -42,14 +42,17 @@ ignore_doctypes = [""] def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_permissions=False): if type(module) is list: - out = [] - for m in module: - out.append( - import_file( - m[0], m[1], m[2], force=force, pre_process=pre_process, reset_permissions=reset_permissions - ) + return [ + import_file( + m[0], + m[1], + m[2], + force=force, + pre_process=pre_process, + reset_permissions=reset_permissions, ) - return out + for m in module + ] else: return import_file( module, dt, dn, force=force, pre_process=pre_process, reset_permissions=reset_permissions @@ -59,10 +62,9 @@ def import_files(module, dt=None, dn=None, force=False, pre_process=None, reset_ def import_file(module, dt, dn, force=False, pre_process=None, reset_permissions=False): """Sync a file from txt if modifed, return false if not updated""" path = get_file_path(module, dt, dn) - ret = import_file_by_path( + return import_file_by_path( path, force, pre_process=pre_process, reset_permissions=reset_permissions ) - return ret def get_file_path(module, dt, dn): diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py index bf0bd3d869..c9bf443248 100644 --- a/frappe/modules/patch_handler.py +++ b/frappe/modules/patch_handler.py @@ -100,8 +100,7 @@ def get_patches_from_app(app: str, patch_type: PatchType | None = None) -> list[ 1. ini like file with section for different patch_type 2. plain text file with each line representing a patch. """ - - patches_file = frappe.get_pymodule_path(app, "patches.txt") + patches_file = frappe.get_app_path(app, "patches.txt") try: return parse_as_configfile(patches_file, patch_type) diff --git a/frappe/monitor.py b/frappe/monitor.py index da2deb859e..9b8f500358 100644 --- a/frappe/monitor.py +++ b/frappe/monitor.py @@ -32,6 +32,12 @@ def add_data_to_monitor(**kwargs) -> None: frappe.local.monitor.add_custom_data(**kwargs) +def get_trace_id() -> str | None: + """Get unique ID for current transaction.""" + if monitor := getattr(frappe.local, "monitor", None): + return monitor.data.uuid + + def log_file(): return os.path.join(frappe.utils.get_bench_path(), "logs", "monitor.json.log") @@ -66,14 +72,16 @@ class Monitor: } ) + if request_id := frappe.request.headers.get("X-Frappe-Request-Id"): + self.data.uuid = request_id + def collect_job_meta(self, method, kwargs): self.data.job = frappe._dict({"method": method, "scheduled": False, "wait": 0}) if "run_scheduled_job" in method: self.data.job.method = kwargs["job_type"] self.data.job.scheduled = True - job = rq.get_current_job() - if job: + if job := rq.get_current_job(): self.data.uuid = job.id waitdiff = self.data.timestamp - job.enqueued_at self.data.job.wait = int(waitdiff.total_seconds() * 1000000) diff --git a/frappe/oauth.py b/frappe/oauth.py index aa486fe8ba..b338651dab 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -43,8 +43,7 @@ class OAuthWebRequestValidator(RequestValidator): # The redirect used if none has been supplied. # Prefer your clients to pre register a redirect uri rather than # supplying one on each authorization request. - redirect_uri = frappe.db.get_value("OAuth Client", client_id, "default_redirect_uri") - return redirect_uri + return frappe.db.get_value("OAuth Client", client_id, "default_redirect_uri") def validate_scopes(self, client_id, scopes, client, request, *args, **kwargs): # Is the client allowed to access the requested scopes? @@ -150,11 +149,7 @@ class OAuthWebRequestValidator(RequestValidator): filters={"client": client_id, "validity": "Valid"}, ) - checkcodes = [] - for vcode in validcodes: - checkcodes.append(vcode["name"]) - - if code in checkcodes: + if code in [vcode["name"] for vcode in validcodes]: request.scopes = frappe.db.get_value("OAuth Authorization Code", code, "scopes").split( get_url_delimiter() ) @@ -231,10 +226,7 @@ class OAuthWebRequestValidator(RequestValidator): otoken.save(ignore_permissions=True) frappe.db.commit() - default_redirect_uri = frappe.db.get_value( - "OAuth Client", request.client["name"], "default_redirect_uri" - ) - return default_redirect_uri + return frappe.db.get_value("OAuth Client", request.client["name"], "default_redirect_uri") def invalidate_authorization_code(self, client_id, code, request, *args, **kwargs): # Authorization codes are use once, invalidate it when a Bearer token @@ -375,8 +367,7 @@ class OAuthWebRequestValidator(RequestValidator): def get_userinfo_claims(self, request): user = frappe.get_doc("User", frappe.session.user) - userinfo = get_userinfo(user) - return userinfo + return get_userinfo(user) def validate_id_token(self, token, scopes, request): try: @@ -580,7 +571,7 @@ def get_userinfo(user): else: picture = urljoin(frappe_server_url, user.user_image) - userinfo = frappe._dict( + return frappe._dict( { "sub": frappe.db.get_value( "User Social Login", @@ -597,8 +588,6 @@ def get_userinfo(user): } ) - return userinfo - def get_url_delimiter(separator_character=" "): return separator_character diff --git a/frappe/parallel_test_runner.py b/frappe/parallel_test_runner.py index b7c3966df1..250e8bec76 100644 --- a/frappe/parallel_test_runner.py +++ b/frappe/parallel_test_runner.py @@ -97,7 +97,7 @@ class ParallelTestRunner: make_test_records(doctype, commit=True) def get_module(self, path, filename): - app_path = frappe.get_pymodule_path(self.app) + app_path = frappe.get_app_path(self.app) relative_path = os.path.relpath(path, app_path) if relative_path == ".": module_name = self.app @@ -217,7 +217,7 @@ class ParallelTestResult(unittest.TextTestResult): def get_all_tests(app): test_file_list = [] - for path, folders, files in os.walk(frappe.get_pymodule_path(app)): + for path, folders, files in os.walk(frappe.get_app_path(app)): for dontwalk in ("locals", ".git", "public", "__pycache__"): if dontwalk in folders: folders.remove(dontwalk) @@ -230,10 +230,11 @@ def get_all_tests(app): # in /doctype/doctype/boilerplate/ continue - for filename in files: - if filename.startswith("test_") and filename.endswith(".py") and filename != "test_runner.py": - test_file_list.append([path, filename]) - + test_file_list.extend( + [path, filename] + for filename in files + if filename.startswith("test_") and filename.endswith(".py") and filename != "test_runner.py" + ) return test_file_list diff --git a/frappe/patches.txt b/frappe/patches.txt index 054fe9b946..120a2b07d8 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -193,6 +193,7 @@ frappe.patches.v14_0.delete_payment_gateways frappe.patches.v15_0.remove_event_streaming frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report execute:frappe.reload_doc("desk", "doctype", "Form Tour") +execute:frappe.delete_doc('Page', 'recorder', ignore_missing=True, force=True) [post_model_sync] execute:frappe.get_doc('Role', 'Guest').save() # remove desk access @@ -227,3 +228,4 @@ execute:frappe.delete_doc_if_exists("Workspace", "Customization") execute:frappe.db.set_single_value("Document Naming Settings", "default_amend_naming", "Amend Counter") execute:frappe.delete_doc_if_exists("DocType", "Error Snapshot") frappe.patches.v15_0.move_event_cancelled_to_status +frappe.patches.v15_0.set_file_type diff --git a/frappe/patches/v11_0/remove_skip_for_doctype.py b/frappe/patches/v11_0/remove_skip_for_doctype.py index b3471ca4e8..ce0e43302a 100644 --- a/frappe/patches/v11_0/remove_skip_for_doctype.py +++ b/frappe/patches/v11_0/remove_skip_for_doctype.py @@ -55,21 +55,20 @@ def execute(): user_permissions_to_delete.append(user_permission.name) user_permission.name = None user_permission.skip_for_doctype = None - for doctype in applicable_for_doctypes: - if doctype: - # Maintain sequence (name, user, allow, for_value, applicable_for, apply_to_all_doctypes, creation, modified) - new_user_permissions_list.append( - ( - frappe.generate_hash(length=10), - user_permission.user, - user_permission.allow, - user_permission.for_value, - doctype, - 0, - user_permission.creation, - user_permission.modified, - ) - ) + new_user_permissions_list.extend( + ( + frappe.generate_hash(length=10), + user_permission.user, + user_permission.allow, + user_permission.for_value, + doctype, + 0, + user_permission.creation, + user_permission.modified, + ) + for doctype in applicable_for_doctypes + if doctype + ) else: # No skip_for_doctype found! Just update apply_to_all_doctypes. frappe.db.set_value("User Permission", user_permission.name, "apply_to_all_doctypes", 1) diff --git a/frappe/patches/v11_0/replicate_old_user_permissions.py b/frappe/patches/v11_0/replicate_old_user_permissions.py index 999a5d7698..b66818d252 100644 --- a/frappe/patches/v11_0/replicate_old_user_permissions.py +++ b/frappe/patches/v11_0/replicate_old_user_permissions.py @@ -67,10 +67,8 @@ def get_doctypes_to_skip(doctype, user): else: doctypes_to_skip.append(parent_doctype) - # to remove possible duplicates - doctypes_to_skip = list(set(doctypes_to_skip)) - - return doctypes_to_skip + # remove possible duplicates + return list(set(doctypes_to_skip)) # store user's valid perms to avoid repeated query diff --git a/frappe/patches/v13_0/remove_duplicate_navbar_items.py b/frappe/patches/v13_0/remove_duplicate_navbar_items.py index 593a529efc..88ab8e399e 100644 --- a/frappe/patches/v13_0/remove_duplicate_navbar_items.py +++ b/frappe/patches/v13_0/remove_duplicate_navbar_items.py @@ -3,11 +3,11 @@ import frappe def execute(): navbar_settings = frappe.get_single("Navbar Settings") - duplicate_items = [] - - for navbar_item in navbar_settings.settings_dropdown: - if navbar_item.item_label == "Toggle Full Width": - duplicate_items.append(navbar_item) + duplicate_items = [ + navbar_item + for navbar_item in navbar_settings.settings_dropdown + if navbar_item.item_label == "Toggle Full Width" + ] if len(duplicate_items) > 1: navbar_settings.remove(duplicate_items[0]) diff --git a/frappe/patches/v15_0/set_file_type.py b/frappe/patches/v15_0/set_file_type.py new file mode 100644 index 0000000000..2c90b216e5 --- /dev/null +++ b/frappe/patches/v15_0/set_file_type.py @@ -0,0 +1,32 @@ +import mimetypes + +import frappe + + +def execute(): + """Set 'File Type' for all files based on file extension.""" + files = frappe.db.get_all( + "File", + fields=["name", "file_name", "file_url"], + filters={"is_folder": 0, "file_type": ("is", "not set")}, + ) + + frappe.db.auto_commit_on_many_writes = 1 + + for file in files: + file_extension = get_file_extension(file.file_name or file.file_url) + if file_extension: + frappe.db.set_value("File", file.name, "file_type", file_extension, update_modified=False) + + frappe.db.auto_commit_on_many_writes = 0 + + +def get_file_extension(file_name): + if not file_name: + return None + file_type = mimetypes.guess_type(file_name)[0] + if not file_type: + return None + + file_extension = mimetypes.guess_extension(file_type) + return file_extension.lstrip(".").upper() if file_extension else None diff --git a/frappe/permissions.py b/frappe/permissions.py index e71e2be20f..0b44f1e791 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -630,8 +630,7 @@ def allow_everything(): returns a dict with access to everything eg. {"read": 1, "write": 1, ...} """ - perm = {ptype: 1 for ptype in rights} - return perm + return {ptype: 1 for ptype in rights} def get_allowed_docs_for_doctype(user_permissions, doctype): diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py index 273ad9c4d1..7bae0996eb 100644 --- a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py @@ -37,9 +37,10 @@ class NetworkPrinterSettings(Document): cups.setPort(self.port) conn = cups.Connection() printers = conn.getPrinters() - for printer_id, printer in printers.items(): - printer_list.append({"value": printer_id, "label": printer["printer-make-and-model"]}) - + printer_list.extend( + {"value": printer_id, "label": printer["printer-make-and-model"]} + for printer_id, printer in printers.items() + ) except RuntimeError: frappe.throw(_("Failed to connect to server")) except frappe.ValidationError: diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 468172007c..11c3b8cd19 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -713,8 +713,7 @@ frappe.ui.form.PrintView = class { get_print_format_printer_map() { // returns the whole object "print_format_printer_map" stored in the localStorage. try { - let print_format_printer_map = JSON.parse(localStorage.print_format_printer_map); - return print_format_printer_map; + return JSON.parse(localStorage.print_format_printer_map); } catch (e) { return {}; } diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js index 5b79cd6bc3..1f20a009b5 100644 --- a/frappe/public/js/form_builder/store.js +++ b/frappe/public/js/form_builder/store.js @@ -71,7 +71,7 @@ export const useStore = defineStore("form-builder-store", () => { async function fetch() { doc.value = frm.value.doc; - if (doctype.value.startsWith("new-doctype-")) { + if (doctype.value.startsWith("new-doctype-") && !doc.value.fields) { doc.value.fields = [get_df("Data", "", __("Title"))]; } @@ -91,9 +91,11 @@ export const useStore = defineStore("form-builder-store", () => { form.value.selected_field = null; nextTick(() => { - dirty.value = false; - frm.value.doc.__unsaved = 0; - frm.value.page.clear_indicator(); + if (!doctype.value.startsWith("new-doctype-")) { + dirty.value = false; + frm.value.doc.__unsaved = 0; + frm.value.page.clear_indicator(); + } read_only.value = !is_customize_form.value && !frappe.boot.developer_mode && !doc.value.custom; preview.value = false; diff --git a/frappe/public/js/frappe/defaults.js b/frappe/public/js/frappe/defaults.js index bca2b0dad4..09ea90d047 100644 --- a/frappe/public/js/frappe/defaults.js +++ b/frappe/public/js/frappe/defaults.js @@ -107,10 +107,9 @@ frappe.defaults = { let user_permission = this.get_user_permissions()[frappe.model.unscrub(key)]; if (user_permission && user_permission.length) { - let doc_found = user_permission.some((perm) => { + return user_permission.some((perm) => { return perm.doc === value; }); - return doc_found; } else { // there is no user permission for this doctype // so we can allow this doc i.e., value diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js index a12e56d0d7..bb67cef187 100644 --- a/frappe/public/js/frappe/dom.js +++ b/frappe/public/js/frappe/dom.js @@ -405,6 +405,7 @@ frappe.create_shadow_element = function (wrapper, html, css, js) { // bind online/offline events $(window).on("online", function () { + if (document.hidden) return; frappe.show_alert({ indicator: "green", message: __("You are connected to internet."), @@ -412,6 +413,7 @@ $(window).on("online", function () { }); $(window).on("offline", function () { + if (document.hidden) return; frappe.show_alert({ indicator: "orange", message: __("Connection lost. Some features might not work."), diff --git a/frappe/public/js/frappe/form/controls/multiselect.js b/frappe/public/js/frappe/form/controls/multiselect.js index 0e91d6fc39..995847afbf 100644 --- a/frappe/public/js/frappe/form/controls/multiselect.js +++ b/frappe/public/js/frappe/form/controls/multiselect.js @@ -70,9 +70,7 @@ frappe.ui.form.ControlMultiSelect = class ControlMultiSelect extends ( get_values() { const value = this.get_value() || ""; - const values = value.split(/\s*,\s*/).filter((d) => d); - - return values; + return value.split(/\s*,\s*/).filter((d) => d); } get_data() { diff --git a/frappe/public/js/frappe/form/controls/text_editor.js b/frappe/public/js/frappe/form/controls/text_editor.js index fd0e878567..15e11cd9e4 100644 --- a/frappe/public/js/frappe/form/controls/text_editor.js +++ b/frappe/public/js/frappe/form/controls/text_editor.js @@ -198,14 +198,18 @@ frappe.ui.form.ControlTextEditor = class ControlTextEditor extends frappe.ui.for get_quill_options() { return { modules: { - toolbar: this.get_toolbar_options(), + toolbar: Object.keys(this.df).includes("get_toolbar_options") + ? this.df.get_toolbar_options() + : this.get_toolbar_options(), table: true, imageResize: {}, magicUrl: true, mention: this.get_mention_options(), }, - theme: "snow", + theme: this.df.theme || "snow", readOnly: this.disabled, + bounds: this.quill_container[0], + placeholder: this.df.placeholder || "", }; } diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 0f3083ed69..f80ee0897b 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -175,10 +175,9 @@ frappe.ui.form.Dashboard = class FormDashboard { make_progress_chart(title) { this.progress_area.show(); - let progress_chart = $( - '
' - ).appendTo(this.progress_area.body); - return progress_chart; + return $('
').appendTo( + this.progress_area.body + ); } refresh() { @@ -369,7 +368,10 @@ frappe.ui.form.Dashboard = class FormDashboard { let doctype = $link.attr("data-doctype"), names = $link.attr("data-names") || []; - if (this.data.internal_links[doctype]) { + if ( + this.internal_links_found && + this.internal_links_found.find((d) => d.doctype === doctype) + ) { if (names.length) { frappe.route_options = { name: ["in", names] }; } else { @@ -437,32 +439,7 @@ frappe.ui.form.Dashboard = class FormDashboard { me.update_heatmap(r.message.timeline_data); } - // update badges - $.each(r.message.count, function (i, d) { - me.frm.dashboard.set_badge_count(d.name, cint(d.open_count), cint(d.count)); - }); - - // update from internal links - $.each(me.data.internal_links, (doctype, link) => { - let names = []; - if (typeof link === "string" || link instanceof String) { - // get internal links in parent document - let value = me.frm.doc[link]; - if (value && !names.includes(value)) { - names.push(value); - } - } else if (Array.isArray(link)) { - // get internal links in child documents - let [table_fieldname, link_fieldname] = link; - (me.frm.doc[table_fieldname] || []).forEach((d) => { - let value = d[link_fieldname]; - if (value && !names.includes(value)) { - names.push(value); - } - }); - } - me.frm.dashboard.set_badge_count(doctype, 0, names.length, names); - }); + me.update_badges(r.message.count); me.frm.dashboard_data = r.message; me._fetched_counts = true; @@ -471,11 +448,52 @@ frappe.ui.form.Dashboard = class FormDashboard { }); } - set_badge_count(doctype, open_count, count, names) { + update_badges(count) { + let me = this; + + this.internal_links_found = count.internal_links_found; + + $.each(count.internal_links_found, function (i, d) { + me.frm.dashboard.set_badge_count_for_internal_link( + d.doctype, + cint(d.open_count), + cint(d.count), + d.names + ); + }); + + $.each(count.external_links_found, function (i, d) { + me.frm.dashboard.set_badge_count_for_external_link( + d.doctype, + cint(d.open_count), + cint(d.count) + ); + }); + } + + set_badge_count_for_external_link(doctype, open_count, count) { let $link = $(this.transactions_area).find( '.document-link[data-doctype="' + doctype + '"]' ); + this.set_badge_count_common(open_count, count, $link); + } + + set_badge_count_for_internal_link(doctype, open_count, count, names) { + let $link = $(this.transactions_area).find( + '.document-link[data-doctype="' + doctype + '"]' + ); + + this.set_badge_count_common(open_count, count, $link); + + if (names && names.length) { + $link.attr("data-names", names ? names.join(",") : ""); + } else { + $link.find("a").attr("disabled", true); + } + } + + set_badge_count_common(open_count, count, $link) { if (open_count) { $link .find(".open-notification") @@ -489,14 +507,6 @@ frappe.ui.form.Dashboard = class FormDashboard { .removeClass("hidden") .text(count > 99 ? "99+" : count); } - - if (this.data.internal_links[doctype]) { - if (names && names.length) { - $link.attr("data-names", names ? names.join(",") : ""); - } else { - $link.find("a").attr("disabled", true); - } - } } update_heatmap(data) { @@ -551,7 +561,7 @@ frappe.ui.form.Dashboard = class FormDashboard { .addClass("indicator-column"); } - let indicator = $( + return $( '
" ).appendTo(this.stats_area_row); - - return indicator; } // graphs diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js index 2f2df4e4c1..e42b4af681 100644 --- a/frappe/public/js/frappe/form/footer/base_timeline.js +++ b/frappe/public/js/frappe/form/footer/base_timeline.js @@ -138,7 +138,7 @@ class BaseTimeline { let timeline_content = timeline_item.find(".timeline-content"); timeline_content.append(item.content); if (!item.hide_timestamp && !item.is_card) { - timeline_content.append(` - ${comment_when(item.creation)}`); + timeline_content.append(` · ${comment_when(item.creation)}`); } if (item.id) { timeline_content.attr("id", item.id); diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index b3258e8506..71423e4433 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -118,7 +118,7 @@ class FormTimeline extends BaseTimeline { if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) { frappe.utils.get_page_view_count(this.frm.doc.route).then((res) => { this.add_timeline_item({ - content: __("{0} Web page views", [res.message], "Form timeline"), + content: __("{0} Web page views", [res.message]), hide_timestamp: true, }); }); @@ -126,27 +126,23 @@ class FormTimeline extends BaseTimeline { } get_creation_message() { - const user_link = get_user_link(this.frm.doc.owner); - return { creation: this.frm.doc.creation, content: get_user_message( this.frm.doc.owner, - __("You created this", null, "Form timeline"), - __("{0} created this", [user_link], "Form timeline") + __("You created this"), + __("{0} created this", [get_user_link(this.frm.doc.owner)]) ), }; } get_modified_message() { - const user_link = get_user_link(this.frm.doc.modified_by); - return { creation: this.frm.doc.modified, content: get_user_message( this.frm.doc.modified_by, - __("You last edited this", null, "Form timeline"), - __("{0} last edited this", [user_link], "Form timeline") + __("You last edited this"), + __("{0} last edited this", [get_user_link(this.frm.doc.modified_by)]) ), }; } @@ -174,18 +170,13 @@ class FormTimeline extends BaseTimeline { get_view_timeline_contents() { let view_timeline_contents = []; (this.doc_info.views || []).forEach((view) => { - const view_time = comment_when(view.creation); - const user_link = get_user_link(view.owner); - const timeline_content = get_user_message( - view.owner, - __("You viewed this {0}", [view_time], "Form timeline"), - __("{0} viewed this {1}", [user_link, view_time], "Form timeline") - ); - view_timeline_contents.push({ creation: view.creation, - content: timeline_content, - hide_timestamp: true, + content: get_user_message( + view.owner, + __("You viewed this"), + __("{0} viewed this", [get_user_link(view.owner)]) + ), }); }); @@ -463,8 +454,8 @@ class FormTimeline extends BaseTimeline { (this.doc_info.like_logs || []).forEach((like_log) => { const timeline_content = get_user_message( like_log.owner, - __("You Liked", null, "Form timeline"), - __("{0} Liked", [get_user_link(like_log.owner)], "Form timeline") + __("You Liked"), + __("{0} Liked", [get_user_link(like_log.owner)]) ); like_timeline_contents.push({ diff --git a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js index 84ee4fd67d..77e6cacd66 100644 --- a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js +++ b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js @@ -284,7 +284,7 @@ function format_content_for_timeline(content) { } function get_user_link(user) { - const user_display_text = (frappe.user_info(user).fullname || "").bold(); + const user_display_text = frappe.user_info(user).fullname || ""; return frappe.utils.get_form_link("User", user, true, user_display_text); } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 6fb84709ee..d9b07b6252 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -338,7 +338,7 @@ export default class GridRow { this.open_form_button = $(`
${frappe.utils.icon("edit", "xs")} - +
${__("Edit")}
`) .appendTo(this.open_form_button) diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 3313834dc1..b7f0770a72 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -102,6 +102,7 @@ frappe.ui.form.Layout = class Layout { // remove previous color this.message.removeClass(this.message_color); } + let close_message = $(`
${frappe.utils.icon("close")}
`); this.message_color = color && ["yellow", "blue", "red", "green", "orange"].includes(color) ? color : "blue"; if (html) { @@ -111,6 +112,8 @@ frappe.ui.form.Layout = class Layout { } this.message.removeClass("hidden").addClass(this.message_color); $(html).appendTo(this.message); + close_message.appendTo(this.message); + close_message.on("click", () => this.message.empty().addClass("hidden")); } else { this.message.empty().addClass("hidden"); } diff --git a/frappe/public/js/frappe/form/link_selector.js b/frappe/public/js/frappe/form/link_selector.js index e90ed3e394..1040233b61 100644 --- a/frappe/public/js/frappe/form/link_selector.js +++ b/frappe/public/js/frappe/form/link_selector.js @@ -19,6 +19,7 @@ frappe.ui.form.LinkSelector = class LinkSelector { var me = this; this.start = 0; + this.page_length = 10; this.dialog = new frappe.ui.Dialog({ title: __("Select {0}", [this.doctype == "[Select]" ? __("value") : __(this.doctype)]), fields: [ @@ -37,7 +38,7 @@ frappe.ui.form.LinkSelector = class LinkSelector { fieldname: "more", label: __("More"), click: () => { - me.start += 20; + me.start += me.page_length; me.search(); }, }, @@ -65,6 +66,7 @@ frappe.ui.form.LinkSelector = class LinkSelector { txt: this.dialog.fields_dict.txt.get_value(), searchfield: "name", start: this.start, + page_length: this.page_length, }; var me = this; @@ -91,7 +93,7 @@ frappe.ui.form.LinkSelector = class LinkSelector { } if (r.values.length) { - $.each(r.values, function (i, v) { + for (const v of r.values) { var row = $( repl( ' -
- `); - - return $empty_state; + return $(`
+
+
+ + ${title} +
+

${message}

+
+ +
+
+
`); } load_lib(callback) { 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/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js index 0746b794f0..309c68cdb7 100644 --- a/frappe/public/js/frappe/utils/datetime.js +++ b/frappe/public/js/frappe/utils/datetime.js @@ -82,6 +82,10 @@ $.extend(frappe.datetime, { return moment(d1).diff(d2, "hours"); }, + get_minute_diff: function (d1, d2) { + return moment(d1).diff(d2, "minutes"); + }, + get_day_diff: function (d1, d2) { return moment(d1).diff(d2, "days"); }, diff --git a/frappe/public/js/frappe/utils/diffview.js b/frappe/public/js/frappe/utils/diffview.js index a326fd74bc..73bd4c1eae 100644 --- a/frappe/public/js/frappe/utils/diffview.js +++ b/frappe/public/js/frappe/utils/diffview.js @@ -19,7 +19,7 @@ frappe.ui.DiffView = class DiffView { filters: { docname: this.docname, ref_doctype: this.doctype }, }); const onchange = () => this.compute_diff(); - let dialog = new frappe.ui.Dialog({ + return new frappe.ui.Dialog({ title: __("Compare Versions"), fields: [ { @@ -56,7 +56,6 @@ frappe.ui.DiffView = class DiffView { ], size: "extra-large", }); - return dialog; } compute_diff() { diff --git a/frappe/public/js/frappe/utils/energy_point_utils.js b/frappe/public/js/frappe/utils/energy_point_utils.js index 16c5cbcb7e..edf232b677 100644 --- a/frappe/public/js/frappe/utils/energy_point_utils.js +++ b/frappe/public/js/frappe/utils/energy_point_utils.js @@ -11,26 +11,24 @@ Object.assign(frappe.energy_points, { }, format_form_log(log) { const separator = ` - `; - const formatted_log = ` - - ${this.get_form_log_message(log)} - ${log.reason ? separator + log.reason : ""} - `; - return formatted_log; + return ` + + ${this.get_form_log_message(log)} + ${log.reason ? separator + log.reason : ""} + `; }, format_history_log(log) { // redundant code to honor readability and to avoid confusion const separator = ` - `; const route = frappe.utils.get_form_link(log.reference_doctype, log.reference_name); - const formatted_log = `
- - ${this.get_points(log.points)} - - ${this.get_history_log_message(log)} - ${log.reason ? separator + log.reason : ""} - ${separator + frappe.datetime.comment_when(log.creation)} -
`; - return formatted_log; + return `
+ + ${this.get_points(log.points)} + + ${this.get_history_log_message(log)} + ${log.reason ? separator + log.reason : ""} + ${separator + frappe.datetime.comment_when(log.creation)} +
`; }, get_history_log_message(log) { const owner_name = frappe.user.full_name(log.owner).bold(); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 803413f03b..d16b4e627c 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -374,8 +374,7 @@ Object.assign(frappe.utils, { get_scroll_position: function (element, additional_offset) { let header_offset = $(".navbar").height() + $(".page-head:visible").height() || $(".navbar").height(); - let scroll_top = $(element).offset().top - header_offset - cint(additional_offset); - return scroll_top; + return $(element).offset().top - header_offset - cint(additional_offset); }, filter_dict: function (dict, filters) { var ret = []; @@ -1180,11 +1179,10 @@ Object.assign(frappe.utils, { }, get_duration_options: function (docfield) { - let duration_options = { + return { hide_days: docfield.hide_days, hide_seconds: docfield.hide_seconds, }; - return duration_options; }, get_number_system: function (country) { diff --git a/frappe/public/js/frappe/utils/web_template.js b/frappe/public/js/frappe/utils/web_template.js index d27f31cd4b..3864caf25e 100644 --- a/frappe/public/js/frappe/utils/web_template.js +++ b/frappe/public/js/frappe/utils/web_template.js @@ -48,7 +48,7 @@ function open_web_template_values_editor(template, current_values = {}) { } } - let fields = [ + return [ ...normal_fields, ...table_fields.map((tf) => { let data = current_values[tf.fieldname] || []; @@ -66,7 +66,5 @@ function open_web_template_values_editor(template, current_values = {}) { }; }), ]; - - return fields; } } diff --git a/frappe/public/js/frappe/views/file/file_view.js b/frappe/public/js/frappe/views/file/file_view.js index 2c54011e8a..8c180616d0 100644 --- a/frappe/public/js/frappe/views/file/file_view.js +++ b/frappe/public/js/frappe/views/file/file_view.js @@ -83,7 +83,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { } file_menu_items() { - const items = [ + return [ { label: __("Home"), action: () => { @@ -137,8 +137,6 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { }, }, ]; - - return items; } add_file_action_buttons() { @@ -319,6 +317,9 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { ? ` + ` @@ -370,6 +371,9 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { + diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js index 957e087010..22523cf825 100644 --- a/frappe/public/js/frappe/views/inbox/inbox_view.js +++ b/frappe/public/js/frappe/views/inbox/inbox_view.js @@ -74,11 +74,9 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView { } get_seen_class(doc) { - const seen = - Boolean(doc.seen) || JSON.parse(doc._seen || "[]").includes(frappe.session.user) - ? "" - : "bold"; - return seen; + return Boolean(doc.seen) || JSON.parse(doc._seen || "[]").includes(frappe.session.user) + ? "" + : "bold"; } get is_sent_emails() { diff --git a/frappe/public/js/frappe/views/interaction.js b/frappe/public/js/frappe/views/interaction.js index 1d1ed24efe..4dcda889d8 100644 --- a/frappe/public/js/frappe/views/interaction.js +++ b/frappe/public/js/frappe/views/interaction.js @@ -49,7 +49,7 @@ frappe.views.InteractionComposer = class InteractionComposer { let me = this; let interaction_docs = Object.keys(get_doc_mappings()); - let fields = [ + return [ { label: __("Reference"), fieldtype: "Select", @@ -95,8 +95,6 @@ frappe.views.InteractionComposer = class InteractionComposer { fieldname: "select_attachments", }, ]; - - return fields; } get_event_categories() { @@ -336,7 +334,7 @@ frappe.views.InteractionComposer = class InteractionComposer { }; function get_doc_mappings() { - const doc_map = { + return { Event: { field_map: { interaction_type: "doctype", @@ -362,6 +360,4 @@ function get_doc_mappings() { hidden_fields: ["public", "category"], }, }; - - return doc_map; } diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 150ccc5e0e..ac5bf92414 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -57,6 +57,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { // throttle refresh for 300ms this.refresh = frappe.utils.throttle(this.refresh, 300); + this.ignore_prepared_report = false; this.menu_items = []; } @@ -175,12 +176,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { } add_card_button_to_toolbar() { + if (!frappe.model.can_create("Number Card")) return; this.page.add_inner_button(__("Create Card"), () => { this.add_card_to_dashboard(); }); } add_chart_buttons_to_toolbar(show) { + if (!frappe.model.can_create("Dashboard Chart")) return; if (show) { this.create_chart_button && this.create_chart_button.remove(); this.create_chart_button = this.page.add_button(__("Set Chart"), () => { @@ -590,6 +593,8 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { frappe.route_options = null; }); + this.ignore_prepared_report = route_options["ignore_prepared_report"] || false; + return frappe.run_serially(promises); } } @@ -637,6 +642,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { args: { report_name: this.report_name, filters: filters, + ignore_prepared_report: this.ignore_prepared_report, is_tree: this.report_settings.tree, parent_field: this.report_settings.parent_field, are_default_filters: are_default_filters, @@ -732,8 +738,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { get_query_params() { const query_string = frappe.utils.get_query_string(frappe.get_route_str()); - const query_params = frappe.utils.get_query_params(query_string); - return query_params; + return frappe.utils.get_query_params(query_string); } add_prepared_report_buttons(doc) { @@ -1292,7 +1297,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { raise && this.toggle_message(false); - const filters = this.filters + return this.filters .filter((f) => f.get_value()) .map((f) => { var v = f.get_value(); @@ -1310,7 +1315,6 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { Object.assign(acc, f); return acc; }, {}); - return filters; } get_filter(fieldname) { @@ -1536,7 +1540,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { }) .filter(Boolean); - if (this.raw_data.add_total_row) { + if (this.raw_data.add_total_row && !this.report_settings.tree) { let totalRow = this.datatable.bodyRenderer.getTotalRow().reduce((row, cell) => { row[cell.column.id] = cell.content; row.is_total_row = true; @@ -1693,10 +1697,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { args: { field: values.field, doctype: values.doctype, + names: Array.from( + this.doctype_field_map[values.doctype].names + ), }, callback: (r) => { const custom_data = r.message; - const link_field = this.doctype_field_map[values.doctype]; + const link_field = + this.doctype_field_map[values.doctype].fieldname; this.add_custom_column( custom_columns, @@ -1834,7 +1842,13 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { ); doctypes.forEach((doc) => { - this.doctype_field_map[doc.doctype] = doc.fieldname; + this.doctype_field_map[doc.doctype] = { fieldname: doc.fieldname, names: new Set() }; + }); + + this.data.forEach((row) => { + doctypes.forEach((doc) => { + this.doctype_field_map[doc.doctype].names.add(row[doc.fieldname]); + }); }); return doctypes; diff --git a/frappe/public/js/frappe/views/reports/report_utils.js b/frappe/public/js/frappe/views/reports/report_utils.js index 869d7d5584..7c6a1790c5 100644 --- a/frappe/public/js/frappe/views/reports/report_utils.js +++ b/frappe/public/js/frappe/views/reports/report_utils.js @@ -142,7 +142,7 @@ frappe.report_utils = { }, get_filter_values(filters) { - let filter_values = filters + return filters .map((f) => { var v = f.default; return { @@ -153,7 +153,6 @@ frappe.report_utils = { Object.assign(acc, f); return acc; }, {}); - return filter_values; }, get_result_of_fn(fn, values) { 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/views/treeview.js b/frappe/public/js/frappe/views/treeview.js index 6cbc02c649..22810e8e35 100644 --- a/frappe/public/js/frappe/views/treeview.js +++ b/frappe/public/js/frappe/views/treeview.js @@ -57,6 +57,7 @@ frappe.views.TreeView = class TreeView { this.get_tree_nodes = me.opts.get_tree_nodes || "frappe.desk.treeview.get_children"; this.get_permissions(); + this.make_page(); this.make_filters(); this.root_value = null; @@ -66,7 +67,11 @@ frappe.views.TreeView = class TreeView { } this.onload(); - this.set_menu_item(); + + if (!this.opts.do_not_setup_menu) { + this.set_menu_item(); + } + this.set_primary_action(); } get_permissions() { @@ -79,23 +84,27 @@ frappe.views.TreeView = class TreeView { } make_page() { var me = this; - this.parent = frappe.container.add_page(this.page_name); - frappe.ui.make_app_page({ parent: this.parent, single_column: true }); + if (!this.opts || !this.opts.do_not_make_page) { + this.parent = frappe.container.add_page(this.page_name); + frappe.ui.make_app_page({ parent: this.parent, single_column: true }); + this.page = this.parent.page; + frappe.container.change_to(this.page_name); + frappe.breadcrumbs.add( + me.opts.breadcrumb || locals.DocType[me.doctype].module, + me.doctype + ); - this.page = this.parent.page; - frappe.container.change_to(this.page_name); - frappe.breadcrumbs.add( - me.opts.breadcrumb || locals.DocType[me.doctype].module, - me.doctype - ); + this.set_title(); - this.set_title(); + this.page.main.css({ + "min-height": "300px", + }); - this.page.main.css({ - "min-height": "300px", - }); - - this.page.main.addClass("frappe-card"); + this.page.main.addClass("frappe-card"); + } else { + this.page = this.opts.page; + $(this.page[0]).addClass("frappe-card"); + } if (this.opts.show_expand_all) { this.page.add_inner_button(__("Collapse All"), function () { @@ -154,6 +163,7 @@ frappe.views.TreeView = class TreeView { } get_root() { var me = this; + frappe.call({ method: me.get_tree_nodes, args: me.args, diff --git a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js index 107c0d0285..c279da79e5 100644 --- a/frappe/public/js/frappe/views/workspace/blocks/onboarding.js +++ b/frappe/public/js/frappe/views/workspace/blocks/onboarding.js @@ -106,6 +106,8 @@ export default class Onboarding extends Block { } render() { + if (frappe.is_mobile()) return; + this.wrapper = document.createElement("div"); this.new("onboarding"); diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index f13541e4f7..24d726ba85 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(); } } @@ -280,8 +280,7 @@ export default class ChartWidget extends Widget { return frappe.report_utils.prepare_field_from_column(col); }); - let data = frappe.report_utils.make_chart_options(columns, result, chart_fields).data; - return data; + return frappe.report_utils.make_chart_options(columns, result, chart_fields).data; } } diff --git a/frappe/public/js/frappe/widgets/number_card_widget.js b/frappe/public/js/frappe/widgets/number_card_widget.js index bea6f9e9d1..340bb51276 100644 --- a/frappe/public/js/frappe/widgets/number_card_widget.js +++ b/frappe/public/js/frappe/widgets/number_card_widget.js @@ -136,8 +136,7 @@ export default class NumberCardWidget extends Widget { } get_filters() { - const filters = frappe.dashboard_utils.get_all_filters(this.card_doc); - return filters; + return frappe.dashboard_utils.get_all_filters(this.card_doc); } render_card() { diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 6818003bb6..04b892b55e 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -395,7 +395,7 @@ class ShortcutDialog extends WidgetDialog { } const views = ["List", "Report Builder", "Dashboard", "New"]; - if (meta.is_tree === "Tree") views.push("Tree"); + if (meta.is_tree === 1) views.push("Tree"); if (frappe.boot.calendars.includes(doctype)) views.push("Calendar"); const response = await frappe.db.get_value( @@ -575,7 +575,7 @@ class NumberCardDialog extends WidgetDialog { { fieldtype: "Link", fieldname: "number_card_name", - label: __("Number Cards"), + label: __("Number Card"), options: "Number Card", reqd: 1, get_query: () => { diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js index 462d364fe9..3100329f16 100644 --- a/frappe/public/js/onboarding_tours/onboarding_tours.js +++ b/frappe/public/js/onboarding_tours/onboarding_tours.js @@ -12,8 +12,10 @@ frappe.ui.OnboardingTour = class OnboardingTour { padding: 10, overlayClickNext: false, keyboardControl: true, - nextBtnText: "Next", - prevBtnText: "Previous", + nextBtnText: __("Next"), + prevBtnText: __("Previous"), + doneBtnText: __("Done"), + closeBtnText: __("Close"), opacity: 0.5, onHighlighted: (step) => { frappe.ui.next_form_tour = step.options.step_info?.next_form_tour; diff --git a/frappe/public/js/telemetry/index.js b/frappe/public/js/telemetry/index.js index b9dee3be1c..751c13994e 100644 --- a/frappe/public/js/telemetry/index.js +++ b/frappe/public/js/telemetry/index.js @@ -7,7 +7,6 @@ class TelemetryManager { this.project_id = frappe.boot.posthog_project_id; this.telemetry_host = frappe.boot.posthog_host; this.site_age = frappe.boot.telemetry_site_age; - if (cint(frappe.boot.enable_telemetry) && this.project_id && this.telemetry_host) { this.enabled = true; } @@ -15,13 +14,14 @@ class TelemetryManager { initialize() { if (!this.enabled) return; + let disable_decide = !this.should_record_session(); try { posthog.init(this.project_id, { api_host: this.telemetry_host, autocapture: false, capture_pageview: false, capture_pageleave: false, - advanced_disable_decide: true, + advanced_disable_decide: disable_decide, }); posthog.identify(frappe.boot.sitename); this.send_heartbeat(); @@ -42,6 +42,10 @@ class TelemetryManager { posthog.opt_out_capturing(); } + can_enable() { + return Boolean(this.telemetry_host && this.project_id); + } + send_heartbeat() { const KEY = "ph_last_heartbeat"; const now = frappe.datetime.system_datetime(true); @@ -54,7 +58,7 @@ class TelemetryManager { } register_pageview_handler() { - if (this.site_age && this.site_age > 5) { + if (this.site_age && this.site_age > 6) { return; } @@ -62,6 +66,16 @@ class TelemetryManager { posthog.capture("$pageview"); }); } + + should_record_session() { + let start = frappe.boot.sysdefaults.session_recording_start; + if (!start) return; + + let start_datetime = frappe.datetime.str_to_obj(start); + let now = frappe.datetime.now_datetime(); + // if user allowed recording only record for first 2 hours, never again. + return frappe.datetime.get_minute_diff(now, start_datetime) < 120; + } } frappe.telemetry = new TelemetryManager(); diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index 247c05da9b..1f915367f4 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -163,6 +163,7 @@ .grid-row > .row { .col:last-child { border-right: none; + min-width: 0; } .col { diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss index 4b7f028c79..bc918f5b93 100644 --- a/frappe/public/scss/common/modal.scss +++ b/frappe/public/scss/common/modal.scss @@ -254,7 +254,6 @@ body.modal-open[style^="padding-right"] { .dialog-assignment-row { display: flex; align-items: center; - justify-content: space-between; padding: 5px 15px; border-radius: var(--border-radius-md); color: var(--text-color); @@ -262,13 +261,22 @@ body.modal-open[style^="padding-right"] { margin-bottom: 5px; } @extend .row; - .remove-btn { - display: none; + .btn-group { + opacity: 0; + transition: opacity .3s ease-in-out; + + button { + display: inline-flex; + align-items: center; + } + } + .assignee { + flex: 1; } &:hover { - background: var(--fg-hover-color); - .remove-btn { - display: block; + .btn-group { + opacity: 1; + transition: opacity 0.1s ease-in-out; } } .avatar { diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 5e20b2d12a..852efb3cb4 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -20,7 +20,7 @@ margin-top: 0.5em; margin-bottom: 0.25em; } - a[href] { + a[href]:not(.btn) { text-decoration: underline; } .ql-direction-rtl { diff --git a/frappe/public/scss/desk/form.scss b/frappe/public/scss/desk/form.scss index 7b31dc360e..375d8671ea 100644 --- a/frappe/public/scss/desk/form.scss +++ b/frappe/public/scss/desk/form.scss @@ -290,6 +290,7 @@ } .form-message { + position: relative; border-radius: var(--border-radius); padding: 8px 10px; font-size: var(--text-md, 13px); @@ -314,6 +315,15 @@ &.red { @include form-message-background("red"); } + + .close-message { + position: absolute; + top: 0; + right: 0; + padding-top: var(--padding-sm); + padding-right: var(--padding-sm); + cursor: pointer; + } } .help-box { diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss index 64fcd4531d..29d080c1ee 100644 --- a/frappe/public/scss/desk/global.scss +++ b/frappe/public/scss/desk/global.scss @@ -333,6 +333,7 @@ select.input-xs { a { transition: none; cursor: pointer; + text-decoration: none; } a:active { --icon-stroke: #{$component-active-color}; diff --git a/frappe/public/scss/desk/sidebar.scss b/frappe/public/scss/desk/sidebar.scss index 4124b0cdf2..fc3421a02d 100644 --- a/frappe/public/scss/desk/sidebar.scss +++ b/frappe/public/scss/desk/sidebar.scss @@ -392,6 +392,21 @@ body[data-route^="Module"] .main-menu { display: inline-flex; } +.attachments-actions { + display: flex; + gap: 5px; + max-width: 100%; +} + +.explore-full-btn, +.attachments-actions { + margin-bottom: var(--margin-md); +} + +.show-all-btn { + margin-top: var(--margin-md); + text-align: center; +} .add-assignment-btn, .add-attachment-btn, .shares, .followed-by { 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 ac1c4951ac..2bc14e9f2f 100644 --- a/frappe/recorder.py +++ b/frappe/recorder.py @@ -193,6 +193,7 @@ def _unpatch(): def do_not_record(function): + @functools.wraps(function) def wrapper(*args, **kwargs): if hasattr(frappe.local, "_recorder"): del frappe.local._recorder @@ -203,6 +204,7 @@ def do_not_record(function): def administrator_only(function): + @functools.wraps(function) def wrapper(*args, **kwargs): if frappe.session.user != "Administrator": frappe.throw(_("Only Administrator is allowed to use Recorder")) @@ -274,3 +276,15 @@ def record_queries(func: Callable): return ret return wrapped + + +@frappe.whitelist() +@do_not_record +@administrator_only +def import_data(file: str) -> None: + file_doc = frappe.get_doc("File", {"file_url": file}) + file_content = json.loads(file_doc.get_content()) + for request in file_content: + frappe.cache.hset(RECORDER_REQUEST_SPARSE_HASH, request["uuid"], request) + frappe.cache.hset(RECORDER_REQUEST_HASH, request["uuid"], request) + file_doc.delete(delete_permanently=True) diff --git a/frappe/search/full_text_search.py b/frappe/search/full_text_search.py index 6a2cfd9f70..151799a6e1 100644 --- a/frappe/search/full_text_search.py +++ b/frappe/search/full_text_search.py @@ -121,7 +121,6 @@ class FullTextSearch: ix = self.get_index() results = None - out = [] search_fields = self.get_fields_to_search() fieldboosts = {} @@ -143,10 +142,7 @@ class FullTextSearch: filter_scoped = Prefix(self.id, scope) results = searcher.search(query, limit=limit, filter=filter_scoped) - for r in results: - out.append(self.parse_result(r)) - - return out + return [self.parse_result(r) for r in results] class FuzzyTermExtended(FuzzyTerm): 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/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index 9ba64642ff..07c91396d7 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -95,7 +95,7 @@ class EnergyPointLog(Document): self.reverted = 1 self.save(ignore_permissions=True) - revert_log = frappe.get_doc( + return frappe.get_doc( { "doctype": "Energy Point Log", "points": -(self.points), @@ -108,8 +108,6 @@ class EnergyPointLog(Document): } ).insert(ignore_permissions=True) - return revert_log - def get_notification_message(doc): owner_name = get_fullname(doc.owner) 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 @@
- +