ソースを参照

Merge pull request #18560 from frappe/version-14-hotfix

chore: release v14
version-14
Ankush Menat 2年前
committed by GitHub
コミット
e805bc6ee0
この署名に対応する既知のキーがデータベースに存在しません GPGキーID: 4AEE18F83AFDEB23
45個のファイルの変更951行の追加139行の削除
  1. +1
    -1
      .github/workflows/on_release.yml
  2. +8
    -1
      cypress/integration/folder_navigation.js
  3. +231
    -0
      cypress/integration/view_routing.js
  4. +7
    -1
      frappe/cache_manager.py
  5. +22
    -1
      frappe/client.py
  6. +4
    -3
      frappe/core/doctype/communication/communication.json
  7. +19
    -0
      frappe/core/doctype/doctype/doctype.js
  8. +22
    -1
      frappe/core/doctype/doctype/doctype.json
  9. +12
    -0
      frappe/core/doctype/doctype/test_doctype.py
  10. +3
    -1
      frappe/core/doctype/file/file.json
  11. +0
    -0
      frappe/core/report/database_storage_usage_by_tables/__init__.py
  12. +7
    -0
      frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js
  13. +28
    -0
      frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json
  14. +40
    -0
      frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py
  15. +15
    -0
      frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py
  16. +11
    -1
      frappe/custom/doctype/customize_form/customize_form.js
  17. +23
    -1
      frappe/custom/doctype/customize_form/customize_form.json
  18. +4
    -0
      frappe/custom/doctype/customize_form/customize_form.py
  19. +2
    -2
      frappe/desk/doctype/todo/todo_list.js
  20. +15
    -6
      frappe/integrations/doctype/google_calendar/google_calendar.py
  21. +4
    -0
      frappe/model/delete_doc.py
  22. +12
    -5
      frappe/model/document.py
  23. +3
    -1
      frappe/public/js/frappe/form/controls/datetime.js
  24. +1
    -1
      frappe/public/js/frappe/list/base_list.js
  25. +12
    -2
      frappe/public/js/frappe/microtemplate.js
  26. +37
    -1
      frappe/public/js/frappe/model/model.js
  27. +86
    -22
      frappe/public/js/frappe/router.js
  28. +4
    -7
      frappe/public/js/frappe/ui/toolbar/search_utils.js
  29. +5
    -14
      frappe/public/js/frappe/utils/utils.js
  30. +1
    -1
      frappe/public/js/frappe/views/breadcrumbs.js
  31. +2
    -2
      frappe/public/js/frappe/views/file/file_view.js
  32. +42
    -20
      frappe/public/js/frappe/views/kanban/kanban_view.js
  33. +8
    -0
      frappe/public/js/frappe/views/reports/query_report.js
  34. +5
    -0
      frappe/public/js/frappe/views/treeview.js
  35. +17
    -13
      frappe/public/js/frappe/widgets/widget_dialog.js
  36. +20
    -4
      frappe/tests/test_client.py
  37. +6
    -0
      frappe/tests/test_document.py
  38. +151
    -4
      frappe/tests/ui_test_helpers.py
  39. +6
    -4
      frappe/tests/utils.py
  40. +1
    -0
      frappe/translations/de.csv
  41. +1
    -1
      frappe/utils/redis_wrapper.py
  42. +10
    -15
      frappe/website/doctype/web_form/web_form.py
  43. +29
    -2
      frappe/website/doctype/website_settings/test_website_settings.py
  44. +12
    -0
      frappe/website/doctype/website_settings/website_settings.py
  45. +2
    -1
      frappe/www/list.py

+ 1
- 1
.github/workflows/on_release.yml ファイルの表示

@@ -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


+ 8
- 1
cypress/integration/folder_navigation.js ファイルの表示

@@ -24,6 +24,7 @@ context("Folder Navigation", () => {


it("Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct", () => { 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();




+ 231
- 0
cypress/integration/view_routing.js ファイルの表示

@@ -0,0 +1,231 @@
context("View", () => {
before(() => {
cy.login();
cy.visit("/app/website");
});

it("Route to ToDo List View", () => {
cy.visit("/app/todo/view/list");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("List");
});
});

it("Route to ToDo Report View", () => {
cy.visit("/app/todo/view/report");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});

it("Route to ToDo Dashboard View", () => {
cy.visit("/app/todo/view/dashboard");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Dashboard");
});
});

it("Route to ToDo Gantt View", () => {
cy.visit("/app/todo/view/gantt");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Gantt");
});
});

it("Route to ToDo Kanban View", () => {
cy.call("frappe.tests.ui_test_helpers.create_kanban").then(() => {
cy.visit("/app/note/view/kanban/_Note _Kanban");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Kanban");
});
});
});

