diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 3c6f8b744c..8422378ef4 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -11,7 +11,7 @@ jobs: timeout-minutes: 10 steps: - name: Checkout Actions - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: repository: "frappe/backport" path: ./actions diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 3ae8a35454..f41171784c 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -174,7 +174,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Download artifacts uses: actions/download-artifact@v3 diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js index e15a354de0..60fa46bc88 100644 --- a/cypress/integration/folder_navigation.js +++ b/cypress/integration/folder_navigation.js @@ -24,6 +24,7 @@ context("Folder Navigation", () => { it("Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct", () => { //Navigating inside the Attachments folder + cy.wait(500); cy.get('[title="Attachments"] > span').click(); //To check if the URL formed after visiting the attachments folder is correct @@ -36,6 +37,7 @@ context("Folder Navigation", () => { cy.click_modal_primary_button("Create"); //Navigating inside the added folder in the Attachments folder + cy.wait(500); cy.get('[title="Test Folder"] > span').click(); //To check if the URL is correct after visiting the Test Folder @@ -51,7 +53,12 @@ context("Folder Navigation", () => { cy.click_modal_primary_button("Upload"); //To check if the added file is present in the Test Folder - cy.get("span.level-item > span").should("contain", "Test Folder"); + cy.visit("/app/file/view/home/Attachments"); + cy.wait(500); + cy.get("span.level-item > a > span").should("contain", "Test Folder"); + cy.visit("/app/file/view/home/Attachments/Test%20Folder"); + + cy.wait(500); cy.get(".list-row-container").eq(0).should("contain.text", "72402.jpg"); cy.get(".list-row-checkbox").eq(0).click(); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js index 7835819334..c6076088fb 100644 --- a/cypress/integration/timeline.js +++ b/cypress/integration/timeline.js @@ -72,14 +72,14 @@ context("Timeline", () => { cy.click_listview_row_item(0); //To check if the submission of the documemt is visible in the timeline content - cy.get(".timeline-content").should("contain", "Frappe submitted this document"); + cy.get(".timeline-content").should("contain", "You submitted this document"); cy.get('[id="page-Custom Submittable DocType"] .page-actions') .findByRole("button", { name: "Cancel" }) .click(); cy.get_open_dialog().findByRole("button", { name: "Yes" }).click(); //To check if the cancellation of the documemt is visible in the timeline content - cy.get(".timeline-content").should("contain", "Frappe cancelled this document"); + cy.get(".timeline-content").should("contain", "You cancelled this document"); //Deleting the document cy.visit("/app/custom-submittable-doctype"); diff --git a/cypress/integration/view_routing.js b/cypress/integration/view_routing.js new file mode 100644 index 0000000000..1e3b841c79 --- /dev/null +++ b/cypress/integration/view_routing.js @@ -0,0 +1,231 @@ +context("View", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + it("Route to ToDo List View", () => { + cy.visit("/app/todo/view/list"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("List"); + }); + }); + + it("Route to ToDo Report View", () => { + cy.visit("/app/todo/view/report"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + + it("Route to ToDo Dashboard View", () => { + cy.visit("/app/todo/view/dashboard"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Dashboard"); + }); + }); + + it("Route to ToDo Gantt View", () => { + cy.visit("/app/todo/view/gantt"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Gantt"); + }); + }); + + it("Route to ToDo Kanban View", () => { + cy.call("frappe.tests.ui_test_helpers.create_kanban").then(() => { + cy.visit("/app/note/view/kanban/_Note _Kanban"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Kanban"); + }); + }); + }); + + it("Route to ToDo Calendar View", () => { + cy.visit("/app/todo/view/calendar"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Calendar"); + }); + }); + + it("Route to Custom Tree View", () => { + cy.call("frappe.tests.ui_test_helpers.setup_tree_doctype").then(() => { + cy.visit("/app/custom-tree/view/tree"); + cy.wait(500); + cy.window() + .its("cur_tree") + .then((list) => { + expect(list.view_name).to.equal("Tree"); + }); + }); + }); + + it("Route to Custom Image View", () => { + cy.call("frappe.tests.ui_test_helpers.setup_image_doctype").then(() => { + cy.visit("app/custom-image/view/image"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Image"); + }); + }); + }); + + it("Route to Communication Inbox View", () => { + cy.call("frappe.tests.ui_test_helpers.setup_inbox").then(() => { + cy.visit("app/communication/view/inbox"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Inbox"); + }); + }); + }); + + it("Route to File View", () => { + cy.visit("app/file"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("File"); + expect(list.current_folder).to.equal("Home"); + }); + + cy.visit("app/file/view/home/Attachments"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("File"); + expect(list.current_folder).to.equal("Home/Attachments"); + }); + }); + + it("Re-route to default view", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("app/event"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Route to default view from app/{doctype}", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("/app/event"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Route to default view from app/{doctype}/view", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("/app/event/view"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Force Route to default view from app/{doctype}", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { + view: "Report", + force_reroute: true, + }).then(() => { + cy.visit("/app/event"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Force Route to default view from app/{doctype}/view", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { + view: "Report", + force_reroute: true, + }).then(() => { + cy.visit("/app/event/view"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Force Route to default view from app/{doctype}/view", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { + view: "Report", + force_reroute: true, + }).then(() => { + cy.visit("/app/event/view/list"); + cy.wait(500); + cy.window() + .its("cur_list") + .then((list) => { + expect(list.view_name).to.equal("Report"); + }); + }); + }); + + it("Validate Route History for Default View", () => { + cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => { + cy.visit("/app/event"); + cy.visit("/app/event/view/list"); + cy.location("pathname").should("eq", "/app/event/view/list"); + cy.go("back"); + cy.location("pathname").should("eq", "/app/event"); + }); + }); + + it("Route to Form", () => { + cy.call("frappe.tests.ui_test_helpers.create_note").then(() => { + cy.visit("/app/note/Routing Test"); + cy.window() + .its("cur_frm") + .then((frm) => { + expect(frm.doc.title).to.equal("Routing Test"); + }); + }); + }); + + it("Route to Settings Workspace", () => { + cy.visit("/app/settings"); + cy.get(".title-text").should("contain", "Settings"); + }); +}); diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js index 2db646f38e..1f641de6c3 100644 --- a/cypress/integration/workspace_blocks.js +++ b/cypress/integration/workspace_blocks.js @@ -42,7 +42,7 @@ context("Workspace Blocks", () => { cy.wait("@new_page"); }); - it("Quick List Block", () => { + it.skip("Quick List Block", () => { cy.create_records([ { doctype: "ToDo", diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index aae7b804d0..d4ce92f384 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -129,12 +129,18 @@ def clear_doctype_cache(doctype=None): clear_single(doctype) # clear all parent doctypes - for dt in frappe.get_all( "DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype) ): clear_single(dt.parent) + # clear all parent doctypes + if not frappe.flags.in_install: + for dt in frappe.get_all( + "Custom Field", "dt", dict(fieldtype=["in", frappe.model.table_fields], options=doctype) + ): + clear_single(dt.dt) + # clear all notifications delete_notification_count_for(doctype) diff --git a/frappe/client.py b/frappe/client.py index 3c0ce8ea8a..bec3aefb7b 100644 --- a/frappe/client.py +++ b/frappe/client.py @@ -270,7 +270,7 @@ def delete(doctype, name): :param doctype: DocType of the document to be deleted :param name: name of the document to be deleted""" - frappe.delete_doc(doctype, name, ignore_missing=False) + delete_doc(doctype, name) @frappe.whitelist(methods=["POST", "PUT"]) @@ -462,3 +462,24 @@ def insert_doc(doc) -> "Document": return parent return frappe.get_doc(doc).insert() + + +def delete_doc(doctype, name): + """Deletes document + if doctype is a child table, then deletes the child record using the parent doc + so that the parent doc's `on_update` is called + """ + + if frappe.is_table(doctype): + values = frappe.db.get_value(doctype, name, ["parenttype", "parent", "parentfield"]) + if not values: + raise frappe.DoesNotExistError + parenttype, parent, parentfield = values + parent = frappe.get_doc(parenttype, parent) + for row in parent.get(parentfield): + if row.name == name: + parent.remove(row) + parent.save() + break + else: + frappe.delete_doc(doctype, name, ignore_missing=False) diff --git a/frappe/config/__init__.py b/frappe/config/__init__.py index 947f61e9e0..02626aedf5 100644 --- a/frappe/config/__init__.py +++ b/frappe/config/__init__.py @@ -1,11 +1,5 @@ import frappe from frappe import _ -from frappe.desk.moduleview import ( - config_exists, - get_data, - get_module_link_items_from_list, - get_onboard_items, -) def get_modules_from_all_apps_for_user(user: str = None) -> list[dict]: @@ -25,9 +19,6 @@ def get_modules_from_all_apps_for_user(user: str = None) -> list[dict]: if module_name in empty_tables_by_module: module["onboard_present"] = 1 - # Set defaults links - module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5] - return allowed_modules_list diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 42dbdd6177..5fe22eb7f2 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -228,11 +228,12 @@ def get_company_address(company): def address_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond + doctype = "Address" link_doctype = filters.pop("link_doctype") link_name = filters.pop("link_name") condition = "" - meta = frappe.get_meta("Address") + meta = frappe.get_meta(doctype) for fieldname, value in filters.items(): if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS: condition += f" and {fieldname}={frappe.db.escape(value)}" diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index a17f46216b..1ed4307d8d 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -210,8 +210,9 @@ def update_contact(doc, method): def contact_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond + doctype = "Contact" if ( - not frappe.get_meta("Contact").get_field(searchfield) + not frappe.get_meta(doctype).get_field(searchfield) and searchfield not in frappe.db.DEFAULT_COLUMNS ): return [] diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json index f9d15af483..293a6b2c87 100644 --- a/frappe/core/doctype/communication/communication.json +++ b/frappe/core/doctype/communication/communication.json @@ -2,6 +2,7 @@ "actions": [], "allow_import": 1, "creation": "2013-01-29 10:47:14", + "default_view": "Inbox", "description": "Keeps track of all communications", "doctype": "DocType", "document_type": "Setup", @@ -198,7 +199,6 @@ "label": "More Information" }, { - "bold": 0, "default": "Now", "fieldname": "communication_date", "fieldtype": "Datetime", @@ -395,7 +395,7 @@ "icon": "fa fa-comment", "idx": 1, "links": [], - "modified": "2022-03-30 11:24:25.728637", + "modified": "2022-05-09 00:13:45.310564", "modified_by": "Administrator", "module": "Core", "name": "Communication", @@ -454,8 +454,9 @@ "sender_field": "sender", "sort_field": "modified", "sort_order": "DESC", + "states": [], "subject_field": "subject", "title_field": "subject", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/frappe/core/doctype/data_export/exporter.py b/frappe/core/doctype/data_export/exporter.py index 5e92d2dcaa..e3bf669630 100644 --- a/frappe/core/doctype/data_export/exporter.py +++ b/frappe/core/doctype/data_export/exporter.py @@ -53,7 +53,7 @@ def export_data( template_bool = template if isinstance(template, str): template_bool = template.lower() == "true" - + export_without_column_meta_bool = export_without_column_meta if isinstance(export_without_column_meta, str): export_without_column_meta_bool = export_without_column_meta.lower() == "true" diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js index e91a05e17d..24c367b115 100644 --- a/frappe/core/doctype/doctype/doctype.js +++ b/frappe/core/doctype/doctype/doctype.js @@ -55,6 +55,7 @@ frappe.ui.form.on("DocType", { if (frm.is_new()) { frm.events.set_default_permission(frm); + frm.set_value("default_view", "List"); } else { frm.toggle_enable("engine", 0); } @@ -66,12 +67,14 @@ frappe.ui.form.on("DocType", { frm.cscript.autoname(frm); frm.cscript.set_naming_rule_description(frm); + frm.trigger("setup_default_views"); }, istable: (frm) => { if (frm.doc.istable && frm.is_new()) { frm.set_value("autoname", "autoincrement"); frm.set_value("allow_rename", 0); + frm.set_value("default_view", null); } else if (!frm.doc.istable && !frm.is_new()) { frm.events.set_default_permission(frm); } @@ -82,6 +85,18 @@ frappe.ui.form.on("DocType", { frm.add_child("permissions", { role: "System Manager" }); } }, + + is_tree: (frm) => { + frm.trigger("setup_default_views"); + }, + + is_calendar_and_gantt: (frm) => { + frm.trigger("setup_default_views"); + }, + + setup_default_views: (frm) => { + frappe.model.set_default_views_for_doctype(frm.doc.name, frm); + }, }); frappe.ui.form.on("DocField", { @@ -171,6 +186,10 @@ frappe.ui.form.on("DocField", { fieldtype: function (frm) { frm.trigger("max_attachments"); }, + + fields_add: (frm) => { + frm.trigger("setup_default_views"); + }, }); extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm })); diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index b051408362..f507ee6d2d 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -14,6 +14,7 @@ "istable", "issingle", "is_tree", + "is_calendar_and_gantt", "editable_grid", "quick_entry", "cb01", @@ -54,6 +55,8 @@ "default_print_format", "sort_field", "sort_order", + "default_view", + "force_re_route_to_default_view", "column_break_29", "document_type", "icon", @@ -614,6 +617,24 @@ "fieldname": "queue_in_background", "fieldtype": "Check", "label": "Queue in Background" + }, + { + "fieldname": "default_view", + "fieldtype": "Select", + "label": "Default View" + }, + { + "default": "0", + "fieldname": "force_re_route_to_default_view", + "fieldtype": "Check", + "label": "Force Re-route to Default View" + }, + { + "default": "0", + "description": "Enables Calendar and Gantt views.", + "fieldname": "is_calendar_and_gantt", + "fieldtype": "Check", + "label": "Is Calendar and Gantt" } ], "icon": "fa fa-bolt", @@ -696,7 +717,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2022-09-02 12:05:59.589751", + "modified": "2022-10-12 14:13:27.315351", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 44f2877e2b..3722e5d1fa 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -670,6 +670,18 @@ class TestDocType(FrappeTestCase): self.assertEqual(test_json.test_json_field["hello"], "world") + @patch.dict(frappe.conf, {"developer_mode": 1}) + def test_custom_field_deletion(self): + """Custom child tables whose doctype doesn't exist should be auto deleted.""" + doctype = new_doctype(custom=0).insert().name + child = new_doctype(custom=0, istable=1).insert().name + + field = "abc" + create_custom_fields({doctype: [{"fieldname": field, "fieldtype": "Table", "options": child}]}) + + frappe.delete_doc("DocType", child) + self.assertFalse(frappe.get_meta(doctype).get_field(field)) + @patch.dict(frappe.conf, {"developer_mode": 1}) def test_delete_doctype_with_customization(self): from frappe.custom.doctype.property_setter.property_setter import make_property_setter diff --git a/frappe/core/doctype/file/file.json b/frappe/core/doctype/file/file.json index cb727e48f0..d6c4a99bc3 100644 --- a/frappe/core/doctype/file/file.json +++ b/frappe/core/doctype/file/file.json @@ -2,6 +2,7 @@ "actions": [], "allow_import": 1, "creation": "2012-12-12 11:19:22", + "default_view": "File", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -169,10 +170,11 @@ "read_only": 1 } ], + "force_re_route_to_default_view": 1, "icon": "fa fa-file", "idx": 1, "links": [], - "modified": "2022-09-13 15:50:15.508250", + "modified": "2022-09-13 15:50:15.508251", "modified_by": "Administrator", "module": "Core", "name": "File", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 50f8697296..092f7fa45d 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -237,7 +237,7 @@ class User(Document): ) def share_with_self(self): - frappe.share.add( + frappe.share.add_docshare( self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True} ) @@ -901,6 +901,7 @@ def reset_password(user): def user_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_filters_cond, get_match_cond + doctype = "User" conditions = [] user_type_condition = "and user_type != 'Website User'" diff --git a/frappe/core/report/database_storage_usage_by_tables/__init__.py b/frappe/core/report/database_storage_usage_by_tables/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js new file mode 100644 index 0000000000..b2cf268b36 --- /dev/null +++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js @@ -0,0 +1,7 @@ +// Copyright (c) 2022, Frappe Technologies and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["Database Storage Usage By Tables"] = { + filters: [], +}; diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json new file mode 100644 index 0000000000..20deb78ad6 --- /dev/null +++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 1, + "columns": [], + "creation": "2022-10-19 02:25:24.326791", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letter_head": "abc", + "modified": "2022-10-19 02:59:00.365307", + "modified_by": "Administrator", + "module": "Core", + "name": "Database Storage Usage By Tables", + "owner": "Administrator", + "prepared_report": 0, + "query": "", + "ref_doctype": "Error Log", + "report_name": "Database Storage Usage By Tables", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py new file mode 100644 index 0000000000..c88262552e --- /dev/null +++ b/frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py @@ -0,0 +1,40 @@ +# Copyright (c) 2022, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe + +COLUMNS = [ + {"label": "Table", "fieldname": "table", "fieldtype": "Data", "width": 200}, + {"label": "Size (MB)", "fieldname": "size", "fieldtype": "Float"}, + {"label": "Data (MB)", "fieldname": "data_size", "fieldtype": "Float"}, + {"label": "Index (MB)", "fieldname": "index_size", "fieldtype": "Float"}, +] + + +def execute(filters=None): + frappe.only_for("System Manager") + + data = frappe.db.multisql( + { + "mariadb": """ + SELECT table_name AS `table`, + round(((data_length + index_length) / 1024 / 1024), 2) `size`, + round((data_length / 1024 / 1024), 2) as data_size, + round((index_length / 1024 / 1024), 2) as index_size + FROM information_schema.TABLES + ORDER BY (data_length + index_length) DESC; + """, + "postgres": """ + SELECT + table_name as "table", + round(pg_total_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "size", + round(pg_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "data_size", + round(pg_indexes_size(quote_ident(table_name)) / 1024 / 1024, 2) as "index_size" + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY 2 DESC; + """, + }, + as_dict=1, + ) + return COLUMNS, data diff --git a/frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py b/frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py new file mode 100644 index 0000000000..e82cbe9caf --- /dev/null +++ b/frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py @@ -0,0 +1,15 @@ +# Copyright (c) 2022, Frappe Technologies and contributors +# For license information, please see license.txt + + +from frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables import ( + execute, +) +from frappe.tests.utils import FrappeTestCase + + +class TestDBUsageReport(FrappeTestCase): + def test_basic_query(self): + _, data = execute() + tables = [d.table for d in data] + self.assertFalse({"tabUser", "tabDocField"}.difference(tables)) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 8d8249b532..759a9e1b3a 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -72,6 +72,7 @@ frappe.ui.form.on("Customize Form", { } else { frm.refresh(); frm.trigger("setup_sortable"); + frm.trigger("setup_default_views"); } } localStorage["customize_doctype"] = frm.doc.doc_type; @@ -82,8 +83,12 @@ frappe.ui.form.on("Customize Form", { } }, + is_calendar_and_gantt: function (frm) { + frm.trigger("setup_default_views"); + }, + setup_sortable: function (frm) { - frm.doc.fields.forEach(function (f, i) { + frm.doc.fields.forEach(function (f) { if (!f.is_custom_field) { f._sortable = false; } @@ -222,6 +227,10 @@ frappe.ui.form.on("Customize Form", { frm.set_df_property("sort_field", "options", fields); } }, + + setup_default_views(frm) { + frappe.model.set_default_views_for_doctype(frm.doc.doc_type, frm); + }, }); // can't delete standard fields @@ -237,6 +246,7 @@ frappe.ui.form.on("Customize Form Field", { var f = frappe.model.get_doc(cdt, cdn); f.is_system_generated = false; f.is_custom_field = true; + frm.trigger("setup_default_views"); }, }); diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 0d71aff577..a64d1fa05a 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -14,6 +14,7 @@ "column_break_5", "is_submittable", "istable", + "is_calendar_and_gantt", "editable_grid", "quick_entry", "track_changes", @@ -37,6 +38,8 @@ "show_title_field_in_link", "translated_doctype", "default_print_format", + "default_view", + "force_re_route_to_default_view", "column_break_29", "show_preview_popup", "email_settings_section", @@ -341,20 +344,39 @@ "label": "Make Attachments Public by Default" }, { - "default": "0", - "depends_on": "eval: doc.is_submittable", - "fieldname": "queue_in_background", - "fieldtype": "Check", - "label": "Queue in Background" + "default": "0", + "depends_on": "eval: doc.is_submittable", + "fieldname": "queue_in_background", + "fieldtype": "Check", + "label": "Queue in Background" + }, + { + "default": "0", + "depends_on": "eval: doc.is_submittable", + "fetch_from": "doc_type.is_submittable", + "fieldname": "is_submittable", + "fieldtype": "Check", + "label": "Is Submittable", + "read_only": 1 + }, + { + "fieldname": "default_view", + "fieldtype": "Select", + "label": "Default View" }, { "default": "0", - "depends_on": "eval: doc.is_submittable", - "fetch_from": "doc_type.is_submittable", - "fieldname": "is_submittable", + "depends_on": "default_view", + "fieldname": "force_re_route_to_default_view", "fieldtype": "Check", - "label": "Is Submittable", - "read_only": 1 + "label": "Force Re-route to Default View" + }, + { + "default": "0", + "description": "Enables Calendar and Gantt views.", + "fieldname": "is_calendar_and_gantt", + "fieldtype": "Check", + "label": "Is Calendar and Gantt" } ], "hide_toolbar": 1, @@ -363,7 +385,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2022-10-11 21:23:36.669135", + "modified": "2022-08-30 11:45:16.772277", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 2a42d249fc..920df661e3 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -587,6 +587,10 @@ 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", "translated_doctype": "Check", } diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py index 6c4564c958..661652c74c 100644 --- a/frappe/custom/doctype/customize_form/test_customize_form.py +++ b/frappe/custom/doctype/customize_form/test_customize_form.py @@ -54,7 +54,7 @@ class TestCustomizeForm(FrappeTestCase): d = self.get_customize_form("Event") self.assertEqual(d.doc_type, "Event") - self.assertEqual(len(d.get("fields")), 36) + self.assertEqual(len(d.get("fields")), 38) d = self.get_customize_form("Event") self.assertEqual(d.doc_type, "Event") diff --git a/frappe/desk/doctype/event/event.js b/frappe/desk/doctype/event/event.js index 61bf66f5e5..299cbe5cc3 100644 --- a/frappe/desk/doctype/event/event.js +++ b/frappe/desk/doctype/event/event.js @@ -41,6 +41,18 @@ frappe.ui.form.on("Event", { }, __("Add Participants") ); + + const [ends_on_date] = frm.doc.ends_on + ? frm.doc.ends_on.split(" ") + : frm.doc.starts_on.split(" "); + + if (frm.doc.google_meet_link && frappe.datetime.now_date() <= ends_on_date) { + frm.dashboard.set_headline( + __("Join video conference with {0}", [ + `Google Meet`, + ]) + ); + } }, repeat_on: function (frm) { if (frm.doc.repeat_on === "Every Day") { diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json index cb2e42aab2..5ca49f3831 100644 --- a/frappe/desk/doctype/event/event.json +++ b/frappe/desk/doctype/event/event.json @@ -22,12 +22,14 @@ "sender", "all_day", "sync_with_google_calendar", + "add_video_conferencing", "sb_00", "google_calendar", - "pulled_from_google_calendar", - "cb_00", "google_calendar_id", + "cb_00", "google_calendar_event_id", + "google_meet_link", + "pulled_from_google_calendar", "section_break_13", "repeat_on", "repeat_till", @@ -225,7 +227,7 @@ }, { "collapsible": 1, - "depends_on": "eval:doc.sync_with_google_calendar", + "depends_on": "eval:doc.sync_with_google_calendar || doc.pulled_from_google_calendar", "fieldname": "sb_00", "fieldtype": "Section Break", "label": "Google Calendar" @@ -245,6 +247,7 @@ "fieldname": "google_calendar_event_id", "fieldtype": "Data", "label": "Google Calendar Event ID", + "no_copy": 1, "read_only": 1 }, { @@ -272,12 +275,27 @@ "label": "Sender", "options": "Email", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.sync_with_google_calendar", + "description": "via Google Meet", + "fieldname": "add_video_conferencing", + "fieldtype": "Check", + "label": "Add Video Conferencing" + }, + { + "fieldname": "google_meet_link", + "fieldtype": "Data", + "label": "Google Meet Link", + "no_copy": 1, + "read_only": 1 } ], "icon": "fa fa-calendar", "idx": 1, "links": [], - "modified": "2022-05-12 05:43:27.935510", + "modified": "2022-08-12 19:24:34.794098", "modified_by": "Administrator", "module": "Desk", "name": "Event", diff --git a/frappe/desk/doctype/event/event.py b/frappe/desk/doctype/event/event.py index 7bcb8207a7..fafd317155 100644 --- a/frappe/desk/doctype/event/event.py +++ b/frappe/desk/doctype/event/event.py @@ -6,6 +6,7 @@ import json import frappe from frappe import _ +from frappe.contacts.doctype.contact.contact import get_default_contact from frappe.desk.doctype.notification_settings.notification_settings import ( is_email_notifications_enabled_for_type, ) @@ -55,6 +56,12 @@ class Event(Document): if self.sync_with_google_calendar and not self.google_calendar: frappe.throw(_("Select Google Calendar to which event should be synced.")) + if not self.sync_with_google_calendar: + self.add_video_conferencing = 0 + + def before_save(self): + self.set_participants_email() + def on_update(self): self.sync_communication() @@ -131,6 +138,22 @@ class Event(Document): for participant in participants: self.add_participant(participant["doctype"], participant["docname"]) + def set_participants_email(self): + for participant in self.event_participants: + if participant.email: + continue + + if participant.reference_doctype != "Contact": + participant_contact = get_default_contact( + participant.reference_doctype, participant.reference_docname + ) + else: + participant_contact = participant.reference_docname + + participant.email = ( + frappe.get_value("Contact", participant_contact, "email_id") if participant_contact else None + ) + @frappe.whitelist() def delete_communication(event, reference_doctype, reference_docname): diff --git a/frappe/desk/doctype/event_participants/event_participants.json b/frappe/desk/doctype/event_participants/event_participants.json index 1b40e7042b..bbb0a24f3e 100644 --- a/frappe/desk/doctype/event_participants/event_participants.json +++ b/frappe/desk/doctype/event_participants/event_participants.json @@ -6,7 +6,8 @@ "engine": "InnoDB", "field_order": [ "reference_doctype", - "reference_docname" + "reference_docname", + "email" ], "fields": [ { @@ -24,11 +25,17 @@ "label": "Reference Name", "options": "reference_doctype", "reqd": 1 + }, + { + "fieldname": "email", + "fieldtype": "Data", + "label": "Email", + "options": "Email" } ], "istable": 1, "links": [], - "modified": "2022-08-03 12:20:50.466370", + "modified": "2022-10-18 17:49:33.549459", "modified_by": "Administrator", "module": "Desk", "name": "Event Participants", diff --git a/frappe/desk/doctype/todo/todo_list.js b/frappe/desk/doctype/todo/todo_list.js index 2e4534e05c..bf62088f1d 100644 --- a/frappe/desk/doctype/todo/todo_list.js +++ b/frappe/desk/doctype/todo/todo_list.js @@ -17,10 +17,10 @@ frappe.listview_settings["ToDo"] = { return doc.reference_name; }, get_label: function () { - return __("Open"); + return __("Open", null, "Access"); }, get_description: function (doc) { - return __("Open {0}", [`${doc.reference_type} ${doc.reference_name}`]); + return __("Open {0}", [`${__(doc.reference_type)}: ${doc.reference_name}`]); }, action: function (doc) { frappe.set_route("Form", doc.reference_type, doc.reference_name); diff --git a/frappe/desk/moduleview.py b/frappe/desk/moduleview.py deleted file mode 100644 index 913b3406e3..0000000000 --- a/frappe/desk/moduleview.py +++ /dev/null @@ -1,615 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: MIT. See LICENSE - -import json - -import frappe -from frappe import _ -from frappe.boot import get_allowed_pages, get_allowed_reports -from frappe.cache_manager import ( - build_domain_restriced_doctype_cache, - build_domain_restriced_page_cache, - build_table_count_cache, -) -from frappe.desk.doctype.desktop_icon.desktop_icon import clear_desktop_icons_cache, set_hidden - - -@frappe.whitelist() -def get(module): - """Returns data (sections, list of reports, counts) to render module view in desk: - `/desk/#Module/[name]`.""" - data = get_data(module) - - out = {"data": data} - - return out - - -@frappe.whitelist() -def hide_module(module): - set_hidden(module, frappe.session.user, 1) - clear_desktop_icons_cache() - - -def get_table_with_counts(): - counts = frappe.cache().get_value("information_schema:counts") - if counts: - return counts - else: - return build_table_count_cache() - - -def get_data(module, build=True): - """Get module data for the module view `desk/#Module/[name]`""" - doctype_info = get_doctype_info(module) - data = build_config_from_file(module) - - if not data: - data = build_standard_config(module, doctype_info) - else: - add_custom_doctypes(data, doctype_info) - - add_section(data, _("Custom Reports"), "fa fa-list-alt", get_report_list(module)) - - data = combine_common_sections(data) - data = apply_permissions(data) - - # set_last_modified(data) - - if build: - exists_cache = get_table_with_counts() - - def doctype_contains_a_record(name): - exists = exists_cache.get(name) - if not exists: - if not frappe.db.get_value("DocType", name, "issingle"): - exists = frappe.db.count(name) - else: - exists = True - exists_cache[name] = exists - return exists - - for section in data: - for item in section["items"]: - # Onboarding - - # First disable based on exists of depends_on list - doctype = item.get("doctype") - dependencies = item.get("dependencies") or None - if not dependencies and doctype: - item["dependencies"] = [doctype] - - dependencies = item.get("dependencies") - if dependencies: - incomplete_dependencies = [d for d in dependencies if not doctype_contains_a_record(d)] - if len(incomplete_dependencies): - item["incomplete_dependencies"] = incomplete_dependencies - - if item.get("onboard"): - # Mark Spotlights for initial - if item.get("type") == "doctype": - name = item.get("name") - count = doctype_contains_a_record(name) - - item["count"] = count - - return data - - -def build_config_from_file(module): - """Build module info from `app/config/desktop.py` files.""" - data = [] - module = frappe.scrub(module) - - for app in frappe.get_installed_apps(): - try: - data += get_config(app, module) - except ImportError: - pass - - return filter_by_restrict_to_domain(data) - - -def filter_by_restrict_to_domain(data): - """filter Pages and DocType depending on the Active Module(s)""" - doctypes = ( - frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache() - ) - pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache() - - for d in data: - _items = [] - for item in d.get("items", []): - - item_type = item.get("type") - item_name = item.get("name") - - if (item_name in pages) or (item_name in doctypes) or item_type == "report": - _items.append(item) - - d.update({"items": _items}) - - return data - - -def build_standard_config(module, doctype_info): - """Build standard module data from DocTypes.""" - if not frappe.db.get_value("Module Def", module): - frappe.throw(_("Module Not Found")) - - data = [] - - add_section( - data, - _("Documents"), - "fa fa-star", - [d for d in doctype_info if d.document_type in ("Document", "Transaction")], - ) - - add_section( - data, - _("Setup"), - "fa fa-cog", - [d for d in doctype_info if d.document_type in ("Master", "Setup", "")], - ) - - add_section(data, _("Standard Reports"), "fa fa-list", get_report_list(module, is_standard="Yes")) - - return data - - -def add_section(data, label, icon, items): - """Adds a section to the module data.""" - if not items: - return - data.append({"label": label, "icon": icon, "items": items}) - - -def add_custom_doctypes(data, doctype_info): - """Adds Custom DocTypes to modules setup via `config/desktop.py`.""" - add_section( - data, - _("Documents"), - "fa fa-star", - [d for d in doctype_info if (d.custom and d.document_type in ("Document", "Transaction"))], - ) - - add_section( - data, - _("Setup"), - "fa fa-cog", - [d for d in doctype_info if (d.custom and d.document_type in ("Setup", "Master", ""))], - ) - - -def get_doctype_info(module): - """Returns list of non child DocTypes for given module.""" - active_domains = frappe.get_active_domains() - - doctype_info = frappe.get_all( - "DocType", - filters={"module": module, "istable": 0}, - or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)}, - fields=["'doctype' as type", "name", "description", "document_type", "custom", "issingle"], - order_by="custom asc, document_type desc, name asc", - ) - - for d in doctype_info: - d.document_type = d.document_type or "" - d.description = _(d.description or "") - - return doctype_info - - -def combine_common_sections(data): - """Combine sections declared in separate apps.""" - sections = [] - sections_dict = {} - for each in data: - if each["label"] not in sections_dict: - sections_dict[each["label"]] = each - sections.append(each) - else: - sections_dict[each["label"]]["items"] += each["items"] - - return sections - - -def apply_permissions(data): - default_country = frappe.db.get_default("country") - - user = frappe.get_user() - user.build_permissions() - - allowed_pages = get_allowed_pages() - allowed_reports = get_allowed_reports() - - new_data = [] - for section in data: - new_items = [] - - for item in section.get("items") or []: - item = frappe._dict(item) - - if item.country and item.country != default_country: - continue - - if ( - (item.type == "doctype" and item.name in user.can_read) - or (item.type == "page" and item.name in allowed_pages) - or (item.type == "report" and item.name in allowed_reports) - or item.type == "help" - ): - - new_items.append(item) - - if new_items: - new_section = section.copy() - new_section["items"] = new_items - new_data.append(new_section) - - return new_data - - -def get_disabled_reports(): - if not hasattr(frappe.local, "disabled_reports"): - frappe.local.disabled_reports = {r.name for r in frappe.get_all("Report", {"disabled": 1})} - return frappe.local.disabled_reports - - -def get_config(app, module): - """Load module info from `[app].config.[module]`.""" - config = frappe.get_module(f"{app}.config.{module}") - config = config.get_data() - - sections = [s for s in config if s.get("condition", True)] - - disabled_reports = get_disabled_reports() - for section in sections: - items = [] - for item in section["items"]: - if item["type"] == "report" and item["name"] in disabled_reports: - continue - # some module links might not have name - if not item.get("name"): - item["name"] = item.get("label") - if not item.get("label"): - item["label"] = _(item.get("name")) - items.append(item) - section["items"] = items - - return sections - - -def config_exists(app, module): - try: - frappe.get_module(f"{app}.config.{module}") - return True - except ImportError: - return False - - -def add_setup_section(config, app, module, label, icon): - """Add common sections to `/desk#Module/Setup`""" - try: - setup_section = get_setup_section(app, module, label, icon) - if setup_section: - config.append(setup_section) - except ImportError: - pass - - -def get_setup_section(app, module, label, icon): - """Get the setup section from each module (for global Setup page).""" - config = get_config(app, module) - for section in config: - if section.get("label") == _("Setup"): - return {"label": label, "icon": icon, "items": section["items"]} - - -def get_onboard_items(app, module): - try: - sections = get_config(app, module) - except ImportError: - return [] - - onboard_items = [] - fallback_items = [] - - if not sections: - doctype_info = get_doctype_info(module) - sections = build_standard_config(module, doctype_info) - - for section in sections: - for item in section["items"]: - if item.get("onboard", 0) == 1: - onboard_items.append(item) - - # in case onboard is not set - fallback_items.append(item) - - if len(onboard_items) > 5: - return onboard_items - - return onboard_items or fallback_items - - -@frappe.whitelist() -def get_links_for_module(app, module): - return [{"value": l.get("name"), "label": l.get("label")} for l in get_links(app, module)] - - -def get_links(app, module): - try: - sections = get_config(app, frappe.scrub(module)) - except ImportError: - return [] - - links = [] - for section in sections: - for item in section["items"]: - links.append(item) - return links - - -@frappe.whitelist() -def get_desktop_settings(): - from frappe.config import get_modules_from_all_apps_for_user - - all_modules = get_modules_from_all_apps_for_user() - home_settings = get_home_settings() - - modules_by_name = {} - for m in all_modules: - modules_by_name[m["module_name"]] = m - - module_categories = ["Modules", "Domains", "Places", "Administration"] - user_modules_by_category = {} - - user_saved_modules_by_category = home_settings.modules_by_category or {} - user_saved_links_by_module = home_settings.links_by_module or {} - - def apply_user_saved_links(module): - module = frappe._dict(module) - all_links = get_links(module.app, module.module_name) - module_links_by_name = {} - for link in all_links: - module_links_by_name[link["name"]] = link - - if module.module_name in user_saved_links_by_module: - user_links = frappe.parse_json(user_saved_links_by_module[module.module_name]) - module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name] - - return module - - for category in module_categories: - if category in user_saved_modules_by_category: - user_modules = user_saved_modules_by_category[category] - user_modules_by_category[category] = [ - apply_user_saved_links(modules_by_name[m]) for m in user_modules if modules_by_name.get(m) - ] - else: - user_modules_by_category[category] = [ - apply_user_saved_links(m) for m in all_modules if m.get("category") == category - ] - - # filter out hidden modules - if home_settings.hidden_modules: - for category in user_modules_by_category: - hidden_modules = home_settings.hidden_modules or [] - modules = user_modules_by_category[category] - user_modules_by_category[category] = [ - module for module in modules if module.module_name not in hidden_modules - ] - - return user_modules_by_category - - -@frappe.whitelist() -def update_hidden_modules(category_map): - category_map = frappe.parse_json(category_map) - home_settings = get_home_settings() - - saved_hidden_modules = home_settings.hidden_modules or [] - - for category in category_map: - config = frappe._dict(category_map[category]) - saved_hidden_modules += config.removed or [] - saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])] - - if home_settings.get("modules_by_category") and home_settings.modules_by_category.get(category): - module_placement = [ - d for d in (config.added or []) if d not in home_settings.modules_by_category[category] - ] - home_settings.modules_by_category[category] += module_placement - - home_settings.hidden_modules = saved_hidden_modules - set_home_settings(home_settings) - - return get_desktop_settings() - - -@frappe.whitelist() -def update_global_hidden_modules(modules): - modules = frappe.parse_json(modules) - frappe.only_for("System Manager") - - doc = frappe.get_doc("User", "Administrator") - doc.set("block_modules", []) - for module in modules: - doc.append("block_modules", {"module": module}) - - doc.save(ignore_permissions=True) - - return get_desktop_settings() - - -@frappe.whitelist() -def update_modules_order(module_category, modules): - modules = frappe.parse_json(modules) - home_settings = get_home_settings() - - home_settings.modules_by_category = home_settings.modules_by_category or {} - home_settings.modules_by_category[module_category] = modules - - set_home_settings(home_settings) - - -@frappe.whitelist() -def update_links_for_module(module_name, links): - links = frappe.parse_json(links) - home_settings = get_home_settings() - - home_settings.setdefault("links_by_module", {}) - home_settings["links_by_module"].setdefault(module_name, None) - home_settings["links_by_module"][module_name] = links - - set_home_settings(home_settings) - - return get_desktop_settings() - - -@frappe.whitelist() -def get_options_for_show_hide_cards(): - global_options = [] - - if "System Manager" in frappe.get_roles(): - global_options = get_options_for_global_modules() - - return {"user_options": get_options_for_user_blocked_modules(), "global_options": global_options} - - -@frappe.whitelist() -def get_options_for_global_modules(): - from frappe.config import get_modules_from_all_apps - - all_modules = get_modules_from_all_apps() - - blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules() - - options = [] - for module in all_modules: - module = frappe._dict(module) - options.append( - { - "category": module.category, - "label": module.label, - "value": module.module_name, - "checked": module.module_name not in blocked_modules, - } - ) - - return options - - -@frappe.whitelist() -def get_options_for_user_blocked_modules(): - from frappe.config import get_modules_from_all_apps_for_user - - all_modules = get_modules_from_all_apps_for_user() - home_settings = get_home_settings() - - hidden_modules = home_settings.hidden_modules or [] - - options = [] - for module in all_modules: - module = frappe._dict(module) - options.append( - { - "category": module.category, - "label": module.label, - "value": module.module_name, - "checked": module.module_name not in hidden_modules, - } - ) - - return options - - -def set_home_settings(home_settings): - frappe.cache().hset("home_settings", frappe.session.user, home_settings) - frappe.db.set_value("User", frappe.session.user, "home_settings", json.dumps(home_settings)) - - -@frappe.whitelist() -def get_home_settings(): - def get_from_db(): - settings = frappe.db.get_value("User", frappe.session.user, "home_settings") - return frappe.parse_json(settings or "{}") - - home_settings = frappe.cache().hget("home_settings", frappe.session.user, get_from_db) - return home_settings - - -def get_module_link_items_from_list(app, module, list_of_link_names): - try: - sections = get_config(app, frappe.scrub(module)) - except ImportError: - return [] - - links = [] - for section in sections: - for item in section["items"]: - if item.get("label", "") in list_of_link_names: - links.append(item) - - return links - - -def set_last_modified(data): - for section in data: - for item in section["items"]: - if item["type"] == "doctype": - item["last_modified"] = get_last_modified(item["name"]) - - -def get_last_modified(doctype): - def _get(): - try: - last_modified = frappe.get_all( - doctype, fields=["max(modified)"], as_list=True, limit_page_length=1 - )[0][0] - except Exception as e: - if frappe.db.is_table_missing(e): - last_modified = None - else: - raise - - # hack: save as -1 so that it is cached - if last_modified is None: - last_modified = -1 - - return last_modified - - last_modified = frappe.cache().hget("last_modified", doctype, _get) - - if last_modified == -1: - last_modified = None - - return last_modified - - -def get_report_list(module, is_standard="No"): - """Returns list on new style reports for modules.""" - reports = frappe.get_list( - "Report", - fields=["name", "ref_doctype", "report_type"], - filters={"is_standard": is_standard, "disabled": 0, "module": module}, - order_by="name", - ) - - out = [] - for r in reports: - out.append( - { - "type": "report", - "doctype": r.ref_doctype, - "is_query_report": 1 - if r.report_type in ("Query Report", "Script Report", "Custom Report") - else 0, - "label": _(r.name), - "name": r.name, - } - ) - - return out diff --git a/frappe/desk/report_dump.py b/frappe/desk/report_dump.py deleted file mode 100644 index 6650d24757..0000000000 --- a/frappe/desk/report_dump.py +++ /dev/null @@ -1,107 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: MIT. See LICENSE - - -import copy -import json - -import frappe - - -@frappe.whitelist() -def get_data(doctypes, last_modified): - data_map = {} - for dump_report_map in frappe.get_hooks().dump_report_map: - data_map.update(frappe.get_attr(dump_report_map)) - - out = {} - - doctypes = json.loads(doctypes) - last_modified = json.loads(last_modified) - - for d in doctypes: - args = copy.deepcopy(data_map[d]) - dt = d.find("[") != -1 and d[: d.find("[")] or d - out[dt] = {} - - if args.get("from"): - modified_table = "item." - else: - modified_table = "" - - conditions = order_by = "" - table = args.get("from") or ("`tab%s`" % dt) - - if d in last_modified: - if not args.get("conditions"): - args["conditions"] = [] - args["conditions"].append(modified_table + "modified > '" + last_modified[d] + "'") - out[dt]["modified_names"] = frappe.db.sql_list( - """select %sname from %s - where %smodified > %s""" - % (modified_table, table, modified_table, "%s"), - last_modified[d], - ) - - if args.get("force_index"): - conditions = " force index (%s) " % args["force_index"] - if args.get("conditions"): - conditions += " where " + " and ".join(args["conditions"]) - if args.get("order_by"): - order_by = " order by " + args["order_by"] - - out[dt]["data"] = [ - list(t) - for t in frappe.db.sql( - """select {} from {} {} {}""".format(",".join(args["columns"]), table, conditions, order_by) - ) - ] - - # last modified - modified_table = table - if "," in table: - modified_table = " ".join(table.split(",")[0].split(" ")[:-1]) - - tmp = frappe.db.sql( - """select `modified` - from %s order by modified desc limit 1""" - % modified_table - ) - out[dt]["last_modified"] = tmp and tmp[0][0] or "" - out[dt]["columns"] = list(map(lambda c: c.split(" as ")[-1], args["columns"])) - - if args.get("links"): - out[dt]["links"] = args["links"] - - for d in out: - unused_links = [] - # only compress full dumps (not partial) - if out[d].get("links") and (d not in last_modified): - for link_key in out[d]["links"]: - link = out[d]["links"][link_key] - if link[0] in out and (link[0] not in last_modified): - - # make a map of link ids - # to index - link_map = {} - doctype_data = out[link[0]] - - col_idx = doctype_data["columns"].index(link[1]) - for row_idx in range(len(doctype_data["data"])): - row = doctype_data["data"][row_idx] - link_map[row[col_idx]] = row_idx - - for row in out[d]["data"]: - columns = list(out[d]["columns"]) - if link_key in columns: - col_idx = columns.index(link_key) - # replace by id - if row[col_idx]: - row[col_idx] = link_map.get(row[col_idx]) - else: - unused_links.append(link_key) - - for link in unused_links: - del out[d]["links"][link] - - return out diff --git a/frappe/email/doctype/notification/notification.js b/frappe/email/doctype/notification/notification.js index a07dca4870..a149aacd57 100644 --- a/frappe/email/doctype/notification/notification.js +++ b/frappe/email/doctype/notification/notification.js @@ -176,7 +176,7 @@ frappe.ui.form.on("Notification", { }, callback: function (r) { if (r.message && r.message.length > 0) { - frappe.msgprint(r.message); + frappe.msgprint(r.message.toString()); } else { frappe.msgprint(__("No alerts for today")); } diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index 9f9aca1123..534e3c1ac7 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta from urllib.parse import quote +from zoneinfo import ZoneInfo import google.oauth2.credentials import requests @@ -274,7 +275,7 @@ def sync_events_from_google_calendar(g_calendar, method=None): if err.resp.status == 410: set_encrypted_password("Google Calendar", account.name, "", "next_sync_token") frappe.db.commit() - msg += " " + _("Sync token was invalid and has been resetted, Retry syncing.") + msg += " " + _("Sync token was invalid and has been reset, Retry syncing.") frappe.msgprint(msg, title="Invalid Sync Token", indicator="blue") else: frappe.throw(msg) @@ -356,6 +357,7 @@ def insert_event_to_calendar(account, event, recurrence=None): "google_calendar": account.name, "google_calendar_id": account.google_calendar_id, "google_calendar_event_id": event.get("id"), + "google_meet_link": event.get("hangoutLink"), "pulled_from_google_calendar": 1, } calendar_event.update( @@ -373,6 +375,7 @@ def update_event_in_calendar(account, event, recurrence=None): calendar_event = frappe.get_doc("Event", {"google_calendar_event_id": event.get("id")}) calendar_event.subject = event.get("summary") calendar_event.description = event.get("description") + calendar_event.google_meet_link = event.get("hangoutLink") calendar_event.update( google_calendar_to_repeat_on( recurrence=recurrence, start=event.get("start"), end=event.get("end") @@ -407,11 +410,30 @@ def insert_event_in_google_calendar(doc, method=None): if doc.repeat_on: event.update({"recurrence": repeat_on_to_google_calendar_recurrence_rule(doc)}) + event.update({"attendees": get_attendees(doc)}) + + conference_data_version = 0 + + if doc.add_video_conferencing: + event.update({"conferenceData": get_conference_data(doc)}) + conference_data_version = 1 + try: - event = google_calendar.events().insert(calendarId=doc.google_calendar_id, body=event).execute() - frappe.db.set_value( - "Event", doc.name, "google_calendar_event_id", event.get("id"), update_modified=False + event = ( + google_calendar.events() + .insert( + calendarId=doc.google_calendar_id, body=event, conferenceDataVersion=conference_data_version + ) + .execute() ) + + frappe.db.set_value( + "Event", + doc.name, + {"google_calendar_event_id": event.get("id"), "google_meet_link": event.get("hangoutLink")}, + update_modified=False, + ) + frappe.msgprint(_("Event Synced with Google Calendar.")) except HttpError as err: frappe.throw( @@ -450,6 +472,7 @@ def update_event_in_google_calendar(doc, method=None): .get(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id) .execute() ) + event["summary"] = doc.subject event["description"] = doc.description event["recurrence"] = repeat_on_to_google_calendar_recurrence_rule(doc) @@ -462,9 +485,38 @@ def update_event_in_google_calendar(doc, method=None): ) ) - google_calendar.events().update( - calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id, body=event - ).execute() + conference_data_version = 0 + + if doc.add_video_conferencing: + event.update({"conferenceData": get_conference_data(doc)}) + conference_data_version = 1 + elif doc.get_doc_before_save().add_video_conferencing or event.get("hangoutLink"): + # remove google meet from google calendar event, if turning off add_video_conferencing + event.update({"conferenceData": None}) + conference_data_version = 1 + + event.update({"attendees": get_attendees(doc)}) + + event = ( + google_calendar.events() + .update( + calendarId=doc.google_calendar_id, + eventId=doc.google_calendar_event_id, + body=event, + conferenceDataVersion=conference_data_version, + ) + .execute() + ) + + # if add_video_conferencing enabled or disabled during update, overwrite + frappe.db.set_value( + "Event", + doc.name, + {"google_meet_link": event.get("hangoutLink")}, + update_modified=False, + ) + doc.notify_update() + frappe.msgprint(_("Event Synced with Google Calendar.")) except HttpError as err: frappe.throw( @@ -515,12 +567,20 @@ def google_calendar_to_repeat_on(start, end, recurrence=None): Both have been mapped in a dict for easier mapping. """ repeat_on = { - "starts_on": get_datetime(start.get("date")) - if start.get("date") - else parser.parse(start.get("dateTime")).astimezone().replace(tzinfo=None), - "ends_on": get_datetime(end.get("date")) - if end.get("date") - else parser.parse(end.get("dateTime")).astimezone().replace(tzinfo=None), + "starts_on": ( + get_datetime(start.get("date")) + if start.get("date") + else parser.parse(start.get("dateTime")) + .astimezone(ZoneInfo(get_time_zone())) + .replace(tzinfo=None) + ), + "ends_on": ( + get_datetime(end.get("date")) + if end.get("date") + else parser.parse(end.get("dateTime")) + .astimezone(ZoneInfo(get_time_zone())) + .replace(tzinfo=None) + ), "all_day": 1 if start.get("date") else 0, "repeat_this_event": 1 if recurrence else 0, "repeat_on": None, @@ -682,6 +742,39 @@ def get_recurrence_parameters(recurrence): return frequency, until, byday +def get_conference_data(doc): + return { + "createRequest": {"requestId": doc.name, "conferenceSolutionKey": {"type": "hangoutsMeet"}}, + "notes": doc.description, + } + + +def get_attendees(doc): + """ + Returns a list of dicts with attendee emails, if available in event_participants table + """ + attendees, email_not_found = [], [] + + for participant in doc.event_participants: + if participant.get("email"): + attendees.append({"email": participant.email}) + else: + email_not_found.append( + {"dt": participant.reference_doctype, "dn": participant.reference_docname} + ) + + if email_not_found: + frappe.msgprint( + _("Google Calendar - Contact / email not found. Did not add attendee for -
{0}").format( + "
".join(f"{d.get('dt')} {d.get('dn')}" for d in email_not_found) + ), + alert=True, + indicator="yellow", + ) + + return attendees + + """API Response { 'kind': 'calendar#events', @@ -721,6 +814,32 @@ def get_recurrence_parameters(recurrence): 'recurrence': *recurrence, 'iCalUID': 'uid', 'sequence': 1, + 'hangoutLink': 'https://meet.google.com/mee-ting-uri', + 'conferenceData': { + 'createRequest': { + 'requestId': 'EV00001', + 'conferenceSolutionKey': { + 'type': 'hangoutsMeet' + }, + 'status': { + 'statusCode': 'success' + } + }, + 'entryPoints': [ + { + 'entryPointType': 'video', + 'uri': 'https://meet.google.com/mee-ting-uri', + 'label': 'meet.google.com/mee-ting-uri' + } + ], + 'conferenceSolution': { + 'key': { + 'type': 'hangoutsMeet' + }, + 'name': 'Google Meet', + 'iconUri': 'https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v6/web-512dp/logo_meet_2020q4_color_2x_web_512dp.png' + }, + 'conferenceId': 'mee-ting-uri' 'reminders': { 'useDefault': True } diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index 072b9a1d66..d1120cc22d 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -95,6 +95,10 @@ def delete_doc( update_flags(doc, flags, ignore_permissions) check_permission_and_not_submitted(doc) + # delete custom table fields using this doctype. + frappe.db.delete( + "Custom Field", {"options": name, "fieldtype": ("in", frappe.model.table_fields)} + ) frappe.db.delete("__global_search", {"doctype": name}) delete_from_table(doctype, name, ignore_doctypes, None) diff --git a/frappe/model/document.py b/frappe/model/document.py index 99e51765af..ba38e7f753 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -959,15 +959,19 @@ class Document(BaseDocument): from frappe.email.doctype.notification.notification import evaluate_alert if self.flags.notifications is None: - alerts = frappe.cache().hget("notifications", self.doctype) - if alerts is None: - alerts = frappe.get_all( + + def _get_notifications(): + """returns enabled notifications for the current doctype""" + + return frappe.get_all( "Notification", fields=["name", "event", "method"], filters={"enabled": 1, "document_type": self.doctype}, ) - frappe.cache().hset("notifications", self.doctype, alerts) - self.flags.notifications = alerts + + self.flags.notifications = frappe.cache().hget( + "notifications", self.doctype, _get_notifications + ) if not self.flags.notifications: return @@ -1183,6 +1187,9 @@ class Document(BaseDocument): # to trigger notification on value change self.run_method("before_change") + if self.name is None: + return + frappe.db.set_value( self.doctype, self.name, diff --git a/frappe/patches.txt b/frappe/patches.txt index 24b07012da..278a351093 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -214,3 +214,4 @@ execute:frappe.delete_doc('Page', 'background_jobs', ignore_missing=True, force= frappe.patches.v14_0.drop_unused_indexes frappe.patches.v15_0.drop_modified_index frappe.patches.v14_0.add_manage_subscriptions_in_navbar_settings +frappe.patches.v14_0.update_attachment_comment diff --git a/frappe/patches/v14_0/update_attachment_comment.py b/frappe/patches/v14_0/update_attachment_comment.py new file mode 100644 index 0000000000..042579d86d --- /dev/null +++ b/frappe/patches/v14_0/update_attachment_comment.py @@ -0,0 +1,33 @@ +import frappe + + +def execute(): + frappe.db.auto_commit_on_many_writes = 1 + + # Strip everything except link to attachment and icon from comments of type "Attached" + for name, content in frappe.get_all( + "Comment", filters={"comment_type": "Attachment"}, fields=["name", "content"], as_list=True + ): + if not content: + continue + + start = content.find("") + end = content.find("") if end == -1 else end + if end != -1: + content = content[: end + 4] + + frappe.db.set_value("Comment", name, "content", content, update_modified=False) + + # Strip "Removed " from comments of type "Attachment Removed" + for name, content in frappe.get_all( + "Comment", + filters={"comment_type": "Attachment Removed"}, + fields=["name", "content"], + as_list=True, + ): + if content and content.startswith("Removed "): + frappe.db.set_value("Comment", name, "content", content[8:], update_modified=False) diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js index dcf23fee62..5d87c209e9 100644 --- a/frappe/public/js/frappe/form/controls/datetime.js +++ b/frappe/public/js/frappe/form/controls/datetime.js @@ -5,8 +5,10 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co if (!value) { this.datepicker.clear(); return; - } else if (value === "Today") { + } else if (value.toLowerCase() === "today") { value = this.get_now_date(); + } else if (value.toLowerCase() === "now") { + value = frappe.datetime.now_datetime(); } value = this.format_for_input(value); this.$input && this.$input.val(value); diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index fe4b096166..d70da5f030 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -1,7 +1,11 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt import BaseTimeline from "./base_timeline"; -import { get_version_timeline_content } from "./version_timeline_content_builder"; +import { + get_version_timeline_content, + get_user_link, + get_user_message, +} from "./version_timeline_content_builder"; class FormTimeline extends BaseTimeline { make() { @@ -106,54 +110,50 @@ class FormTimeline extends BaseTimeline { render_timeline_items() { super.render_timeline_items(); - this.set_document_info(); + this.add_web_page_view_count(); frappe.utils.bind_actions_with_object(this.timeline_items_wrapper, this); } - set_document_info() { - // TODO: handle creation via automation - const creation = comment_when(this.frm.doc.creation); - let creation_message = frappe.utils.is_current_user(this.frm.doc.owner) - ? __("You created this {0}", [creation], "Form timeline") - : __( - "{0} created this {1}", - [this.get_user_link(this.frm.doc.owner), creation], - "Form timeline" - ); - - const modified = comment_when(this.frm.doc.modified); - let modified_message = frappe.utils.is_current_user(this.frm.doc.modified_by) - ? __("You edited this {0}", [modified], "Form timeline") - : __( - "{0} edited this {1}", - [this.get_user_link(this.frm.doc.modified_by), modified], - "Form timeline" - ); - + add_web_page_view_count() { if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) { - let route = this.frm.doc.route; - frappe.utils.get_page_view_count(route).then((res) => { - let page_view_count_message = __("{0} Page views", [res.message], "Form timeline"); - this.add_timeline_item( - { - content: `${creation_message} • ${modified_message} • ${page_view_count_message}`, - hide_timestamp: true, - }, - true - ); - }); - } else { - this.add_timeline_item( - { - content: `${creation_message} • ${modified_message}`, + 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"), hide_timestamp: true, - }, - true - ); + }); + }); } } + 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") + ), + }; + } + + 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") + ), + }; + } + prepare_timeline_contents() { + this.timeline_items.push(this.get_creation_message()); + this.timeline_items.push(this.get_modified_message()); this.timeline_items.push(...this.get_communication_timeline_contents()); this.timeline_items.push(...this.get_auto_messages_timeline_contents()); this.timeline_items.push(...this.get_comment_timeline_contents()); @@ -172,29 +172,24 @@ class FormTimeline extends BaseTimeline { } } - get_user_link(user) { - const user_display_text = (frappe.user_info(user).fullname || "").bold(); - return frappe.utils.get_form_link("User", user, true, user_display_text); - } - get_view_timeline_contents() { let view_timeline_contents = []; (this.doc_info.views || []).forEach((view) => { const view_time = comment_when(view.creation); - let view_message = frappe.utils.is_current_user(view.owner) - ? __("You viewed this {0}", [view_time], "Form timeline") - : __( - "{0} viewed this {1}", - [this.get_user_link(view.owner), view_time], - "Form timeline" - ); + 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: view_message, + content: timeline_content, hide_timestamp: true, }); }); + return view_timeline_contents; } @@ -337,7 +332,7 @@ class FormTimeline extends BaseTimeline { (this.doc_info.info_logs || []).forEach((info_log) => { info_timeline_contents.push({ creation: info_log.creation, - content: `${this.get_user_link(info_log.owner)} ${info_log.content}`, + content: `${get_user_link(info_log.owner)} ${info_log.content}`, }); }); return info_timeline_contents; @@ -345,45 +340,76 @@ class FormTimeline extends BaseTimeline { get_attachment_timeline_contents() { let attachment_timeline_contents = []; + (this.doc_info.attachment_logs || []).forEach((attachment_log) => { - let is_file_upload = attachment_log.comment_type == "Attachment"; + const is_file_upload = attachment_log.comment_type == "Attachment"; + const user_link = get_user_link(attachment_log.owner); + const filename = attachment_log.content; + const timeline_content = is_file_upload + ? get_user_message( + attachment_log.owner, + __("You attached {0}", [filename], "Form timeline"), + __("{0} attached {1}", [user_link, filename], "Form timeline") + ) + : get_user_message( + attachment_log.owner, + __("You removed attachment {0}", [filename], "Form timeline"), + __("{0} removed attachment {1}", [user_link, filename], "Form timeline") + ); + attachment_timeline_contents.push({ icon: is_file_upload ? "upload" : "delete", icon_size: "sm", creation: attachment_log.creation, - content: `${this.get_user_link(attachment_log.owner)} ${attachment_log.content}`, + content: timeline_content, }); }); + return attachment_timeline_contents; } get_milestone_timeline_contents() { let milestone_timeline_contents = []; + (this.doc_info.milestones || []).forEach((milestone_log) => { + const field = frappe.meta.get_label(this.frm.doctype, milestone_log.track_field); + const value = milestone_log.value.bold(); + const user_link = get_user_link(milestone_log.owner); + const timeline_content = get_user_message( + milestone_log.owner, + __("You changed {0} to {1}", [field, value], "Form timeline"), + __("{0} changed {1} to {2}", [user_link, field, value], "Form timeline") + ); + milestone_timeline_contents.push({ icon: "milestone", creation: milestone_log.creation, - content: __("{0} changed {1} to {2}", [ - this.get_user_link(milestone_log.owner), - frappe.meta.get_label(this.frm.doctype, milestone_log.track_field), - milestone_log.value.bold(), - ]), + content: timeline_content, }); }); + return milestone_timeline_contents; } get_like_timeline_contents() { let like_timeline_contents = []; + (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") + ); + like_timeline_contents.push({ icon: "heart", icon_size: "sm", creation: like_log.creation, - content: __("{0} Liked", [this.get_user_link(like_log.owner)]), + content: timeline_content, title: "Like", }); }); + return like_timeline_contents; } @@ -394,7 +420,7 @@ class FormTimeline extends BaseTimeline { icon: "branch", icon_size: "sm", creation: workflow_log.creation, - content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`, + content: `${get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`, title: "Workflow", }); }); 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 33c87e458a..1912b5928e 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 @@ -30,19 +30,55 @@ function get_version_timeline_content(version_doc, frm) { if (p[0] === "docstatus") { if (p[2] === 1) { let message = updater_reference_link - ? __("{0} submitted this document {1}", [ - get_user_link(version_doc), - updater_reference_link, - ]) - : __("{0} submitted this document", [get_user_link(version_doc)]); + ? get_user_message( + version_doc.owner, + __( + "You submitted this document {0}", + [updater_reference_link], + "Form timeline" + ), + __( + "{0} submitted this document {1}", + [get_user_link(version_doc.owner), updater_reference_link], + "Form timeline" + ) + ) + : get_user_message( + version_doc.owner, + __("You submitted this document", null, "Form timeline"), + __( + "{0} submitted this document", + [get_user_link(version_doc.owner)], + "Form timeline" + ) + ); + out.push(get_version_comment(version_doc, message)); } else if (p[2] === 2) { let message = updater_reference_link - ? __("{0} cancelled this document {1}", [ - get_user_link(version_doc), - updater_reference_link, - ]) - : __("{0} cancelled this document", [get_user_link(version_doc)]); + ? get_user_message( + version_doc.owner, + __( + "You cancelled this document {1}", + [updater_reference_link], + "Form timeline" + ), + __( + "{0} cancelled this document {1}", + [get_user_link(version_doc.owner), updater_reference_link], + "Form timeline" + ) + ) + : get_user_message( + version_doc.owner, + __("You cancelled this document", null, "Form timeline"), + __( + "{0} cancelled this document", + [get_user_link(version_doc.owner)], + "Form timeline" + ) + ); + out.push(get_version_comment(version_doc, message)); } } else { @@ -67,19 +103,28 @@ function get_version_timeline_content(version_doc, frm) { return parts.length < 3; }); if (parts.length) { - let message; - if (updater_reference_link) { - message = __("{0} changed value of {1} {2}", [ - get_user_link(version_doc), - parts.join(", "), - updater_reference_link, - ]); - } else { - message = __("{0} changed value of {1}", [ - get_user_link(version_doc), - parts.join(", "), - ]); - } + let message = updater_reference_link + ? get_user_message( + version_doc.owner, + __("You changed the value of {0} {1}", [ + parts.join(", "), + updater_reference_link, + ]), + __("{0} changed the value of {1} {2}", [ + get_user_link(version_doc.owner), + parts.join(", "), + updater_reference_link, + ]) + ) + : get_user_message( + version_doc.owner, + __("You changed the value of {0}", [parts.join(", ")]), + __("{0} changed the value of {1}", [ + get_user_link(version_doc.owner), + parts.join(", "), + ]) + ); + out.push(get_version_comment(version_doc, message)); } } @@ -120,19 +165,28 @@ function get_version_timeline_content(version_doc, frm) { return parts.length < 3; }); if (parts.length) { - let message; - if (updater_reference_link) { - message = __("{0} changed values for {1} {2}", [ - get_user_link(version_doc), - parts.join(", "), - updater_reference_link, - ]); - } else { - message = __("{0} changed values for {1}", [ - get_user_link(version_doc), - parts.join(", "), - ]); - } + let message = updater_reference_link + ? get_user_message( + version_doc.owner, + __("You changed the values for {0} {1}", [ + parts.join(", "), + updater_reference_link, + ]), + __("{0} changed the values for {1} {2}", [ + get_user_link(version_doc.owner), + parts.join(", "), + updater_reference_link, + ]) + ) + : get_user_message( + version_doc.owner, + __("You changed the values for {0}", [parts.join(", ")]), + __("{0} changed the values for {1}", [ + get_user_link(version_doc.owner), + parts.join(", "), + ]) + ); + out.push(get_version_comment(version_doc, message)); } } @@ -168,7 +222,7 @@ function get_version_timeline_content(version_doc, frm) { } let version_comment = get_version_comment(version_doc, message); - let user_link = get_user_link(version_doc); + let user_link = get_user_link(version_doc.owner); out.push(`${user_link} ${version_comment}`); } } @@ -230,10 +284,13 @@ function format_content_for_timeline(content) { return content.bold(); } -function get_user_link(doc) { - const user = doc.owner; +function get_user_link(user) { const user_display_text = (frappe.user_info(user).fullname || "").bold(); return frappe.utils.get_form_link("User", user, true, user_display_text); } -export { get_version_timeline_content }; +function get_user_message(user, message_self, message_other) { + return frappe.utils.is_current_user(user) ? message_self : message_other; +} + +export { get_version_timeline_content, get_user_link, get_user_message }; diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 8da073ff59..2ffba97efa 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -52,9 +52,7 @@ export default class Grid { } allow_on_grid_editing() { - if (frappe.utils.is_xs()) { - return false; - } else if ((this.meta && this.meta.editable_grid) || !this.meta) { + if ((this.meta && this.meta.editable_grid) || !this.meta) { return true; } else { return false; @@ -66,17 +64,19 @@ export default class Grid {

-
-
-
-
-
- Grid Empty State - ${__("No Data")} +
+
+
+
+
+
+ Grid Empty State + ${__("No Data")} +
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 65f7be3dd1..4a30ad68e0 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -254,7 +254,7 @@ export default class GridRow { ).appendTo(this.row); this.row_index = $( - ` -
+ {%- if browse_by_category -%} - - +
+ + +
{%- endif -%}
-
+
{% if not result -%}
{{ no_result_message or _("Nothing to show") }} @@ -54,10 +55,10 @@ {% block script %}