chore: release v14version-14
@@ -41,7 +41,7 @@ jobs: | |||
- name: 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 | |||
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", () => { | |||
//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(); | |||
@@ -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 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) | |||
@@ -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) |
@@ -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 | |||
} | |||
} |
@@ -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 })); |
@@ -14,6 +14,7 @@ | |||
"istable", | |||
"issingle", | |||
"is_tree", | |||
"is_calendar_and_gantt", | |||
"editable_grid", | |||
"quick_entry", | |||
"cb01", | |||
@@ -53,6 +54,8 @@ | |||
"default_print_format", | |||
"sort_field", | |||
"sort_order", | |||
"default_view", | |||
"force_re_route_to_default_view", | |||
"column_break_29", | |||
"document_type", | |||
"icon", | |||
@@ -606,6 +609,24 @@ | |||
"fieldname": "make_attachments_public", | |||
"fieldtype": "Check", | |||
"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", | |||
@@ -688,7 +709,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", | |||
@@ -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 | |||
@@ -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", | |||
@@ -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 { | |||
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"); | |||
}, | |||
}); | |||
@@ -13,6 +13,7 @@ | |||
"search_fields", | |||
"column_break_5", | |||
"istable", | |||
"is_calendar_and_gantt", | |||
"editable_grid", | |||
"quick_entry", | |||
"track_changes", | |||
@@ -35,6 +36,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", | |||
@@ -337,6 +340,25 @@ | |||
"fieldname": "make_attachments_public", | |||
"fieldtype": "Check", | |||
"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, | |||
@@ -345,7 +367,7 @@ | |||
"index_web_pages_for_search": 1, | |||
"issingle": 1, | |||
"links": [], | |||
"modified": "2022-08-24 06:57:47.966331", | |||
"modified": "2022-08-30 11:45:16.772277", | |||
"modified_by": "Administrator", | |||
"module": "Custom", | |||
"name": "Customize Form", | |||
@@ -586,6 +586,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", | |||
} | |||
@@ -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); | |||
@@ -4,6 +4,7 @@ | |||
from datetime import datetime, timedelta | |||
from urllib.parse import quote | |||
from zoneinfo import ZoneInfo | |||
import google.oauth2.credentials | |||
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. | |||
""" | |||
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, | |||
@@ -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) | |||
@@ -953,15 +953,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 | |||
@@ -1173,6 +1177,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, | |||
@@ -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); | |||
@@ -196,7 +196,7 @@ frappe.views.BaseList = class BaseList { | |||
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 | |||
for translation, don't remove | |||
__("List View") __("Report View") __("Dashboard View") __("Gantt View"), | |||
@@ -175,6 +175,7 @@ frappe.render_template = function (name, data) { | |||
w.document.write(tree); | |||
w.document.close(); | |||
}); | |||
frappe.render_pdf = function (html, opts = {}) { | |||
//Create a form to place the HTML content | |||
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 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); | |||
@@ -349,7 +349,7 @@ $.extend(frappe.model, { | |||
is_tree: function (doctype) { | |||
if (!doctype) return false; | |||
return frappe.boot.treeviews.indexOf(doctype) != -1; | |||
return locals.DocType[doctype] && locals.DocType[doctype].is_tree; | |||
}, | |||
is_fresh(doc) { | |||
@@ -754,6 +754,42 @@ $.extend(frappe.model, { | |||
} | |||
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 | |||
@@ -88,7 +88,21 @@ frappe.router = { | |||
"dashboard", | |||
"image", | |||
"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: {}, | |||
is_app_route(path) { | |||
@@ -115,7 +129,7 @@ frappe.router = { | |||
} | |||
}, | |||
route() { | |||
async route() { | |||
// resolve the route from the URL or hash | |||
// translate it so the objects are well defined | |||
// and render the page as required | |||
@@ -126,22 +140,22 @@ frappe.router = { | |||
if (this.re_route(sub_path)) return; | |||
this.current_sub_path = sub_path; | |||
this.current_route = this.parse(); | |||
this.current_route = await this.parse(); | |||
this.set_history(sub_path); | |||
this.render(); | |||
this.set_title(sub_path); | |||
this.trigger("change"); | |||
}, | |||
parse(route) { | |||
async parse(route) { | |||
route = this.get_sub_path_string(route).split("/"); | |||
if (!route) return []; | |||
route = $.map(route, this.decode_component); | |||
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/private/settings = ["Workspaces", "private", "Settings"] | |||
// /app/user = ["List", "User"] | |||
@@ -161,7 +175,7 @@ frappe.router = { | |||
route = ["Workspaces", "private", frappe.workspaces[private_workspace].title]; | |||
} else if (this.routes[route[0]]) { | |||
// route | |||
route = this.set_doctype_route(route); | |||
route = await this.set_doctype_route(route); | |||
} | |||
return route; | |||
@@ -174,36 +188,85 @@ frappe.router = { | |||
set_doctype_route(route) { | |||
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]; | |||
if (route.length > 2) { | |||
docname = route.slice(1).join("/"); | |||
} | |||
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; | |||
if (route[2].toLowerCase() === "tree") { | |||
let _route = default_view || route[2] || ""; | |||
if (_route.toLowerCase() === "tree") { | |||
standard_route = ["Tree", doctype_route.doctype]; | |||
} 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 | |||
if (route[3]) standard_route.push(...route.slice(3, route.length)); | |||
} | |||
return standard_route; | |||
}, | |||
@@ -345,6 +408,7 @@ frappe.router = { | |||
} else if (view === "tree") { | |||
new_route = [this.slug(route[1]), "view", "tree"]; | |||
} | |||
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)) { | |||
route = doctype_slug; | |||
} 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) { | |||
case "List": | |||
if (item.filters) { | |||
frappe.route_options = item.filters; | |||
} | |||
route = doctype_slug; | |||
route = `${doctype_slug}/view/list`; | |||
break; | |||
case "Tree": | |||
route = `${doctype_slug}/view/tree`; | |||
@@ -1290,12 +1282,11 @@ Object.assign(frappe.utils, { | |||
case "Calendar": | |||
route = `${doctype_slug}/view/calendar/default`; | |||
break; | |||
case "Kanban": | |||
route = `${doctype_slug}/view/kanban`; | |||
break; | |||
default: | |||
frappe.throw({ | |||
message: __("Not a valid view:") + item.doc_view, | |||
title: __("Unknown View"), | |||
}); | |||
route = ""; | |||
route = doctype_slug; | |||
} | |||
} | |||
} else if (type === "report") { | |||
@@ -144,7 +144,7 @@ frappe.breadcrumbs = { | |||
} else { | |||
let route; | |||
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"; | |||
route = `${doctype_route}/view/${view}`; | |||
} else { | |||
@@ -74,7 +74,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView { | |||
this.page_title = __("File Manager"); | |||
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.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() { | |||
const route = frappe.router.parse(); | |||
const route = frappe.get_route(); | |||
const folders = route.slice(2); | |||
return folders | |||
@@ -9,14 +9,9 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
const doctype = route[1]; | |||
const user_settings = frappe.get_user_settings(doctype)["Kanban"] || {}; | |||
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); | |||
frappe.set_route(route); | |||
return true; | |||
@@ -28,9 +23,35 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
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() { | |||
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.card_meta = this.get_card_meta(); | |||
this.page_length = 0; | |||
@@ -143,21 +164,22 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView { | |||
render() { | |||
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) { | |||
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() { | |||
@@ -1383,6 +1383,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { | |||
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); | |||
} | |||
@@ -37,6 +37,10 @@ frappe.views.TreeFactory = class TreeFactory extends frappe.views.Factory { | |||
let treeview = frappe.views.trees[route[1]]; | |||
treeview && treeview.make_tree(); | |||
} | |||
get view_name() { | |||
return "Tree"; | |||
} | |||
}; | |||
frappe.views.TreeView = class TreeView { | |||
@@ -196,6 +200,7 @@ frappe.views.TreeView = class TreeView { | |||
}); | |||
cur_tree = this.tree; | |||
cur_tree.view_name = "Tree"; | |||
this.post_render(); | |||
} | |||
@@ -384,18 +384,22 @@ class ShortcutDialog extends WidgetDialog { | |||
onchange: () => { | |||
if (this.dialog.get_value("type") == "DocType") { | |||
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 { | |||
this.hide_filters(); | |||
} | |||
@@ -405,7 +409,7 @@ class ShortcutDialog extends WidgetDialog { | |||
fieldtype: "Select", | |||
fieldname: "doc_view", | |||
label: "DocType View", | |||
options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar", | |||
options: "List\nReport Builder\nDashboard\nTree\nNew\nCalendar\nKanban", | |||
description: __( | |||
"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 | |||
from unittest.mock import patch | |||
import frappe | |||
from frappe.tests.utils import FrappeTestCase | |||
@@ -15,12 +17,26 @@ class TestClient(FrappeTestCase): | |||
def test_delete(self): | |||
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): | |||
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.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): | |||
d = self.test_insert() | |||
d.starts_on = "2014-09-09" | |||
@@ -43,16 +43,32 @@ def create_todo_records(): | |||
frappe.db.truncate("ToDo") | |||
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() | |||
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() | |||
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() | |||
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() | |||
@@ -431,3 +447,134 @@ def create_test_user(): | |||
user.append("roles", {"role": role}) | |||
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 | |||
def assertQueryCount(self, count): | |||
queries = [] | |||
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: | |||
orig_sql = frappe.db.sql | |||
frappe.db.sql_query_count = 0 | |||
frappe.db.sql = _sql_with_count | |||
yield | |||
self.assertLessEqual(frappe.db.sql_query_count, count) | |||
self.assertLessEqual(len(queries), count, msg="Queries executed: " + "\n\n".join(queries)) | |||
finally: | |||
frappe.db.sql = orig_sql | |||
@@ -2592,6 +2592,7 @@ Tree,Baum, | |||
Trigger Method,Trigger-Methode, | |||
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)", | |||
Try a naming Series, Nummernkreis testen, | |||
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", | |||
Two Factor Authentication,Zwei-Faktor-Authentifizierung, | |||
@@ -194,7 +194,7 @@ class RedisWrapper(redis.Redis): | |||
except redis.exceptions.ConnectionError: | |||
pass | |||
if value: | |||
if value is not None: | |||
value = pickle.loads(value) | |||
frappe.local.cache[_name][key] = value | |||
elif generator: | |||
@@ -98,7 +98,6 @@ def get_context(context): | |||
"""Build context to render the `web_form.html` template""" | |||
context.in_edit_mode = False | |||
context.in_view_mode = False | |||
self.set_web_form_module() | |||
if frappe.form_dict.is_list: | |||
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): | |||
"""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: | |||
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): | |||
script = frappe.render_template(open(js_path).read(), context) | |||
@@ -300,9 +300,7 @@ def get_context(context): | |||
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): | |||
style = open(css_path).read() | |||
@@ -322,14 +320,6 @@ def get_context(context): | |||
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): | |||
"""Validate mandatory web form fields""" | |||
missing = [] | |||
@@ -368,6 +358,11 @@ def get_context(context): | |||
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) | |||
@rate_limit(key="web_form", limit=5, seconds=60, methods=["POST"]) | |||
def accept(web_form, data, docname=None): | |||
@@ -1,8 +1,35 @@ | |||
# Copyright (c) 2020, Frappe Technologies and Contributors | |||
# License: MIT. See LICENSE | |||
# import frappe | |||
import frappe | |||
from frappe.tests.utils import FrappeTestCase | |||
from frappe.website.doctype.website_settings.website_settings import get_website_settings | |||
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 | |||
# License: MIT. See LICENSE | |||
import re | |||
from urllib.parse import quote | |||
import frappe | |||
@@ -16,6 +17,7 @@ class WebsiteSettings(Document): | |||
self.validate_footer_items() | |||
self.validate_home_page() | |||
self.validate_google_settings() | |||
self.validate_redirects() | |||
def validate_home_page(self): | |||
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"): | |||
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): | |||
self.clear_cache() | |||
@@ -163,6 +163,7 @@ def prepare_filters(doctype, controller, kwargs): | |||
def get_list_context(context, doctype, web_form_name=None): | |||
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() | |||
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 | |||
if 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 | |||
if not meta.custom and not list_context.row_template: | |||