Просмотр исходного кода

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

chore: release v14
version-14
Ankush Menat 2 лет назад
committed by GitHub
Родитель
Сommit
e805bc6ee0
Не найден GPG ключ соответствующий данной подписи Идентификатор GPG ключа: 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
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


+ 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", () => {
//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();



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



+ 22
- 1
frappe/client.py Просмотреть файл

@@ -270,7 +270,7 @@ def delete(doctype, name):

:param doctype: DocType of the document to be deleted
:param name: name of the document to be deleted"""
frappe.delete_doc(doctype, name, ignore_missing=False)
delete_doc(doctype, name)


@frappe.whitelist(methods=["POST", "PUT"])
@@ -462,3 +462,24 @@ def insert_doc(doc) -> "Document":
return parent

return frappe.get_doc(doc).insert()


def delete_doc(doctype, name):
"""Deletes document
if doctype is a child table, then deletes the child record using the parent doc
so that the parent doc's `on_update` is called
"""

if frappe.is_table(doctype):
values = frappe.db.get_value(doctype, name, ["parenttype", "parent", "parentfield"])
if not values:
raise frappe.DoesNotExistError
parenttype, parent, parentfield = values
parent = frappe.get_doc(parenttype, parent)
for row in parent.get(parentfield):
if row.name == name:
parent.remove(row)
parent.save()
break
else:
frappe.delete_doc(doctype, name, ignore_missing=False)

+ 4
- 3
frappe/core/doctype/communication/communication.json Просмотреть файл

@@ -2,6 +2,7 @@
"actions": [],
"allow_import": 1,
"creation": "2013-01-29 10:47:14",
"default_view": "Inbox",
"description": "Keeps track of all communications",
"doctype": "DocType",
"document_type": "Setup",
@@ -198,7 +199,6 @@
"label": "More Information"
},
{
"bold": 0,
"default": "Now",
"fieldname": "communication_date",
"fieldtype": "Datetime",
@@ -395,7 +395,7 @@
"icon": "fa fa-comment",
"idx": 1,
"links": [],
"modified": "2022-03-30 11:24:25.728637",
"modified": "2022-05-09 00:13:45.310564",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
@@ -454,8 +454,9 @@
"sender_field": "sender",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"subject_field": "subject",
"title_field": "subject",
"track_changes": 1,
"track_seen": 1
}
}

+ 19
- 0
frappe/core/doctype/doctype/doctype.js Просмотреть файл

@@ -55,6 +55,7 @@ frappe.ui.form.on("DocType", {

if (frm.is_new()) {
frm.events.set_default_permission(frm);
frm.set_value("default_view", "List");
} else {
frm.toggle_enable("engine", 0);
}
@@ -66,12 +67,14 @@ frappe.ui.form.on("DocType", {

frm.cscript.autoname(frm);
frm.cscript.set_naming_rule_description(frm);
frm.trigger("setup_default_views");
},

istable: (frm) => {
if (frm.doc.istable && frm.is_new()) {
frm.set_value("autoname", "autoincrement");
frm.set_value("allow_rename", 0);
frm.set_value("default_view", null);
} else if (!frm.doc.istable && !frm.is_new()) {
frm.events.set_default_permission(frm);
}
@@ -82,6 +85,18 @@ frappe.ui.form.on("DocType", {
frm.add_child("permissions", { role: "System Manager" });
}
},

is_tree: (frm) => {
frm.trigger("setup_default_views");
},

is_calendar_and_gantt: (frm) => {
frm.trigger("setup_default_views");
},

setup_default_views: (frm) => {
frappe.model.set_default_views_for_doctype(frm.doc.name, frm);
},
});

frappe.ui.form.on("DocField", {
@@ -171,6 +186,10 @@ frappe.ui.form.on("DocField", {
fieldtype: function (frm) {
frm.trigger("max_attachments");
},

fields_add: (frm) => {
frm.trigger("setup_default_views");
},
});

extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm }));

+ 22
- 1
frappe/core/doctype/doctype/doctype.json Просмотреть файл

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


+ 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")

@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


+ 3
- 1
frappe/core/doctype/file/file.json Просмотреть файл

@@ -2,6 +2,7 @@
"actions": [],
"allow_import": 1,
"creation": "2012-12-12 11:19:22",
"default_view": "File",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -169,10 +170,11 @@
"read_only": 1
}
],
"force_re_route_to_default_view": 1,
"icon": "fa fa-file",
"idx": 1,
"links": [],
"modified": "2022-09-13 15:50:15.508250",
"modified": "2022-09-13 15:50:15.508251",
"modified_by": "Administrator",
"module": "Core",
"name": "File",


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



+ 23
- 1
frappe/custom/doctype/customize_form/customize_form.json Просмотреть файл

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


+ 4
- 0
frappe/custom/doctype/customize_form/customize_form.py Просмотреть файл

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



+ 2
- 2
frappe/desk/doctype/todo/todo_list.js Просмотреть файл

@@ -17,10 +17,10 @@ frappe.listview_settings["ToDo"] = {
return doc.reference_name;
},
get_label: function () {
return __("Open");
return __("Open", null, "Access");
},
get_description: function (doc) {
return __("Open {0}", [`${doc.reference_type} ${doc.reference_name}`]);
return __("Open {0}", [`${__(doc.reference_type)}: ${doc.reference_name}`]);
},
action: function (doc) {
frappe.set_route("Form", doc.reference_type, doc.reference_name);


+ 15
- 6
frappe/integrations/doctype/google_calendar/google_calendar.py Просмотреть файл

@@ -4,6 +4,7 @@

from datetime import datetime, timedelta
from urllib.parse import quote
from zoneinfo import ZoneInfo

import google.oauth2.credentials
import requests
@@ -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,


+ 4
- 0
frappe/model/delete_doc.py Просмотреть файл

@@ -95,6 +95,10 @@ def delete_doc(

update_flags(doc, flags, ignore_permissions)
check_permission_and_not_submitted(doc)
# delete custom table fields using this doctype.
frappe.db.delete(
"Custom Field", {"options": name, "fieldtype": ("in", frappe.model.table_fields)}
)
frappe.db.delete("__global_search", {"doctype": name})

delete_from_table(doctype, name, ignore_doctypes, None)


+ 12
- 5
frappe/model/document.py Просмотреть файл

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


+ 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) {
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);


+ 1
- 1
frappe/public/js/frappe/list/base_list.js Просмотреть файл

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


+ 12
- 2
frappe/public/js/frappe/microtemplate.js Просмотреть файл

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


+ 37
- 1
frappe/public/js/frappe/model/model.js Просмотреть файл

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


+ 86
- 22
frappe/public/js/frappe/router.js Просмотреть файл

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



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


+ 1
- 1
frappe/public/js/frappe/views/breadcrumbs.js Просмотреть файл

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


+ 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");

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


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


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

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



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



+ 17
- 13
frappe/public/js/frappe/widgets/widget_dialog.js Просмотреть файл

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


+ 20
- 4
frappe/tests/test_client.py Просмотреть файл

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


+ 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.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"


+ 151
- 4
frappe/tests/ui_test_helpers.py Просмотреть файл

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

+ 6
- 4
frappe/tests/utils.py Просмотреть файл

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



+ 1
- 0
frappe/translations/de.csv Просмотреть файл

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


+ 1
- 1
frappe/utils/redis_wrapper.py Просмотреть файл

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


+ 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"""
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):


+ 29
- 2
frappe/website/doctype/website_settings/test_website_settings.py Просмотреть файл

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

+ 12
- 0
frappe/website/doctype/website_settings/website_settings.py Просмотреть файл

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



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


Загрузка…
Отмена
Сохранить