@@ -41,7 +41,7 @@ jobs: | |||||
- name: Get release | - name: Get release | ||||
id: get_release | id: get_release | ||||
uses: bruceadams/get-release@v1.2.3 | |||||
uses: bruceadams/get-release@v1.3.1 | |||||
- name: Upload built Assets to Release | - name: Upload built Assets to Release | ||||
uses: actions/upload-release-asset@v1.0.2 | uses: actions/upload-release-asset@v1.0.2 | ||||
@@ -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", () => { | 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 | //Navigating inside the Attachments folder | ||||
cy.wait(500); | |||||
cy.get('[title="Attachments"] > span').click(); | cy.get('[title="Attachments"] > span').click(); | ||||
//To check if the URL formed after visiting the attachments folder is correct | //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"); | cy.click_modal_primary_button("Create"); | ||||
//Navigating inside the added folder in the Attachments folder | //Navigating inside the added folder in the Attachments folder | ||||
cy.wait(500); | |||||
cy.get('[title="Test Folder"] > span').click(); | cy.get('[title="Test Folder"] > span').click(); | ||||
//To check if the URL is correct after visiting the Test Folder | //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"); | cy.click_modal_primary_button("Upload"); | ||||
//To check if the added file is present in the Test Folder | //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-container").eq(0).should("contain.text", "72402.jpg"); | ||||
cy.get(".list-row-checkbox").eq(0).click(); | cy.get(".list-row-checkbox").eq(0).click(); | ||||
@@ -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"); | |||||
}); | |||||
}); |
@@ -130,12 +130,18 @@ def clear_doctype_cache(doctype=None): | |||||
clear_single(doctype) | clear_single(doctype) | ||||
# clear all parent doctypes | # clear all parent doctypes | ||||
for dt in frappe.get_all( | for dt in frappe.get_all( | ||||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype) | "DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype) | ||||
): | ): | ||||
clear_single(dt.parent) | 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 | # clear all notifications | ||||
delete_notification_count_for(doctype) | delete_notification_count_for(doctype) | ||||
@@ -270,7 +270,7 @@ def delete(doctype, name): | |||||
:param doctype: DocType of the document to be deleted | :param doctype: DocType of the document to be deleted | ||||
:param name: name 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"]) | @frappe.whitelist(methods=["POST", "PUT"]) | ||||
@@ -462,3 +462,24 @@ def insert_doc(doc) -> "Document": | |||||
return parent | return parent | ||||
return frappe.get_doc(doc).insert() | 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) |
@@ -2,6 +2,7 @@ | |||||
"actions": [], | "actions": [], | ||||
"allow_import": 1, | "allow_import": 1, | ||||
"creation": "2013-01-29 10:47:14", | "creation": "2013-01-29 10:47:14", | ||||
"default_view": "Inbox", | |||||
"description": "Keeps track of all communications", | "description": "Keeps track of all communications", | ||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"document_type": "Setup", | "document_type": "Setup", | ||||
@@ -198,7 +199,6 @@ | |||||
"label": "More Information" | "label": "More Information" | ||||
}, | }, | ||||
{ | { | ||||
"bold": 0, | |||||
"default": "Now", | "default": "Now", | ||||
"fieldname": "communication_date", | "fieldname": "communication_date", | ||||
"fieldtype": "Datetime", | "fieldtype": "Datetime", | ||||
@@ -395,7 +395,7 @@ | |||||
"icon": "fa fa-comment", | "icon": "fa fa-comment", | ||||
"idx": 1, | "idx": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2022-03-30 11:24:25.728637", | |||||
"modified": "2022-05-09 00:13:45.310564", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "Communication", | "name": "Communication", | ||||
@@ -454,8 +454,9 @@ | |||||
"sender_field": "sender", | "sender_field": "sender", | ||||
"sort_field": "modified", | "sort_field": "modified", | ||||
"sort_order": "DESC", | "sort_order": "DESC", | ||||
"states": [], | |||||
"subject_field": "subject", | "subject_field": "subject", | ||||
"title_field": "subject", | "title_field": "subject", | ||||
"track_changes": 1, | "track_changes": 1, | ||||
"track_seen": 1 | "track_seen": 1 | ||||
} | |||||
} |
@@ -55,6 +55,7 @@ frappe.ui.form.on("DocType", { | |||||
if (frm.is_new()) { | if (frm.is_new()) { | ||||
frm.events.set_default_permission(frm); | frm.events.set_default_permission(frm); | ||||
frm.set_value("default_view", "List"); | |||||
} else { | } else { | ||||
frm.toggle_enable("engine", 0); | frm.toggle_enable("engine", 0); | ||||
} | } | ||||
@@ -66,12 +67,14 @@ frappe.ui.form.on("DocType", { | |||||
frm.cscript.autoname(frm); | frm.cscript.autoname(frm); | ||||
frm.cscript.set_naming_rule_description(frm); | frm.cscript.set_naming_rule_description(frm); | ||||
frm.trigger("setup_default_views"); | |||||
}, | }, | ||||
istable: (frm) => { | istable: (frm) => { | ||||
if (frm.doc.istable && frm.is_new()) { | if (frm.doc.istable && frm.is_new()) { | ||||
frm.set_value("autoname", "autoincrement"); | frm.set_value("autoname", "autoincrement"); | ||||
frm.set_value("allow_rename", 0); | frm.set_value("allow_rename", 0); | ||||
frm.set_value("default_view", null); | |||||
} else if (!frm.doc.istable && !frm.is_new()) { | } else if (!frm.doc.istable && !frm.is_new()) { | ||||
frm.events.set_default_permission(frm); | frm.events.set_default_permission(frm); | ||||
} | } | ||||
@@ -82,6 +85,18 @@ frappe.ui.form.on("DocType", { | |||||
frm.add_child("permissions", { role: "System Manager" }); | 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", { | frappe.ui.form.on("DocField", { | ||||
@@ -171,6 +186,10 @@ frappe.ui.form.on("DocField", { | |||||
fieldtype: function (frm) { | fieldtype: function (frm) { | ||||
frm.trigger("max_attachments"); | frm.trigger("max_attachments"); | ||||
}, | }, | ||||
fields_add: (frm) => { | |||||
frm.trigger("setup_default_views"); | |||||
}, | |||||
}); | }); | ||||
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm })); | extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm })); |
@@ -14,6 +14,7 @@ | |||||
"istable", | "istable", | ||||
"issingle", | "issingle", | ||||
"is_tree", | "is_tree", | ||||
"is_calendar_and_gantt", | |||||
"editable_grid", | "editable_grid", | ||||
"quick_entry", | "quick_entry", | ||||
"cb01", | "cb01", | ||||
@@ -53,6 +54,8 @@ | |||||
"default_print_format", | "default_print_format", | ||||
"sort_field", | "sort_field", | ||||
"sort_order", | "sort_order", | ||||
"default_view", | |||||
"force_re_route_to_default_view", | |||||
"column_break_29", | "column_break_29", | ||||
"document_type", | "document_type", | ||||
"icon", | "icon", | ||||
@@ -606,6 +609,24 @@ | |||||
"fieldname": "make_attachments_public", | "fieldname": "make_attachments_public", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Make Attachments Public by Default" | "label": "Make Attachments Public by Default" | ||||
}, | |||||
{ | |||||
"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", | "icon": "fa fa-bolt", | ||||
@@ -688,7 +709,7 @@ | |||||
"link_fieldname": "reference_doctype" | "link_fieldname": "reference_doctype" | ||||
} | } | ||||
], | ], | ||||
"modified": "2022-09-02 12:05:59.589751", | |||||
"modified": "2022-10-12 14:13:27.315351", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "DocType", | "name": "DocType", | ||||
@@ -670,6 +670,18 @@ class TestDocType(FrappeTestCase): | |||||
self.assertEqual(test_json.test_json_field["hello"], "world") | 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}) | @patch.dict(frappe.conf, {"developer_mode": 1}) | ||||
def test_delete_doctype_with_customization(self): | def test_delete_doctype_with_customization(self): | ||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter | from frappe.custom.doctype.property_setter.property_setter import make_property_setter | ||||
@@ -2,6 +2,7 @@ | |||||
"actions": [], | "actions": [], | ||||
"allow_import": 1, | "allow_import": 1, | ||||
"creation": "2012-12-12 11:19:22", | "creation": "2012-12-12 11:19:22", | ||||
"default_view": "File", | |||||
"doctype": "DocType", | "doctype": "DocType", | ||||
"engine": "InnoDB", | "engine": "InnoDB", | ||||
"field_order": [ | "field_order": [ | ||||
@@ -169,10 +170,11 @@ | |||||
"read_only": 1 | "read_only": 1 | ||||
} | } | ||||
], | ], | ||||
"force_re_route_to_default_view": 1, | |||||
"icon": "fa fa-file", | "icon": "fa fa-file", | ||||
"idx": 1, | "idx": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2022-09-13 15:50:15.508250", | |||||
"modified": "2022-09-13 15:50:15.508251", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Core", | "module": "Core", | ||||
"name": "File", | "name": "File", | ||||
@@ -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: [], | |||||
}; |
@@ -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" | |||||
} | |||||
] | |||||
} |
@@ -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 |
@@ -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)) |
@@ -72,6 +72,7 @@ frappe.ui.form.on("Customize Form", { | |||||
} else { | } else { | ||||
frm.refresh(); | frm.refresh(); | ||||
frm.trigger("setup_sortable"); | frm.trigger("setup_sortable"); | ||||
frm.trigger("setup_default_views"); | |||||
} | } | ||||
} | } | ||||
localStorage["customize_doctype"] = frm.doc.doc_type; | 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) { | setup_sortable: function (frm) { | ||||
frm.doc.fields.forEach(function (f, i) { | |||||
frm.doc.fields.forEach(function (f) { | |||||
if (!f.is_custom_field) { | if (!f.is_custom_field) { | ||||
f._sortable = false; | f._sortable = false; | ||||
} | } | ||||
@@ -222,6 +227,10 @@ frappe.ui.form.on("Customize Form", { | |||||
frm.set_df_property("sort_field", "options", fields); | 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 | // can't delete standard fields | ||||
@@ -237,6 +246,7 @@ frappe.ui.form.on("Customize Form Field", { | |||||
var f = frappe.model.get_doc(cdt, cdn); | var f = frappe.model.get_doc(cdt, cdn); | ||||
f.is_system_generated = false; | f.is_system_generated = false; | ||||
f.is_custom_field = true; | f.is_custom_field = true; | ||||
frm.trigger("setup_default_views"); | |||||
}, | }, | ||||
}); | }); | ||||
@@ -13,6 +13,7 @@ | |||||
"search_fields", | "search_fields", | ||||
"column_break_5", | "column_break_5", | ||||
"istable", | "istable", | ||||
"is_calendar_and_gantt", | |||||
"editable_grid", | "editable_grid", | ||||
"quick_entry", | "quick_entry", | ||||
"track_changes", | "track_changes", | ||||
@@ -35,6 +36,8 @@ | |||||
"show_title_field_in_link", | "show_title_field_in_link", | ||||
"translated_doctype", | "translated_doctype", | ||||
"default_print_format", | "default_print_format", | ||||
"default_view", | |||||
"force_re_route_to_default_view", | |||||
"column_break_29", | "column_break_29", | ||||
"show_preview_popup", | "show_preview_popup", | ||||
"email_settings_section", | "email_settings_section", | ||||
@@ -337,6 +340,25 @@ | |||||
"fieldname": "make_attachments_public", | "fieldname": "make_attachments_public", | ||||
"fieldtype": "Check", | "fieldtype": "Check", | ||||
"label": "Make Attachments Public by Default" | "label": "Make Attachments Public by Default" | ||||
}, | |||||
{ | |||||
"fieldname": "default_view", | |||||
"fieldtype": "Select", | |||||
"label": "Default View" | |||||
}, | |||||
{ | |||||
"default": "0", | |||||
"depends_on": "default_view", | |||||
"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" | |||||
} | } | ||||
], | ], | ||||
"hide_toolbar": 1, | "hide_toolbar": 1, | ||||
@@ -345,7 +367,7 @@ | |||||
"index_web_pages_for_search": 1, | "index_web_pages_for_search": 1, | ||||
"issingle": 1, | "issingle": 1, | ||||
"links": [], | "links": [], | ||||
"modified": "2022-08-24 06:57:47.966331", | |||||
"modified": "2022-08-30 11:45:16.772277", | |||||
"modified_by": "Administrator", | "modified_by": "Administrator", | ||||
"module": "Custom", | "module": "Custom", | ||||
"name": "Customize Form", | "name": "Customize Form", | ||||
@@ -586,6 +586,10 @@ doctype_properties = { | |||||
"naming_rule": "Data", | "naming_rule": "Data", | ||||
"autoname": "Data", | "autoname": "Data", | ||||
"show_title_field_in_link": "Check", | "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", | "translated_doctype": "Check", | ||||
} | } | ||||
@@ -17,10 +17,10 @@ frappe.listview_settings["ToDo"] = { | |||||
return doc.reference_name; | return doc.reference_name; | ||||
}, | }, | ||||
get_label: function () { | get_label: function () { | ||||
return __("Open"); | |||||
return __("Open", null, "Access"); | |||||
}, | }, | ||||
get_description: function (doc) { | 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) { | action: function (doc) { | ||||
frappe.set_route("Form", doc.reference_type, doc.reference_name); | frappe.set_route("Form", doc.reference_type, doc.reference_name); | ||||
@@ -4,6 +4,7 @@ | |||||
from datetime import datetime, timedelta | from datetime import datetime, timedelta | ||||
from urllib.parse import quote | from urllib.parse import quote | ||||
from zoneinfo import ZoneInfo | |||||
import google.oauth2.credentials | import google.oauth2.credentials | ||||
import requests | import requests | ||||
@@ -515,12 +516,20 @@ def google_calendar_to_repeat_on(start, end, recurrence=None): | |||||
Both have been mapped in a dict for easier mapping. | Both have been mapped in a dict for easier mapping. | ||||
""" | """ | ||||
repeat_on = { | 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, | "all_day": 1 if start.get("date") else 0, | ||||
"repeat_this_event": 1 if recurrence else 0, | "repeat_this_event": 1 if recurrence else 0, | ||||
"repeat_on": None, | "repeat_on": None, | ||||
@@ -95,6 +95,10 @@ def delete_doc( | |||||
update_flags(doc, flags, ignore_permissions) | update_flags(doc, flags, ignore_permissions) | ||||
check_permission_and_not_submitted(doc) | 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}) | frappe.db.delete("__global_search", {"doctype": name}) | ||||
delete_from_table(doctype, name, ignore_doctypes, None) | delete_from_table(doctype, name, ignore_doctypes, None) | ||||
@@ -953,15 +953,19 @@ class Document(BaseDocument): | |||||
from frappe.email.doctype.notification.notification import evaluate_alert | from frappe.email.doctype.notification.notification import evaluate_alert | ||||
if self.flags.notifications is None: | 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", | "Notification", | ||||
fields=["name", "event", "method"], | fields=["name", "event", "method"], | ||||
filters={"enabled": 1, "document_type": self.doctype}, | 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: | if not self.flags.notifications: | ||||
return | return | ||||
@@ -1173,6 +1177,9 @@ class Document(BaseDocument): | |||||
# to trigger notification on value change | # to trigger notification on value change | ||||
self.run_method("before_change") | self.run_method("before_change") | ||||
if self.name is None: | |||||
return | |||||
frappe.db.set_value( | frappe.db.set_value( | ||||
self.doctype, | self.doctype, | ||||
self.name, | self.name, | ||||
@@ -5,8 +5,10 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co | |||||
if (!value) { | if (!value) { | ||||
this.datepicker.clear(); | this.datepicker.clear(); | ||||
return; | return; | ||||
} else if (value === "Today") { | |||||
} else if (value.toLowerCase() === "today") { | |||||
value = this.get_now_date(); | value = this.get_now_date(); | ||||
} else if (value.toLowerCase() === "now") { | |||||
value = frappe.datetime.now_datetime(); | |||||
} | } | ||||
value = this.format_for_input(value); | value = this.format_for_input(value); | ||||
this.$input && this.$input.val(value); | this.$input && this.$input.val(value); | ||||
@@ -196,7 +196,7 @@ frappe.views.BaseList = class BaseList { | |||||
Map: "map", | Map: "map", | ||||
}; | }; | ||||
if (frappe.boot.desk_settings.view_switcher) { | |||||
if (frappe.boot.desk_settings.view_switcher && !this.meta.force_re_route_to_default_view) { | |||||
/* @preserve | /* @preserve | ||||
for translation, don't remove | for translation, don't remove | ||||
__("List View") __("Report View") __("Dashboard View") __("Gantt View"), | __("List View") __("Report View") __("Dashboard View") __("Gantt View"), | ||||
@@ -175,6 +175,7 @@ frappe.render_template = function (name, data) { | |||||
w.document.write(tree); | w.document.write(tree); | ||||
w.document.close(); | w.document.close(); | ||||
}); | }); | ||||
frappe.render_pdf = function (html, opts = {}) { | frappe.render_pdf = function (html, opts = {}) { | ||||
//Create a form to place the HTML content | //Create a form to place the HTML content | ||||
var formData = new FormData(); | var formData = new FormData(); | ||||
@@ -197,8 +198,17 @@ frappe.render_pdf = function (html, opts = {}) { | |||||
var blob = new Blob([success.currentTarget.response], { type: "application/pdf" }); | var blob = new Blob([success.currentTarget.response], { type: "application/pdf" }); | ||||
var objectUrl = URL.createObjectURL(blob); | var objectUrl = URL.createObjectURL(blob); | ||||
//Open report in a new window | |||||
window.open(objectUrl); | |||||
// Create a hidden a tag to force set report name | |||||
// https://stackoverflow.com/questions/19327749/javascript-blob-filename-without-link | |||||
let hidden_a_tag = document.createElement("a"); | |||||
document.body.appendChild(hidden_a_tag); | |||||
hidden_a_tag.style = "display: none"; | |||||
hidden_a_tag.href = objectUrl; | |||||
hidden_a_tag.download = opts.report_name || "report.pdf"; | |||||
// Open report in a new window | |||||
hidden_a_tag.click(); | |||||
window.URL.revokeObjectURL(objectUrl); | |||||
} | } | ||||
}; | }; | ||||
xhr.send(formData); | xhr.send(formData); | ||||
@@ -349,7 +349,7 @@ $.extend(frappe.model, { | |||||
is_tree: function (doctype) { | is_tree: function (doctype) { | ||||
if (!doctype) return false; | if (!doctype) return false; | ||||
return frappe.boot.treeviews.indexOf(doctype) != -1; | |||||
return locals.DocType[doctype] && locals.DocType[doctype].is_tree; | |||||
}, | }, | ||||
is_fresh(doc) { | is_fresh(doc) { | ||||
@@ -754,6 +754,42 @@ $.extend(frappe.model, { | |||||
} | } | ||||
return frappe.model.numeric_fieldtypes.includes(fieldtype); | return frappe.model.numeric_fieldtypes.includes(fieldtype); | ||||
}, | }, | ||||
set_default_views_for_doctype(doctype, frm) { | |||||
frappe.model.with_doctype(doctype, () => { | |||||
let meta = frappe.get_meta(doctype); | |||||
let default_views = ["List", "Report", "Dashboard", "Kanban"]; | |||||
if (meta.is_calendar_and_gantt && frappe.views.calendar[doctype]) { | |||||
let views = ["Calendar", "Gantt"]; | |||||
default_views.push(...views); | |||||
} | |||||
if (meta.is_tree) { | |||||
default_views.push("Tree"); | |||||
} | |||||
if (frm.doc.image_field) { | |||||
default_views.push("Image"); | |||||
} | |||||
if (doctype === "Communication" && frappe.boot.email_accounts.length) { | |||||
default_views.push("Inbox"); | |||||
} | |||||
if ( | |||||
(frm.doc.fields.find((i) => i.fieldname === "latitude") && | |||||
frm.doc.fields.find((i) => i.fieldname === "longitude")) || | |||||
frm.doc.fields.find( | |||||
(i) => i.fieldname === "location" && i.fieldtype == "Geolocation" | |||||
) | |||||
) { | |||||
default_views.push("Map"); | |||||
} | |||||
frm.set_df_property("default_view", "options", default_views); | |||||
}); | |||||
}, | |||||
}); | }); | ||||
// legacy | // legacy | ||||
@@ -88,7 +88,21 @@ frappe.router = { | |||||
"dashboard", | "dashboard", | ||||
"image", | "image", | ||||
"inbox", | "inbox", | ||||
"map", | |||||
], | ], | ||||
list_views_route: { | |||||
list: "List", | |||||
kanban: "Kanban", | |||||
report: "Report", | |||||
calendar: "Calendar", | |||||
tree: "Tree", | |||||
gantt: "Gantt", | |||||
dashboard: "Dashboard", | |||||
image: "Image", | |||||
inbox: "Inbox", | |||||
file: "Home", | |||||
map: "Map", | |||||
}, | |||||
layout_mapped: {}, | layout_mapped: {}, | ||||
is_app_route(path) { | is_app_route(path) { | ||||
@@ -115,7 +129,7 @@ frappe.router = { | |||||
} | } | ||||
}, | }, | ||||
route() { | |||||
async route() { | |||||
// resolve the route from the URL or hash | // resolve the route from the URL or hash | ||||
// translate it so the objects are well defined | // translate it so the objects are well defined | ||||
// and render the page as required | // and render the page as required | ||||
@@ -126,22 +140,22 @@ frappe.router = { | |||||
if (this.re_route(sub_path)) return; | if (this.re_route(sub_path)) return; | ||||
this.current_sub_path = sub_path; | this.current_sub_path = sub_path; | ||||
this.current_route = this.parse(); | |||||
this.current_route = await this.parse(); | |||||
this.set_history(sub_path); | this.set_history(sub_path); | ||||
this.render(); | this.render(); | ||||
this.set_title(sub_path); | this.set_title(sub_path); | ||||
this.trigger("change"); | this.trigger("change"); | ||||
}, | }, | ||||
parse(route) { | |||||
async parse(route) { | |||||
route = this.get_sub_path_string(route).split("/"); | route = this.get_sub_path_string(route).split("/"); | ||||
if (!route) return []; | if (!route) return []; | ||||
route = $.map(route, this.decode_component); | route = $.map(route, this.decode_component); | ||||
this.set_route_options_from_url(); | this.set_route_options_from_url(); | ||||
return this.convert_to_standard_route(route); | |||||
return await this.convert_to_standard_route(route); | |||||
}, | }, | ||||
convert_to_standard_route(route) { | |||||
async convert_to_standard_route(route) { | |||||
// /app/settings = ["Workspaces", "Settings"] | // /app/settings = ["Workspaces", "Settings"] | ||||
// /app/private/settings = ["Workspaces", "private", "Settings"] | // /app/private/settings = ["Workspaces", "private", "Settings"] | ||||
// /app/user = ["List", "User"] | // /app/user = ["List", "User"] | ||||
@@ -161,7 +175,7 @@ frappe.router = { | |||||
route = ["Workspaces", "private", frappe.workspaces[private_workspace].title]; | route = ["Workspaces", "private", frappe.workspaces[private_workspace].title]; | ||||
} else if (this.routes[route[0]]) { | } else if (this.routes[route[0]]) { | ||||
// route | // route | ||||
route = this.set_doctype_route(route); | |||||
route = await this.set_doctype_route(route); | |||||
} | } | ||||
return route; | return route; | ||||
@@ -174,36 +188,85 @@ frappe.router = { | |||||
set_doctype_route(route) { | set_doctype_route(route) { | ||||
let doctype_route = this.routes[route[0]]; | let doctype_route = this.routes[route[0]]; | ||||
// doctype route | |||||
if (route[1]) { | |||||
if (route[2] && route[1] === "view") { | |||||
route = this.get_standard_route_for_list(route, doctype_route); | |||||
} else { | |||||
return frappe.model.with_doctype(doctype_route.doctype).then(() => { | |||||
// doctype route | |||||
let meta = frappe.get_meta(doctype_route.doctype); | |||||
if (route[1] && route[1] === "view" && route[2]) { | |||||
route = this.get_standard_route_for_list( | |||||
route, | |||||
doctype_route, | |||||
meta.force_re_route_to_default_view && meta.default_view | |||||
? meta.default_view | |||||
: null | |||||
); | |||||
} else if (route[1] && route[1] !== "view" && !route[2]) { | |||||
let docname = route[1]; | let docname = route[1]; | ||||
if (route.length > 2) { | if (route.length > 2) { | ||||
docname = route.slice(1).join("/"); | docname = route.slice(1).join("/"); | ||||
} | } | ||||
route = ["Form", doctype_route.doctype, docname]; | route = ["Form", doctype_route.doctype, docname]; | ||||
} else if (frappe.model.is_single(doctype_route.doctype)) { | |||||
route = ["Form", doctype_route.doctype, doctype_route.doctype]; | |||||
} else if (meta.default_view) { | |||||
route = [ | |||||
"List", | |||||
doctype_route.doctype, | |||||
this.list_views_route[meta.default_view.toLowerCase()], | |||||
]; | |||||
} else { | |||||
route = ["List", doctype_route.doctype, "List"]; | |||||
} | } | ||||
} else if (frappe.model.is_single(doctype_route.doctype)) { | |||||
route = ["Form", doctype_route.doctype, doctype_route.doctype]; | |||||
} else { | |||||
route = ["List", doctype_route.doctype, "List"]; | |||||
} | |||||
// reset the layout to avoid using incorrect views | |||||
this.doctype_layout = doctype_route.doctype_layout; | |||||
return route; | |||||
// reset the layout to avoid using incorrect views | |||||
this.doctype_layout = doctype_route.doctype_layout; | |||||
return route; | |||||
}); | |||||
}, | }, | ||||
get_standard_route_for_list(route, doctype_route) { | |||||
get_standard_route_for_list(route, doctype_route, default_view) { | |||||
let standard_route; | let standard_route; | ||||
if (route[2].toLowerCase() === "tree") { | |||||
let _route = default_view || route[2] || ""; | |||||
if (_route.toLowerCase() === "tree") { | |||||
standard_route = ["Tree", doctype_route.doctype]; | standard_route = ["Tree", doctype_route.doctype]; | ||||
} else { | } else { | ||||
standard_route = ["List", doctype_route.doctype, frappe.utils.to_title_case(route[2])]; | |||||
let new_route = this.list_views_route[_route.toLowerCase()]; | |||||
let re_route = route[2].toLowerCase() !== new_route.toLowerCase(); | |||||
if (re_route) { | |||||
/** | |||||
* In case of force_re_route, the url of the route should change, | |||||
* if the _route and route[2] are different, it means there is a default_view | |||||
* with force_re_route enabled. | |||||
* | |||||
* To change the url, to the correct view, the route[2] is changed with default_view | |||||
* | |||||
* Eg: If default_view is set to Report with force_re_route enabled and user routes | |||||
* to List, | |||||
* route: [todo, view, list] | |||||
* default_view: report | |||||
* | |||||
* replaces the list to report and re-routes to the new route but should be replaced in | |||||
* the history since the list route should not exist in history as we are rerouting it to | |||||
* report | |||||
*/ | |||||
frappe.route_flags.replace_route = true; | |||||
route[2] = _route.toLowerCase(); | |||||
this.set_route(route); | |||||
} | |||||
standard_route = [ | |||||
"List", | |||||
doctype_route.doctype, | |||||
this.list_views_route[_route.toLowerCase()], | |||||
]; | |||||
// calendar / kanban / dashboard / folder | // calendar / kanban / dashboard / folder | ||||
if (route[3]) standard_route.push(...route.slice(3, route.length)); | if (route[3]) standard_route.push(...route.slice(3, route.length)); | ||||
} | } | ||||
return standard_route; | return standard_route; | ||||
}, | }, | ||||
@@ -345,6 +408,7 @@ frappe.router = { | |||||
} else if (view === "tree") { | } else if (view === "tree") { | ||||
new_route = [this.slug(route[1]), "view", "tree"]; | new_route = [this.slug(route[1]), "view", "tree"]; | ||||
} | } | ||||
return new_route; | return new_route; | ||||
}, | }, | ||||
@@ -208,13 +208,10 @@ frappe.search.utils = { | |||||
}, | }, | ||||
}); | }); | ||||
} | } | ||||
if (in_list(frappe.boot.treeviews, item)) { | |||||
out.push(option("Tree", ["Tree", item], 0.05)); | |||||
} else { | |||||
out.push(option("List", ["List", item], 0.05)); | |||||
if (frappe.model.can_get_report(item)) { | |||||
out.push(option("Report", ["List", item, "Report"], 0.04)); | |||||
} | |||||
out.push(option("List", ["List", item], 0.05)); | |||||
if (frappe.model.can_get_report(item)) { | |||||
out.push(option("Report", ["List", item, "Report"], 0.04)); | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -1260,20 +1260,12 @@ Object.assign(frappe.utils, { | |||||
if (frappe.model.is_single(item.doctype)) { | if (frappe.model.is_single(item.doctype)) { | ||||
route = doctype_slug; | route = doctype_slug; | ||||
} else { | } else { | ||||
if (!item.doc_view) { | |||||
if (frappe.model.is_tree(item.doctype)) { | |||||
item.doc_view = "Tree"; | |||||
} else { | |||||
item.doc_view = "List"; | |||||
} | |||||
} | |||||
switch (item.doc_view) { | switch (item.doc_view) { | ||||
case "List": | case "List": | ||||
if (item.filters) { | if (item.filters) { | ||||
frappe.route_options = item.filters; | frappe.route_options = item.filters; | ||||
} | } | ||||
route = doctype_slug; | |||||
route = `${doctype_slug}/view/list`; | |||||
break; | break; | ||||
case "Tree": | case "Tree": | ||||
route = `${doctype_slug}/view/tree`; | route = `${doctype_slug}/view/tree`; | ||||
@@ -1290,12 +1282,11 @@ Object.assign(frappe.utils, { | |||||
case "Calendar": | case "Calendar": | ||||
route = `${doctype_slug}/view/calendar/default`; | route = `${doctype_slug}/view/calendar/default`; | ||||
break; | break; | ||||
case "Kanban": | |||||
route = `${doctype_slug}/view/kanban`; | |||||
break; | |||||
default: | default: | ||||
frappe.throw({ | |||||
message: __("Not a valid view:") + item.doc_view, | |||||
title: __("Unknown View"), | |||||
}); | |||||
route = ""; | |||||
route = doctype_slug; | |||||
} | } | ||||
} | } | ||||
} else if (type === "report") { | } else if (type === "report") { | ||||
@@ -144,7 +144,7 @@ frappe.breadcrumbs = { | |||||
} else { | } else { | ||||
let route; | let route; | ||||
const doctype_route = frappe.router.slug(frappe.router.doctype_layout || doctype); | const doctype_route = frappe.router.slug(frappe.router.doctype_layout || doctype); | ||||
if (frappe.boot.treeviews.indexOf(doctype) !== -1) { | |||||
if (doctype_meta.is_tree) { | |||||
let view = frappe.model.user_settings[doctype].last_view || "Tree"; | let view = frappe.model.user_settings[doctype].last_view || "Tree"; | ||||
route = `${doctype_route}/view/${view}`; | route = `${doctype_route}/view/${view}`; | ||||
} else { | } else { | ||||
@@ -74,7 +74,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { | |||||
this.page_title = __("File Manager"); | this.page_title = __("File Manager"); | ||||
const route = frappe.get_route(); | const route = frappe.get_route(); | ||||
this.current_folder = route.slice(2).join("/"); | |||||
this.current_folder = route.slice(2).join("/") || "Home"; | |||||
this.filters = [["File", "folder", "=", this.current_folder, true]]; | this.filters = [["File", "folder", "=", this.current_folder, true]]; | ||||
this.order_by = this.view_user_settings.order_by || "file_name asc"; | this.order_by = this.view_user_settings.order_by || "file_name asc"; | ||||
@@ -286,7 +286,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { | |||||
} | } | ||||
get_breadcrumbs_html() { | get_breadcrumbs_html() { | ||||
const route = frappe.router.parse(); | |||||
const route = frappe.get_route(); | |||||
const folders = route.slice(2); | const folders = route.slice(2); | ||||
return folders | return folders | ||||
@@ -9,14 +9,9 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||||
const doctype = route[1]; | const doctype = route[1]; | ||||
const user_settings = frappe.get_user_settings(doctype)["Kanban"] || {}; | const user_settings = frappe.get_user_settings(doctype)["Kanban"] || {}; | ||||
if (!user_settings.last_kanban_board) { | if (!user_settings.last_kanban_board) { | ||||
frappe.msgprint({ | |||||
title: __("Error"), | |||||
indicator: "red", | |||||
message: __("Missing parameter Kanban Board Name"), | |||||
}); | |||||
frappe.set_route("List", doctype, "List"); | |||||
return true; | |||||
return new frappe.views.KanbanView({ doctype: doctype }); | |||||
} | } | ||||
route.push(user_settings.last_kanban_board); | route.push(user_settings.last_kanban_board); | ||||
frappe.set_route(route); | frappe.set_route(route); | ||||
return true; | return true; | ||||
@@ -28,9 +23,35 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||||
return "Kanban"; | return "Kanban"; | ||||
} | } | ||||
show() { | |||||
frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) => { | |||||
if (!kanbans.length) { | |||||
return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); | |||||
} else if (kanbans.length && frappe.get_route().length !== 4) { | |||||
return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); | |||||
} else { | |||||
this.kanbans = kanbans; | |||||
return frappe.run_serially([ | |||||
() => this.show_skeleton(), | |||||
() => this.fetch_meta(), | |||||
() => this.hide_skeleton(), | |||||
() => this.check_permissions(), | |||||
() => this.init(), | |||||
() => this.before_refresh(), | |||||
() => this.refresh(), | |||||
]); | |||||
} | |||||
}); | |||||
} | |||||
setup_defaults() { | setup_defaults() { | ||||
return super.setup_defaults().then(() => { | return super.setup_defaults().then(() => { | ||||
this.board_name = frappe.get_route()[3]; | |||||
let get_board_name = () => { | |||||
return this.kanbans.length && this.kanbans[0].name; | |||||
}; | |||||
this.board_name = frappe.get_route()[3] || get_board_name() || null; | |||||
this.page_title = __(this.board_name); | this.page_title = __(this.board_name); | ||||
this.card_meta = this.get_card_meta(); | this.card_meta = this.get_card_meta(); | ||||
this.page_length = 0; | this.page_length = 0; | ||||
@@ -143,21 +164,22 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||||
render() { | render() { | ||||
const board_name = this.board_name; | const board_name = this.board_name; | ||||
if (!this.kanban) { | |||||
this.kanban = new frappe.views.KanbanBoard({ | |||||
doctype: this.doctype, | |||||
board: this.board, | |||||
board_name: board_name, | |||||
cards: this.data, | |||||
card_meta: this.card_meta, | |||||
wrapper: this.$result, | |||||
cur_list: this, | |||||
user_settings: this.view_user_settings, | |||||
}); | |||||
} | |||||
if (this.kanban && board_name === this.kanban.board_name) { | if (this.kanban && board_name === this.kanban.board_name) { | ||||
this.kanban.update(this.data); | this.kanban.update(this.data); | ||||
return; | |||||
} | } | ||||
this.kanban = new frappe.views.KanbanBoard({ | |||||
doctype: this.doctype, | |||||
board: this.board, | |||||
board_name: board_name, | |||||
cards: this.data, | |||||
card_meta: this.card_meta, | |||||
wrapper: this.$result, | |||||
cur_list: this, | |||||
user_settings: this.view_user_settings, | |||||
}); | |||||
} | } | ||||
get_card_meta() { | get_card_meta() { | ||||
@@ -1383,6 +1383,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { | |||||
layout_direction: frappe.utils.is_rtl() ? "rtl" : "ltr", | layout_direction: frappe.utils.is_rtl() ? "rtl" : "ltr", | ||||
}); | }); | ||||
let filter_values = [], | |||||
name_len = 0; | |||||
for (var key of Object.keys(applied_filters)) { | |||||
name_len = name_len + applied_filters[key].toString().length; | |||||
if (name_len > 200) break; | |||||
filter_values.push(applied_filters[key]); | |||||
} | |||||
print_settings.report_name = `${__(this.report_name)}_${filter_values.join("_")}.pdf`; | |||||
frappe.render_pdf(html, print_settings); | frappe.render_pdf(html, print_settings); | ||||
} | } | ||||
@@ -37,6 +37,10 @@ frappe.views.TreeFactory = class TreeFactory extends frappe.views.Factory { | |||||
let treeview = frappe.views.trees[route[1]]; | let treeview = frappe.views.trees[route[1]]; | ||||
treeview && treeview.make_tree(); | treeview && treeview.make_tree(); | ||||
} | } | ||||
get view_name() { | |||||
return "Tree"; | |||||
} | |||||
}; | }; | ||||
frappe.views.TreeView = class TreeView { | frappe.views.TreeView = class TreeView { | ||||
@@ -196,6 +200,7 @@ frappe.views.TreeView = class TreeView { | |||||
}); | }); | ||||
cur_tree = this.tree; | cur_tree = this.tree; | ||||
cur_tree.view_name = "Tree"; | |||||
this.post_render(); | this.post_render(); | ||||
} | } | ||||
@@ -384,18 +384,22 @@ class ShortcutDialog extends WidgetDialog { | |||||
onchange: () => { | onchange: () => { | ||||
if (this.dialog.get_value("type") == "DocType") { | if (this.dialog.get_value("type") == "DocType") { | ||||
let doctype = this.dialog.get_value("link_to"); | let doctype = this.dialog.get_value("link_to"); | ||||
if (doctype && frappe.boot.single_types.includes(doctype)) { | |||||
this.hide_filters(); | |||||
} else if (doctype) { | |||||
this.setup_filter(doctype); | |||||
this.show_filters(); | |||||
} | |||||
const views = ["List", "Report Builder", "Dashboard", "New"]; | |||||
if (frappe.boot.treeviews.includes(doctype)) views.push("Tree"); | |||||
if (frappe.boot.calendars.includes(doctype)) views.push("Calendar"); | |||||
this.dialog.set_df_property("doc_view", "options", views.join("\n")); | |||||
frappe.model.with_doctype(doctype, () => { | |||||
let meta = frappe.get_meta(doctype); | |||||
if (doctype && frappe.boot.single_types.includes(doctype)) { | |||||
this.hide_filters(); | |||||
} else if (doctype) { | |||||
this.setup_filter(doctype); | |||||
this.show_filters(); | |||||
} | |||||
const views = ["List", "Report Builder", "Dashboard", "New"]; | |||||
if (meta.is_tree === "Tree") views.push("Tree"); | |||||
if (frappe.boot.calendars.includes(doctype)) views.push("Calendar"); | |||||
this.dialog.set_df_property("doc_view", "options", views.join("\n")); | |||||
}); | |||||
} else { | } else { | ||||
this.hide_filters(); | this.hide_filters(); | ||||
} | } | ||||
@@ -405,7 +409,7 @@ class ShortcutDialog extends WidgetDialog { | |||||
fieldtype: "Select", | fieldtype: "Select", | ||||
fieldname: "doc_view", | fieldname: "doc_view", | ||||
label: "DocType View", | label: "DocType View", | ||||
options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar", | |||||
options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar\nKanban", | |||||
description: __( | description: __( | ||||
"Which view of the associated DocType should this shortcut take you to?" | "Which view of the associated DocType should this shortcut take you to?" | ||||
), | ), | ||||
@@ -1,5 +1,7 @@ | |||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
from unittest.mock import patch | |||||
import frappe | import frappe | ||||
from frappe.tests.utils import FrappeTestCase | from frappe.tests.utils import FrappeTestCase | ||||
@@ -15,12 +17,26 @@ class TestClient(FrappeTestCase): | |||||
def test_delete(self): | def test_delete(self): | ||||
from frappe.client import delete | from frappe.client import delete | ||||
from frappe.desk.doctype.note.note import Note | |||||
todo = frappe.get_doc(dict(doctype="ToDo", description="description")).insert() | |||||
delete("ToDo", todo.name) | |||||
note = frappe.get_doc( | |||||
doctype="Note", | |||||
title=frappe.generate_hash(length=8), | |||||
content="test", | |||||
seen_by=[{"user": "Administrator"}], | |||||
).insert() | |||||
child_row_name = note.seen_by[0].name | |||||
with patch.object(Note, "save") as save: | |||||
delete("Note Seen By", child_row_name) | |||||
save.assert_called() | |||||
delete("Note", note.name) | |||||
self.assertFalse(frappe.db.exists("ToDo", todo.name)) | |||||
self.assertRaises(frappe.DoesNotExistError, delete, "ToDo", todo.name) | |||||
self.assertFalse(frappe.db.exists("Note", note.name)) | |||||
self.assertRaises(frappe.DoesNotExistError, delete, "Note", note.name) | |||||
self.assertRaises(frappe.DoesNotExistError, delete, "Note Seen By", child_row_name) | |||||
def test_http_valid_method_access(self): | def test_http_valid_method_access(self): | ||||
from frappe.client import delete | from frappe.client import delete | ||||
@@ -163,6 +163,12 @@ class TestDocument(FrappeTestCase): | |||||
self.assertRaises(frappe.ValidationError, d.run_method, "validate") | self.assertRaises(frappe.ValidationError, d.run_method, "validate") | ||||
self.assertRaises(frappe.ValidationError, d.save) | self.assertRaises(frappe.ValidationError, d.save) | ||||
def test_db_set_no_query_on_new_docs(self): | |||||
user = frappe.new_doc("User") | |||||
user.db_set("user_type", "Magical Wizard") | |||||
with self.assertQueryCount(0): | |||||
user.db_set("user_type", "Magical Wizard") | |||||
def test_update_after_submit(self): | def test_update_after_submit(self): | ||||
d = self.test_insert() | d = self.test_insert() | ||||
d.starts_on = "2014-09-09" | d.starts_on = "2014-09-09" | ||||
@@ -43,16 +43,32 @@ def create_todo_records(): | |||||
frappe.db.truncate("ToDo") | frappe.db.truncate("ToDo") | ||||
frappe.get_doc( | frappe.get_doc( | ||||
{"doctype": "ToDo", "date": add_to_date(now(), days=7), "description": "this is first todo"} | |||||
{ | |||||
"doctype": "ToDo", | |||||
"date": add_to_date(now(), days=7), | |||||
"description": "this is first todo", | |||||
} | |||||
).insert() | ).insert() | ||||
frappe.get_doc( | frappe.get_doc( | ||||
{"doctype": "ToDo", "date": add_to_date(now(), days=-7), "description": "this is second todo"} | |||||
{ | |||||
"doctype": "ToDo", | |||||
"date": add_to_date(now(), days=-7), | |||||
"description": "this is second todo", | |||||
} | |||||
).insert() | ).insert() | ||||
frappe.get_doc( | frappe.get_doc( | ||||
{"doctype": "ToDo", "date": add_to_date(now(), months=2), "description": "this is third todo"} | |||||
{ | |||||
"doctype": "ToDo", | |||||
"date": add_to_date(now(), months=2), | |||||
"description": "this is third todo", | |||||
} | |||||
).insert() | ).insert() | ||||
frappe.get_doc( | frappe.get_doc( | ||||
{"doctype": "ToDo", "date": add_to_date(now(), months=-2), "description": "this is fourth todo"} | |||||
{ | |||||
"doctype": "ToDo", | |||||
"date": add_to_date(now(), months=-2), | |||||
"description": "this is fourth todo", | |||||
} | |||||
).insert() | ).insert() | ||||
@@ -431,3 +447,134 @@ def create_test_user(): | |||||
user.append("roles", {"role": role}) | user.append("roles", {"role": role}) | ||||
user.save() | user.save() | ||||
@frappe.whitelist() | |||||
def setup_tree_doctype(): | |||||
frappe.delete_doc_if_exists("DocType", "Custom Tree") | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [ | |||||
{"fieldname": "tree", "fieldtype": "Data", "label": "Tree"}, | |||||
], | |||||
"permissions": [{"role": "System Manager", "read": 1}], | |||||
"name": "Custom Tree", | |||||
"is_tree": True, | |||||
"naming_rule": "By fieldname", | |||||
"autoname": "field:tree", | |||||
} | |||||
).insert() | |||||
if not frappe.db.exists("Custom Tree", "All Trees"): | |||||
frappe.get_doc({"doctype": "Custom Tree", "tree": "All Trees"}).insert() | |||||
@frappe.whitelist() | |||||
def setup_image_doctype(): | |||||
frappe.delete_doc_if_exists("DocType", "Custom Image") | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "DocType", | |||||
"module": "Core", | |||||
"custom": 1, | |||||
"fields": [ | |||||
{"fieldname": "image", "fieldtype": "Attach Image", "label": "Image"}, | |||||
], | |||||
"permissions": [{"role": "System Manager", "read": 1}], | |||||
"name": "Custom Image", | |||||
"image_field": "image", | |||||
} | |||||
).insert() | |||||
@frappe.whitelist() | |||||
def setup_inbox(): | |||||
frappe.db.sql("DELETE FROM `tabUser Email`") | |||||
user = frappe.get_doc("User", frappe.session.user) | |||||
user.append("user_emails", {"email_account": "Email Linking"}) | |||||
user.save() | |||||
@frappe.whitelist() | |||||
def setup_default_view(view, force_reroute=None): | |||||
frappe.delete_doc_if_exists("Property Setter", "Event-main-default_view") | |||||
frappe.delete_doc_if_exists("Property Setter", "Event-main-force_re_route_to_default_view") | |||||
frappe.get_doc( | |||||
{ | |||||
"is_system_generated": 0, | |||||
"doctype_or_field": "DocType", | |||||
"doc_type": "Event", | |||||
"property": "default_view", | |||||
"property_type": "Select", | |||||
"value": view, | |||||
"doctype": "Property Setter", | |||||
} | |||||
).insert() | |||||
if force_reroute: | |||||
frappe.get_doc( | |||||
{ | |||||
"is_system_generated": 0, | |||||
"doctype_or_field": "DocType", | |||||
"doc_type": "Event", | |||||
"property": "force_re_route_to_default_view", | |||||
"property_type": "Check", | |||||
"value": "1", | |||||
"doctype": "Property Setter", | |||||
} | |||||
).insert() | |||||
@frappe.whitelist() | |||||
def create_note(): | |||||
if not frappe.db.exists("Note", "Routing Test"): | |||||
frappe.get_doc({"doctype": "Note", "title": "Routing Test"}).insert() | |||||
@frappe.whitelist() | |||||
def create_kanban(): | |||||
if not frappe.db.exists("Custom Field", "Note-kanban"): | |||||
frappe.get_doc( | |||||
{ | |||||
"is_system_generated": 0, | |||||
"dt": "Note", | |||||
"label": "Kanban", | |||||
"fieldname": "kanban", | |||||
"insert_after": "seen_by", | |||||
"fieldtype": "Select", | |||||
"options": "Open\nClosed", | |||||
"doctype": "Custom Field", | |||||
} | |||||
).insert() | |||||
if not frappe.db.exists("Kanban Board", "_Note _Kanban"): | |||||
frappe.get_doc( | |||||
{ | |||||
"doctype": "Kanban Board", | |||||
"name": "_Note _Kanban", | |||||
"kanban_board_name": "_Note _Kanban", | |||||
"reference_doctype": "Note", | |||||
"field_name": "kanban", | |||||
"private": 1, | |||||
"show_labels": 0, | |||||
"columns": [ | |||||
{ | |||||
"column_name": "Open", | |||||
"status": "Active", | |||||
"indicator": "Gray", | |||||
}, | |||||
{ | |||||
"column_name": "Closed", | |||||
"status": "Active", | |||||
"indicator": "Gray", | |||||
}, | |||||
], | |||||
} | |||||
).insert() |
@@ -69,16 +69,18 @@ class FrappeTestCase(unittest.TestCase): | |||||
@contextmanager | @contextmanager | ||||
def assertQueryCount(self, count): | def assertQueryCount(self, count): | ||||
queries = [] | |||||
def _sql_with_count(*args, **kwargs): | def _sql_with_count(*args, **kwargs): | ||||
frappe.db.sql_query_count += 1 | |||||
return orig_sql(*args, **kwargs) | |||||
ret = orig_sql(*args, **kwargs) | |||||
queries.append(frappe.db.last_query) | |||||
return ret | |||||
try: | try: | ||||
orig_sql = frappe.db.sql | orig_sql = frappe.db.sql | ||||
frappe.db.sql_query_count = 0 | |||||
frappe.db.sql = _sql_with_count | frappe.db.sql = _sql_with_count | ||||
yield | yield | ||||
self.assertLessEqual(frappe.db.sql_query_count, count) | |||||
self.assertLessEqual(len(queries), count, msg="Queries executed: " + "\n\n".join(queries)) | |||||
finally: | finally: | ||||
frappe.db.sql = orig_sql | frappe.db.sql = orig_sql | ||||
@@ -2592,6 +2592,7 @@ Tree,Baum, | |||||
Trigger Method,Trigger-Methode, | Trigger Method,Trigger-Methode, | ||||
Trigger Name,Name des Auslösers, | Trigger Name,Name des Auslösers, | ||||
"Trigger on valid methods like ""before_insert"", ""after_update"", etc (will depend on the DocType selected)","Trigger auf gültige Methoden wie "before_insert", "after_update" usw. (hängt von der DocType ausgewählt)", | "Trigger on valid methods like ""before_insert"", ""after_update"", etc (will depend on the DocType selected)","Trigger auf gültige Methoden wie "before_insert", "after_update" usw. (hängt von der DocType ausgewählt)", | ||||
Try a naming Series, Nummernkreis testen, | |||||
Try to avoid repeated words and characters,"Versuchen Sie, wiederholte Wörter und Zeichen zu vermeiden", | Try to avoid repeated words and characters,"Versuchen Sie, wiederholte Wörter und Zeichen zu vermeiden", | ||||
Try to use a longer keyboard pattern with more turns,"Versuchen Sie, eine längere Tastaturmuster mit mehr Windungen zu verwenden", | Try to use a longer keyboard pattern with more turns,"Versuchen Sie, eine längere Tastaturmuster mit mehr Windungen zu verwenden", | ||||
Two Factor Authentication,Zwei-Faktor-Authentifizierung, | Two Factor Authentication,Zwei-Faktor-Authentifizierung, | ||||
@@ -194,7 +194,7 @@ class RedisWrapper(redis.Redis): | |||||
except redis.exceptions.ConnectionError: | except redis.exceptions.ConnectionError: | ||||
pass | pass | ||||
if value: | |||||
if value is not None: | |||||
value = pickle.loads(value) | value = pickle.loads(value) | ||||
frappe.local.cache[_name][key] = value | frappe.local.cache[_name][key] = value | ||||
elif generator: | elif generator: | ||||
@@ -98,7 +98,6 @@ def get_context(context): | |||||
"""Build context to render the `web_form.html` template""" | """Build context to render the `web_form.html` template""" | ||||
context.in_edit_mode = False | context.in_edit_mode = False | ||||
context.in_view_mode = False | context.in_view_mode = False | ||||
self.set_web_form_module() | |||||
if frappe.form_dict.is_list: | if frappe.form_dict.is_list: | ||||
context.template = "website/doctype/web_form/templates/web_list.html" | context.template = "website/doctype/web_form/templates/web_list.html" | ||||
@@ -284,13 +283,14 @@ def get_context(context): | |||||
def add_custom_context_and_script(self, context): | def add_custom_context_and_script(self, context): | ||||
"""Update context from module if standard and append script""" | """Update context from module if standard and append script""" | ||||
if self.web_form_module: | |||||
new_context = self.web_form_module.get_context(context) | |||||
if self.is_standard: | |||||
web_form_module = get_web_form_module(self) | |||||
new_context = web_form_module.get_context(context) | |||||
if new_context: | if new_context: | ||||
context.update(new_context) | context.update(new_context) | ||||
js_path = os.path.join(os.path.dirname(self.web_form_module.__file__), scrub(self.name) + ".js") | |||||
js_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".js") | |||||
if os.path.exists(js_path): | if os.path.exists(js_path): | ||||
script = frappe.render_template(open(js_path).read(), context) | script = frappe.render_template(open(js_path).read(), context) | ||||
@@ -300,9 +300,7 @@ def get_context(context): | |||||
context.script = script | context.script = script | ||||
css_path = os.path.join( | |||||
os.path.dirname(self.web_form_module.__file__), scrub(self.name) + ".css" | |||||
) | |||||
css_path = os.path.join(os.path.dirname(web_form_module.__file__), scrub(self.name) + ".css") | |||||
if os.path.exists(css_path): | if os.path.exists(css_path): | ||||
style = open(css_path).read() | style = open(css_path).read() | ||||
@@ -322,14 +320,6 @@ def get_context(context): | |||||
return parents | return parents | ||||
def set_web_form_module(self): | |||||
"""Get custom web form module if exists""" | |||||
self.web_form_module = self.get_web_form_module() | |||||
def get_web_form_module(self): | |||||
if self.is_standard: | |||||
return get_doc_module(self.module, self.doctype, self.name) | |||||
def validate_mandatory(self, doc): | def validate_mandatory(self, doc): | ||||
"""Validate mandatory web form fields""" | """Validate mandatory web form fields""" | ||||
missing = [] | missing = [] | ||||
@@ -368,6 +358,11 @@ def get_context(context): | |||||
return False | return False | ||||
def get_web_form_module(doc): | |||||
if doc.is_standard: | |||||
return get_doc_module(doc.module, doc.doctype, doc.name) | |||||
@frappe.whitelist(allow_guest=True) | @frappe.whitelist(allow_guest=True) | ||||
@rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"]) | @rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"]) | ||||
def accept(web_form, data, docname=None): | def accept(web_form, data, docname=None): | ||||
@@ -1,8 +1,35 @@ | |||||
# Copyright (c) 2020, Frappe Technologies and Contributors | # Copyright (c) 2020, Frappe Technologies and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
# import frappe | |||||
import frappe | |||||
from frappe.tests.utils import FrappeTestCase | from frappe.tests.utils import FrappeTestCase | ||||
from frappe.website.doctype.website_settings.website_settings import get_website_settings | |||||
class TestWebsiteSettings(FrappeTestCase): | class TestWebsiteSettings(FrappeTestCase): | ||||
pass | |||||
def test_child_items_in_top_bar(self): | |||||
ws = frappe.get_doc("Website Settings") | |||||
ws.append( | |||||
"top_bar_items", | |||||
{"label": "Parent Item"}, | |||||
) | |||||
ws.append( | |||||
"top_bar_items", | |||||
{"parent_label": "Parent Item", "label": "Child Item"}, | |||||
) | |||||
ws.save() | |||||
context = get_website_settings() | |||||
for item in context.top_bar_items: | |||||
if item.label == "Parent Item": | |||||
self.assertEqual(item.child_items[0].label, "Child Item") | |||||
break | |||||
else: | |||||
self.fail("Child items not found") | |||||
def test_redirect_setups(self): | |||||
ws = frappe.get_doc("Website Settings") | |||||
ws.append("route_redirects", {"source": "/engineering/(*.)", "target": "/development/(*.)"}) | |||||
self.assertRaises(frappe.ValidationError, ws.validate) |
@@ -1,5 +1,6 @@ | |||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors | ||||
# License: MIT. See LICENSE | # License: MIT. See LICENSE | ||||
import re | |||||
from urllib.parse import quote | from urllib.parse import quote | ||||
import frappe | import frappe | ||||
@@ -16,6 +17,7 @@ class WebsiteSettings(Document): | |||||
self.validate_footer_items() | self.validate_footer_items() | ||||
self.validate_home_page() | self.validate_home_page() | ||||
self.validate_google_settings() | self.validate_google_settings() | ||||
self.validate_redirects() | |||||
def validate_home_page(self): | def validate_home_page(self): | ||||
if frappe.flags.in_install: | if frappe.flags.in_install: | ||||
@@ -72,6 +74,16 @@ class WebsiteSettings(Document): | |||||
if self.enable_google_indexing and not frappe.db.get_single_value("Google Settings", "enable"): | if self.enable_google_indexing and not frappe.db.get_single_value("Google Settings", "enable"): | ||||
frappe.throw(_("Enable Google API in Google Settings.")) | frappe.throw(_("Enable Google API in Google Settings.")) | ||||
def validate_redirects(self): | |||||
for idx, row in enumerate(self.route_redirects): | |||||
try: | |||||
source = row.source.strip("/ ") + "$" | |||||
re.compile(source) | |||||
re.sub(source, row.target, "") | |||||
except Exception as e: | |||||
if not frappe.flags.in_migrate: | |||||
frappe.throw(_("Invalid redirect regex in row #{}: {}").format(idx, str(e))) | |||||
def on_update(self): | def on_update(self): | ||||
self.clear_cache() | self.clear_cache() | ||||
@@ -163,6 +163,7 @@ def prepare_filters(doctype, controller, kwargs): | |||||
def get_list_context(context, doctype, web_form_name=None): | def get_list_context(context, doctype, web_form_name=None): | ||||
from frappe.modules import load_doctype_module | from frappe.modules import load_doctype_module | ||||
from frappe.website.doctype.web_form.web_form import get_web_form_module | |||||
list_context = context or frappe._dict() | list_context = context or frappe._dict() | ||||
meta = frappe.get_meta(doctype) | meta = frappe.get_meta(doctype) | ||||
@@ -193,7 +194,7 @@ def get_list_context(context, doctype, web_form_name=None): | |||||
# get context from web form module | # get context from web form module | ||||
if web_form_name: | if web_form_name: | ||||
web_form = frappe.get_doc("Web Form", web_form_name) | web_form = frappe.get_doc("Web Form", web_form_name) | ||||
list_context = update_context_from_module(web_form.get_web_form_module(), list_context) | |||||
list_context = update_context_from_module(get_web_form_module(web_form), list_context) | |||||
# get path from '/templates/' folder of the doctype | # get path from '/templates/' folder of the doctype | ||||
if not meta.custom and not list_context.row_template: | if not meta.custom and not list_context.row_template: | ||||