it("Route to ToDo Calendar View", () => {
cy.visit("/app/todo/view/calendar");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Calendar");
});
});

it("Route to Custom Tree View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_tree_doctype").then(() => {
cy.visit("/app/custom-tree/view/tree");
cy.wait(500);
cy.window()
.its("cur_tree")
.then((list) => {
expect(list.view_name).to.equal("Tree");
});
});
});

it("Route to Custom Image View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_image_doctype").then(() => {
cy.visit("app/custom-image/view/image");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Image");
});
});
});

it("Route to Communication Inbox View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_inbox").then(() => {
cy.visit("app/communication/view/inbox");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Inbox");
});
});
});

it("Route to File View", () => {
cy.visit("app/file");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("File");
expect(list.current_folder).to.equal("Home");
});

cy.visit("app/file/view/home/Attachments");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("File");
expect(list.current_folder).to.equal("Home/Attachments");
});
});

it("Re-route to default view", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("app/event");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});
});

it("Route to default view from app/{doctype}", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("/app/event");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});
});

it("Route to default view from app/{doctype}/view", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("/app/event/view");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});
});

it("Force Route to default view from app/{doctype}", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", {
view: "Report",
force_reroute: true,
}).then(() => {
cy.visit("/app/event");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});
});

it("Force Route to default view from app/{doctype}/view", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", {
view: "Report",
force_reroute: true,
}).then(() => {
cy.visit("/app/event/view");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});
});

it("Force Route to default view from app/{doctype}/view", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", {
view: "Report",
force_reroute: true,
}).then(() => {
cy.visit("/app/event/view/list");
cy.wait(500);
cy.window()
.its("cur_list")
.then((list) => {
expect(list.view_name).to.equal("Report");
});
});
});

it("Validate Route History for Default View", () => {
cy.call("frappe.tests.ui_test_helpers.setup_default_view", { view: "Report" }).then(() => {
cy.visit("/app/event");
cy.visit("/app/event/view/list");
cy.location("pathname").should("eq", "/app/event/view/list");
cy.go("back");
cy.location("pathname").should("eq", "/app/event");
});
});

it("Route to Form", () => {
cy.call("frappe.tests.ui_test_helpers.create_note").then(() => {
cy.visit("/app/note/Routing Test");
cy.window()
.its("cur_frm")
.then((frm) => {
expect(frm.doc.title).to.equal("Routing Test");
});
});
});

it("Route to Settings Workspace", () => {
cy.visit("/app/settings");
cy.get(".title-text").should("contain", "Settings");
});
});

+ 7
- 1
frappe/cache_manager.py ファイルの表示

@@ -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)




+ 22
- 1
frappe/client.py ファイルの表示

@@ -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)

+ 4
- 3
frappe/core/doctype/communication/communication.json ファイルの表示

@@ -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
}
}

+ 19
- 0
frappe/core/doctype/doctype/doctype.js ファイルの表示

@@ -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 }));

+ 22
- 1
frappe/core/doctype/doctype/doctype.json ファイルの表示

@@ -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",


+ 12
- 0
frappe/core/doctype/doctype/test_doctype.py ファイルの表示

@@ -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


+ 3
- 1
frappe/core/doctype/file/file.json ファイルの表示

@@ -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
frappe/core/report/database_storage_usage_by_tables/__init__.py ファイルの表示


+ 7
- 0
frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.js ファイルの表示

@@ -0,0 +1,7 @@
// Copyright (c) 2022, Frappe Technologies and contributors
// For license information, please see license.txt
/* eslint-disable */

frappe.query_reports["Database Storage Usage By Tables"] = {
filters: [],
};

+ 28
- 0
frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.json ファイルの表示

@@ -0,0 +1,28 @@
{
"add_total_row": 1,
"columns": [],
"creation": "2022-10-19 02:25:24.326791",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "abc",
"modified": "2022-10-19 02:59:00.365307",
"modified_by": "Administrator",
"module": "Core",
"name": "Database Storage Usage By Tables",
"owner": "Administrator",
"prepared_report": 0,
"query": "",
"ref_doctype": "Error Log",
"report_name": "Database Storage Usage By Tables",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
}
]
}

+ 40
- 0
frappe/core/report/database_storage_usage_by_tables/database_storage_usage_by_tables.py ファイルの表示

@@ -0,0 +1,40 @@
# Copyright (c) 2022, Frappe Technologies and contributors
# For license information, please see license.txt

import frappe

COLUMNS = [
{"label": "Table", "fieldname": "table", "fieldtype": "Data", "width": 200},
{"label": "Size (MB)", "fieldname": "size", "fieldtype": "Float"},
{"label": "Data (MB)", "fieldname": "data_size", "fieldtype": "Float"},
{"label": "Index (MB)", "fieldname": "index_size", "fieldtype": "Float"},
]


