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 {