fix: merge conflicts

This commit is contained in:
Aradhya 2022-10-21 14:22:51 +05:30
commit 7cefc240ac
86 changed files with 1780 additions and 1186 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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'"

View file

@ -0,0 +1,7 @@
// Copyright (c) 2022, Frappe Technologies and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Database Storage Usage By Tables"] = {
filters: [],
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &quot;name&quot; 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,

1 A4 A4
1527 Madam gnädige Frau
1528 Main Section Hauptbereich
1529 Make "name" searchable in Global Search Machen Sie &quot;name&quot; durchsuchbar in Global Search
1530 Make Attachments Public by Default Anhänge im Standard als öffentlich markieren
1531 Make use of longer keyboard patterns Nutzen Sie mehr Tastaturmuster
1532 Manage Third Party Apps Verwalten von Apps von Drittanbietern
1533 Mandatory Information missing: Pflichtangaben fehlen:
4728 Hide Saved Gespeicherte ausblenden
4729 Show Saved Gespeicherte anzeigen
4730 {0} created this {1} {0} erstellte dies {1}
4731 {0} created this {0} erstellte dies
4732 You created this Sie erstellten dies
4733 {0} edited this {1} {0} bearbeitete dies {1}
4734 You edited this {0} Sie bearbeiteten dies {0}
4735 {0} viewed this {1} {0} sah sich dies {1} an
4736 You viewed this {0} Sie sahen sich dies {0} an
4737 Toggle Full Width Toggle Volle Breite
4738 Documentation Dokumentation
4739 About Über

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 &lt;head&gt; 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
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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