def execute(filters=None):
frappe.only_for("System Manager")

data = frappe.db.multisql(
{
"mariadb": """
SELECT table_name AS `table`,
round(((data_length + index_length) / 1024 / 1024), 2) `size`,
round((data_length / 1024 / 1024), 2) as data_size,
round((index_length / 1024 / 1024), 2) as index_size
FROM information_schema.TABLES
ORDER BY (data_length + index_length) DESC;
""",
"postgres": """
SELECT
table_name as "table",
round(pg_total_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "size",
round(pg_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "data_size",
round(pg_indexes_size(quote_ident(table_name)) / 1024 / 1024, 2) as "index_size"
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY 2 DESC;
""",
},
as_dict=1,
)
return COLUMNS, data

+ 15
- 0
frappe/core/report/database_storage_usage_by_tables/test_database_storage_usage_by_tables.py ファイルの表示

@@ -0,0 +1,15 @@
# Copyright (c) 2022, Frappe Technologies and contributors
# For license information, please see license.txt


from frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables import (
execute,
)
from frappe.tests.utils import FrappeTestCase


class TestDBUsageReport(FrappeTestCase):
def test_basic_query(self):
_, data = execute()
tables = [d.table for d in data]
self.assertFalse({"tabUser", "tabDocField"}.difference(tables))

+ 11
- 1
frappe/custom/doctype/customize_form/customize_form.js ファイルの表示

@@ -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");
}, },
}); });




+ 23
- 1
frappe/custom/doctype/customize_form/customize_form.json ファイルの表示

@@ -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",


+ 4
- 0
frappe/custom/doctype/customize_form/customize_form.py ファイルの表示

@@ -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",
} }




+ 2
- 2
frappe/desk/doctype/todo/todo_list.js ファイルの表示

@@ -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);


+ 15
- 6
frappe/integrations/doctype/google_calendar/google_calendar.py ファイルの表示

@@ -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,


+ 4
- 0
frappe/model/delete_doc.py ファイルの表示

@@ -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)


+ 12
- 5
frappe/model/document.py ファイルの表示

@@ -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,


+ 3
- 1
frappe/public/js/frappe/form/controls/datetime.js ファイルの表示

@@ -5,8 +5,10 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
if (!value) { 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);


+ 1
- 1
frappe/public/js/frappe/list/base_list.js ファイルの表示

@@ -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"),


+ 12
- 2
frappe/public/js/frappe/microtemplate.js ファイルの表示

@@ -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);


+ 37
- 1
frappe/public/js/frappe/model/model.js ファイルの表示

@@ -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


+ 86
- 22
frappe/public/js/frappe/router.js ファイルの表示

@@ -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;
}, },




+ 4
- 7
frappe/public/js/frappe/ui/toolbar/search_utils.js ファイルの表示

@@ -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));
} }
} }
} }


+ 5
- 14
frappe/public/js/frappe/utils/utils.js ファイルの表示

@@ -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") {


+ 1
- 1
frappe/public/js/frappe/views/breadcrumbs.js ファイルの表示

@@ -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 {


+ 2
- 2
frappe/public/js/frappe/views/file/file_view.js ファイルの表示

@@ -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


+ 42
- 20
frappe/public/js/frappe/views/kanban/kanban_view.js ファイルの表示

@@ -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() {


+ 8
- 0
frappe/public/js/frappe/views/reports/query_report.js ファイルの表示

@@ -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);
} }




+ 5
- 0
frappe/public/js/frappe/views/treeview.js ファイルの表示

@@ -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();
} }




+ 17
- 13
frappe/public/js/frappe/widgets/widget_dialog.js ファイルの表示

@@ -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?"
), ),


+ 20
- 4
frappe/tests/test_client.py ファイルの表示

@@ -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


+ 6
- 0
frappe/tests/test_document.py ファイルの表示

@@ -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"


+ 151
- 4
frappe/tests/ui_test_helpers.py ファイルの表示

@@ -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()

+ 6
- 4
frappe/tests/utils.py ファイルの表示

@@ -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




+ 1
- 0
frappe/translations/de.csv ファイルの表示

@@ -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,


+ 1
- 1
frappe/utils/redis_wrapper.py ファイルの表示

@@ -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:


+ 10
- 15
frappe/website/doctype/web_form/web_form.py ファイルの表示

@@ -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):


+ 29
- 2
frappe/website/doctype/website_settings/test_website_settings.py ファイルの表示

@@ -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)

+ 12
- 0
frappe/website/doctype/website_settings/website_settings.py ファイルの表示

@@ -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()




+ 2
- 1
frappe/www/list.py ファイルの表示

@@ -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:


読み込み中…
キャンセル
保存