fix: merge conflicts
This commit is contained in:
commit
7cefc240ac
86 changed files with 1780 additions and 1186 deletions
2
.github/workflows/backport.yml
vendored
2
.github/workflows/backport.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
|||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Checkout Actions
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: "frappe/backport"
|
||||
path: ./actions
|
||||
|
|
|
|||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
|
|
@ -174,7 +174,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -72,14 +72,14 @@ context("Timeline", () => {
|
|||
cy.click_listview_row_item(0);
|
||||
|
||||
//To check if the submission of the documemt is visible in the timeline content
|
||||
cy.get(".timeline-content").should("contain", "Frappe submitted this document");
|
||||
cy.get(".timeline-content").should("contain", "You submitted this document");
|
||||
cy.get('[id="page-Custom Submittable DocType"] .page-actions')
|
||||
.findByRole("button", { name: "Cancel" })
|
||||
.click();
|
||||
cy.get_open_dialog().findByRole("button", { name: "Yes" }).click();
|
||||
|
||||
//To check if the cancellation of the documemt is visible in the timeline content
|
||||
cy.get(".timeline-content").should("contain", "Frappe cancelled this document");
|
||||
cy.get(".timeline-content").should("contain", "You cancelled this document");
|
||||
|
||||
//Deleting the document
|
||||
cy.visit("/app/custom-submittable-doctype");
|
||||
|
|
|
|||
231
cypress/integration/view_routing.js
Normal file
231
cypress/integration/view_routing.js
Normal file
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
@ -42,7 +42,7 @@ context("Workspace Blocks", () => {
|
|||
cy.wait("@new_page");
|
||||
});
|
||||
|
||||
it("Quick List Block", () => {
|
||||
it.skip("Quick List Block", () => {
|
||||
cy.create_records([
|
||||
{
|
||||
doctype: "ToDo",
|
||||
|
|
|
|||
|
|
@ -129,12 +129,18 @@ def clear_doctype_cache(doctype=None):
|
|||
clear_single(doctype)
|
||||
|
||||
# clear all parent doctypes
|
||||
|
||||
for dt in frappe.get_all(
|
||||
"DocField", "parent", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
|
||||
):
|
||||
clear_single(dt.parent)
|
||||
|
||||
# clear all parent doctypes
|
||||
if not frappe.flags.in_install:
|
||||
for dt in frappe.get_all(
|
||||
"Custom Field", "dt", dict(fieldtype=["in", frappe.model.table_fields], options=doctype)
|
||||
):
|
||||
clear_single(dt.dt)
|
||||
|
||||
# clear all notifications
|
||||
delete_notification_count_for(doctype)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,5 @@
|
|||
import frappe
|
||||
from frappe import _
|
||||
from frappe.desk.moduleview import (
|
||||
config_exists,
|
||||
get_data,
|
||||
get_module_link_items_from_list,
|
||||
get_onboard_items,
|
||||
)
|
||||
|
||||
|
||||
def get_modules_from_all_apps_for_user(user: str = None) -> list[dict]:
|
||||
|
|
@ -25,9 +19,6 @@ def get_modules_from_all_apps_for_user(user: str = None) -> list[dict]:
|
|||
if module_name in empty_tables_by_module:
|
||||
module["onboard_present"] = 1
|
||||
|
||||
# Set defaults links
|
||||
module["links"] = get_onboard_items(module["app"], frappe.scrub(module_name))[:5]
|
||||
|
||||
return allowed_modules_list
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -228,11 +228,12 @@ def get_company_address(company):
|
|||
def address_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
doctype = "Address"
|
||||
link_doctype = filters.pop("link_doctype")
|
||||
link_name = filters.pop("link_name")
|
||||
|
||||
condition = ""
|
||||
meta = frappe.get_meta("Address")
|
||||
meta = frappe.get_meta(doctype)
|
||||
for fieldname, value in filters.items():
|
||||
if meta.get_field(fieldname) or fieldname in frappe.db.DEFAULT_COLUMNS:
|
||||
condition += f" and {fieldname}={frappe.db.escape(value)}"
|
||||
|
|
|
|||
|
|
@ -210,8 +210,9 @@ def update_contact(doc, method):
|
|||
def contact_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_match_cond
|
||||
|
||||
doctype = "Contact"
|
||||
if (
|
||||
not frappe.get_meta("Contact").get_field(searchfield)
|
||||
not frappe.get_meta(doctype).get_field(searchfield)
|
||||
and searchfield not in frappe.db.DEFAULT_COLUMNS
|
||||
):
|
||||
return []
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -53,7 +53,7 @@ def export_data(
|
|||
template_bool = template
|
||||
if isinstance(template, str):
|
||||
template_bool = template.lower() == "true"
|
||||
|
||||
|
||||
export_without_column_meta_bool = export_without_column_meta
|
||||
if isinstance(export_without_column_meta, str):
|
||||
export_without_column_meta_bool = export_without_column_meta.lower() == "true"
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ frappe.ui.form.on("DocType", {
|
|||
|
||||
if (frm.is_new()) {
|
||||
frm.events.set_default_permission(frm);
|
||||
frm.set_value("default_view", "List");
|
||||
} else {
|
||||
frm.toggle_enable("engine", 0);
|
||||
}
|
||||
|
|
@ -66,12 +67,14 @@ frappe.ui.form.on("DocType", {
|
|||
|
||||
frm.cscript.autoname(frm);
|
||||
frm.cscript.set_naming_rule_description(frm);
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
|
||||
istable: (frm) => {
|
||||
if (frm.doc.istable && frm.is_new()) {
|
||||
frm.set_value("autoname", "autoincrement");
|
||||
frm.set_value("allow_rename", 0);
|
||||
frm.set_value("default_view", null);
|
||||
} else if (!frm.doc.istable && !frm.is_new()) {
|
||||
frm.events.set_default_permission(frm);
|
||||
}
|
||||
|
|
@ -82,6 +85,18 @@ frappe.ui.form.on("DocType", {
|
|||
frm.add_child("permissions", { role: "System Manager" });
|
||||
}
|
||||
},
|
||||
|
||||
is_tree: (frm) => {
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
|
||||
is_calendar_and_gantt: (frm) => {
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
|
||||
setup_default_views: (frm) => {
|
||||
frappe.model.set_default_views_for_doctype(frm.doc.name, frm);
|
||||
},
|
||||
});
|
||||
|
||||
frappe.ui.form.on("DocField", {
|
||||
|
|
@ -171,6 +186,10 @@ frappe.ui.form.on("DocField", {
|
|||
fieldtype: function (frm) {
|
||||
frm.trigger("max_attachments");
|
||||
},
|
||||
|
||||
fields_add: (frm) => {
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
});
|
||||
|
||||
extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({ frm: cur_frm }));
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"istable",
|
||||
"issingle",
|
||||
"is_tree",
|
||||
"is_calendar_and_gantt",
|
||||
"editable_grid",
|
||||
"quick_entry",
|
||||
"cb01",
|
||||
|
|
@ -54,6 +55,8 @@
|
|||
"default_print_format",
|
||||
"sort_field",
|
||||
"sort_order",
|
||||
"default_view",
|
||||
"force_re_route_to_default_view",
|
||||
"column_break_29",
|
||||
"document_type",
|
||||
"icon",
|
||||
|
|
@ -614,6 +617,24 @@
|
|||
"fieldname": "queue_in_background",
|
||||
"fieldtype": "Check",
|
||||
"label": "Queue in Background"
|
||||
},
|
||||
{
|
||||
"fieldname": "default_view",
|
||||
"fieldtype": "Select",
|
||||
"label": "Default View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "force_re_route_to_default_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "Force Re-route to Default View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enables Calendar and Gantt views.",
|
||||
"fieldname": "is_calendar_and_gantt",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Calendar and Gantt"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-bolt",
|
||||
|
|
@ -696,7 +717,7 @@
|
|||
"link_fieldname": "reference_doctype"
|
||||
}
|
||||
],
|
||||
"modified": "2022-09-02 12:05:59.589751",
|
||||
"modified": "2022-10-12 14:13:27.315351",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "DocType",
|
||||
|
|
|
|||
|
|
@ -670,6 +670,18 @@ class TestDocType(FrappeTestCase):
|
|||
|
||||
self.assertEqual(test_json.test_json_field["hello"], "world")
|
||||
|
||||
@patch.dict(frappe.conf, {"developer_mode": 1})
|
||||
def test_custom_field_deletion(self):
|
||||
"""Custom child tables whose doctype doesn't exist should be auto deleted."""
|
||||
doctype = new_doctype(custom=0).insert().name
|
||||
child = new_doctype(custom=0, istable=1).insert().name
|
||||
|
||||
field = "abc"
|
||||
create_custom_fields({doctype: [{"fieldname": field, "fieldtype": "Table", "options": child}]})
|
||||
|
||||
frappe.delete_doc("DocType", child)
|
||||
self.assertFalse(frappe.get_meta(doctype).get_field(field))
|
||||
|
||||
@patch.dict(frappe.conf, {"developer_mode": 1})
|
||||
def test_delete_doctype_with_customization(self):
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"creation": "2012-12-12 11:19:22",
|
||||
"default_view": "File",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
|
|
@ -169,10 +170,11 @@
|
|||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"force_re_route_to_default_view": 1,
|
||||
"icon": "fa fa-file",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-09-13 15:50:15.508250",
|
||||
"modified": "2022-09-13 15:50:15.508251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "File",
|
||||
|
|
|
|||
|
|
@ -237,7 +237,7 @@ class User(Document):
|
|||
)
|
||||
|
||||
def share_with_self(self):
|
||||
frappe.share.add(
|
||||
frappe.share.add_docshare(
|
||||
self.doctype, self.name, self.name, write=1, share=1, flags={"ignore_share_permission": True}
|
||||
)
|
||||
|
||||
|
|
@ -901,6 +901,7 @@ def reset_password(user):
|
|||
def user_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
from frappe.desk.reportview import get_filters_cond, get_match_cond
|
||||
|
||||
doctype = "User"
|
||||
conditions = []
|
||||
|
||||
user_type_condition = "and user_type != 'Website User'"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// Copyright (c) 2022, Frappe Technologies and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Database Storage Usage By Tables"] = {
|
||||
filters: [],
|
||||
};
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2022-10-19 02:25:24.326791",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"letter_head": "abc",
|
||||
"modified": "2022-10-19 02:59:00.365307",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Core",
|
||||
"name": "Database Storage Usage By Tables",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"query": "",
|
||||
"ref_doctype": "Error Log",
|
||||
"report_name": "Database Storage Usage By Tables",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "System Manager"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
|
||||
COLUMNS = [
|
||||
{"label": "Table", "fieldname": "table", "fieldtype": "Data", "width": 200},
|
||||
{"label": "Size (MB)", "fieldname": "size", "fieldtype": "Float"},
|
||||
{"label": "Data (MB)", "fieldname": "data_size", "fieldtype": "Float"},
|
||||
{"label": "Index (MB)", "fieldname": "index_size", "fieldtype": "Float"},
|
||||
]
|
||||
|
||||
|
||||
def execute(filters=None):
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
data = frappe.db.multisql(
|
||||
{
|
||||
"mariadb": """
|
||||
SELECT table_name AS `table`,
|
||||
round(((data_length + index_length) / 1024 / 1024), 2) `size`,
|
||||
round((data_length / 1024 / 1024), 2) as data_size,
|
||||
round((index_length / 1024 / 1024), 2) as index_size
|
||||
FROM information_schema.TABLES
|
||||
ORDER BY (data_length + index_length) DESC;
|
||||
""",
|
||||
"postgres": """
|
||||
SELECT
|
||||
table_name as "table",
|
||||
round(pg_total_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "size",
|
||||
round(pg_relation_size(quote_ident(table_name)) / 1024 / 1024, 2) as "data_size",
|
||||
round(pg_indexes_size(quote_ident(table_name)) / 1024 / 1024, 2) as "index_size"
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY 2 DESC;
|
||||
""",
|
||||
},
|
||||
as_dict=1,
|
||||
)
|
||||
return COLUMNS, data
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# Copyright (c) 2022, Frappe Technologies and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from frappe.core.report.database_storage_usage_by_tables.database_storage_usage_by_tables import (
|
||||
execute,
|
||||
)
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
|
||||
class TestDBUsageReport(FrappeTestCase):
|
||||
def test_basic_query(self):
|
||||
_, data = execute()
|
||||
tables = [d.table for d in data]
|
||||
self.assertFalse({"tabUser", "tabDocField"}.difference(tables))
|
||||
|
|
@ -72,6 +72,7 @@ frappe.ui.form.on("Customize Form", {
|
|||
} else {
|
||||
frm.refresh();
|
||||
frm.trigger("setup_sortable");
|
||||
frm.trigger("setup_default_views");
|
||||
}
|
||||
}
|
||||
localStorage["customize_doctype"] = frm.doc.doc_type;
|
||||
|
|
@ -82,8 +83,12 @@ frappe.ui.form.on("Customize Form", {
|
|||
}
|
||||
},
|
||||
|
||||
is_calendar_and_gantt: function (frm) {
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
|
||||
setup_sortable: function (frm) {
|
||||
frm.doc.fields.forEach(function (f, i) {
|
||||
frm.doc.fields.forEach(function (f) {
|
||||
if (!f.is_custom_field) {
|
||||
f._sortable = false;
|
||||
}
|
||||
|
|
@ -222,6 +227,10 @@ frappe.ui.form.on("Customize Form", {
|
|||
frm.set_df_property("sort_field", "options", fields);
|
||||
}
|
||||
},
|
||||
|
||||
setup_default_views(frm) {
|
||||
frappe.model.set_default_views_for_doctype(frm.doc.doc_type, frm);
|
||||
},
|
||||
});
|
||||
|
||||
// can't delete standard fields
|
||||
|
|
@ -237,6 +246,7 @@ frappe.ui.form.on("Customize Form Field", {
|
|||
var f = frappe.model.get_doc(cdt, cdn);
|
||||
f.is_system_generated = false;
|
||||
f.is_custom_field = true;
|
||||
frm.trigger("setup_default_views");
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"column_break_5",
|
||||
"is_submittable",
|
||||
"istable",
|
||||
"is_calendar_and_gantt",
|
||||
"editable_grid",
|
||||
"quick_entry",
|
||||
"track_changes",
|
||||
|
|
@ -37,6 +38,8 @@
|
|||
"show_title_field_in_link",
|
||||
"translated_doctype",
|
||||
"default_print_format",
|
||||
"default_view",
|
||||
"force_re_route_to_default_view",
|
||||
"column_break_29",
|
||||
"show_preview_popup",
|
||||
"email_settings_section",
|
||||
|
|
@ -341,20 +344,39 @@
|
|||
"label": "Make Attachments Public by Default"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_submittable",
|
||||
"fieldname": "queue_in_background",
|
||||
"fieldtype": "Check",
|
||||
"label": "Queue in Background"
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_submittable",
|
||||
"fieldname": "queue_in_background",
|
||||
"fieldtype": "Check",
|
||||
"label": "Queue in Background"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_submittable",
|
||||
"fetch_from": "doc_type.is_submittable",
|
||||
"fieldname": "is_submittable",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Submittable",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "default_view",
|
||||
"fieldtype": "Select",
|
||||
"label": "Default View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_submittable",
|
||||
"fetch_from": "doc_type.is_submittable",
|
||||
"fieldname": "is_submittable",
|
||||
"depends_on": "default_view",
|
||||
"fieldname": "force_re_route_to_default_view",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Submittable",
|
||||
"read_only": 1
|
||||
"label": "Force Re-route to Default View"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enables Calendar and Gantt views.",
|
||||
"fieldname": "is_calendar_and_gantt",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Calendar and Gantt"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
|
|
@ -363,7 +385,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-10-11 21:23:36.669135",
|
||||
"modified": "2022-08-30 11:45:16.772277",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Custom",
|
||||
"name": "Customize Form",
|
||||
|
|
|
|||
|
|
@ -587,6 +587,10 @@ doctype_properties = {
|
|||
"naming_rule": "Data",
|
||||
"autoname": "Data",
|
||||
"show_title_field_in_link": "Check",
|
||||
"translate_link_fields": "Check",
|
||||
"is_calendar_and_gantt": "Check",
|
||||
"default_view": "Select",
|
||||
"force_re_route_to_default_view": "Check",
|
||||
"translated_doctype": "Check",
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ class TestCustomizeForm(FrappeTestCase):
|
|||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEqual(d.doc_type, "Event")
|
||||
self.assertEqual(len(d.get("fields")), 36)
|
||||
self.assertEqual(len(d.get("fields")), 38)
|
||||
|
||||
d = self.get_customize_form("Event")
|
||||
self.assertEqual(d.doc_type, "Event")
|
||||
|
|
|
|||
|
|
@ -41,6 +41,18 @@ frappe.ui.form.on("Event", {
|
|||
},
|
||||
__("Add Participants")
|
||||
);
|
||||
|
||||
const [ends_on_date] = frm.doc.ends_on
|
||||
? frm.doc.ends_on.split(" ")
|
||||
: frm.doc.starts_on.split(" ");
|
||||
|
||||
if (frm.doc.google_meet_link && frappe.datetime.now_date() <= ends_on_date) {
|
||||
frm.dashboard.set_headline(
|
||||
__("Join video conference with {0}", [
|
||||
`<a target='_blank' href='${frm.doc.google_meet_link}'>Google Meet</a>`,
|
||||
])
|
||||
);
|
||||
}
|
||||
},
|
||||
repeat_on: function (frm) {
|
||||
if (frm.doc.repeat_on === "Every Day") {
|
||||
|
|
|
|||
|
|
@ -22,12 +22,14 @@
|
|||
"sender",
|
||||
"all_day",
|
||||
"sync_with_google_calendar",
|
||||
"add_video_conferencing",
|
||||
"sb_00",
|
||||
"google_calendar",
|
||||
"pulled_from_google_calendar",
|
||||
"cb_00",
|
||||
"google_calendar_id",
|
||||
"cb_00",
|
||||
"google_calendar_event_id",
|
||||
"google_meet_link",
|
||||
"pulled_from_google_calendar",
|
||||
"section_break_13",
|
||||
"repeat_on",
|
||||
"repeat_till",
|
||||
|
|
@ -225,7 +227,7 @@
|
|||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:doc.sync_with_google_calendar",
|
||||
"depends_on": "eval:doc.sync_with_google_calendar || doc.pulled_from_google_calendar",
|
||||
"fieldname": "sb_00",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Google Calendar"
|
||||
|
|
@ -245,6 +247,7 @@
|
|||
"fieldname": "google_calendar_event_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Google Calendar Event ID",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -272,12 +275,27 @@
|
|||
"label": "Sender",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.sync_with_google_calendar",
|
||||
"description": "via Google Meet",
|
||||
"fieldname": "add_video_conferencing",
|
||||
"fieldtype": "Check",
|
||||
"label": "Add Video Conferencing"
|
||||
},
|
||||
{
|
||||
"fieldname": "google_meet_link",
|
||||
"fieldtype": "Data",
|
||||
"label": "Google Meet Link",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-calendar",
|
||||
"idx": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-12 05:43:27.935510",
|
||||
"modified": "2022-08-12 19:24:34.794098",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event",
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import json
|
|||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.contact.contact import get_default_contact
|
||||
from frappe.desk.doctype.notification_settings.notification_settings import (
|
||||
is_email_notifications_enabled_for_type,
|
||||
)
|
||||
|
|
@ -55,6 +56,12 @@ class Event(Document):
|
|||
if self.sync_with_google_calendar and not self.google_calendar:
|
||||
frappe.throw(_("Select Google Calendar to which event should be synced."))
|
||||
|
||||
if not self.sync_with_google_calendar:
|
||||
self.add_video_conferencing = 0
|
||||
|
||||
def before_save(self):
|
||||
self.set_participants_email()
|
||||
|
||||
def on_update(self):
|
||||
self.sync_communication()
|
||||
|
||||
|
|
@ -131,6 +138,22 @@ class Event(Document):
|
|||
for participant in participants:
|
||||
self.add_participant(participant["doctype"], participant["docname"])
|
||||
|
||||
def set_participants_email(self):
|
||||
for participant in self.event_participants:
|
||||
if participant.email:
|
||||
continue
|
||||
|
||||
if participant.reference_doctype != "Contact":
|
||||
participant_contact = get_default_contact(
|
||||
participant.reference_doctype, participant.reference_docname
|
||||
)
|
||||
else:
|
||||
participant_contact = participant.reference_docname
|
||||
|
||||
participant.email = (
|
||||
frappe.get_value("Contact", participant_contact, "email_id") if participant_contact else None
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_communication(event, reference_doctype, reference_docname):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,8 @@
|
|||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"reference_doctype",
|
||||
"reference_docname"
|
||||
"reference_docname",
|
||||
"email"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -24,11 +25,17 @@
|
|||
"label": "Reference Name",
|
||||
"options": "reference_doctype",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email",
|
||||
"options": "Email"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-08-03 12:20:50.466370",
|
||||
"modified": "2022-10-18 17:49:33.549459",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Desk",
|
||||
"name": "Event Participants",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1,615 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.boot import get_allowed_pages, get_allowed_reports
|
||||
from frappe.cache_manager import (
|
||||
build_domain_restriced_doctype_cache,
|
||||
build_domain_restriced_page_cache,
|
||||
build_table_count_cache,
|
||||
)
|
||||
from frappe.desk.doctype.desktop_icon.desktop_icon import clear_desktop_icons_cache, set_hidden
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get(module):
|
||||
"""Returns data (sections, list of reports, counts) to render module view in desk:
|
||||
`/desk/#Module/[name]`."""
|
||||
data = get_data(module)
|
||||
|
||||
out = {"data": data}
|
||||
|
||||
return out
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def hide_module(module):
|
||||
set_hidden(module, frappe.session.user, 1)
|
||||
clear_desktop_icons_cache()
|
||||
|
||||
|
||||
def get_table_with_counts():
|
||||
counts = frappe.cache().get_value("information_schema:counts")
|
||||
if counts:
|
||||
return counts
|
||||
else:
|
||||
return build_table_count_cache()
|
||||
|
||||
|
||||
def get_data(module, build=True):
|
||||
"""Get module data for the module view `desk/#Module/[name]`"""
|
||||
doctype_info = get_doctype_info(module)
|
||||
data = build_config_from_file(module)
|
||||
|
||||
if not data:
|
||||
data = build_standard_config(module, doctype_info)
|
||||
else:
|
||||
add_custom_doctypes(data, doctype_info)
|
||||
|
||||
add_section(data, _("Custom Reports"), "fa fa-list-alt", get_report_list(module))
|
||||
|
||||
data = combine_common_sections(data)
|
||||
data = apply_permissions(data)
|
||||
|
||||
# set_last_modified(data)
|
||||
|
||||
if build:
|
||||
exists_cache = get_table_with_counts()
|
||||
|
||||
def doctype_contains_a_record(name):
|
||||
exists = exists_cache.get(name)
|
||||
if not exists:
|
||||
if not frappe.db.get_value("DocType", name, "issingle"):
|
||||
exists = frappe.db.count(name)
|
||||
else:
|
||||
exists = True
|
||||
exists_cache[name] = exists
|
||||
return exists
|
||||
|
||||
for section in data:
|
||||
for item in section["items"]:
|
||||
# Onboarding
|
||||
|
||||
# First disable based on exists of depends_on list
|
||||
doctype = item.get("doctype")
|
||||
dependencies = item.get("dependencies") or None
|
||||
if not dependencies and doctype:
|
||||
item["dependencies"] = [doctype]
|
||||
|
||||
dependencies = item.get("dependencies")
|
||||
if dependencies:
|
||||
incomplete_dependencies = [d for d in dependencies if not doctype_contains_a_record(d)]
|
||||
if len(incomplete_dependencies):
|
||||
item["incomplete_dependencies"] = incomplete_dependencies
|
||||
|
||||
if item.get("onboard"):
|
||||
# Mark Spotlights for initial
|
||||
if item.get("type") == "doctype":
|
||||
name = item.get("name")
|
||||
count = doctype_contains_a_record(name)
|
||||
|
||||
item["count"] = count
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def build_config_from_file(module):
|
||||
"""Build module info from `app/config/desktop.py` files."""
|
||||
data = []
|
||||
module = frappe.scrub(module)
|
||||
|
||||
for app in frappe.get_installed_apps():
|
||||
try:
|
||||
data += get_config(app, module)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
return filter_by_restrict_to_domain(data)
|
||||
|
||||
|
||||
def filter_by_restrict_to_domain(data):
|
||||
"""filter Pages and DocType depending on the Active Module(s)"""
|
||||
doctypes = (
|
||||
frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
|
||||
)
|
||||
pages = frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
|
||||
|
||||
for d in data:
|
||||
_items = []
|
||||
for item in d.get("items", []):
|
||||
|
||||
item_type = item.get("type")
|
||||
item_name = item.get("name")
|
||||
|
||||
if (item_name in pages) or (item_name in doctypes) or item_type == "report":
|
||||
_items.append(item)
|
||||
|
||||
d.update({"items": _items})
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def build_standard_config(module, doctype_info):
|
||||
"""Build standard module data from DocTypes."""
|
||||
if not frappe.db.get_value("Module Def", module):
|
||||
frappe.throw(_("Module Not Found"))
|
||||
|
||||
data = []
|
||||
|
||||
add_section(
|
||||
data,
|
||||
_("Documents"),
|
||||
"fa fa-star",
|
||||
[d for d in doctype_info if d.document_type in ("Document", "Transaction")],
|
||||
)
|
||||
|
||||
add_section(
|
||||
data,
|
||||
_("Setup"),
|
||||
"fa fa-cog",
|
||||
[d for d in doctype_info if d.document_type in ("Master", "Setup", "")],
|
||||
)
|
||||
|
||||
add_section(data, _("Standard Reports"), "fa fa-list", get_report_list(module, is_standard="Yes"))
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def add_section(data, label, icon, items):
|
||||
"""Adds a section to the module data."""
|
||||
if not items:
|
||||
return
|
||||
data.append({"label": label, "icon": icon, "items": items})
|
||||
|
||||
|
||||
def add_custom_doctypes(data, doctype_info):
|
||||
"""Adds Custom DocTypes to modules setup via `config/desktop.py`."""
|
||||
add_section(
|
||||
data,
|
||||
_("Documents"),
|
||||
"fa fa-star",
|
||||
[d for d in doctype_info if (d.custom and d.document_type in ("Document", "Transaction"))],
|
||||
)
|
||||
|
||||
add_section(
|
||||
data,
|
||||
_("Setup"),
|
||||
"fa fa-cog",
|
||||
[d for d in doctype_info if (d.custom and d.document_type in ("Setup", "Master", ""))],
|
||||
)
|
||||
|
||||
|
||||
def get_doctype_info(module):
|
||||
"""Returns list of non child DocTypes for given module."""
|
||||
active_domains = frappe.get_active_domains()
|
||||
|
||||
doctype_info = frappe.get_all(
|
||||
"DocType",
|
||||
filters={"module": module, "istable": 0},
|
||||
or_filters={"ifnull(restrict_to_domain, '')": "", "restrict_to_domain": ("in", active_domains)},
|
||||
fields=["'doctype' as type", "name", "description", "document_type", "custom", "issingle"],
|
||||
order_by="custom asc, document_type desc, name asc",
|
||||
)
|
||||
|
||||
for d in doctype_info:
|
||||
d.document_type = d.document_type or ""
|
||||
d.description = _(d.description or "")
|
||||
|
||||
return doctype_info
|
||||
|
||||
|
||||
def combine_common_sections(data):
|
||||
"""Combine sections declared in separate apps."""
|
||||
sections = []
|
||||
sections_dict = {}
|
||||
for each in data:
|
||||
if each["label"] not in sections_dict:
|
||||
sections_dict[each["label"]] = each
|
||||
sections.append(each)
|
||||
else:
|
||||
sections_dict[each["label"]]["items"] += each["items"]
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def apply_permissions(data):
|
||||
default_country = frappe.db.get_default("country")
|
||||
|
||||
user = frappe.get_user()
|
||||
user.build_permissions()
|
||||
|
||||
allowed_pages = get_allowed_pages()
|
||||
allowed_reports = get_allowed_reports()
|
||||
|
||||
new_data = []
|
||||
for section in data:
|
||||
new_items = []
|
||||
|
||||
for item in section.get("items") or []:
|
||||
item = frappe._dict(item)
|
||||
|
||||
if item.country and item.country != default_country:
|
||||
continue
|
||||
|
||||
if (
|
||||
(item.type == "doctype" and item.name in user.can_read)
|
||||
or (item.type == "page" and item.name in allowed_pages)
|
||||
or (item.type == "report" and item.name in allowed_reports)
|
||||
or item.type == "help"
|
||||
):
|
||||
|
||||
new_items.append(item)
|
||||
|
||||
if new_items:
|
||||
new_section = section.copy()
|
||||
new_section["items"] = new_items
|
||||
new_data.append(new_section)
|
||||
|
||||
return new_data
|
||||
|
||||
|
||||
def get_disabled_reports():
|
||||
if not hasattr(frappe.local, "disabled_reports"):
|
||||
frappe.local.disabled_reports = {r.name for r in frappe.get_all("Report", {"disabled": 1})}
|
||||
return frappe.local.disabled_reports
|
||||
|
||||
|
||||
def get_config(app, module):
|
||||
"""Load module info from `[app].config.[module]`."""
|
||||
config = frappe.get_module(f"{app}.config.{module}")
|
||||
config = config.get_data()
|
||||
|
||||
sections = [s for s in config if s.get("condition", True)]
|
||||
|
||||
disabled_reports = get_disabled_reports()
|
||||
for section in sections:
|
||||
items = []
|
||||
for item in section["items"]:
|
||||
if item["type"] == "report" and item["name"] in disabled_reports:
|
||||
continue
|
||||
# some module links might not have name
|
||||
if not item.get("name"):
|
||||
item["name"] = item.get("label")
|
||||
if not item.get("label"):
|
||||
item["label"] = _(item.get("name"))
|
||||
items.append(item)
|
||||
section["items"] = items
|
||||
|
||||
return sections
|
||||
|
||||
|
||||
def config_exists(app, module):
|
||||
try:
|
||||
frappe.get_module(f"{app}.config.{module}")
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
|
||||
def add_setup_section(config, app, module, label, icon):
|
||||
"""Add common sections to `/desk#Module/Setup`"""
|
||||
try:
|
||||
setup_section = get_setup_section(app, module, label, icon)
|
||||
if setup_section:
|
||||
config.append(setup_section)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def get_setup_section(app, module, label, icon):
|
||||
"""Get the setup section from each module (for global Setup page)."""
|
||||
config = get_config(app, module)
|
||||
for section in config:
|
||||
if section.get("label") == _("Setup"):
|
||||
return {"label": label, "icon": icon, "items": section["items"]}
|
||||
|
||||
|
||||
def get_onboard_items(app, module):
|
||||
try:
|
||||
sections = get_config(app, module)
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
onboard_items = []
|
||||
fallback_items = []
|
||||
|
||||
if not sections:
|
||||
doctype_info = get_doctype_info(module)
|
||||
sections = build_standard_config(module, doctype_info)
|
||||
|
||||
for section in sections:
|
||||
for item in section["items"]:
|
||||
if item.get("onboard", 0) == 1:
|
||||
onboard_items.append(item)
|
||||
|
||||
# in case onboard is not set
|
||||
fallback_items.append(item)
|
||||
|
||||
if len(onboard_items) > 5:
|
||||
return onboard_items
|
||||
|
||||
return onboard_items or fallback_items
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_links_for_module(app, module):
|
||||
return [{"value": l.get("name"), "label": l.get("label")} for l in get_links(app, module)]
|
||||
|
||||
|
||||
def get_links(app, module):
|
||||
try:
|
||||
sections = get_config(app, frappe.scrub(module))
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
links = []
|
||||
for section in sections:
|
||||
for item in section["items"]:
|
||||
links.append(item)
|
||||
return links
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_desktop_settings():
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
|
||||
all_modules = get_modules_from_all_apps_for_user()
|
||||
home_settings = get_home_settings()
|
||||
|
||||
modules_by_name = {}
|
||||
for m in all_modules:
|
||||
modules_by_name[m["module_name"]] = m
|
||||
|
||||
module_categories = ["Modules", "Domains", "Places", "Administration"]
|
||||
user_modules_by_category = {}
|
||||
|
||||
user_saved_modules_by_category = home_settings.modules_by_category or {}
|
||||
user_saved_links_by_module = home_settings.links_by_module or {}
|
||||
|
||||
def apply_user_saved_links(module):
|
||||
module = frappe._dict(module)
|
||||
all_links = get_links(module.app, module.module_name)
|
||||
module_links_by_name = {}
|
||||
for link in all_links:
|
||||
module_links_by_name[link["name"]] = link
|
||||
|
||||
if module.module_name in user_saved_links_by_module:
|
||||
user_links = frappe.parse_json(user_saved_links_by_module[module.module_name])
|
||||
module.links = [module_links_by_name[l] for l in user_links if l in module_links_by_name]
|
||||
|
||||
return module
|
||||
|
||||
for category in module_categories:
|
||||
if category in user_saved_modules_by_category:
|
||||
user_modules = user_saved_modules_by_category[category]
|
||||
user_modules_by_category[category] = [
|
||||
apply_user_saved_links(modules_by_name[m]) for m in user_modules if modules_by_name.get(m)
|
||||
]
|
||||
else:
|
||||
user_modules_by_category[category] = [
|
||||
apply_user_saved_links(m) for m in all_modules if m.get("category") == category
|
||||
]
|
||||
|
||||
# filter out hidden modules
|
||||
if home_settings.hidden_modules:
|
||||
for category in user_modules_by_category:
|
||||
hidden_modules = home_settings.hidden_modules or []
|
||||
modules = user_modules_by_category[category]
|
||||
user_modules_by_category[category] = [
|
||||
module for module in modules if module.module_name not in hidden_modules
|
||||
]
|
||||
|
||||
return user_modules_by_category
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_hidden_modules(category_map):
|
||||
category_map = frappe.parse_json(category_map)
|
||||
home_settings = get_home_settings()
|
||||
|
||||
saved_hidden_modules = home_settings.hidden_modules or []
|
||||
|
||||
for category in category_map:
|
||||
config = frappe._dict(category_map[category])
|
||||
saved_hidden_modules += config.removed or []
|
||||
saved_hidden_modules = [d for d in saved_hidden_modules if d not in (config.added or [])]
|
||||
|
||||
if home_settings.get("modules_by_category") and home_settings.modules_by_category.get(category):
|
||||
module_placement = [
|
||||
d for d in (config.added or []) if d not in home_settings.modules_by_category[category]
|
||||
]
|
||||
home_settings.modules_by_category[category] += module_placement
|
||||
|
||||
home_settings.hidden_modules = saved_hidden_modules
|
||||
set_home_settings(home_settings)
|
||||
|
||||
return get_desktop_settings()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_global_hidden_modules(modules):
|
||||
modules = frappe.parse_json(modules)
|
||||
frappe.only_for("System Manager")
|
||||
|
||||
doc = frappe.get_doc("User", "Administrator")
|
||||
doc.set("block_modules", [])
|
||||
for module in modules:
|
||||
doc.append("block_modules", {"module": module})
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
return get_desktop_settings()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_modules_order(module_category, modules):
|
||||
modules = frappe.parse_json(modules)
|
||||
home_settings = get_home_settings()
|
||||
|
||||
home_settings.modules_by_category = home_settings.modules_by_category or {}
|
||||
home_settings.modules_by_category[module_category] = modules
|
||||
|
||||
set_home_settings(home_settings)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_links_for_module(module_name, links):
|
||||
links = frappe.parse_json(links)
|
||||
home_settings = get_home_settings()
|
||||
|
||||
home_settings.setdefault("links_by_module", {})
|
||||
home_settings["links_by_module"].setdefault(module_name, None)
|
||||
home_settings["links_by_module"][module_name] = links
|
||||
|
||||
set_home_settings(home_settings)
|
||||
|
||||
return get_desktop_settings()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options_for_show_hide_cards():
|
||||
global_options = []
|
||||
|
||||
if "System Manager" in frappe.get_roles():
|
||||
global_options = get_options_for_global_modules()
|
||||
|
||||
return {"user_options": get_options_for_user_blocked_modules(), "global_options": global_options}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options_for_global_modules():
|
||||
from frappe.config import get_modules_from_all_apps
|
||||
|
||||
all_modules = get_modules_from_all_apps()
|
||||
|
||||
blocked_modules = frappe.get_doc("User", "Administrator").get_blocked_modules()
|
||||
|
||||
options = []
|
||||
for module in all_modules:
|
||||
module = frappe._dict(module)
|
||||
options.append(
|
||||
{
|
||||
"category": module.category,
|
||||
"label": module.label,
|
||||
"value": module.module_name,
|
||||
"checked": module.module_name not in blocked_modules,
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options_for_user_blocked_modules():
|
||||
from frappe.config import get_modules_from_all_apps_for_user
|
||||
|
||||
all_modules = get_modules_from_all_apps_for_user()
|
||||
home_settings = get_home_settings()
|
||||
|
||||
hidden_modules = home_settings.hidden_modules or []
|
||||
|
||||
options = []
|
||||
for module in all_modules:
|
||||
module = frappe._dict(module)
|
||||
options.append(
|
||||
{
|
||||
"category": module.category,
|
||||
"label": module.label,
|
||||
"value": module.module_name,
|
||||
"checked": module.module_name not in hidden_modules,
|
||||
}
|
||||
)
|
||||
|
||||
return options
|
||||
|
||||
|
||||
def set_home_settings(home_settings):
|
||||
frappe.cache().hset("home_settings", frappe.session.user, home_settings)
|
||||
frappe.db.set_value("User", frappe.session.user, "home_settings", json.dumps(home_settings))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_home_settings():
|
||||
def get_from_db():
|
||||
settings = frappe.db.get_value("User", frappe.session.user, "home_settings")
|
||||
return frappe.parse_json(settings or "{}")
|
||||
|
||||
home_settings = frappe.cache().hget("home_settings", frappe.session.user, get_from_db)
|
||||
return home_settings
|
||||
|
||||
|
||||
def get_module_link_items_from_list(app, module, list_of_link_names):
|
||||
try:
|
||||
sections = get_config(app, frappe.scrub(module))
|
||||
except ImportError:
|
||||
return []
|
||||
|
||||
links = []
|
||||
for section in sections:
|
||||
for item in section["items"]:
|
||||
if item.get("label", "") in list_of_link_names:
|
||||
links.append(item)
|
||||
|
||||
return links
|
||||
|
||||
|
||||
def set_last_modified(data):
|
||||
for section in data:
|
||||
for item in section["items"]:
|
||||
if item["type"] == "doctype":
|
||||
item["last_modified"] = get_last_modified(item["name"])
|
||||
|
||||
|
||||
def get_last_modified(doctype):
|
||||
def _get():
|
||||
try:
|
||||
last_modified = frappe.get_all(
|
||||
doctype, fields=["max(modified)"], as_list=True, limit_page_length=1
|
||||
)[0][0]
|
||||
except Exception as e:
|
||||
if frappe.db.is_table_missing(e):
|
||||
last_modified = None
|
||||
else:
|
||||
raise
|
||||
|
||||
# hack: save as -1 so that it is cached
|
||||
if last_modified is None:
|
||||
last_modified = -1
|
||||
|
||||
return last_modified
|
||||
|
||||
last_modified = frappe.cache().hget("last_modified", doctype, _get)
|
||||
|
||||
if last_modified == -1:
|
||||
last_modified = None
|
||||
|
||||
return last_modified
|
||||
|
||||
|
||||
def get_report_list(module, is_standard="No"):
|
||||
"""Returns list on new style reports for modules."""
|
||||
reports = frappe.get_list(
|
||||
"Report",
|
||||
fields=["name", "ref_doctype", "report_type"],
|
||||
filters={"is_standard": is_standard, "disabled": 0, "module": module},
|
||||
order_by="name",
|
||||
)
|
||||
|
||||
out = []
|
||||
for r in reports:
|
||||
out.append(
|
||||
{
|
||||
"type": "report",
|
||||
"doctype": r.ref_doctype,
|
||||
"is_query_report": 1
|
||||
if r.report_type in ("Query Report", "Script Report", "Custom Report")
|
||||
else 0,
|
||||
"label": _(r.name),
|
||||
"name": r.name,
|
||||
}
|
||||
)
|
||||
|
||||
return out
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
import copy
|
||||
import json
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_data(doctypes, last_modified):
|
||||
data_map = {}
|
||||
for dump_report_map in frappe.get_hooks().dump_report_map:
|
||||
data_map.update(frappe.get_attr(dump_report_map))
|
||||
|
||||
out = {}
|
||||
|
||||
doctypes = json.loads(doctypes)
|
||||
last_modified = json.loads(last_modified)
|
||||
|
||||
for d in doctypes:
|
||||
args = copy.deepcopy(data_map[d])
|
||||
dt = d.find("[") != -1 and d[: d.find("[")] or d
|
||||
out[dt] = {}
|
||||
|
||||
if args.get("from"):
|
||||
modified_table = "item."
|
||||
else:
|
||||
modified_table = ""
|
||||
|
||||
conditions = order_by = ""
|
||||
table = args.get("from") or ("`tab%s`" % dt)
|
||||
|
||||
if d in last_modified:
|
||||
if not args.get("conditions"):
|
||||
args["conditions"] = []
|
||||
args["conditions"].append(modified_table + "modified > '" + last_modified[d] + "'")
|
||||
out[dt]["modified_names"] = frappe.db.sql_list(
|
||||
"""select %sname from %s
|
||||
where %smodified > %s"""
|
||||
% (modified_table, table, modified_table, "%s"),
|
||||
last_modified[d],
|
||||
)
|
||||
|
||||
if args.get("force_index"):
|
||||
conditions = " force index (%s) " % args["force_index"]
|
||||
if args.get("conditions"):
|
||||
conditions += " where " + " and ".join(args["conditions"])
|
||||
if args.get("order_by"):
|
||||
order_by = " order by " + args["order_by"]
|
||||
|
||||
out[dt]["data"] = [
|
||||
list(t)
|
||||
for t in frappe.db.sql(
|
||||
"""select {} from {} {} {}""".format(",".join(args["columns"]), table, conditions, order_by)
|
||||
)
|
||||
]
|
||||
|
||||
# last modified
|
||||
modified_table = table
|
||||
if "," in table:
|
||||
modified_table = " ".join(table.split(",")[0].split(" ")[:-1])
|
||||
|
||||
tmp = frappe.db.sql(
|
||||
"""select `modified`
|
||||
from %s order by modified desc limit 1"""
|
||||
% modified_table
|
||||
)
|
||||
out[dt]["last_modified"] = tmp and tmp[0][0] or ""
|
||||
out[dt]["columns"] = list(map(lambda c: c.split(" as ")[-1], args["columns"]))
|
||||
|
||||
if args.get("links"):
|
||||
out[dt]["links"] = args["links"]
|
||||
|
||||
for d in out:
|
||||
unused_links = []
|
||||
# only compress full dumps (not partial)
|
||||
if out[d].get("links") and (d not in last_modified):
|
||||
for link_key in out[d]["links"]:
|
||||
link = out[d]["links"][link_key]
|
||||
if link[0] in out and (link[0] not in last_modified):
|
||||
|
||||
# make a map of link ids
|
||||
# to index
|
||||
link_map = {}
|
||||
doctype_data = out[link[0]]
|
||||
|
||||
col_idx = doctype_data["columns"].index(link[1])
|
||||
for row_idx in range(len(doctype_data["data"])):
|
||||
row = doctype_data["data"][row_idx]
|
||||
link_map[row[col_idx]] = row_idx
|
||||
|
||||
for row in out[d]["data"]:
|
||||
columns = list(out[d]["columns"])
|
||||
if link_key in columns:
|
||||
col_idx = columns.index(link_key)
|
||||
# replace by id
|
||||
if row[col_idx]:
|
||||
row[col_idx] = link_map.get(row[col_idx])
|
||||
else:
|
||||
unused_links.append(link_key)
|
||||
|
||||
for link in unused_links:
|
||||
del out[d]["links"][link]
|
||||
|
||||
return out
|
||||
|
|
@ -176,7 +176,7 @@ frappe.ui.form.on("Notification", {
|
|||
},
|
||||
callback: function (r) {
|
||||
if (r.message && r.message.length > 0) {
|
||||
frappe.msgprint(r.message);
|
||||
frappe.msgprint(r.message.toString());
|
||||
} else {
|
||||
frappe.msgprint(__("No alerts for today"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
from datetime import datetime, timedelta
|
||||
from urllib.parse import quote
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import google.oauth2.credentials
|
||||
import requests
|
||||
|
|
@ -274,7 +275,7 @@ def sync_events_from_google_calendar(g_calendar, method=None):
|
|||
if err.resp.status == 410:
|
||||
set_encrypted_password("Google Calendar", account.name, "", "next_sync_token")
|
||||
frappe.db.commit()
|
||||
msg += " " + _("Sync token was invalid and has been resetted, Retry syncing.")
|
||||
msg += " " + _("Sync token was invalid and has been reset, Retry syncing.")
|
||||
frappe.msgprint(msg, title="Invalid Sync Token", indicator="blue")
|
||||
else:
|
||||
frappe.throw(msg)
|
||||
|
|
@ -356,6 +357,7 @@ def insert_event_to_calendar(account, event, recurrence=None):
|
|||
"google_calendar": account.name,
|
||||
"google_calendar_id": account.google_calendar_id,
|
||||
"google_calendar_event_id": event.get("id"),
|
||||
"google_meet_link": event.get("hangoutLink"),
|
||||
"pulled_from_google_calendar": 1,
|
||||
}
|
||||
calendar_event.update(
|
||||
|
|
@ -373,6 +375,7 @@ def update_event_in_calendar(account, event, recurrence=None):
|
|||
calendar_event = frappe.get_doc("Event", {"google_calendar_event_id": event.get("id")})
|
||||
calendar_event.subject = event.get("summary")
|
||||
calendar_event.description = event.get("description")
|
||||
calendar_event.google_meet_link = event.get("hangoutLink")
|
||||
calendar_event.update(
|
||||
google_calendar_to_repeat_on(
|
||||
recurrence=recurrence, start=event.get("start"), end=event.get("end")
|
||||
|
|
@ -407,11 +410,30 @@ def insert_event_in_google_calendar(doc, method=None):
|
|||
if doc.repeat_on:
|
||||
event.update({"recurrence": repeat_on_to_google_calendar_recurrence_rule(doc)})
|
||||
|
||||
event.update({"attendees": get_attendees(doc)})
|
||||
|
||||
conference_data_version = 0
|
||||
|
||||
if doc.add_video_conferencing:
|
||||
event.update({"conferenceData": get_conference_data(doc)})
|
||||
conference_data_version = 1
|
||||
|
||||
try:
|
||||
event = google_calendar.events().insert(calendarId=doc.google_calendar_id, body=event).execute()
|
||||
frappe.db.set_value(
|
||||
"Event", doc.name, "google_calendar_event_id", event.get("id"), update_modified=False
|
||||
event = (
|
||||
google_calendar.events()
|
||||
.insert(
|
||||
calendarId=doc.google_calendar_id, body=event, conferenceDataVersion=conference_data_version
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Event",
|
||||
doc.name,
|
||||
{"google_calendar_event_id": event.get("id"), "google_meet_link": event.get("hangoutLink")},
|
||||
update_modified=False,
|
||||
)
|
||||
|
||||
frappe.msgprint(_("Event Synced with Google Calendar."))
|
||||
except HttpError as err:
|
||||
frappe.throw(
|
||||
|
|
@ -450,6 +472,7 @@ def update_event_in_google_calendar(doc, method=None):
|
|||
.get(calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id)
|
||||
.execute()
|
||||
)
|
||||
|
||||
event["summary"] = doc.subject
|
||||
event["description"] = doc.description
|
||||
event["recurrence"] = repeat_on_to_google_calendar_recurrence_rule(doc)
|
||||
|
|
@ -462,9 +485,38 @@ def update_event_in_google_calendar(doc, method=None):
|
|||
)
|
||||
)
|
||||
|
||||
google_calendar.events().update(
|
||||
calendarId=doc.google_calendar_id, eventId=doc.google_calendar_event_id, body=event
|
||||
).execute()
|
||||
conference_data_version = 0
|
||||
|
||||
if doc.add_video_conferencing:
|
||||
event.update({"conferenceData": get_conference_data(doc)})
|
||||
conference_data_version = 1
|
||||
elif doc.get_doc_before_save().add_video_conferencing or event.get("hangoutLink"):
|
||||
# remove google meet from google calendar event, if turning off add_video_conferencing
|
||||
event.update({"conferenceData": None})
|
||||
conference_data_version = 1
|
||||
|
||||
event.update({"attendees": get_attendees(doc)})
|
||||
|
||||
event = (
|
||||
google_calendar.events()
|
||||
.update(
|
||||
calendarId=doc.google_calendar_id,
|
||||
eventId=doc.google_calendar_event_id,
|
||||
body=event,
|
||||
conferenceDataVersion=conference_data_version,
|
||||
)
|
||||
.execute()
|
||||
)
|
||||
|
||||
# if add_video_conferencing enabled or disabled during update, overwrite
|
||||
frappe.db.set_value(
|
||||
"Event",
|
||||
doc.name,
|
||||
{"google_meet_link": event.get("hangoutLink")},
|
||||
update_modified=False,
|
||||
)
|
||||
doc.notify_update()
|
||||
|
||||
frappe.msgprint(_("Event Synced with Google Calendar."))
|
||||
except HttpError as err:
|
||||
frappe.throw(
|
||||
|
|
@ -515,12 +567,20 @@ def google_calendar_to_repeat_on(start, end, recurrence=None):
|
|||
Both have been mapped in a dict for easier mapping.
|
||||
"""
|
||||
repeat_on = {
|
||||
"starts_on": get_datetime(start.get("date"))
|
||||
if start.get("date")
|
||||
else parser.parse(start.get("dateTime")).astimezone().replace(tzinfo=None),
|
||||
"ends_on": get_datetime(end.get("date"))
|
||||
if end.get("date")
|
||||
else parser.parse(end.get("dateTime")).astimezone().replace(tzinfo=None),
|
||||
"starts_on": (
|
||||
get_datetime(start.get("date"))
|
||||
if start.get("date")
|
||||
else parser.parse(start.get("dateTime"))
|
||||
.astimezone(ZoneInfo(get_time_zone()))
|
||||
.replace(tzinfo=None)
|
||||
),
|
||||
"ends_on": (
|
||||
get_datetime(end.get("date"))
|
||||
if end.get("date")
|
||||
else parser.parse(end.get("dateTime"))
|
||||
.astimezone(ZoneInfo(get_time_zone()))
|
||||
.replace(tzinfo=None)
|
||||
),
|
||||
"all_day": 1 if start.get("date") else 0,
|
||||
"repeat_this_event": 1 if recurrence else 0,
|
||||
"repeat_on": None,
|
||||
|
|
@ -682,6 +742,39 @@ def get_recurrence_parameters(recurrence):
|
|||
return frequency, until, byday
|
||||
|
||||
|
||||
def get_conference_data(doc):
|
||||
return {
|
||||
"createRequest": {"requestId": doc.name, "conferenceSolutionKey": {"type": "hangoutsMeet"}},
|
||||
"notes": doc.description,
|
||||
}
|
||||
|
||||
|
||||
def get_attendees(doc):
|
||||
"""
|
||||
Returns a list of dicts with attendee emails, if available in event_participants table
|
||||
"""
|
||||
attendees, email_not_found = [], []
|
||||
|
||||
for participant in doc.event_participants:
|
||||
if participant.get("email"):
|
||||
attendees.append({"email": participant.email})
|
||||
else:
|
||||
email_not_found.append(
|
||||
{"dt": participant.reference_doctype, "dn": participant.reference_docname}
|
||||
)
|
||||
|
||||
if email_not_found:
|
||||
frappe.msgprint(
|
||||
_("Google Calendar - Contact / email not found. Did not add attendee for -<br>{0}").format(
|
||||
"<br>".join(f"{d.get('dt')} {d.get('dn')}" for d in email_not_found)
|
||||
),
|
||||
alert=True,
|
||||
indicator="yellow",
|
||||
)
|
||||
|
||||
return attendees
|
||||
|
||||
|
||||
"""API Response
|
||||
{
|
||||
'kind': 'calendar#events',
|
||||
|
|
@ -721,6 +814,32 @@ def get_recurrence_parameters(recurrence):
|
|||
'recurrence': *recurrence,
|
||||
'iCalUID': 'uid',
|
||||
'sequence': 1,
|
||||
'hangoutLink': 'https://meet.google.com/mee-ting-uri',
|
||||
'conferenceData': {
|
||||
'createRequest': {
|
||||
'requestId': 'EV00001',
|
||||
'conferenceSolutionKey': {
|
||||
'type': 'hangoutsMeet'
|
||||
},
|
||||
'status': {
|
||||
'statusCode': 'success'
|
||||
}
|
||||
},
|
||||
'entryPoints': [
|
||||
{
|
||||
'entryPointType': 'video',
|
||||
'uri': 'https://meet.google.com/mee-ting-uri',
|
||||
'label': 'meet.google.com/mee-ting-uri'
|
||||
}
|
||||
],
|
||||
'conferenceSolution': {
|
||||
'key': {
|
||||
'type': 'hangoutsMeet'
|
||||
},
|
||||
'name': 'Google Meet',
|
||||
'iconUri': 'https://fonts.gstatic.com/s/i/productlogos/meet_2020q4/v6/web-512dp/logo_meet_2020q4_color_2x_web_512dp.png'
|
||||
},
|
||||
'conferenceId': 'mee-ting-uri'
|
||||
'reminders': {
|
||||
'useDefault': True
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -959,15 +959,19 @@ class Document(BaseDocument):
|
|||
from frappe.email.doctype.notification.notification import evaluate_alert
|
||||
|
||||
if self.flags.notifications is None:
|
||||
alerts = frappe.cache().hget("notifications", self.doctype)
|
||||
if alerts is None:
|
||||
alerts = frappe.get_all(
|
||||
|
||||
def _get_notifications():
|
||||
"""returns enabled notifications for the current doctype"""
|
||||
|
||||
return frappe.get_all(
|
||||
"Notification",
|
||||
fields=["name", "event", "method"],
|
||||
filters={"enabled": 1, "document_type": self.doctype},
|
||||
)
|
||||
frappe.cache().hset("notifications", self.doctype, alerts)
|
||||
self.flags.notifications = alerts
|
||||
|
||||
self.flags.notifications = frappe.cache().hget(
|
||||
"notifications", self.doctype, _get_notifications
|
||||
)
|
||||
|
||||
if not self.flags.notifications:
|
||||
return
|
||||
|
|
@ -1183,6 +1187,9 @@ class Document(BaseDocument):
|
|||
# to trigger notification on value change
|
||||
self.run_method("before_change")
|
||||
|
||||
if self.name is None:
|
||||
return
|
||||
|
||||
frappe.db.set_value(
|
||||
self.doctype,
|
||||
self.name,
|
||||
|
|
|
|||
|
|
@ -214,3 +214,4 @@ execute:frappe.delete_doc('Page', 'background_jobs', ignore_missing=True, force=
|
|||
frappe.patches.v14_0.drop_unused_indexes
|
||||
frappe.patches.v15_0.drop_modified_index
|
||||
frappe.patches.v14_0.add_manage_subscriptions_in_navbar_settings
|
||||
frappe.patches.v14_0.update_attachment_comment
|
||||
|
|
|
|||
33
frappe/patches/v14_0/update_attachment_comment.py
Normal file
33
frappe/patches/v14_0/update_attachment_comment.py
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.db.auto_commit_on_many_writes = 1
|
||||
|
||||
# Strip everything except link to attachment and icon from comments of type "Attached"
|
||||
for name, content in frappe.get_all(
|
||||
"Comment", filters={"comment_type": "Attachment"}, fields=["name", "content"], as_list=True
|
||||
):
|
||||
if not content:
|
||||
continue
|
||||
|
||||
start = content.find("<a href")
|
||||
if start != -1:
|
||||
content = content[start:]
|
||||
|
||||
end = content.find("</i>")
|
||||
end = content.find("</a>") if end == -1 else end
|
||||
if end != -1:
|
||||
content = content[: end + 4]
|
||||
|
||||
frappe.db.set_value("Comment", name, "content", content, update_modified=False)
|
||||
|
||||
# Strip "Removed " from comments of type "Attachment Removed"
|
||||
for name, content in frappe.get_all(
|
||||
"Comment",
|
||||
filters={"comment_type": "Attachment Removed"},
|
||||
fields=["name", "content"],
|
||||
as_list=True,
|
||||
):
|
||||
if content and content.startswith("Removed "):
|
||||
frappe.db.set_value("Comment", name, "content", content[8:], update_modified=False)
|
||||
|
|
@ -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,7 +1,11 @@
|
|||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// MIT License. See license.txt
|
||||
import BaseTimeline from "./base_timeline";
|
||||
import { get_version_timeline_content } from "./version_timeline_content_builder";
|
||||
import {
|
||||
get_version_timeline_content,
|
||||
get_user_link,
|
||||
get_user_message,
|
||||
} from "./version_timeline_content_builder";
|
||||
|
||||
class FormTimeline extends BaseTimeline {
|
||||
make() {
|
||||
|
|
@ -106,54 +110,50 @@ class FormTimeline extends BaseTimeline {
|
|||
|
||||
render_timeline_items() {
|
||||
super.render_timeline_items();
|
||||
this.set_document_info();
|
||||
this.add_web_page_view_count();
|
||||
frappe.utils.bind_actions_with_object(this.timeline_items_wrapper, this);
|
||||
}
|
||||
|
||||
set_document_info() {
|
||||
// TODO: handle creation via automation
|
||||
const creation = comment_when(this.frm.doc.creation);
|
||||
let creation_message = frappe.utils.is_current_user(this.frm.doc.owner)
|
||||
? __("You created this {0}", [creation], "Form timeline")
|
||||
: __(
|
||||
"{0} created this {1}",
|
||||
[this.get_user_link(this.frm.doc.owner), creation],
|
||||
"Form timeline"
|
||||
);
|
||||
|
||||
const modified = comment_when(this.frm.doc.modified);
|
||||
let modified_message = frappe.utils.is_current_user(this.frm.doc.modified_by)
|
||||
? __("You edited this {0}", [modified], "Form timeline")
|
||||
: __(
|
||||
"{0} edited this {1}",
|
||||
[this.get_user_link(this.frm.doc.modified_by), modified],
|
||||
"Form timeline"
|
||||
);
|
||||
|
||||
add_web_page_view_count() {
|
||||
if (this.frm.doc.route && cint(frappe.boot.website_tracking_enabled)) {
|
||||
let route = this.frm.doc.route;
|
||||
frappe.utils.get_page_view_count(route).then((res) => {
|
||||
let page_view_count_message = __("{0} Page views", [res.message], "Form timeline");
|
||||
this.add_timeline_item(
|
||||
{
|
||||
content: `${creation_message} • ${modified_message} • ${page_view_count_message}`,
|
||||
hide_timestamp: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
} else {
|
||||
this.add_timeline_item(
|
||||
{
|
||||
content: `${creation_message} • ${modified_message}`,
|
||||
frappe.utils.get_page_view_count(this.frm.doc.route).then((res) => {
|
||||
this.add_timeline_item({
|
||||
content: __("{0} Web page views", [res.message], "Form timeline"),
|
||||
hide_timestamp: true,
|
||||
},
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get_creation_message() {
|
||||
const user_link = get_user_link(this.frm.doc.owner);
|
||||
|
||||
return {
|
||||
creation: this.frm.doc.creation,
|
||||
content: get_user_message(
|
||||
this.frm.doc.owner,
|
||||
__("You created this", null, "Form timeline"),
|
||||
__("{0} created this", [user_link], "Form timeline")
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
get_modified_message() {
|
||||
const user_link = get_user_link(this.frm.doc.modified_by);
|
||||
|
||||
return {
|
||||
creation: this.frm.doc.modified,
|
||||
content: get_user_message(
|
||||
this.frm.doc.modified_by,
|
||||
__("You last edited this", null, "Form timeline"),
|
||||
__("{0} last edited this", [user_link], "Form timeline")
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
prepare_timeline_contents() {
|
||||
this.timeline_items.push(this.get_creation_message());
|
||||
this.timeline_items.push(this.get_modified_message());
|
||||
this.timeline_items.push(...this.get_communication_timeline_contents());
|
||||
this.timeline_items.push(...this.get_auto_messages_timeline_contents());
|
||||
this.timeline_items.push(...this.get_comment_timeline_contents());
|
||||
|
|
@ -172,29 +172,24 @@ class FormTimeline extends BaseTimeline {
|
|||
}
|
||||
}
|
||||
|
||||
get_user_link(user) {
|
||||
const user_display_text = (frappe.user_info(user).fullname || "").bold();
|
||||
return frappe.utils.get_form_link("User", user, true, user_display_text);
|
||||
}
|
||||
|
||||
get_view_timeline_contents() {
|
||||
let view_timeline_contents = [];
|
||||
(this.doc_info.views || []).forEach((view) => {
|
||||
const view_time = comment_when(view.creation);
|
||||
let view_message = frappe.utils.is_current_user(view.owner)
|
||||
? __("You viewed this {0}", [view_time], "Form timeline")
|
||||
: __(
|
||||
"{0} viewed this {1}",
|
||||
[this.get_user_link(view.owner), view_time],
|
||||
"Form timeline"
|
||||
);
|
||||
const user_link = get_user_link(view.owner);
|
||||
const timeline_content = get_user_message(
|
||||
view.owner,
|
||||
__("You viewed this {0}", [view_time], "Form timeline"),
|
||||
__("{0} viewed this {1}", [user_link, view_time], "Form timeline")
|
||||
);
|
||||
|
||||
view_timeline_contents.push({
|
||||
creation: view.creation,
|
||||
content: view_message,
|
||||
content: timeline_content,
|
||||
hide_timestamp: true,
|
||||
});
|
||||
});
|
||||
|
||||
return view_timeline_contents;
|
||||
}
|
||||
|
||||
|
|
@ -337,7 +332,7 @@ class FormTimeline extends BaseTimeline {
|
|||
(this.doc_info.info_logs || []).forEach((info_log) => {
|
||||
info_timeline_contents.push({
|
||||
creation: info_log.creation,
|
||||
content: `${this.get_user_link(info_log.owner)} ${info_log.content}`,
|
||||
content: `${get_user_link(info_log.owner)} ${info_log.content}`,
|
||||
});
|
||||
});
|
||||
return info_timeline_contents;
|
||||
|
|
@ -345,45 +340,76 @@ class FormTimeline extends BaseTimeline {
|
|||
|
||||
get_attachment_timeline_contents() {
|
||||
let attachment_timeline_contents = [];
|
||||
|
||||
(this.doc_info.attachment_logs || []).forEach((attachment_log) => {
|
||||
let is_file_upload = attachment_log.comment_type == "Attachment";
|
||||
const is_file_upload = attachment_log.comment_type == "Attachment";
|
||||
const user_link = get_user_link(attachment_log.owner);
|
||||
const filename = attachment_log.content;
|
||||
const timeline_content = is_file_upload
|
||||
? get_user_message(
|
||||
attachment_log.owner,
|
||||
__("You attached {0}", [filename], "Form timeline"),
|
||||
__("{0} attached {1}", [user_link, filename], "Form timeline")
|
||||
)
|
||||
: get_user_message(
|
||||
attachment_log.owner,
|
||||
__("You removed attachment {0}", [filename], "Form timeline"),
|
||||
__("{0} removed attachment {1}", [user_link, filename], "Form timeline")
|
||||
);
|
||||
|
||||
attachment_timeline_contents.push({
|
||||
icon: is_file_upload ? "upload" : "delete",
|
||||
icon_size: "sm",
|
||||
creation: attachment_log.creation,
|
||||
content: `${this.get_user_link(attachment_log.owner)} ${attachment_log.content}`,
|
||||
content: timeline_content,
|
||||
});
|
||||
});
|
||||
|
||||
return attachment_timeline_contents;
|
||||
}
|
||||
|
||||
get_milestone_timeline_contents() {
|
||||
let milestone_timeline_contents = [];
|
||||
|
||||
(this.doc_info.milestones || []).forEach((milestone_log) => {
|
||||
const field = frappe.meta.get_label(this.frm.doctype, milestone_log.track_field);
|
||||
const value = milestone_log.value.bold();
|
||||
const user_link = get_user_link(milestone_log.owner);
|
||||
const timeline_content = get_user_message(
|
||||
milestone_log.owner,
|
||||
__("You changed {0} to {1}", [field, value], "Form timeline"),
|
||||
__("{0} changed {1} to {2}", [user_link, field, value], "Form timeline")
|
||||
);
|
||||
|
||||
milestone_timeline_contents.push({
|
||||
icon: "milestone",
|
||||
creation: milestone_log.creation,
|
||||
content: __("{0} changed {1} to {2}", [
|
||||
this.get_user_link(milestone_log.owner),
|
||||
frappe.meta.get_label(this.frm.doctype, milestone_log.track_field),
|
||||
milestone_log.value.bold(),
|
||||
]),
|
||||
content: timeline_content,
|
||||
});
|
||||
});
|
||||
|
||||
return milestone_timeline_contents;
|
||||
}
|
||||
|
||||
get_like_timeline_contents() {
|
||||
let like_timeline_contents = [];
|
||||
|
||||
(this.doc_info.like_logs || []).forEach((like_log) => {
|
||||
const timeline_content = get_user_message(
|
||||
like_log.owner,
|
||||
__("You Liked", null, "Form timeline"),
|
||||
__("{0} Liked", [get_user_link(like_log.owner)], "Form timeline")
|
||||
);
|
||||
|
||||
like_timeline_contents.push({
|
||||
icon: "heart",
|
||||
icon_size: "sm",
|
||||
creation: like_log.creation,
|
||||
content: __("{0} Liked", [this.get_user_link(like_log.owner)]),
|
||||
content: timeline_content,
|
||||
title: "Like",
|
||||
});
|
||||
});
|
||||
|
||||
return like_timeline_contents;
|
||||
}
|
||||
|
||||
|
|
@ -394,7 +420,7 @@ class FormTimeline extends BaseTimeline {
|
|||
icon: "branch",
|
||||
icon_size: "sm",
|
||||
creation: workflow_log.creation,
|
||||
content: `${this.get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`,
|
||||
content: `${get_user_link(workflow_log.owner)} ${__(workflow_log.content)}`,
|
||||
title: "Workflow",
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,19 +30,55 @@ function get_version_timeline_content(version_doc, frm) {
|
|||
if (p[0] === "docstatus") {
|
||||
if (p[2] === 1) {
|
||||
let message = updater_reference_link
|
||||
? __("{0} submitted this document {1}", [
|
||||
get_user_link(version_doc),
|
||||
updater_reference_link,
|
||||
])
|
||||
: __("{0} submitted this document", [get_user_link(version_doc)]);
|
||||
? get_user_message(
|
||||
version_doc.owner,
|
||||
__(
|
||||
"You submitted this document {0}",
|
||||
[updater_reference_link],
|
||||
"Form timeline"
|
||||
),
|
||||
__(
|
||||
"{0} submitted this document {1}",
|
||||
[get_user_link(version_doc.owner), updater_reference_link],
|
||||
"Form timeline"
|
||||
)
|
||||
)
|
||||
: get_user_message(
|
||||
version_doc.owner,
|
||||
__("You submitted this document", null, "Form timeline"),
|
||||
__(
|
||||
"{0} submitted this document",
|
||||
[get_user_link(version_doc.owner)],
|
||||
"Form timeline"
|
||||
)
|
||||
);
|
||||
|
||||
out.push(get_version_comment(version_doc, message));
|
||||
} else if (p[2] === 2) {
|
||||
let message = updater_reference_link
|
||||
? __("{0} cancelled this document {1}", [
|
||||
get_user_link(version_doc),
|
||||
updater_reference_link,
|
||||
])
|
||||
: __("{0} cancelled this document", [get_user_link(version_doc)]);
|
||||
? get_user_message(
|
||||
version_doc.owner,
|
||||
__(
|
||||
"You cancelled this document {1}",
|
||||
[updater_reference_link],
|
||||
"Form timeline"
|
||||
),
|
||||
__(
|
||||
"{0} cancelled this document {1}",
|
||||
[get_user_link(version_doc.owner), updater_reference_link],
|
||||
"Form timeline"
|
||||
)
|
||||
)
|
||||
: get_user_message(
|
||||
version_doc.owner,
|
||||
__("You cancelled this document", null, "Form timeline"),
|
||||
__(
|
||||
"{0} cancelled this document",
|
||||
[get_user_link(version_doc.owner)],
|
||||
"Form timeline"
|
||||
)
|
||||
);
|
||||
|
||||
out.push(get_version_comment(version_doc, message));
|
||||
}
|
||||
} else {
|
||||
|
|
@ -67,19 +103,28 @@ function get_version_timeline_content(version_doc, frm) {
|
|||
return parts.length < 3;
|
||||
});
|
||||
if (parts.length) {
|
||||
let message;
|
||||
if (updater_reference_link) {
|
||||
message = __("{0} changed value of {1} {2}", [
|
||||
get_user_link(version_doc),
|
||||
parts.join(", "),
|
||||
updater_reference_link,
|
||||
]);
|
||||
} else {
|
||||
message = __("{0} changed value of {1}", [
|
||||
get_user_link(version_doc),
|
||||
parts.join(", "),
|
||||
]);
|
||||
}
|
||||
let message = updater_reference_link
|
||||
? get_user_message(
|
||||
version_doc.owner,
|
||||
__("You changed the value of {0} {1}", [
|
||||
parts.join(", "),
|
||||
updater_reference_link,
|
||||
]),
|
||||
__("{0} changed the value of {1} {2}", [
|
||||
get_user_link(version_doc.owner),
|
||||
parts.join(", "),
|
||||
updater_reference_link,
|
||||
])
|
||||
)
|
||||
: get_user_message(
|
||||
version_doc.owner,
|
||||
__("You changed the value of {0}", [parts.join(", ")]),
|
||||
__("{0} changed the value of {1}", [
|
||||
get_user_link(version_doc.owner),
|
||||
parts.join(", "),
|
||||
])
|
||||
);
|
||||
|
||||
out.push(get_version_comment(version_doc, message));
|
||||
}
|
||||
}
|
||||
|
|
@ -120,19 +165,28 @@ function get_version_timeline_content(version_doc, frm) {
|
|||
return parts.length < 3;
|
||||
});
|
||||
if (parts.length) {
|
||||
let message;
|
||||
if (updater_reference_link) {
|
||||
message = __("{0} changed values for {1} {2}", [
|
||||
get_user_link(version_doc),
|
||||
parts.join(", "),
|
||||
updater_reference_link,
|
||||
]);
|
||||
} else {
|
||||
message = __("{0} changed values for {1}", [
|
||||
get_user_link(version_doc),
|
||||
parts.join(", "),
|
||||
]);
|
||||
}
|
||||
let message = updater_reference_link
|
||||
? get_user_message(
|
||||
version_doc.owner,
|
||||
__("You changed the values for {0} {1}", [
|
||||
parts.join(", "),
|
||||
updater_reference_link,
|
||||
]),
|
||||
__("{0} changed the values for {1} {2}", [
|
||||
get_user_link(version_doc.owner),
|
||||
parts.join(", "),
|
||||
updater_reference_link,
|
||||
])
|
||||
)
|
||||
: get_user_message(
|
||||
version_doc.owner,
|
||||
__("You changed the values for {0}", [parts.join(", ")]),
|
||||
__("{0} changed the values for {1}", [
|
||||
get_user_link(version_doc.owner),
|
||||
parts.join(", "),
|
||||
])
|
||||
);
|
||||
|
||||
out.push(get_version_comment(version_doc, message));
|
||||
}
|
||||
}
|
||||
|
|
@ -168,7 +222,7 @@ function get_version_timeline_content(version_doc, frm) {
|
|||
}
|
||||
|
||||
let version_comment = get_version_comment(version_doc, message);
|
||||
let user_link = get_user_link(version_doc);
|
||||
let user_link = get_user_link(version_doc.owner);
|
||||
out.push(`${user_link} ${version_comment}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -230,10 +284,13 @@ function format_content_for_timeline(content) {
|
|||
return content.bold();
|
||||
}
|
||||
|
||||
function get_user_link(doc) {
|
||||
const user = doc.owner;
|
||||
function get_user_link(user) {
|
||||
const user_display_text = (frappe.user_info(user).fullname || "").bold();
|
||||
return frappe.utils.get_form_link("User", user, true, user_display_text);
|
||||
}
|
||||
|
||||
export { get_version_timeline_content };
|
||||
function get_user_message(user, message_self, message_other) {
|
||||
return frappe.utils.is_current_user(user) ? message_self : message_other;
|
||||
}
|
||||
|
||||
export { get_version_timeline_content, get_user_link, get_user_message };
|
||||
|
|
|
|||
|
|
@ -52,9 +52,7 @@ export default class Grid {
|
|||
}
|
||||
|
||||
allow_on_grid_editing() {
|
||||
if (frappe.utils.is_xs()) {
|
||||
return false;
|
||||
} else if ((this.meta && this.meta.editable_grid) || !this.meta) {
|
||||
if ((this.meta && this.meta.editable_grid) || !this.meta) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
|
@ -66,17 +64,19 @@ export default class Grid {
|
|||
<label class="control-label">${__(this.df.label || "")}</label>
|
||||
<p class="text-muted small grid-description"></p>
|
||||
<div class="grid-custom-buttons grid-field"></div>
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row"></div>
|
||||
<div class="grid-body">
|
||||
<div class="rows"></div>
|
||||
<div class="grid-empty text-center">
|
||||
<img
|
||||
src="/assets/frappe/images/ui-states/grid-empty-state.svg"
|
||||
alt="Grid Empty State"
|
||||
class="grid-empty-illustration"
|
||||
>
|
||||
${__("No Data")}
|
||||
<div class="form-grid-container">
|
||||
<div class="form-grid">
|
||||
<div class="grid-heading-row"></div>
|
||||
<div class="grid-body">
|
||||
<div class="rows"></div>
|
||||
<div class="grid-empty text-center">
|
||||
<img
|
||||
src="/assets/frappe/images/ui-states/grid-empty-state.svg"
|
||||
alt="Grid Empty State"
|
||||
class="grid-empty-illustration"
|
||||
>
|
||||
${__("No Data")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -254,7 +254,7 @@ export default class GridRow {
|
|||
).appendTo(this.row);
|
||||
|
||||
this.row_index = $(
|
||||
`<div class="row-index sortable-handle col hidden-xs">
|
||||
`<div class="row-index sortable-handle col">
|
||||
<span>${txt}</span>
|
||||
</div>`
|
||||
)
|
||||
|
|
@ -268,7 +268,7 @@ export default class GridRow {
|
|||
this.row_check = $(`<div class="row-check col search"></div>`).appendTo(this.row);
|
||||
|
||||
this.row_index = $(
|
||||
`<div class="row-index col search hidden-xs">
|
||||
`<div class="row-index col search">
|
||||
<input type="text" class="form-control input-xs text-center" >
|
||||
</div>`
|
||||
).appendTo(this.row);
|
||||
|
|
@ -327,7 +327,7 @@ export default class GridRow {
|
|||
if (this.doc && !this.grid.df.in_place_edit) {
|
||||
// remove row
|
||||
if (!this.open_form_button) {
|
||||
this.open_form_button = $('<div class="col col-xs-1"></div>').appendTo(this.row);
|
||||
this.open_form_button = $('<div class="col"></div>').appendTo(this.row);
|
||||
|
||||
if (!this.configure_columns) {
|
||||
this.open_form_button = $(`
|
||||
|
|
@ -356,7 +356,7 @@ export default class GridRow {
|
|||
|
||||
if (this.configure_columns && this.frm) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1 d-flex justify-content-center" style="cursor: pointer;">
|
||||
<div class="col grid-static-col d-flex justify-content-center" style="cursor: pointer;">
|
||||
<a>${frappe.utils.icon("setting-gear", "sm", "", "filter: opacity(0.5)")}</a>
|
||||
</div>
|
||||
`)
|
||||
|
|
@ -366,7 +366,7 @@ export default class GridRow {
|
|||
});
|
||||
} else if (this.configure_columns && !this.frm) {
|
||||
this.configure_columns_button = $(`
|
||||
<div class="col grid-static-col col-xs-1"></div>
|
||||
<div class="col grid-static-col"></div>
|
||||
`).appendTo(this.row);
|
||||
}
|
||||
}
|
||||
|
|
@ -688,7 +688,7 @@ export default class GridRow {
|
|||
|
||||
if (this.show_search) {
|
||||
// last empty column
|
||||
$(`<div class="col grid-static-col col-xs-1"></div>`).appendTo(this.row);
|
||||
$(`<div class="col grid-static-col search"></div>`).appendTo(this.row);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -835,6 +835,60 @@ export default class GridRow {
|
|||
: "";
|
||||
add_class += ["Check"].indexOf(df.fieldtype) !== -1 ? " text-center" : "";
|
||||
|
||||
let grid;
|
||||
let grid_container;
|
||||
|
||||
let inital_position_x = 0;
|
||||
let start_x = 0;
|
||||
let start_y = 0;
|
||||
|
||||
let input_in_focus = false;
|
||||
|
||||
let vertical = false;
|
||||
let horizontal = false;
|
||||
|
||||
// prevent random layout shifts caused by widgets and on click position elements inside view (UX).
|
||||
function on_input_focus(el) {
|
||||
input_in_focus = true;
|
||||
|
||||
let container_width = grid_container.getBoundingClientRect().width;
|
||||
let container_left = grid_container.getBoundingClientRect().left;
|
||||
let grid_left = parseFloat(grid.style.left);
|
||||
let element_left = el.offset().left;
|
||||
let fieldtype = el.data("fieldtype");
|
||||
|
||||
let offset_right = container_width - (element_left + el.width());
|
||||
let offset_left = 0;
|
||||
let element_screen_x = element_left - container_left;
|
||||
let element_position_x = container_width - (element_left - container_left);
|
||||
|
||||
if (["Date", "Time", "Datetime"].includes(fieldtype)) {
|
||||
offset_left = element_position_x - 220;
|
||||
}
|
||||
if (["Link", "Dynamic Link"].includes(fieldtype)) {
|
||||
offset_left = element_position_x - 250;
|
||||
}
|
||||
if (element_screen_x < 0) {
|
||||
grid.style.left = `${grid_left - element_screen_x}px`;
|
||||
} else if (offset_left < 0) {
|
||||
grid.style.left = `${grid_left + offset_left}px`;
|
||||
} else if (offset_right < 0) {
|
||||
grid.style.left = `${grid_left + offset_right}px`;
|
||||
}
|
||||
}
|
||||
|
||||
// Delay date_picker widget to prevent temparary layout shift (UX).
|
||||
function handle_date_picker() {
|
||||
let date_time_picker = document.querySelectorAll(".datepicker.active")[0];
|
||||
|
||||
date_time_picker.classList.remove("active");
|
||||
date_time_picker.style.width = "220px";
|
||||
|
||||
setTimeout(() => {
|
||||
date_time_picker.classList.add("active");
|
||||
}, 600);
|
||||
}
|
||||
|
||||
var $col = $(
|
||||
'<div class="col grid-static-col col-xs-' + colsize + " " + add_class + '"></div>'
|
||||
)
|
||||
|
|
@ -842,15 +896,68 @@ export default class GridRow {
|
|||
.attr("data-fieldtype", df.fieldtype)
|
||||
.data("df", df)
|
||||
.appendTo(this.row)
|
||||
.on("click", function () {
|
||||
if (frappe.ui.form.editable_row === me) {
|
||||
return;
|
||||
// initialize grid for horizontal scroll on mobile devices.
|
||||
.on("touchstart", function (event) {
|
||||
grid_container = $(event.currentTarget).closest(".form-grid-container")[0];
|
||||
grid = $(event.currentTarget).closest(".form-grid")[0];
|
||||
|
||||
grid.style.position != "relative" && $(grid).css("position", "relative");
|
||||
!grid.style.left && $(grid).css("left", 0);
|
||||
|
||||
start_x = event.touches[0].clientX;
|
||||
start_y = event.touches[0].clientY;
|
||||
|
||||
inital_position_x = -parseFloat(grid.style.left || 0) + start_x;
|
||||
})
|
||||
// calculate X and Y movement based on touch events.
|
||||
.on("touchmove", function (event) {
|
||||
if (input_in_focus) return;
|
||||
|
||||
let moved_x;
|
||||
let moved_y;
|
||||
|
||||
if (!horizontal && !vertical) {
|
||||
moved_x = Math.abs(start_x - event.touches[0].clientX);
|
||||
moved_y = Math.abs(start_y - event.touches[0].clientY);
|
||||
}
|
||||
|
||||
if (!vertical && moved_x > 16) {
|
||||
horizontal = true;
|
||||
} else if (!horizontal && moved_y > 16) {
|
||||
vertical = true;
|
||||
}
|
||||
if (horizontal) {
|
||||
event.preventDefault();
|
||||
|
||||
let grid_start = inital_position_x - event.touches[0].clientX;
|
||||
let grid_end = grid.clientWidth - grid_container.clientWidth + 2;
|
||||
|
||||
if (grid_start < 0) {
|
||||
grid_start = 0;
|
||||
} else if (grid_start > grid_end) {
|
||||
grid_start = grid_end;
|
||||
}
|
||||
grid.style.left = `-${grid_start}px`;
|
||||
}
|
||||
})
|
||||
.on("touchend", function () {
|
||||
vertical = false;
|
||||
horizontal = false;
|
||||
})
|
||||
.on("click", function () {
|
||||
if (frappe.ui.form.editable_row !== me) {
|
||||
var out = me.toggle_editable_row();
|
||||
}
|
||||
var out = me.toggle_editable_row();
|
||||
var col = this;
|
||||
setTimeout(function () {
|
||||
$(col).find('input[type="Text"]:first').focus();
|
||||
}, 500);
|
||||
let first_input_field = $(col).find('input[type="Text"]:first');
|
||||
|
||||
first_input_field.length && on_input_focus(first_input_field);
|
||||
|
||||
first_input_field.trigger("focus");
|
||||
first_input_field.one("blur", () => (input_in_focus = false));
|
||||
|
||||
first_input_field.data("fieldtype") == "Date" && handle_date_picker();
|
||||
|
||||
return out;
|
||||
});
|
||||
|
||||
|
|
@ -1149,6 +1256,10 @@ export default class GridRow {
|
|||
return this;
|
||||
}
|
||||
show_form() {
|
||||
if (frappe.utils.is_xs()) {
|
||||
$(this.grid.form_grid).css("min-width", "0");
|
||||
$(this.grid.form_grid).css("position", "unset");
|
||||
}
|
||||
if (!this.grid_form) {
|
||||
this.grid_form = new GridRowForm({
|
||||
row: this,
|
||||
|
|
@ -1187,6 +1298,10 @@ export default class GridRow {
|
|||
}
|
||||
}
|
||||
hide_form() {
|
||||
if (frappe.utils.is_xs()) {
|
||||
$(this.grid.form_grid).css("min-width", "738px");
|
||||
$(this.grid.form_grid).css("position", "relative");
|
||||
}
|
||||
frappe.dom.unfreeze();
|
||||
this.row.toggle(true);
|
||||
if (!frappe.dom.is_element_in_modal(this.row)) {
|
||||
|
|
|
|||
|
|
@ -144,8 +144,16 @@ frappe.ui.form.Layout = class Layout {
|
|||
fieldname: "__details",
|
||||
};
|
||||
let first_tab = this.fields[1].fieldtype === "Tab Break" ? this.fields[1] : null;
|
||||
|
||||
if (!first_tab) {
|
||||
this.fields.splice(1, 0, default_tab);
|
||||
this.fields.splice(0, 0, default_tab);
|
||||
} else {
|
||||
// reshuffle __newname field to accomodate under 1st Tab Break
|
||||
let newname_field = this.fields.find((df) => df.fieldname === "__newname");
|
||||
if (newname_field && newname_field.get_status(this) === "Write") {
|
||||
this.fields.splice(0, 1);
|
||||
this.fields.splice(1, 0, newname_field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ frappe.views.BaseList = class BaseList {
|
|||
Map: "map",
|
||||
};
|
||||
|
||||
if (frappe.boot.desk_settings.view_switcher) {
|
||||
if (frappe.boot.desk_settings.view_switcher && !this.meta.force_re_route_to_default_view) {
|
||||
/* @preserve
|
||||
for translation, don't remove
|
||||
__("List View") __("Report View") __("Dashboard View") __("Gantt View"),
|
||||
|
|
|
|||
|
|
@ -175,6 +175,7 @@ frappe.render_template = function (name, data) {
|
|||
w.document.write(tree);
|
||||
w.document.close();
|
||||
});
|
||||
|
||||
frappe.render_pdf = function (html, opts = {}) {
|
||||
//Create a form to place the HTML content
|
||||
var formData = new FormData();
|
||||
|
|
@ -197,8 +198,17 @@ frappe.render_pdf = function (html, opts = {}) {
|
|||
var blob = new Blob([success.currentTarget.response], { type: "application/pdf" });
|
||||
var objectUrl = URL.createObjectURL(blob);
|
||||
|
||||
//Open report in a new window
|
||||
window.open(objectUrl);
|
||||
// Create a hidden a tag to force set report name
|
||||
// https://stackoverflow.com/questions/19327749/javascript-blob-filename-without-link
|
||||
let hidden_a_tag = document.createElement("a");
|
||||
document.body.appendChild(hidden_a_tag);
|
||||
hidden_a_tag.style = "display: none";
|
||||
hidden_a_tag.href = objectUrl;
|
||||
hidden_a_tag.download = opts.report_name || "report.pdf";
|
||||
|
||||
// Open report in a new window
|
||||
hidden_a_tag.click();
|
||||
window.URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
xhr.send(formData);
|
||||
|
|
|
|||
|
|
@ -349,7 +349,7 @@ $.extend(frappe.model, {
|
|||
|
||||
is_tree: function (doctype) {
|
||||
if (!doctype) return false;
|
||||
return frappe.boot.treeviews.indexOf(doctype) != -1;
|
||||
return locals.DocType[doctype] && locals.DocType[doctype].is_tree;
|
||||
},
|
||||
|
||||
is_fresh(doc) {
|
||||
|
|
@ -754,6 +754,42 @@ $.extend(frappe.model, {
|
|||
}
|
||||
return frappe.model.numeric_fieldtypes.includes(fieldtype);
|
||||
},
|
||||
|
||||
set_default_views_for_doctype(doctype, frm) {
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
let meta = frappe.get_meta(doctype);
|
||||
let default_views = ["List", "Report", "Dashboard", "Kanban"];
|
||||
|
||||
if (meta.is_calendar_and_gantt && frappe.views.calendar[doctype]) {
|
||||
let views = ["Calendar", "Gantt"];
|
||||
default_views.push(...views);
|
||||
}
|
||||
|
||||
if (meta.is_tree) {
|
||||
default_views.push("Tree");
|
||||
}
|
||||
|
||||
if (frm.doc.image_field) {
|
||||
default_views.push("Image");
|
||||
}
|
||||
|
||||
if (doctype === "Communication" && frappe.boot.email_accounts.length) {
|
||||
default_views.push("Inbox");
|
||||
}
|
||||
|
||||
if (
|
||||
(frm.doc.fields.find((i) => i.fieldname === "latitude") &&
|
||||
frm.doc.fields.find((i) => i.fieldname === "longitude")) ||
|
||||
frm.doc.fields.find(
|
||||
(i) => i.fieldname === "location" && i.fieldtype == "Geolocation"
|
||||
)
|
||||
) {
|
||||
default_views.push("Map");
|
||||
}
|
||||
|
||||
frm.set_df_property("default_view", "options", default_views);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// legacy
|
||||
|
|
|
|||
|
|
@ -89,6 +89,18 @@ frappe.router = {
|
|||
"image",
|
||||
"inbox",
|
||||
],
|
||||
list_views_route: {
|
||||
list: "List",
|
||||
kanban: "Kanban",
|
||||
report: "Report",
|
||||
calendar: "Calendar",
|
||||
tree: "Tree",
|
||||
gantt: "Gantt",
|
||||
dashboard: "Dashboard",
|
||||
image: "Image",
|
||||
inbox: "Inbox",
|
||||
file: "Home",
|
||||
},
|
||||
layout_mapped: {},
|
||||
|
||||
is_app_route(path) {
|
||||
|
|
@ -115,7 +127,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 +138,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 +173,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 +186,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 +406,7 @@ frappe.router = {
|
|||
} else if (view === "tree") {
|
||||
new_route = [this.slug(route[1]), "view", "tree"];
|
||||
}
|
||||
|
||||
return new_route;
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -208,13 +208,10 @@ frappe.search.utils = {
|
|||
},
|
||||
});
|
||||
}
|
||||
if (in_list(frappe.boot.treeviews, item)) {
|
||||
out.push(option("Tree", ["Tree", item], 0.05));
|
||||
} else {
|
||||
out.push(option("List", ["List", item], 0.05));
|
||||
if (frappe.model.can_get_report(item)) {
|
||||
out.push(option("Report", ["List", item, "Report"], 0.04));
|
||||
}
|
||||
|
||||
out.push(option("List", ["List", item], 0.05));
|
||||
if (frappe.model.can_get_report(item)) {
|
||||
out.push(option("Report", ["List", item, "Report"], 0.04));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1260,20 +1260,12 @@ Object.assign(frappe.utils, {
|
|||
if (frappe.model.is_single(item.doctype)) {
|
||||
route = doctype_slug;
|
||||
} else {
|
||||
if (!item.doc_view) {
|
||||
if (frappe.model.is_tree(item.doctype)) {
|
||||
item.doc_view = "Tree";
|
||||
} else {
|
||||
item.doc_view = "List";
|
||||
}
|
||||
}
|
||||
|
||||
switch (item.doc_view) {
|
||||
case "List":
|
||||
if (item.filters) {
|
||||
frappe.route_options = item.filters;
|
||||
}
|
||||
route = doctype_slug;
|
||||
route = `${doctype_slug}/view/list`;
|
||||
break;
|
||||
case "Tree":
|
||||
route = `${doctype_slug}/view/tree`;
|
||||
|
|
@ -1290,12 +1282,11 @@ Object.assign(frappe.utils, {
|
|||
case "Calendar":
|
||||
route = `${doctype_slug}/view/calendar/default`;
|
||||
break;
|
||||
case "Kanban":
|
||||
route = `${doctype_slug}/view/kanban`;
|
||||
break;
|
||||
default:
|
||||
frappe.throw({
|
||||
message: __("Not a valid view:") + item.doc_view,
|
||||
title: __("Unknown View"),
|
||||
});
|
||||
route = "";
|
||||
route = doctype_slug;
|
||||
}
|
||||
}
|
||||
} else if (type === "report") {
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ frappe.breadcrumbs = {
|
|||
} else {
|
||||
let route;
|
||||
const doctype_route = frappe.router.slug(frappe.router.doctype_layout || doctype);
|
||||
if (frappe.boot.treeviews.indexOf(doctype) !== -1) {
|
||||
if (doctype_meta.is_tree) {
|
||||
let view = frappe.model.user_settings[doctype].last_view || "Tree";
|
||||
route = `${doctype_route}/view/${view}`;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
|
|||
this.page_title = __("File Manager");
|
||||
|
||||
const route = frappe.get_route();
|
||||
this.current_folder = route.slice(2).join("/");
|
||||
this.current_folder = route.slice(2).join("/") || "Home";
|
||||
this.filters = [["File", "folder", "=", this.current_folder, true]];
|
||||
this.order_by = this.view_user_settings.order_by || "file_name asc";
|
||||
|
||||
|
|
@ -286,7 +286,7 @@ frappe.views.FileView = class FileView extends frappe.views.ListView {
|
|||
}
|
||||
|
||||
get_breadcrumbs_html() {
|
||||
const route = frappe.router.parse();
|
||||
const route = frappe.get_route();
|
||||
const folders = route.slice(2);
|
||||
|
||||
return folders
|
||||
|
|
|
|||
|
|
@ -9,14 +9,9 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
const doctype = route[1];
|
||||
const user_settings = frappe.get_user_settings(doctype)["Kanban"] || {};
|
||||
if (!user_settings.last_kanban_board) {
|
||||
frappe.msgprint({
|
||||
title: __("Error"),
|
||||
indicator: "red",
|
||||
message: __("Missing parameter Kanban Board Name"),
|
||||
});
|
||||
frappe.set_route("List", doctype, "List");
|
||||
return true;
|
||||
return new frappe.views.KanbanView({ doctype: doctype });
|
||||
}
|
||||
|
||||
route.push(user_settings.last_kanban_board);
|
||||
frappe.set_route(route);
|
||||
return true;
|
||||
|
|
@ -28,9 +23,35 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
return "Kanban";
|
||||
}
|
||||
|
||||
show() {
|
||||
frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) => {
|
||||
if (!kanbans.length) {
|
||||
return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true);
|
||||
} else if (kanbans.length && frappe.get_route().length !== 4) {
|
||||
return frappe.views.KanbanView.show_kanban_dialog(this.doctype, true);
|
||||
} else {
|
||||
this.kanbans = kanbans;
|
||||
|
||||
return frappe.run_serially([
|
||||
() => this.show_skeleton(),
|
||||
() => this.fetch_meta(),
|
||||
() => this.hide_skeleton(),
|
||||
() => this.check_permissions(),
|
||||
() => this.init(),
|
||||
() => this.before_refresh(),
|
||||
() => this.refresh(),
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setup_defaults() {
|
||||
return super.setup_defaults().then(() => {
|
||||
this.board_name = frappe.get_route()[3];
|
||||
let get_board_name = () => {
|
||||
return this.kanbans.length && this.kanbans[0].name;
|
||||
};
|
||||
|
||||
this.board_name = frappe.get_route()[3] || get_board_name() || null;
|
||||
this.page_title = __(this.board_name);
|
||||
this.card_meta = this.get_card_meta();
|
||||
this.page_length = 0;
|
||||
|
|
@ -143,21 +164,22 @@ frappe.views.KanbanView = class KanbanView extends frappe.views.ListView {
|
|||
|
||||
render() {
|
||||
const board_name = this.board_name;
|
||||
if (this.kanban && board_name === this.kanban.board_name) {
|
||||
this.kanban.update(this.data);
|
||||
return;
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
get_card_meta() {
|
||||
|
|
|
|||
|
|
@ -1383,6 +1383,14 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList {
|
|||
layout_direction: frappe.utils.is_rtl() ? "rtl" : "ltr",
|
||||
});
|
||||
|
||||
let filter_values = [],
|
||||
name_len = 0;
|
||||
for (var key of Object.keys(applied_filters)) {
|
||||
name_len = name_len + applied_filters[key].toString().length;
|
||||
if (name_len > 200) break;
|
||||
filter_values.push(applied_filters[key]);
|
||||
}
|
||||
print_settings.report_name = `${__(this.report_name)}_${filter_values.join("_")}.pdf`;
|
||||
frappe.render_pdf(html, print_settings);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ frappe.views.TreeFactory = class TreeFactory extends frappe.views.Factory {
|
|||
let treeview = frappe.views.trees[route[1]];
|
||||
treeview && treeview.make_tree();
|
||||
}
|
||||
|
||||
get view_name() {
|
||||
return "Tree";
|
||||
}
|
||||
};
|
||||
|
||||
frappe.views.TreeView = class TreeView {
|
||||
|
|
@ -196,6 +200,7 @@ frappe.views.TreeView = class TreeView {
|
|||
});
|
||||
|
||||
cur_tree = this.tree;
|
||||
cur_tree.view_name = "Tree";
|
||||
this.post_render();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -384,18 +384,22 @@ class ShortcutDialog extends WidgetDialog {
|
|||
onchange: () => {
|
||||
if (this.dialog.get_value("type") == "DocType") {
|
||||
let doctype = this.dialog.get_value("link_to");
|
||||
if (doctype && frappe.boot.single_types.includes(doctype)) {
|
||||
this.hide_filters();
|
||||
} else if (doctype) {
|
||||
this.setup_filter(doctype);
|
||||
this.show_filters();
|
||||
}
|
||||
frappe.model.with_doctype(doctype, () => {
|
||||
let meta = frappe.get_meta(doctype);
|
||||
|
||||
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");
|
||||
if (doctype && frappe.boot.single_types.includes(doctype)) {
|
||||
this.hide_filters();
|
||||
} else if (doctype) {
|
||||
this.setup_filter(doctype);
|
||||
this.show_filters();
|
||||
}
|
||||
|
||||
this.dialog.set_df_property("doc_view", "options", views.join("\n"));
|
||||
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?"
|
||||
),
|
||||
|
|
|
|||
|
|
@ -268,8 +268,8 @@
|
|||
.editable-row .frappe-control {
|
||||
padding-top: 0px !important;
|
||||
padding-bottom: 0px !important;
|
||||
margin-left: -5px !important;
|
||||
margin-right: -5px !important;
|
||||
margin-left: -1px !important;
|
||||
margin-right: -1px !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -484,6 +484,31 @@
|
|||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, "md")) {
|
||||
.form-grid-container {
|
||||
overflow-x: clip;
|
||||
|
||||
.form-grid {
|
||||
min-width: 738px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-column.col-sm-6 .form-grid {
|
||||
.row-index {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: map-get($grid-breakpoints, "md")) {
|
||||
.form-grid-container {
|
||||
overflow-x: unset!important;
|
||||
|
||||
.form-grid {
|
||||
position: unset!important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: map-get($grid-breakpoints, "sm")) {
|
||||
.form-in-grid .form-section .form-column {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,22 @@ from frappe.utils import cint
|
|||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add(
|
||||
def add(doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0, notify=0):
|
||||
"""Expose function without flags to the client-side"""
|
||||
return add_docshare(
|
||||
doctype,
|
||||
name,
|
||||
user=user,
|
||||
read=read,
|
||||
write=write,
|
||||
submit=submit,
|
||||
share=share,
|
||||
everyone=everyone,
|
||||
notify=notify,
|
||||
)
|
||||
|
||||
|
||||
def add_docshare(
|
||||
doctype, name, user=None, read=1, write=0, submit=0, share=0, everyone=0, flags=None, notify=0
|
||||
):
|
||||
"""Share the given document with a user."""
|
||||
|
|
@ -66,21 +81,29 @@ def remove(doctype, name, user, flags=None):
|
|||
|
||||
@frappe.whitelist()
|
||||
def set_permission(doctype, name, user, permission_to, value=1, everyone=0):
|
||||
"""Expose function without flags to the client-side"""
|
||||
set_docshare_permission(doctype, name, user, permission_to, value=value, everyone=everyone)
|
||||
|
||||
|
||||
def set_docshare_permission(doctype, name, user, permission_to, value=1, everyone=0, flags=None):
|
||||
"""Set share permission."""
|
||||
check_share_permission(doctype, name)
|
||||
if not (flags or {}).get("ignore_share_permission"):
|
||||
check_share_permission(doctype, name)
|
||||
|
||||
share_name = get_share_name(doctype, name, user, everyone)
|
||||
value = int(value)
|
||||
|
||||
if not share_name:
|
||||
if value:
|
||||
share = add(doctype, name, user, everyone=everyone, **{permission_to: 1})
|
||||
share = add_docshare(doctype, name, user, everyone=everyone, **{permission_to: 1}, flags=flags)
|
||||
else:
|
||||
# no share found, nothing to remove
|
||||
share = {}
|
||||
pass
|
||||
else:
|
||||
share = frappe.get_doc("DocShare", share_name)
|
||||
if flags:
|
||||
share.flags.update(flags)
|
||||
share.flags.ignore_permissions = True
|
||||
share.set(permission_to, value)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
self.assertFalse(frappe.db.exists("ToDo", todo.name))
|
||||
self.assertRaises(frappe.DoesNotExistError, delete, "ToDo", todo.name)
|
||||
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("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
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ class TestConfig(FrappeTestCase):
|
|||
def test_get_modules(self):
|
||||
frappe_modules = frappe.get_all("Module Def", filters={"app_name": "frappe"}, pluck="name")
|
||||
all_modules_data = get_modules_from_all_apps_for_user()
|
||||
first_module_entry = all_modules_data[0]
|
||||
all_modules = [x["module_name"] for x in all_modules_data]
|
||||
self.assertIn("links", first_module_entry)
|
||||
self.assertIsInstance(all_modules_data, list)
|
||||
self.assertFalse([x for x in frappe_modules if x not in all_modules])
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@
|
|||
|
||||
|
||||
import frappe
|
||||
from frappe.app import make_form_dict
|
||||
from frappe.desk.search import get_names_for_mentions, search_link, search_widget
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import set_request
|
||||
from frappe.website.serve import get_response
|
||||
|
||||
|
||||
class TestSearch(FrappeTestCase):
|
||||
|
|
@ -235,3 +238,22 @@ def teardown_test_link_field_order(TestCase):
|
|||
)
|
||||
|
||||
TestCase.tree_doc.delete()
|
||||
|
||||
|
||||
class TestWebsiteSearch(FrappeTestCase):
|
||||
def get(self, path, user="Guest"):
|
||||
frappe.set_user(user)
|
||||
set_request(method="GET", path=path)
|
||||
make_form_dict(frappe.local.request)
|
||||
response = get_response()
|
||||
frappe.set_user("Administrator")
|
||||
return response
|
||||
|
||||
def test_basic_search(self):
|
||||
|
||||
no_search = self.get("/search")
|
||||
self.assertEqual(no_search.status_code, 200)
|
||||
|
||||
response = self.get("/search?q=b")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn("Search Results", response.get_data(as_text=True))
|
||||
|
|
|
|||
|
|
@ -317,6 +317,22 @@ class TestWebsite(FrappeTestCase):
|
|||
self.assertIn('<meta name="title" content="Test Title Metatag">', content)
|
||||
self.assertIn('<meta name="description" content="Test Description for Metatag">', content)
|
||||
|
||||
def test_resolve_class(self):
|
||||
from frappe.utils.jinja_globals import resolve_class
|
||||
|
||||
context = frappe._dict(primary=True)
|
||||
self.assertEqual(resolve_class("test"), "test")
|
||||
self.assertEqual(resolve_class("test", "test-2"), "test test-2")
|
||||
self.assertEqual(resolve_class("test", {"test-2": False, "test-3": True}), "test test-3")
|
||||
self.assertEqual(
|
||||
resolve_class(["test1", "test2", context.primary and "primary"]), "test1 test2 primary"
|
||||
)
|
||||
|
||||
content = '<a class="{{ resolve_class("btn btn-default", primary and "btn-primary") }}">Test</a>'
|
||||
self.assertEqual(
|
||||
frappe.render_template(content, context), '<a class="btn btn-default btn-primary">Test</a>'
|
||||
)
|
||||
|
||||
|
||||
def set_home_page_hook(key, value):
|
||||
from frappe import hooks
|
||||
|
|
|
|||
|
|
@ -43,16 +43,32 @@ def create_todo_records():
|
|||
frappe.db.truncate("ToDo")
|
||||
|
||||
frappe.get_doc(
|
||||
{"doctype": "ToDo", "date": add_to_date(now(), days=7), "description": "this is first todo"}
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"date": add_to_date(now(), days=7),
|
||||
"description": "this is first todo",
|
||||
}
|
||||
).insert()
|
||||
frappe.get_doc(
|
||||
{"doctype": "ToDo", "date": add_to_date(now(), days=-7), "description": "this is second todo"}
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"date": add_to_date(now(), days=-7),
|
||||
"description": "this is second todo",
|
||||
}
|
||||
).insert()
|
||||
frappe.get_doc(
|
||||
{"doctype": "ToDo", "date": add_to_date(now(), months=2), "description": "this is third todo"}
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"date": add_to_date(now(), months=2),
|
||||
"description": "this is third todo",
|
||||
}
|
||||
).insert()
|
||||
frappe.get_doc(
|
||||
{"doctype": "ToDo", "date": add_to_date(now(), months=-2), "description": "this is fourth todo"}
|
||||
{
|
||||
"doctype": "ToDo",
|
||||
"date": add_to_date(now(), months=-2),
|
||||
"description": "this is fourth todo",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
|
|
@ -431,3 +447,134 @@ def create_test_user():
|
|||
user.append("roles", {"role": role})
|
||||
|
||||
user.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_tree_doctype():
|
||||
frappe.delete_doc_if_exists("DocType", "Custom Tree")
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{"fieldname": "tree", "fieldtype": "Data", "label": "Tree"},
|
||||
],
|
||||
"permissions": [{"role": "System Manager", "read": 1}],
|
||||
"name": "Custom Tree",
|
||||
"is_tree": True,
|
||||
"naming_rule": "By fieldname",
|
||||
"autoname": "field:tree",
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Custom Tree", "All Trees"):
|
||||
frappe.get_doc({"doctype": "Custom Tree", "tree": "All Trees"}).insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_image_doctype():
|
||||
frappe.delete_doc_if_exists("DocType", "Custom Image")
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "DocType",
|
||||
"module": "Core",
|
||||
"custom": 1,
|
||||
"fields": [
|
||||
{"fieldname": "image", "fieldtype": "Attach Image", "label": "Image"},
|
||||
],
|
||||
"permissions": [{"role": "System Manager", "read": 1}],
|
||||
"name": "Custom Image",
|
||||
"image_field": "image",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_inbox():
|
||||
frappe.db.sql("DELETE FROM `tabUser Email`")
|
||||
|
||||
user = frappe.get_doc("User", frappe.session.user)
|
||||
user.append("user_emails", {"email_account": "Email Linking"})
|
||||
user.save()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def setup_default_view(view, force_reroute=None):
|
||||
frappe.delete_doc_if_exists("Property Setter", "Event-main-default_view")
|
||||
frappe.delete_doc_if_exists("Property Setter", "Event-main-force_re_route_to_default_view")
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"is_system_generated": 0,
|
||||
"doctype_or_field": "DocType",
|
||||
"doc_type": "Event",
|
||||
"property": "default_view",
|
||||
"property_type": "Select",
|
||||
"value": view,
|
||||
"doctype": "Property Setter",
|
||||
}
|
||||
).insert()
|
||||
|
||||
if force_reroute:
|
||||
frappe.get_doc(
|
||||
{
|
||||
"is_system_generated": 0,
|
||||
"doctype_or_field": "DocType",
|
||||
"doc_type": "Event",
|
||||
"property": "force_re_route_to_default_view",
|
||||
"property_type": "Check",
|
||||
"value": "1",
|
||||
"doctype": "Property Setter",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_note():
|
||||
if not frappe.db.exists("Note", "Routing Test"):
|
||||
frappe.get_doc({"doctype": "Note", "title": "Routing Test"}).insert()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_kanban():
|
||||
if not frappe.db.exists("Custom Field", "Note-kanban"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"is_system_generated": 0,
|
||||
"dt": "Note",
|
||||
"label": "Kanban",
|
||||
"fieldname": "kanban",
|
||||
"insert_after": "seen_by",
|
||||
"fieldtype": "Select",
|
||||
"options": "Open\nClosed",
|
||||
"doctype": "Custom Field",
|
||||
}
|
||||
).insert()
|
||||
|
||||
if not frappe.db.exists("Kanban Board", "_Note _Kanban"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Kanban Board",
|
||||
"name": "_Note _Kanban",
|
||||
"kanban_board_name": "_Note _Kanban",
|
||||
"reference_doctype": "Note",
|
||||
"field_name": "kanban",
|
||||
"private": 1,
|
||||
"show_labels": 0,
|
||||
"columns": [
|
||||
{
|
||||
"column_name": "Open",
|
||||
"status": "Active",
|
||||
"indicator": "Gray",
|
||||
},
|
||||
{
|
||||
"column_name": "Closed",
|
||||
"status": "Active",
|
||||
"indicator": "Gray",
|
||||
},
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
|
|
|||
|
|
@ -69,16 +69,18 @@ class FrappeTestCase(unittest.TestCase):
|
|||
|
||||
@contextmanager
|
||||
def assertQueryCount(self, count):
|
||||
queries = []
|
||||
|
||||
def _sql_with_count(*args, **kwargs):
|
||||
frappe.db.sql_query_count += 1
|
||||
return orig_sql(*args, **kwargs)
|
||||
ret = orig_sql(*args, **kwargs)
|
||||
queries.append(frappe.db.last_query)
|
||||
return ret
|
||||
|
||||
try:
|
||||
orig_sql = frappe.db.sql
|
||||
frappe.db.sql_query_count = 0
|
||||
frappe.db.sql = _sql_with_count
|
||||
yield
|
||||
self.assertLessEqual(frappe.db.sql_query_count, count)
|
||||
self.assertLessEqual(len(queries), count, msg="Queries executed: " + "\n\n".join(queries))
|
||||
finally:
|
||||
frappe.db.sql = orig_sql
|
||||
|
||||
|
|
|
|||
|
|
@ -1527,6 +1527,7 @@ Looks like something is wrong with this site's payment gateway configuration. No
|
|||
Madam,gnädige Frau,
|
||||
Main Section,Hauptbereich,
|
||||
"Make ""name"" searchable in Global Search",Machen Sie "name" durchsuchbar in Global Search,
|
||||
Make Attachments Public by Default, Anhänge im Standard als öffentlich markieren,
|
||||
Make use of longer keyboard patterns,Nutzen Sie mehr Tastaturmuster,
|
||||
Manage Third Party Apps,Verwalten von Apps von Drittanbietern,
|
||||
Mandatory Information missing:,Pflichtangaben fehlen:,
|
||||
|
|
@ -4727,8 +4728,12 @@ Copy,Kopieren,
|
|||
Hide Saved,Gespeicherte ausblenden,
|
||||
Show Saved,Gespeicherte anzeigen,
|
||||
{0} created this {1},{0} erstellte dies {1},
|
||||
{0} created this,{0} erstellte dies,
|
||||
You created this,Sie erstellten dies,
|
||||
{0} edited this {1},{0} bearbeitete dies {1},
|
||||
You edited this {0},Sie bearbeiteten dies {0},
|
||||
{0} viewed this {1},{0} sah sich dies {1} an,
|
||||
You viewed this {0},Sie sahen sich dies {0} an,
|
||||
Toggle Full Width,Toggle Volle Breite,
|
||||
Documentation,Dokumentation,
|
||||
About,Über,
|
||||
|
|
|
|||
|
|
|
@ -136,12 +136,6 @@ def _create_app_boilerplate(dest, hooks, no_git=False):
|
|||
|
||||
touch_file(os.path.join(dest, hooks.app_name, hooks.app_name, "patches.txt"))
|
||||
|
||||
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "desktop.py"), "w") as f:
|
||||
f.write(frappe.as_unicode(desktop_template.format(**hooks)))
|
||||
|
||||
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "config", "docs.py"), "w") as f:
|
||||
f.write(frappe.as_unicode(docs_template.format(**hooks)))
|
||||
|
||||
app_directory = os.path.join(dest, hooks.app_name)
|
||||
|
||||
if hooks.create_github_workflow:
|
||||
|
|
@ -381,18 +375,6 @@ app_license = "{app_license}"
|
|||
# ]
|
||||
"""
|
||||
|
||||
desktop_template = """from frappe import _
|
||||
|
||||
def get_data():
|
||||
return [
|
||||
{{
|
||||
"module_name": "{app_title}",
|
||||
"type": "module",
|
||||
"label": _("{app_title}")
|
||||
}}
|
||||
]
|
||||
"""
|
||||
|
||||
setup_template = """from setuptools import setup, find_packages
|
||||
|
||||
with open("requirements.txt") as f:
|
||||
|
|
@ -419,21 +401,7 @@ gitignore_template = """.DS_Store
|
|||
*.egg-info
|
||||
*.swp
|
||||
tags
|
||||
{app_name}/docs/current
|
||||
node_modules/"""
|
||||
|
||||
docs_template = '''"""
|
||||
Configuration for docs
|
||||
"""
|
||||
|
||||
# source_link = "https://github.com/[org_name]/{app_name}"
|
||||
# headline = "App that does everything"
|
||||
# sub_heading = "Yes, you got that right the first time, everything"
|
||||
|
||||
def get_context(context):
|
||||
context.brand_html = "{app_title}"
|
||||
'''
|
||||
|
||||
node_modules"""
|
||||
|
||||
github_workflow_template = """
|
||||
name: CI
|
||||
|
|
|
|||
|
|
@ -54,19 +54,12 @@ def upload():
|
|||
|
||||
comment = {}
|
||||
if dt and dn:
|
||||
file_url = file_doc.file_url.replace("#", "%23") if file_doc.file_name else file_doc.file_url
|
||||
icon = ' <i class="fa fa-lock text-warning"></i>' if file_doc.is_private else ""
|
||||
file_name = file_doc.file_name or file_doc.file_url
|
||||
comment = frappe.get_doc(dt, dn).add_comment(
|
||||
"Attachment",
|
||||
_("added {0}").format(
|
||||
"<a href='{file_url}' target='_blank'>{file_name}</a>{icon}".format(
|
||||
**{
|
||||
"icon": ' <i class="fa fa-lock text-warning"></i>' if file_doc.is_private else "",
|
||||
"file_url": file_doc.file_url.replace("#", "%23")
|
||||
if file_doc.file_name
|
||||
else file_doc.file_url,
|
||||
"file_name": file_doc.file_name or file_doc.file_url,
|
||||
}
|
||||
)
|
||||
),
|
||||
f"<a href='{file_url}' target='_blank'>{file_name}</a>{icon}",
|
||||
)
|
||||
|
||||
return {
|
||||
|
|
@ -295,7 +288,7 @@ def remove_file(
|
|||
ignore_permissions = True
|
||||
if not file_name:
|
||||
file_name = frappe.db.get_value("File", fid, "file_name")
|
||||
comment = doc.add_comment("Attachment Removed", _("Removed {0}").format(file_name))
|
||||
comment = doc.add_comment("Attachment Removed", file_name)
|
||||
frappe.delete_doc(
|
||||
"File", fid, ignore_permissions=ignore_permissions, delete_permanently=delete_permanently
|
||||
)
|
||||
|
|
|
|||
|
|
@ -2,12 +2,14 @@
|
|||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
def resolve_class(classes):
|
||||
def resolve_class(*classes):
|
||||
if classes and len(classes) == 1:
|
||||
classes = classes[0]
|
||||
|
||||
if classes is None:
|
||||
return ""
|
||||
|
||||
if isinstance(classes, str):
|
||||
return classes
|
||||
if classes is False:
|
||||
return ""
|
||||
|
||||
if isinstance(classes, (list, tuple)):
|
||||
return " ".join(resolve_class(c) for c in classes).strip()
|
||||
|
|
|
|||
|
|
@ -218,7 +218,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:
|
||||
|
|
|
|||
|
|
@ -8,9 +8,12 @@
|
|||
"document_type": "Setup",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"published",
|
||||
"route"
|
||||
"title",
|
||||
"description",
|
||||
"column_break_4",
|
||||
"route",
|
||||
"preview_image"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -35,6 +38,20 @@
|
|||
"label": "Route",
|
||||
"read_only": 1,
|
||||
"unique": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "preview_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Preview Image"
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
|
|
@ -42,8 +59,15 @@
|
|||
"idx": 1,
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"modified": "2020-09-29 10:48:36.886753",
|
||||
"links": [
|
||||
{
|
||||
"group": "Posts",
|
||||
"link_doctype": "Blog Post",
|
||||
"link_fieldname": "blog_category"
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-10-18 15:43:39.789982",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Blog Category",
|
||||
|
|
@ -71,6 +95,7 @@
|
|||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -94,11 +94,11 @@
|
|||
"label": "Blog Intro"
|
||||
},
|
||||
{
|
||||
"default": "Rich Text",
|
||||
"default": "Markdown",
|
||||
"fieldname": "content_type",
|
||||
"fieldtype": "Select",
|
||||
"label": "Content Type",
|
||||
"options": "Rich Text\nMarkdown\nHTML",
|
||||
"options": "Markdown\nRich Text\nHTML",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
|
|
@ -215,7 +215,7 @@
|
|||
"is_published_field": "published",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2022-08-24 07:10:08.620136",
|
||||
"modified": "2022-10-18 10:09:10.550734",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Blog Post",
|
||||
|
|
|
|||
|
|
@ -204,13 +204,19 @@ def get_list_context(context=None):
|
|||
title=_("Blog"),
|
||||
)
|
||||
|
||||
category = frappe.utils.escape_html(
|
||||
blog_settings = frappe.get_doc("Blog Settings").as_dict(no_default_fields=True)
|
||||
list_context.update(blog_settings)
|
||||
|
||||
category_name = frappe.utils.escape_html(
|
||||
frappe.local.form_dict.blog_category or frappe.local.form_dict.category
|
||||
)
|
||||
if category:
|
||||
category_title = get_blog_category(category)
|
||||
list_context.sub_title = _("Posts filed under {0}").format(category_title)
|
||||
list_context.title = category_title
|
||||
if category_name:
|
||||
category = frappe.get_doc("Blog Category", category_name)
|
||||
list_context.blog_introduction = category.description or _("Posts filed under {0}").format(
|
||||
category.title
|
||||
)
|
||||
list_context.blog_title = category.title
|
||||
list_context.preview_image = category.preview_image
|
||||
|
||||
elif frappe.local.form_dict.blogger:
|
||||
blogger = frappe.db.get_value("Blogger", {"name": frappe.local.form_dict.blogger}, "full_name")
|
||||
|
|
@ -225,12 +231,16 @@ def get_list_context(context=None):
|
|||
else:
|
||||
list_context.parents = [{"name": _("Home"), "route": "/"}]
|
||||
|
||||
blog_settings = frappe.get_doc("Blog Settings").as_dict(no_default_fields=True)
|
||||
list_context.update(blog_settings)
|
||||
|
||||
if blog_settings.browse_by_category:
|
||||
list_context.blog_categories = get_blog_categories()
|
||||
|
||||
list_context.metatags = {
|
||||
"name": list_context.blog_title,
|
||||
"title": list_context.blog_title,
|
||||
"description": list_context.blog_introduction,
|
||||
"image": list_context.preview_image,
|
||||
}
|
||||
|
||||
return list_context
|
||||
|
||||
|
||||
|
|
@ -265,10 +275,6 @@ def clear_blog_cache():
|
|||
clear_cache("writers")
|
||||
|
||||
|
||||
def get_blog_category(route):
|
||||
return frappe.db.get_value("Blog Category", {"name": route}, "title") or route
|
||||
|
||||
|
||||
def get_blog_list(
|
||||
doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None
|
||||
):
|
||||
|
|
|
|||
|
|
@ -12,29 +12,30 @@
|
|||
<p>{{ blog_introduction or '' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4 align-self-end">
|
||||
|
||||
{%- if browse_by_category -%}
|
||||
<label for="category-select" class="sr-only">{{ _("Browse by category") }}</label>
|
||||
<select id="category-select" class="custom-select" onchange="window.location.pathname = this.value">
|
||||
<option value="" {{ not frappe.form_dict.category and "selected" or "" }} disabled>
|
||||
{{ _("Browse by category") }}
|
||||
</option>
|
||||
{%- if frappe.form_dict.category -%}
|
||||
<option value="blog">{{ _("Show all blogs") }}</option>
|
||||
{%- endif -%}
|
||||
{%- for category in blog_categories -%}
|
||||
<option value="{{ category.route }}" {{ frappe.form_dict.category == category.name and "selected" or "" }}>
|
||||
{{ _(category.title) }}
|
||||
</option>
|
||||
{%- endfor -%}
|
||||
</select>
|
||||
<div style="max-width: 20rem">
|
||||
<label for="category-select" class="sr-only">{{ _("Browse by category") }}</label>
|
||||
<select id="category-select" class="custom-select" onchange="window.location.pathname = this.value">
|
||||
<option value="" {{ not frappe.form_dict.category and "selected" or "" }} disabled>
|
||||
{{ _("Browse by category") }}
|
||||
</option>
|
||||
{%- if frappe.form_dict.category -%}
|
||||
<option value="blog">{{ _("Show all blogs") }}</option>
|
||||
{%- endif -%}
|
||||
{%- for category in blog_categories -%}
|
||||
<option value="{{ category.route }}" {{ frappe.form_dict.category == category.name and "selected" or "" }}>
|
||||
{{ _(category.title) }}
|
||||
</option>
|
||||
{%- endfor -%}
|
||||
</select>
|
||||
</div>
|
||||
{%- endif -%}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="blog-list-content">
|
||||
<div class="website-list" data-doctype="{{ doctype }}" data-txt="{{ txt or '[notxt]' | e }}">
|
||||
<div data-doctype="{{ doctype }}" data-txt="{{ txt or '[notxt]' | e }}">
|
||||
{% if not result -%}
|
||||
<div class="text-muted" style="min-height: 300px;">
|
||||
{{ no_result_message or _("Nothing to show") }}
|
||||
|
|
@ -54,10 +55,10 @@
|
|||
{% block script %}
|
||||
<script>
|
||||
frappe.ready(() => {
|
||||
let result_wrapper = $(".website-list .result");
|
||||
let result_wrapper = $(".blog-list.result");
|
||||
let next_start = {{ next_start or 0 }};
|
||||
|
||||
$(".website-list .btn-more").on("click", function() {
|
||||
$(".blog-list-content .btn-more").on("click", function() {
|
||||
let $btn = $(this);
|
||||
let args = $.extend(frappe.utils.get_query_params(), {
|
||||
doctype: "Blog Post",
|
||||
|
|
@ -82,7 +83,7 @@
|
|||
|
||||
function toggle_more(show) {
|
||||
if (!show) {
|
||||
$(".website-list .more-block").addClass("hide");
|
||||
$(".btn-more").addClass("hide");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,11 +7,12 @@
|
|||
"field_order": [
|
||||
"blog_title",
|
||||
"blog_introduction",
|
||||
"preview_image",
|
||||
"column_break",
|
||||
"enable_social_sharing",
|
||||
"show_cta_in_blog",
|
||||
"allow_guest_to_comment",
|
||||
"browse_by_category",
|
||||
"show_cta_in_blog",
|
||||
"cta_section",
|
||||
"title",
|
||||
"subtitle",
|
||||
|
|
@ -49,13 +50,13 @@
|
|||
"default": "0",
|
||||
"fieldname": "show_cta_in_blog",
|
||||
"fieldtype": "Check",
|
||||
"label": "Show CTA in Blog"
|
||||
"label": "Show \"Call to Action\" in Blog"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.show_cta_in_blog",
|
||||
"fieldname": "cta_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "CTA"
|
||||
"label": "Call to Action"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
|
|
@ -87,7 +88,8 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Rate Limits"
|
||||
},
|
||||
{
|
||||
"default": "5",
|
||||
|
|
@ -118,13 +120,18 @@
|
|||
"fieldname": "like_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Like limit"
|
||||
},
|
||||
{
|
||||
"fieldname": "preview_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Preview Image"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-07-12 17:45:49.108398",
|
||||
"modified": "2022-10-18 15:01:36.202010",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Blog Settings",
|
||||
|
|
|
|||
|
|
@ -63,11 +63,13 @@
|
|||
"link_fieldname": "blogger"
|
||||
}
|
||||
],
|
||||
"make_attachments_public": 1,
|
||||
"max_attachments": 1,
|
||||
"modified": "2020-05-28 19:22:40.959895",
|
||||
"modified": "2022-10-18 15:44:31.473178",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Blogger",
|
||||
"naming_rule": "By fieldname",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
|
@ -95,6 +97,7 @@
|
|||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "full_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
|
|
@ -27,3 +27,9 @@ class TestWebsiteSettings(FrappeTestCase):
|
|||
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)
|
||||
|
|
|
|||
|
|
@ -11,14 +11,21 @@
|
|||
"document_type": "Other",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"home_tab",
|
||||
"sb0",
|
||||
"home_page",
|
||||
"cb4",
|
||||
"title_prefix",
|
||||
"misc_section",
|
||||
"app_name",
|
||||
"disable_signup",
|
||||
"column_break_9",
|
||||
"app_logo",
|
||||
"section_break_6",
|
||||
"website_theme",
|
||||
"website_theme_image",
|
||||
"website_theme_image_link",
|
||||
"navbar_tab",
|
||||
"brand",
|
||||
"banner_image",
|
||||
"splash_image",
|
||||
|
|
@ -38,17 +45,22 @@
|
|||
"call_to_action_url",
|
||||
"banner",
|
||||
"banner_html",
|
||||
"footer_tab",
|
||||
"footer",
|
||||
"footer_logo",
|
||||
"copyright",
|
||||
"address",
|
||||
"footer_items",
|
||||
"footer_details_section",
|
||||
"hide_footer_signup",
|
||||
"copyright",
|
||||
"footer_logo",
|
||||
"column_break_37",
|
||||
"address",
|
||||
"footer_powered",
|
||||
"custom_footer_section",
|
||||
"footer_template",
|
||||
"footer_template_values",
|
||||
"edit_footer_template_values",
|
||||
"hide_footer_signup",
|
||||
"integrations",
|
||||
"analytics_section",
|
||||
"enable_view_tracking",
|
||||
"enable_google_indexing",
|
||||
"authorize_api_indexing_access",
|
||||
|
|
@ -57,18 +69,15 @@
|
|||
"column_break_17",
|
||||
"google_analytics_id",
|
||||
"google_analytics_anonymize_ip",
|
||||
"misc_section",
|
||||
"app_name",
|
||||
"app_logo",
|
||||
"disable_signup",
|
||||
"account_deletion_settings_section",
|
||||
"auto_account_deletion",
|
||||
"show_account_deletion_link",
|
||||
"section_break_38",
|
||||
"subdomain",
|
||||
"head_html",
|
||||
"robots_txt",
|
||||
"route_redirects",
|
||||
"account_deletion_settings_section",
|
||||
"show_account_deletion_link",
|
||||
"auto_account_deletion"
|
||||
"redirects_tab",
|
||||
"route_redirects"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
|
|
@ -96,7 +105,8 @@
|
|||
},
|
||||
{
|
||||
"fieldname": "section_break_6",
|
||||
"fieldtype": "Section Break"
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Theme"
|
||||
},
|
||||
{
|
||||
"default": "Standard",
|
||||
|
|
@ -143,7 +153,6 @@
|
|||
"label": "Set Banner from Image"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "top_bar",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Navbar"
|
||||
|
|
@ -175,11 +184,10 @@
|
|||
"options": "HTML"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "footer_items",
|
||||
"fieldname": "footer",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Footer"
|
||||
"label": "Footer Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "copyright",
|
||||
|
|
@ -189,7 +197,7 @@
|
|||
{
|
||||
"description": "Address and other legal information you may want to put in the footer.",
|
||||
"fieldname": "address",
|
||||
"fieldtype": "Text Editor",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Address",
|
||||
"max_height": "8rem"
|
||||
},
|
||||
|
|
@ -208,7 +216,7 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "integrations",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Integrations"
|
||||
},
|
||||
{
|
||||
|
|
@ -221,7 +229,6 @@
|
|||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "misc_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Login Page"
|
||||
|
|
@ -249,8 +256,8 @@
|
|||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "section_break_38",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "HTML Header, Robots and Redirects"
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Header, Robots"
|
||||
},
|
||||
{
|
||||
"description": "Added HTML in the <head> section of the web page, primarily used for website verification and SEO",
|
||||
|
|
@ -393,7 +400,6 @@
|
|||
"label": "App Logo"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "account_deletion_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Account Deletion Settings"
|
||||
|
|
@ -413,12 +419,56 @@
|
|||
{
|
||||
"fieldname": "footer_powered",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Footer \"Powered By\""
|
||||
"label": "Footer \"Powered By\"",
|
||||
"max_height": "2rem"
|
||||
},
|
||||
{
|
||||
"fieldname": "splash_image",
|
||||
"fieldtype": "Attach Image",
|
||||
"label": "Splash Image"
|
||||
},
|
||||
{
|
||||
"fieldname": "home_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Home"
|
||||
},
|
||||
{
|
||||
"fieldname": "navbar_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Navbar"
|
||||
},
|
||||
{
|
||||
"fieldname": "footer_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Footer"
|
||||
},
|
||||
{
|
||||
"fieldname": "footer_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Footer Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_37",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "custom_footer_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Custom Footer"
|
||||
},
|
||||
{
|
||||
"fieldname": "redirects_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Redirects"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "analytics_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Analytics"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
|
|
@ -426,7 +476,7 @@
|
|||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-05-27 12:33:29.019998",
|
||||
"modified": "2022-10-18 09:50:24.621839",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Website",
|
||||
"name": "Website Settings",
|
||||
|
|
@ -451,4 +501,4 @@
|
|||
"sort_order": "ASC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -64,7 +64,9 @@ class BaseTemplatePage(BaseRenderer):
|
|||
self.context.url_prefix += "/"
|
||||
|
||||
self.context.path = self.path
|
||||
self.context.pathname = frappe.local.path if hasattr(frappe, "local") else self.path
|
||||
self.context.pathname = (
|
||||
getattr(frappe.local, "path", None) if hasattr(frappe, "local") else self.path
|
||||
)
|
||||
|
||||
def update_website_context(self):
|
||||
# apply context from hooks
|
||||
|
|
|
|||
|
|
@ -92,17 +92,17 @@ def resolve_redirect(path, query_string=None):
|
|||
|
||||
Example:
|
||||
|
||||
website_redirect = [
|
||||
# absolute location
|
||||
{"source": "/from", "target": "https://mysite/from"},
|
||||
website_redirect = [
|
||||
# absolute location
|
||||
{"source": "/from", "target": "https://mysite/from"},
|
||||
|
||||
# relative location
|
||||
{"source": "/from", "target": "/main"},
|
||||
# relative location
|
||||
{"source": "/from", "target": "/main"},
|
||||
|
||||
# use regex
|
||||
{"source": r"/from/(.*)", "target": r"/main/\1"}
|
||||
# use r as a string prefix if you use regex groups or want to escape any string literal
|
||||
]
|
||||
# use regex
|
||||
{"source": r"/from/(.*)", "target": r"/main/\1"}
|
||||
# use r as a string prefix if you use regex groups or want to escape any string literal
|
||||
]
|
||||
"""
|
||||
redirects = frappe.get_hooks("website_redirects")
|
||||
redirects += frappe.get_all("Website Route Redirect", ["source", "target"], order_by=None)
|
||||
|
|
@ -122,7 +122,12 @@ def resolve_redirect(path, query_string=None):
|
|||
if rule.get("match_with_query_string"):
|
||||
path_to_match = path + "?" + frappe.safe_decode(query_string)
|
||||
|
||||
if re.match(pattern, path_to_match):
|
||||
try:
|
||||
match = re.match(pattern, path_to_match)
|
||||
except re.error as e:
|
||||
frappe.log_error("Broken Redirect: " + pattern)
|
||||
|
||||
if match:
|
||||
redirect_to = re.sub(pattern, rule["target"], path_to_match)
|
||||
frappe.flags.redirect_location = redirect_to
|
||||
frappe.cache().hset("website_redirects", path_to_match, redirect_to)
|
||||
|
|
|
|||
|
|
@ -63,34 +63,3 @@ def get_context(context):
|
|||
)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_desk_assets(build_version):
|
||||
"""Get desk assets to be loaded for mobile app"""
|
||||
data = get_context({"for_mobile": True})
|
||||
assets = [{"type": "js", "data": ""}, {"type": "css", "data": ""}]
|
||||
|
||||
if build_version != data["build_version"]:
|
||||
# new build, send assets
|
||||
for path in data["include_js"]:
|
||||
# assets path shouldn't start with /
|
||||
# as it points to different location altogether
|
||||
if path.startswith("/assets/"):
|
||||
path = path.replace("/assets/", "assets/")
|
||||
try:
|
||||
with open(os.path.join(frappe.local.sites_path, path)) as f:
|
||||
assets[0]["data"] = assets[0]["data"] + "\n" + frappe.safe_decode(f.read(), "utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
for path in data["include_css"]:
|
||||
if path.startswith("/assets/"):
|
||||
path = path.replace("/assets/", "assets/")
|
||||
try:
|
||||
with open(os.path.join(frappe.local.sites_path, path)) as f:
|
||||
assets[1]["data"] = assets[1]["data"] + "\n" + frappe.safe_decode(f.read(), "utf-8")
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return {"build_version": data["build_version"], "boot": data["boot"], "assets": assets}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from jinja2 import utils
|
||||
import markupsafe
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
|
@ -10,7 +10,7 @@ from frappe.utils.global_search import web_search
|
|||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
if frappe.form_dict.q:
|
||||
query = str(utils.escape(sanitize_html(frappe.form_dict.q)))
|
||||
query = str(markupsafe.escape(sanitize_html(frappe.form_dict.q)))
|
||||
context.title = _("Search Results for")
|
||||
context.query = query
|
||||
context.route = "/search"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ dependencies = [
|
|||
"PyPika~=0.48.9",
|
||||
"PyQRCode~=1.2.1",
|
||||
"PyYAML~=5.4.1",
|
||||
"RestrictedPython~=5.1",
|
||||
"RestrictedPython~=5.2",
|
||||
"WeasyPrint==52.5",
|
||||
"Werkzeug~=2.2.2",
|
||||
"Whoosh~=2.7.4",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue