From 5171d6edc913a10a0170367259927615f8ee1758 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Sat, 9 Apr 2022 23:38:11 +0200
Subject: [PATCH 001/203] feat: read-only geolocation (GDE-86)
- render map into display_area
- hide draw controls if read-only
- remove useless refresh_button
---
.../js/frappe/form/controls/geolocation.js | 112 ++++++++----------
1 file changed, 50 insertions(+), 62 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 688e7da3e0..008f90bc72 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -6,69 +6,69 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
async make() {
await frappe.require(this.required_libs);
super.make();
+ $(this.input_area).addClass("hidden");
}
- make_wrapper() {
+ set_disp_area(value) {
// Create the elements for map area
- super.make_wrapper();
-
- let $input_wrapper = this.$wrapper.find('.control-input-wrapper');
+ if (!this.disp_area) return;
+
this.map_id = frappe.dom.get_unique_id();
this.map_area = $(
`
`
);
- this.map_area.prependTo($input_wrapper);
- this.$wrapper.find('.control-input').addClass("hidden");
+
+ $(this.disp_area).html(this.map_area);
+ $(this.disp_area).removeClass("like-disabled-input");
+ $(this.disp_area).css("display", "block");
if (this.frm) {
- this.make_map();
+ this.make_map(value);
} else {
$(document).on('frappe.ui.Dialog:shown', () => {
- this.make_map();
+ this.make_map(value);
});
}
}
- make_map() {
+ make_map(value) {
this.bind_leaflet_map();
this.bind_leaflet_draw_control();
+ this.bind_leaflet_event_listeners();
this.bind_leaflet_locate_control();
- this.bind_leaflet_refresh_button();
+ this.bind_leaflet_data(value);
}
- format_for_input(value) {
- if (!this.map) return;
- // render raw value from db into map
+ bind_leaflet_data(value) {
+ /* render raw value from db into map */
+ if (!this.map || !value) return;
this.clear_editable_layers();
- if(value) {
- var data_layers = new L.FeatureGroup()
- .addLayer(L.geoJson(JSON.parse(value),{
- pointToLayer: function(geoJsonPoint, latlng) {
- if (geoJsonPoint.properties.point_type == "circle"){
- return L.circle(latlng, {radius: geoJsonPoint.properties.radius});
- } else if (geoJsonPoint.properties.point_type == "circlemarker") {
- return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius});
- }
- else {
- return L.marker(latlng);
- }
+
+ var data_layers = new L.FeatureGroup()
+ .addLayer(L.geoJson(JSON.parse(value),{
+ pointToLayer: function(geoJsonPoint, latlng) {
+ if (geoJsonPoint.properties.point_type == "circle"){
+ return L.circle(latlng, {radius: geoJsonPoint.properties.radius});
+ } else if (geoJsonPoint.properties.point_type == "circlemarker") {
+ return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius});
}
- }));
- this.add_non_group_layers(data_layers, this.editableLayers);
- try {
- this.map.fitBounds(this.editableLayers.getBounds(), {
- padding: [50,50]
- });
- }
- catch(err) {
- // suppress error if layer has a point.
- }
- this.editableLayers.addTo(this.map);
- } else {
- this.map.setView(frappe.utils.map_defaults.center, frappe.utils.map_defaults.zoom);
+ else {
+ return L.marker(latlng);
+ }
+ }
+ }));
+ this.add_non_group_layers(data_layers, this.editableLayers);
+ try {
+ this.map.fitBounds(this.editableLayers.getBounds(), {
+ padding: [50,50]
+ });
}
+ catch(err) {
+ // suppress error if layer has a point.
+ }
+ this.editableLayers.addTo(this.map);
this.map.invalidateSize();
}
@@ -98,9 +98,12 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/';
this.map = L.map(this.map_id);
+ this.map.setView(frappe.utils.map_defaults.center, frappe.utils.map_defaults.zoom);
L.tileLayer(frappe.utils.map_defaults.tiles,
frappe.utils.map_defaults.options).addTo(this.map);
+
+ this.editableLayers = new L.FeatureGroup();
}
bind_leaflet_locate_control() {
@@ -110,9 +113,13 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
}
bind_leaflet_draw_control() {
- this.editableLayers = new L.FeatureGroup();
+ if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, 'write', this.doc)) return;
- var options = {
+ this.map.addControl(this.get_leaflet_controls());
+ }
+
+ get_leaflet_controls() {
+ return new L.Control.Draw({
position: 'topleft',
draw: {
polyline: {
@@ -142,12 +149,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
featureGroup: this.editableLayers, //REQUIRED!!
remove: true
}
- };
-
- // create control and add to map
- this.drawControl = new L.Control.Draw(options);
- this.map.addControl(this.drawControl);
+ });
+ }
+ bind_leaflet_event_listeners() {
this.map.on('draw:created', (e) => {
var type = e.layerType,
layer = e.layer;
@@ -165,23 +170,6 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
});
}
- bind_leaflet_refresh_button() {
- L.easyButton({
- id: 'refresh-map-'+this.df.fieldname,
- position: 'topright',
- type: 'replace',
- leafletClasses: true,
- states:[{
- stateName: 'refresh-map',
- onClick: function(button, map){
- map._onResize();
- },
- title: 'Refresh map',
- icon: 'fa fa-refresh'
- }]
- }).addTo(this.map);
- }
-
add_non_group_layers(source_layer, target_group) {
// https://gis.stackexchange.com/a/203773
// Would benefit from https://github.com/Leaflet/Leaflet/issues/4461
From 2a84b18d13afb2cf3491db318f23cf0ad1a0d028 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 23 Jan 2023 18:59:16 +0100
Subject: [PATCH 002/203] feat: load address and contact display
- Handle missing permissions
- Split into smaller functions
---
frappe/contacts/address_and_contact.py | 70 ++--------------------
frappe/contacts/doctype/address/address.py | 20 +++++++
frappe/contacts/doctype/contact/contact.py | 42 +++++++++++++
3 files changed, 67 insertions(+), 65 deletions(-)
diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py
index 4df32c6705..dc866a8d94 100644
--- a/frappe/contacts/address_and_contact.py
+++ b/frappe/contacts/address_and_contact.py
@@ -1,77 +1,17 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
-import functools
-import re
-
import frappe
from frappe import _
-def load_address_and_contact(doc, key=None):
+def load_address_and_contact(doc, key=None) -> None:
"""Loads address list and contact list in `__onload`"""
- from frappe.contacts.doctype.address.address import get_address_display, get_condensed_address
+ from frappe.contacts.doctype.address.address import get_address_display_list
+ from frappe.contacts.doctype.contact.contact import get_contact_display_list
- filters = [
- ["Dynamic Link", "link_doctype", "=", doc.doctype],
- ["Dynamic Link", "link_name", "=", doc.name],
- ["Dynamic Link", "parenttype", "=", "Address"],
- ]
- address_list = frappe.get_list("Address", filters=filters, fields=["*"], order_by="creation asc")
-
- address_list = [a.update({"display": get_address_display(a)}) for a in address_list]
-
- address_list = sorted(
- address_list,
- key=functools.cmp_to_key(
- lambda a, b: (int(a.is_primary_address - b.is_primary_address))
- or (1 if a.modified - b.modified else 0)
- ),
- reverse=True,
- )
-
- doc.set_onload("addr_list", address_list)
-
- contact_list = []
- filters = [
- ["Dynamic Link", "link_doctype", "=", doc.doctype],
- ["Dynamic Link", "link_name", "=", doc.name],
- ["Dynamic Link", "parenttype", "=", "Contact"],
- ]
- contact_list = frappe.get_list("Contact", filters=filters, fields=["*"])
-
- for contact in contact_list:
- contact["email_ids"] = frappe.get_all(
- "Contact Email",
- filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0},
- fields=["email_id"],
- )
-
- contact["phone_nos"] = frappe.get_all(
- "Contact Phone",
- filters={
- "parenttype": "Contact",
- "parent": contact.name,
- "is_primary_phone": 0,
- "is_primary_mobile_no": 0,
- },
- fields=["phone"],
- )
-
- if contact.address:
- address = frappe.get_doc("Address", contact.address)
- contact["address"] = get_condensed_address(address)
-
- contact_list = sorted(
- contact_list,
- key=functools.cmp_to_key(
- lambda a, b: (int(a.is_primary_contact - b.is_primary_contact))
- or (1 if a.modified - b.modified else 0)
- ),
- reverse=True,
- )
-
- doc.set_onload("contact_list", contact_list)
+ doc.set_onload("addr_list", get_address_display_list(doc.doctype, doc.name))
+ doc.set_onload("contact_list", get_contact_display_list(doc.doctype, doc.name))
def has_permission(doc, ptype, user):
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index 5fe22eb7f2..70324d1c22 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -289,3 +289,23 @@ def get_condensed_address(doc):
def update_preferred_address(address, field):
frappe.db.set_value("Address", address, field, 0)
+
+
+def get_address_display_list(doctype: str, name: str) -> list[dict]:
+ if not frappe.has_permission("Address", "read"):
+ return []
+
+ address_list = frappe.get_list(
+ "Address",
+ filters=[
+ ["Dynamic Link", "link_doctype", "=", doctype],
+ ["Dynamic Link", "link_name", "=", name],
+ ["Dynamic Link", "parenttype", "=", "Address"],
+ ],
+ fields=["*"],
+ order_by="is_primary_address DESC, creation ASC",
+ )
+ for a in address_list:
+ a["display"] = get_address_display(a)
+
+ return address_list
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
index e7d250148b..e58a5a2b7a 100644
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -341,3 +341,45 @@ def get_full_name(
full_name = company
return full_name
+
+
+def get_contact_display_list(doctype: str, name: str) -> list[dict]:
+ from frappe.contacts.doctype.address.address import get_condensed_address
+
+ if not frappe.has_permission("Contact", "read"):
+ return []
+
+ contact_list = frappe.get_list(
+ "Contact",
+ filters=[
+ ["Dynamic Link", "link_doctype", "=", doctype],
+ ["Dynamic Link", "link_name", "=", name],
+ ["Dynamic Link", "parenttype", "=", "Contact"],
+ ],
+ fields=["*"],
+ order_by="is_primary_contact DESC, creation ASC",
+ )
+
+ for contact in contact_list:
+ contact["email_ids"] = frappe.get_all(
+ "Contact Email",
+ filters={"parenttype": "Contact", "parent": contact.name, "is_primary": 0},
+ fields=["email_id"],
+ )
+
+ contact["phone_nos"] = frappe.get_all(
+ "Contact Phone",
+ filters={
+ "parenttype": "Contact",
+ "parent": contact.name,
+ "is_primary_phone": 0,
+ "is_primary_mobile_no": 0,
+ },
+ fields=["phone"],
+ )
+
+ if contact.address and frappe.has_permission("Address", "read"):
+ address = frappe.get_doc("Address", contact.address)
+ contact["address"] = get_condensed_address(address)
+
+ return contact_list
From 5d838e4fe9bdce3c4637fe68a952cf5fc41bbd20 Mon Sep 17 00:00:00 2001
From: Smit Vora
Date: Sat, 15 Apr 2023 19:38:22 +0530
Subject: [PATCH 003/203] fix: set default user value only if enabled
---
frappe/public/js/frappe/model/perm.js | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/model/perm.js b/frappe/public/js/frappe/model/perm.js
index 8ed7d6b028..fc501a3ae5 100644
--- a/frappe/public/js/frappe/model/perm.js
+++ b/frappe/public/js/frappe/model/perm.js
@@ -290,10 +290,9 @@ $.extend(frappe.perm, {
const allowed_docs = filtered_perms.map((perm) => perm.doc);
if (with_default_doc) {
- const default_doc =
- allowed_docs.length === 1
- ? allowed_docs
- : filtered_perms.filter((perm) => perm.is_default).map((record) => record.doc);
+ const default_doc = filtered_perms
+ .filter((perm) => perm.is_default)
+ .map((record) => record.doc);
return {
allowed_records: allowed_docs,
From acbf784e2a9a2d254948c8d4e53cf218bf65e786 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 02:15:09 +0530
Subject: [PATCH 004/203] feat: add fields for onboarding ui tours.
Fields required for UI Tours.
---
.../doctype/form_tour_settings/__init__.py | 0
.../form_tour_settings/form_tour_settings.js | 8 ++
.../form_tour_settings.json | 51 ++++++++
.../form_tour_settings/form_tour_settings.py | 15 +++
.../test_form_tour_settings.py | 9 ++
.../form_tour_settings_item/__init__.py | 0
.../form_tour_settings_item.json | 61 ++++++++++
.../form_tour_settings_item.py | 9 ++
frappe/desk/doctype/form_tour/form_tour.json | 113 +++++++++++++++++-
.../form_tour_step/form_tour_step.json | 112 ++++++++++++++++-
10 files changed, 368 insertions(+), 10 deletions(-)
create mode 100644 frappe/core/doctype/form_tour_settings/__init__.py
create mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.js
create mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.json
create mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.py
create mode 100644 frappe/core/doctype/form_tour_settings/test_form_tour_settings.py
create mode 100644 frappe/core/doctype/form_tour_settings_item/__init__.py
create mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json
create mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py
diff --git a/frappe/core/doctype/form_tour_settings/__init__.py b/frappe/core/doctype/form_tour_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.js b/frappe/core/doctype/form_tour_settings/form_tour_settings.js
new file mode 100644
index 0000000000..123e51dbf9
--- /dev/null
+++ b/frappe/core/doctype/form_tour_settings/form_tour_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2023, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+// frappe.ui.form.on("Form Tour Settings", {
+// refresh(frm) {
+
+// },
+// });
diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.json b/frappe/core/doctype/form_tour_settings/form_tour_settings.json
new file mode 100644
index 0000000000..15795edde5
--- /dev/null
+++ b/frappe/core/doctype/form_tour_settings/form_tour_settings.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-05-11 18:07:26.879273",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "onboarding_tours",
+ "form_tours"
+ ],
+ "fields": [
+ {
+ "fieldname": "form_tours",
+ "fieldtype": "Table",
+ "label": "Form Tours",
+ "options": "Form Tour Settings Item"
+ },
+ {
+ "default": "\"[]\"",
+ "fieldname": "onboarding_tours",
+ "fieldtype": "JSON",
+ "hidden": 1,
+ "label": "Onboarding Tours"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2023-05-17 16:45:21.362524",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Form Tour Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.py b/frappe/core/doctype/form_tour_settings/form_tour_settings.py
new file mode 100644
index 0000000000..52b68286d7
--- /dev/null
+++ b/frappe/core/doctype/form_tour_settings/form_tour_settings.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe.model.document import Document
+
+
+class FormTourSettings(Document):
+ def on_update(self):
+ onboarding_tours = [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours]
+ frappe.db.set_single_value(
+ "Form Tour Settings", "onboarding_tours", json.dumps(onboarding_tours)
+ )
diff --git a/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py
new file mode 100644
index 0000000000..95838ecb97
--- /dev/null
+++ b/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestFormTourSettings(FrappeTestCase):
+ pass
diff --git a/frappe/core/doctype/form_tour_settings_item/__init__.py b/frappe/core/doctype/form_tour_settings_item/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json
new file mode 100644
index 0000000000..54ab61da21
--- /dev/null
+++ b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json
@@ -0,0 +1,61 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-05-11 18:10:15.194034",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "form_tour",
+ "view",
+ "list_view",
+ "page_route"
+ ],
+ "fields": [
+ {
+ "fieldname": "form_tour",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Form Tour",
+ "options": "Form Tour",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "form_tour.view_name",
+ "fieldname": "view",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "View",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "form_tour.list_name",
+ "fieldname": "list_view",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "List View",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "form_tour.page_route",
+ "fieldname": "page_route",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Page Route",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-05-17 22:22:58.507769",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Form Tour Settings Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py
new file mode 100644
index 0000000000..0958b000ad
--- /dev/null
+++ b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class FormTourSettingsItem(Document):
+ pass
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index 6f3bd56a4e..3890da468a 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -7,27 +7,39 @@
"engine": "InnoDB",
"field_order": [
"title",
+ "view_name",
+ "workspace_name",
+ "list_name",
+ "report_name",
+ "dashboard_name",
+ "new_document_form",
+ "page_name",
"reference_doctype",
"module",
"column_break_6",
+ "ui_tour",
+ "track_steps",
+ "reset_tours",
"is_standard",
"save_on_complete",
"first_document",
"include_name_field",
+ "page_route",
"section_break_3",
"steps"
],
"fields": [
{
+ "depends_on": "eval:(!doc.ui_tour || doc.is_ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Document",
- "options": "DocType",
- "reqd": 1
+ "mandatory_depends_on": "eval:(!doc.ui_tour)",
+ "options": "DocType"
},
{
- "depends_on": "reference_doctype",
+ "depends_on": "eval:(doc.ui_tour || doc.reference_doctype)",
"fieldname": "steps",
"fieldtype": "Table",
"label": "Steps",
@@ -47,6 +59,7 @@
},
{
"default": "0",
+ "depends_on": "eval:(!doc.ui_tour)",
"fieldname": "save_on_complete",
"fieldtype": "Check",
"label": "Save on Completion"
@@ -72,21 +85,110 @@
},
{
"default": "0",
+ "depends_on": "eval:(!doc.ui_tour)",
"fieldname": "first_document",
"fieldtype": "Check",
"label": "Show First Document Tour"
},
{
"default": "0",
- "depends_on": "eval:!doc.first_document",
+ "depends_on": "eval:(!doc.ui_tour && !doc.first_document)",
"fieldname": "include_name_field",
"fieldtype": "Check",
"label": "Include Name Field"
+ },
+ {
+ "default": "0",
+ "fieldname": "ui_tour",
+ "fieldtype": "Check",
+ "label": "UI Tour",
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "is_ui_tour",
+ "fieldname": "page_route",
+ "fieldtype": "JSON",
+ "hidden": 1,
+ "label": "Page Route"
+ },
+ {
+ "default": "0",
+ "depends_on": "ui_tour",
+ "description": "Please check this if you want to reset this tour and show it to all users.",
+ "fieldname": "reset_tours",
+ "fieldtype": "Check",
+ "label": "Reset Tours"
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Dashboard\")",
+ "fetch_from": ".",
+ "fieldname": "dashboard_name",
+ "fieldtype": "Link",
+ "label": "Select Dashboard",
+ "options": "Dashboard"
+ },
+ {
+ "depends_on": "ui_tour",
+ "fieldname": "view_name",
+ "fieldtype": "Select",
+ "label": "View",
+ "mandatory_depends_on": "ui_tour",
+ "options": "Workspaces\nList\nForm\nTree\nPage"
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour && doc.view_name == \"Workspaces\")",
+ "fetch_from": ".",
+ "fieldname": "workspace_name",
+ "fieldtype": "Link",
+ "label": "Select Workspace",
+ "options": "Workspace"
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour && doc.view_name == \"Page\")",
+ "fetch_from": ".",
+ "fieldname": "page_name",
+ "fieldtype": "Link",
+ "label": "Select Page",
+ "mandatory_depends_on": "eval:(doc.ui_tour && doc.view_name == \"Page\")",
+ "options": "Page"
+ },
+ {
+ "default": "List",
+ "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\")",
+ "fetch_from": ".",
+ "fieldname": "list_name",
+ "fieldtype": "Select",
+ "label": "Select List View",
+ "mandatory_depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\")",
+ "options": "List\nReport\nDashboard\nKanban\nGantt\nCalendar\nFile\nImage\nInbox\nMap"
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Report\")",
+ "fetch_from": ".",
+ "fieldname": "report_name",
+ "fieldtype": "Link",
+ "label": "Select Report",
+ "options": "Report"
+ },
+ {
+ "default": "0",
+ "depends_on": "ui_tour",
+ "description": "The next tour will start from where the user left off.",
+ "fieldname": "track_steps",
+ "fieldtype": "Check",
+ "label": "Track Steps"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: (doc.ui_tour && doc.view_name == \"Form\")",
+ "fieldname": "new_document_form",
+ "fieldtype": "Check",
+ "label": "New Document Form"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-11-24 12:03:45.449311",
+ "modified": "2023-05-18 01:28:24.593730",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
@@ -108,5 +210,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
index 7eb6eab223..f0cb8751b7 100644
--- a/frappe/desk/doctype/form_tour_step/form_tour_step.json
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -4,18 +4,29 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "ui_tour",
"is_table_field",
"section_break_2",
+ "title",
"parent_fieldname",
"fieldname",
- "title",
+ "element_selector",
+ "parent_element_selector",
"description",
+ "ondemand_description",
"column_break_2",
"position",
+ "hide_buttons",
+ "popover_element",
+ "modal_trigger",
+ "offset_x",
+ "offset_y",
+ "next_on_click",
"label",
"fieldtype",
"has_next_condition",
"next_step_condition",
+ "next_form_tour",
"section_break_13",
"child_doctype"
],
@@ -31,18 +42,20 @@
"columns": 4,
"fieldname": "description",
"fieldtype": "HTML Editor",
+ "ignore_xss_filter": 1,
"in_list_view": 1,
"label": "Description",
"reqd": 1
},
{
- "depends_on": "eval: (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname))",
+ "depends_on": "eval: (!doc.ui_tour && (!doc.is_table_field || (doc.is_table_field && doc.parent_fieldname)))",
"fieldname": "fieldname",
"fieldtype": "Select",
"label": "Fieldname",
- "reqd": 1
+ "mandatory_depends_on": "eval: (!doc.ui_tour)"
},
{
+ "depends_on": "eval:(!doc.ui_tour)",
"fieldname": "label",
"fieldtype": "Data",
"in_list_view": 1,
@@ -70,12 +83,14 @@
},
{
"default": "0",
+ "depends_on": "eval:(!doc.ui_tour)",
"fieldname": "has_next_condition",
"fieldtype": "Check",
"label": "Has Next Condition"
},
{
"default": "0",
+ "depends_on": "eval:(!doc.ui_tour)",
"fieldname": "fieldtype",
"fieldtype": "Data",
"label": "Fieldtype",
@@ -83,6 +98,7 @@
},
{
"default": "0",
+ "depends_on": "eval:(!doc.ui_tour)",
"fieldname": "is_table_field",
"fieldtype": "Check",
"label": "Is Table Field"
@@ -105,17 +121,103 @@
"read_only": 1
},
{
- "depends_on": "is_table_field",
+ "depends_on": "eval: (!doc.ui_tour || doc.is_table_field)",
"fieldname": "parent_fieldname",
"fieldtype": "Select",
"label": "Parent Field",
"mandatory_depends_on": "is_table_field"
+ },
+ {
+ "default": "0",
+ "fetch_from": "next_form_tour.ui_tour",
+ "fieldname": "ui_tour",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "UI Tour"
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour)",
+ "description": "CSS selector for the element you want to highlight.",
+ "fieldname": "element_selector",
+ "fieldtype": "Data",
+ "label": "Element Selector",
+ "mandatory_depends_on": "eval:(doc.ui_tour)",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour)",
+ "description": "Mozilla doesn't support :has() so you can pass parent selector here as workaround",
+ "fieldname": "parent_element_selector",
+ "fieldtype": "Data",
+ "label": "Parent Element Selector"
+ },
+ {
+ "depends_on": "eval:(doc.ui_tour)",
+ "fieldname": "next_form_tour",
+ "fieldtype": "Link",
+ "label": "Next Form Tour",
+ "options": "Form Tour"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.ui_tour)",
+ "description": "Hide Previous, Next and Close button on highlight dialog.",
+ "fieldname": "hide_buttons",
+ "fieldtype": "Check",
+ "label": "Hide Buttons"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.ui_tour)",
+ "description": "Move to next step when clicked inside highlighted area.",
+ "fieldname": "next_on_click",
+ "fieldtype": "Check",
+ "label": "Next on Click"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.ui_tour)",
+ "description": "when clicked on element it will focus popover if present.",
+ "fieldname": "popover_element",
+ "fieldtype": "Check",
+ "label": "Popover Element"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.ui_tour)",
+ "fieldname": "offset_x",
+ "fieldtype": "Int",
+ "label": "Offset X"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.ui_tour)",
+ "fieldname": "offset_y",
+ "fieldtype": "Int",
+ "label": "Offset Y"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:(doc.ui_tour)",
+ "description": "Enable if on click\nopens modal.",
+ "fieldname": "modal_trigger",
+ "fieldtype": "Check",
+ "label": "Modal Trigger"
+ },
+ {
+ "columns": 4,
+ "depends_on": "eval: (doc.popover_element || doc.modal_trigger)",
+ "fieldname": "ondemand_description",
+ "fieldtype": "HTML Editor",
+ "ignore_xss_filter": 1,
+ "in_list_view": 1,
+ "label": "Popover or Modal Description"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-27 15:18:36.481801",
+ "modified": "2023-05-18 01:55:44.245357",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Step",
From 10628b9b060032e4c89f9ba3833b6e693681a148 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 02:33:40 +0530
Subject: [PATCH 005/203] feat: add onboarding and status on boot
Add List of all onboarding tours to frappe.boot and onboarding status on frappe.boot.user
---
frappe/boot.py | 3 +++
frappe/core/doctype/user/user.json | 18 ++++++++++++++++--
frappe/utils/user.py | 2 ++
3 files changed, 21 insertions(+), 2 deletions(-)
diff --git a/frappe/boot.py b/frappe/boot.py
index 83c9902020..7f4677c249 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -68,6 +68,9 @@ def get_bootinfo():
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
bootinfo.notification_settings = get_notification_settings()
+ bootinfo.onboarding_tours = frappe.parse_json(
+ frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]"
+ )
set_time_zone(bootinfo)
# ipinfo
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 00e1cffa88..683063700f 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -92,6 +92,8 @@
"generate_keys",
"column_break_65",
"api_secret",
+ "onboarding_tours_section",
+ "onboarding_status",
"connections_tab"
],
"fields": [
@@ -691,6 +693,18 @@
"fieldname": "desk_settings_section",
"fieldtype": "Section Break",
"label": "Desk Settings"
+ },
+ {
+ "fieldname": "onboarding_tours_section",
+ "fieldtype": "Section Break",
+ "hidden": 1,
+ "label": "Onboarding Tours"
+ },
+ {
+ "fieldname": "onboarding_status",
+ "fieldtype": "JSON",
+ "hidden": 1,
+ "label": "Onboarding Status"
}
],
"icon": "fa fa-user",
@@ -753,7 +767,7 @@
"link_fieldname": "user"
}
],
- "modified": "2022-09-19 16:05:46.485242",
+ "modified": "2023-05-17 22:56:28.260931",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -792,4 +806,4 @@
"states": [],
"title_field": "full_name",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/utils/user.py b/frappe/utils/user.py
index 8dcb2b7ca3..f37e52f7be 100644
--- a/frappe/utils/user.py
+++ b/frappe/utils/user.py
@@ -221,6 +221,7 @@ class UserPermissions:
"mute_sounds",
"send_me_a_copy",
"user_type",
+ "onboarding_status",
],
as_dict=True,
)
@@ -229,6 +230,7 @@ class UserPermissions:
self.build_permissions()
d.name = self.name
+ d.onboarding_status = frappe.parse_json(d.onboarding_status)
d.roles = self.get_roles()
d.defaults = self.get_defaults()
for key in (
From 458f03525e77a969fd54c5aa86ab8e978505154b Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 02:58:05 +0530
Subject: [PATCH 006/203] fix: typo in reference_doctype depends_on
---
frappe/desk/doctype/form_tour/form_tour.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index 3890da468a..9cc3f65b96 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -30,7 +30,7 @@
],
"fields": [
{
- "depends_on": "eval:(!doc.ui_tour || doc.is_ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);",
+ "depends_on": "eval:(!doc.ui_tour || doc.ui_tour && [\"Workspaces\", \"Page\", \"Tree\"].indexOf(doc.view_name) == -1);",
"fieldname": "reference_doctype",
"fieldtype": "Link",
"in_list_view": 1,
@@ -188,7 +188,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-05-18 01:28:24.593730",
+ "modified": "2023-05-18 02:47:03.528693",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
From 8bd0ab6db4748beaf5fc74d323e0ea2a5197560b Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 03:04:36 +0530
Subject: [PATCH 007/203] feat: form tour logic and ui tour routes
on client-side added some form logic required by ui tours and to generate page_route based on conditions. on server-side added logic to update Form Tour Settings.
---
frappe/desk/doctype/form_tour/form_tour.js | 181 +++++++++++++++++----
frappe/desk/doctype/form_tour/form_tour.py | 58 +++++--
2 files changed, 191 insertions(+), 48 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 1e67e10779..df22f3fd9e 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -2,36 +2,64 @@
// For license information, please see license.txt
frappe.ui.form.on("Form Tour", {
- setup: function (frm) {
- if (!frm.doc.is_standard || frappe.boot.developer_mode) {
- frm.trigger("setup_queries");
- }
- },
-
refresh(frm) {
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
frm.trigger("disable_form");
}
-
- frm.add_custom_button(__("Show Tour"), async () => {
- const issingle = await check_if_single(frm.doc.reference_doctype);
- let route_changed = null;
-
- if (issingle) {
- route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
- } else if (frm.doc.first_document) {
- const name = await get_first_document(frm.doc.reference_doctype);
- route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
- } else {
- route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
+ frm.fields_dict["report_name"].get_query = function (doc) {
+ if (doc.reference_doctype) {
+ return {
+ filters: {
+ ref_doctype: doc.reference_doctype,
+ },
+ };
}
- route_changed.then(() => {
- const tour_name = frm.doc.name;
- cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
- });
- });
- },
+ return {};
+ };
+ frm.fields_dict["reference_doctype"].get_query = function (doc) {
+ return {
+ filters: {
+ istable: 0,
+ },
+ };
+ };
+ !frm.doc.ui_tour &&
+ frm.add_custom_button(__("Show Tour"), async () => {
+ const issingle = await check_if_single(frm.doc.reference_doctype);
+ let route_changed = null;
+ if (issingle) {
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
+ } else if (frm.doc.first_document) {
+ const name = await get_first_document(frm.doc.reference_doctype);
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
+ } else {
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
+ }
+ route_changed.then(() => {
+ const tour_name = frm.doc.name;
+ cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
+ });
+ });
+ },
+ async report_name(frm) {
+ if (!frm.doc.report_name) return;
+ let { message } = await frappe.db.get_value("Report", frm.doc.report_name, "ref_doctype");
+ frm.set_value("reference_doctype", message?.ref_doctype || "");
+ },
+ async before_save(frm) {
+ if (
+ frm.doc.select_view == "List" &&
+ frm.doc.list_name == "Dashboard" &&
+ frm.doc.dashboard_name &&
+ frm.doc.reference_doctype
+ ) {
+ frappe.throw(
+ "Referance Doctype and Dashboard Name both can't be used at the same time."
+ );
+ }
+ frm.doc.page_route = JSON.stringify(await get_path(frm));
+ },
disable_form: function (frm) {
frm.set_read_only();
frm.fields
@@ -42,18 +70,6 @@ frappe.ui.form.on("Form Tour", {
frm.disable_save();
},
- setup_queries(frm) {
- frm.set_query("reference_doctype", function () {
- return {
- filters: {
- istable: 0,
- },
- };
- });
-
- frm.trigger("reference_doctype");
- },
-
reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;
@@ -78,6 +94,23 @@ frappe.ui.form.on("Form Tour", {
[""].concat(options)
);
});
+ // remove report name if reference doctype is changed and report name is not valid.
+ frappe.db
+ .get_list(
+ "Report",
+ {
+ filters: {
+ ref_doctype: frm.doc.reference_doctype,
+ },
+ },
+ { fields: ["name"] }
+ )
+ .then((reports) => {
+ if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) {
+ frm.set_value("report_name", "");
+ frm.refresh_field("report_name");
+ }
+ });
},
});
@@ -115,6 +148,10 @@ async function check_if_single(doctype) {
const { message } = await frappe.db.get_value("DocType", doctype, "issingle");
return message.issingle || 0;
}
+async function check_if_private_workspace(name) {
+ const { message } = await frappe.db.get_value("Workspace", name, "public");
+ return !message.public || 0;
+}
async function get_first_document(doctype) {
let docname;
@@ -125,3 +162,75 @@ async function get_first_document(doctype) {
return docname || "new";
}
+
+async function get_path(frm) {
+ let route = [frm.doc.view_name];
+ switch (route[0]) {
+ case "Workspaces":
+ frm.doc.list_name = "";
+ frm.doc.new_document_form = 0;
+ frm.doc.report_name = "";
+ frm.doc.page_name = "";
+ frm.doc.dashboard_name = "";
+ frm.doc.reference_doctype = "";
+ if (!frm.doc.workspace_name) {
+ route.push("*");
+ return route;
+ }
+ if (await check_if_private_workspace(frm.doc.workspace_name)) {
+ route.push("private");
+ }
+ route.push(frm.doc.workspace_name);
+ return route;
+ case "List":
+ frm.doc.workspace_name = "";
+ frm.doc.new_document_form = 0;
+ frm.doc.list_name != "Report" && (frm.doc.report_name = "");
+ frm.doc.page_name = "";
+ frm.doc.dashboard_name = "";
+ if (frm.doc.list_name == "File") return ["List", "File"];
+ if (!frm.doc.reference_doctype) {
+ if (frm.doc.list_name == "Dashboard")
+ return ["dashboard-view", frm.doc.dashboard_name || "*"];
+ route.push("*");
+ } else {
+ route.push(frm.doc.reference_doctype);
+ }
+ route.push(frm.doc.list_name);
+ return route;
+ case "Form":
+ frm.doc.workspace_name = "";
+ frm.doc.list_name = "";
+ frm.doc.report_name = "";
+ frm.doc.page_name = "";
+ frm.doc.dashboard_name = "";
+ if (!frm.doc.reference_doctype) {
+ route.push("*");
+ frm.doc.new_document_form && route.push("new-*");
+ return route;
+ }
+ route.push(frm.doc.reference_doctype);
+ if (await check_if_single(frm.doc.reference_doctype)) {
+ route.push(frm.doc.reference_doctype);
+ } else if (frm.doc.new_document_form) {
+ route.push("new-" + frappe.router.slug(frm.doc.reference_doctype));
+ }
+ return route;
+ case "Tree":
+ frm.doc.workspace_name = "";
+ frm.doc.list_name = "";
+ frm.doc.new_document_form = 0;
+ frm.doc.report_name = "";
+ frm.doc.page_name = "";
+ frm.doc.dashboard_name = "";
+ return route;
+ case "Page":
+ frm.doc.workspace_name = "";
+ frm.doc.list_name = "";
+ frm.doc.new_document_form = 0;
+ frm.doc.report_name = "";
+ frm.doc.dashboard_name = "";
+ frm.doc.reference_doctype = "";
+ return [frm.doc.page_name];
+ }
+}
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 6248b43e62..48b2bd1f2d 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -8,20 +8,54 @@ from frappe.modules.export_file import export_to_files
class FormTour(Document):
def before_save(self):
- meta = frappe.get_meta(self.reference_doctype)
- for step in self.steps:
- if step.is_table_field and step.parent_fieldname:
- parent_field_df = meta.get_field(step.parent_fieldname)
- step.child_doctype = parent_field_df.options
+ if not self.ui_tour:
+ meta = frappe.get_meta(self.reference_doctype)
+ for step in self.steps:
+ if step.is_table_field and step.parent_fieldname:
+ parent_field_df = meta.get_field(step.parent_fieldname)
+ step.child_doctype = parent_field_df.options
- field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
- step.label = field_df.label
- step.fieldtype = field_df.fieldtype
- else:
- field_df = meta.get_field(step.fieldname)
- step.label = field_df.label
- step.fieldtype = field_df.fieldtype
+ field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
+ step.label = field_df.label
+ step.fieldtype = field_df.fieldtype
+ else:
+ field_df = meta.get_field(step.fieldname)
+ step.label = field_df.label
+ step.fieldtype = field_df.fieldtype
+ elif self.reset_tours:
+ self.reset_tours = 0
+ for user in frappe.get_all("User"):
+ user_doc = frappe.get_doc("User", user.name)
+ onboarding_status = frappe.parse_json(user_doc.onboarding_status)
+ if self.name in onboarding_status:
+ del onboarding_status[self.name]
+ user_doc.onboarding_status = frappe.as_json(onboarding_status)
+ user_doc.save()
def on_update(self):
if frappe.conf.developer_mode and self.is_standard:
export_to_files([["Form Tour", self.name]], self.module)
+ if self.ui_tour:
+ form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
+ in_settings = False
+ child_index = 0
+ for tour in form_tour_settings.form_tours:
+ if tour.form_tour == self.name:
+ in_settings = True
+ child_index = tour.idx
+ form_tour_settings.remove(tour)
+ if not in_settings:
+ child_index = len(form_tour_settings.form_tours) + 1
+ child = frappe.new_doc("Form Tour Settings Item")
+ child.update(
+ {
+ "idx": child_index,
+ "form_tour": self.name,
+ "parent": "Form Tour Settings",
+ "parentfield": "form_tours",
+ "parenttype": "Form Tour Settings",
+ }
+ )
+ child.save()
+ form_tour_settings.form_tours.insert(child_index, child)
+ form_tour_settings.save()
From 7ee486c89607a5a4acb98f76f77d7a1242ace69a Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 03:54:48 +0530
Subject: [PATCH 008/203] fix: clear dashboard_name only if list name is not
Dashboard
---
frappe/desk/doctype/form_tour/form_tour.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index df22f3fd9e..8c14e72f56 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -186,8 +186,8 @@ async function get_path(frm) {
frm.doc.workspace_name = "";
frm.doc.new_document_form = 0;
frm.doc.list_name != "Report" && (frm.doc.report_name = "");
+ frm.doc.list_name != "Dashboard" && (frm.doc.dashboard_name = "");
frm.doc.page_name = "";
- frm.doc.dashboard_name = "";
if (frm.doc.list_name == "File") return ["List", "File"];
if (!frm.doc.reference_doctype) {
if (frm.doc.list_name == "Dashboard")
From b7541cc163e977c8c7b1b19d70e83b43a6900d52 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 03:58:37 +0530
Subject: [PATCH 009/203] feat: onboarding class and tour on router change.
Added frappe.router on_change to find matching tours based on where user is and run them using frappe.ui.OnboardingTour.
---
frappe/hooks.py | 1 +
frappe/public/js/onboarding.bundle.js | 345 ++++++++++++++++++++++++++
2 files changed, 346 insertions(+)
create mode 100644 frappe/public/js/onboarding.bundle.js
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 6d8c00d483..5eb9426f2e 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -30,6 +30,7 @@ app_include_js = [
"controls.bundle.js",
"report.bundle.js",
"telemetry.bundle.js",
+ "onboarding.bundle.js",
]
app_include_css = [
"desk.bundle.css",
diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding.bundle.js
new file mode 100644
index 0000000000..d96b350a70
--- /dev/null
+++ b/frappe/public/js/onboarding.bundle.js
@@ -0,0 +1,345 @@
+frappe.ui.OnboardingTour = class OnboardingTour {
+ constructor() {
+ this.driver_steps = [];
+ this.last_step_saved = null;
+ this.last_element_clicked = null;
+ }
+
+ init_driver() {
+ this.driver = new frappe.Driver({
+ className: "frappe-driver",
+ allowClose: false,
+ padding: 10,
+ overlayClickNext: false,
+ keyboardControl: true,
+ nextBtnText: "Next",
+ prevBtnText: "Previous",
+ opacity: 0.25,
+ onHighlighted: step => {
+ frappe.ui.next_form_tour = step.options.step_info?.next_form_tour;
+ const wait_for_node = setInterval(() => {
+ if (!step.popover.node) return;
+ if (step.options.step_info?.offset_x) {
+ step.popover.node.style.left = `${step.popover.node.offsetLeft +
+ step.options.step_info.offset_x}px`;
+ }
+ if (step.options.step_info?.offset_y) {
+ step.popover.node.style.top = `${step.popover.node.offsetTop +
+ step.options.step_info.offset_y}px`;
+ }
+ if (step.popover.closeBtnNode) {
+ step.popover.closeBtnNode.onclick = () => {
+ if (!this.driver.hasNextStep()) {
+ this.on_finish && this.on_finish();
+ !frappe.boot.user.onboarding_status[this.tour.name] &&
+ (frappe.boot.user.onboarding_status[this.tour.name] = {});
+ frappe.boot.user.onboarding_status[
+ this.tour.name
+ ].is_complete = true;
+ frappe.utils.debounce(
+ () =>
+ frappe.db.set_value(
+ "User",
+ frappe.boot.user.name,
+ "onboarding_status",
+ JSON.stringify(frappe.boot.user.onboarding_status)
+ ),
+ 1000
+ )();
+ }
+ };
+ }
+ clearInterval(wait_for_node);
+ }, 300);
+
+ // focus on first input.
+ // TODO : later add option to select which input to focus as well.
+ const $input = $(step.node)
+ .find("input")
+ .get(0);
+ if ($input) frappe.utils.sleep(200).then(() => $input.focus());
+ }
+ });
+ }
+
+ async init({ tour_name, start_step }) {
+ this.tour = await frappe.db.get_doc("Form Tour", tour_name);
+ this.init_driver();
+ this.build_steps();
+ this.update_driver_steps();
+ if (!this.tour.track_steps) {
+ start_step = 0;
+ }
+ this.start(start_step);
+ }
+
+ build_steps() {
+ this.driver_steps = [];
+ this.tour.steps.forEach(step => {
+ const on_next = async el => {
+ const step_index = this.driver.steps.indexOf(el);
+ if (step_index == -1 || this.last_step_saved?.name == step.name) return;
+ frappe.boot.user.onboarding_status[this.tour.name] = {
+ steps_complete: step_index
+ };
+ if (!this.driver.hasNextStep()) {
+ this.on_finish && this.on_finish();
+ frappe.boot.user.onboarding_status[this.tour.name].is_complete = true;
+ }
+ this.last_step_saved = step;
+ frappe.utils.debounce(
+ () =>
+ frappe.db.set_value(
+ "User",
+ frappe.boot.user.name,
+ "onboarding_status",
+ JSON.stringify(frappe.boot.user.onboarding_status)
+ ),
+ 1000
+ )();
+ };
+ const driver_step = this.get_step(step, on_next);
+ driver_step.element && this.driver_steps.push(driver_step);
+ });
+ }
+
+ get_step(step_info, on_next) {
+ const {
+ name,
+ element_selector,
+ title,
+ description,
+ ondemand_description,
+ position,
+ parent_element_selector,
+ hide_buttons,
+ next_on_click,
+ popover_element,
+ modal_trigger
+ } = step_info;
+ let element = cur_page?.page.querySelector(element_selector);
+ !element && (element = document.querySelector(element_selector));
+ if (parent_element_selector) {
+ element = element.closest(parent_element_selector);
+ }
+ if (element && (next_on_click || hide_buttons || modal_trigger)) {
+ $(element).on("click", () => {
+ if (
+ !this.driver.getHighlightedElement() ||
+ this.driver.getHighlightedElement().node.id?.startsWith("popover")
+ )
+ return;
+
+ if (
+ modal_trigger &&
+ (!this.last_element_clicked ||
+ new Date().getTime() - new Date(this.last_element_clicked).getTime() >
+ 1000)
+ ) {
+ this.last_element_clicked = new Date().getTime();
+ this.handle_modal_steps(this.driver.currentStep, title, ondemand_description);
+ return;
+ }
+
+ if (!popover_element) {
+ on_next(this.driver.getHighlightedElement());
+ this.driver.moveNext();
+ this.driver.overlay.refresh();
+ }
+
+ if (!this.driver.getHighlightedElement()) return;
+ on_next(this.driver.getHighlightedElement());
+ let popover = this.driver
+ .getHighlightedElement()
+ .node.getAttribute("aria-describedby")
+ ? this.driver.getHighlightedElement().node
+ : this.driver
+ .getHighlightedElement()
+ .node.querySelector('[aria-describedby^="popover"]');
+
+ if (!popover) return;
+
+ let popover_id = popover.getAttribute("aria-describedby");
+ let step_index = this.driver.steps.indexOf(this.driver.getHighlightedElement());
+
+ if (this.driver_steps[step_index + 1]?.element.id == popover_id) return;
+
+ this.driver_steps = this.driver_steps.filter(
+ step => !step.element.id?.startsWith("popover")
+ );
+
+ let new_step = { ...this.driver_steps[step_index] };
+ new_step.element = document.getElementById(popover_id);
+ new_step.showButtons = false;
+ ondemand_description && (new_step.popover.description = ondemand_description);
+
+ this.driver_steps.splice(this.driver.currentStep + 1, 0, new_step);
+ this.update_driver_steps();
+ this.driver.moveNext();
+ this.driver.overlay.refresh();
+
+ $(popover).one("hide.bs.popover", e => {
+ this.driver_steps.splice(this.driver.currentStep, 1);
+ this.driver_steps[this.driver.currentStep - 1].showButtons = true;
+ new_step.popover.description = description;
+ this.update_driver_steps();
+ this.driver.movePrevious();
+ this.driver.overlay.refresh();
+ });
+ });
+ }
+
+ let showButtons = true;
+ if (popover_element || hide_buttons) {
+ showButtons = false;
+ }
+ return {
+ element,
+ name,
+ popover: {
+ title,
+ description,
+ position: frappe.router.slug(position || "Bottom")
+ },
+ onNext: on_next,
+ step_info: step_info,
+ showButtons
+ };
+ }
+
+ update_driver_steps(steps = []) {
+ if (steps.length == 0) {
+ steps = this.driver_steps;
+ }
+ this.driver.defineSteps(steps);
+ }
+
+ start(idx = 0) {
+ if (this.driver_steps.length == 0) {
+ return;
+ }
+ this.driver.start(idx);
+ }
+
+ handle_modal_steps(step, title, description) {
+ setTimeout(() => {
+ const modal_element = $(".modal-content");
+ const attach_dialog_step = {
+ element: modal_element[0],
+ allowClose: false,
+ overlayClickNext: false,
+ popover: {
+ title,
+ description,
+ position: "left-center",
+ doneBtnText: __("Next")
+ }
+ };
+ this.driver_steps.splice(step + 1, 0, attach_dialog_step);
+ this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM
+ this.driver.reset();
+ this.driver.start(step + 1);
+ this.driver.overlay.refresh();
+ modal_element.closest(".modal").one("hide.bs.modal", () => {
+ this.driver_steps.splice(this.driver.currentStep, 1);
+ this.update_driver_steps();
+ this.driver.movePrevious();
+ this.driver.moveNext();
+ this.driver.overlay.refresh();
+ });
+ }, 500);
+ }
+};
+// As of now Tours are only for desktop as it is annoying on mobile.
+// Also lot of elements are hidden on mobile so until we find a better way to do it.
+if (window.matchMedia("(max-device-width: 992px)").matches) return;
+
+frappe.router.on("change", () => {
+ let route = frappe.router.current_route;
+
+ if (route[0] === "") return;
+
+ let tour_name;
+ let matching_tours = [];
+ let start_step;
+ if (route[0] == "query-report") {
+ route = ["List", route[1], "Report"];
+ }
+ if (route[0] != "dashboard-view") {
+ frappe.boot.onboarding_tours &&
+ frappe.boot.onboarding_tours.forEach(tour => {
+ let tour_route = tour[1];
+ length = Math.min(route.length, tour_route.length);
+ if (length >= 1 && route[0] != tour_route[0]) return;
+ if (length >= 2 && tour_route[1] != "*" && route[1] != tour_route[1]) return;
+ if (
+ length >= 3 &&
+ ["*", "new-*"].indexOf(tour_route[2]) == -1 &&
+ route[2] != tour_route[2]
+ )
+ return;
+ matching_tours.push(tour);
+ });
+ }
+ console.log(matching_tours);
+ matching_tours = matching_tours.filter(tour => {
+ if (frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true) return false;
+ return true;
+ });
+ matching_tours = matching_tours.map(tour => {
+ if (frappe.boot.user.onboarding_status[tour[0]]?.steps_complete != undefined) {
+ tour.push(frappe.boot.user.onboarding_status[tour[0]].steps_complete);
+ }
+ return tour;
+ });
+ if (matching_tours.length == 0) return;
+ let current_tour = matching_tours.find(
+ tour => tour[0] == frappe.ui.currentTourInstance?.tour.name
+ );
+ if (current_tour) {
+ tour_name = current_tour[0];
+ start_step = current_tour.at(-1);
+ if (typeof start_step != "number") {
+ start_step = 0;
+ }
+ } else if (frappe.ui.next_form_tour) {
+ let current_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour);
+ tour_name = current_tour[0];
+ start_step = current_tour.at(-1);
+ if (typeof start_step != "number") {
+ start_step = 0;
+ } else {
+ start_step += 1;
+ }
+ frappe.ui.next_form_tour = undefined;
+ } else {
+ tour_name = matching_tours[0][0];
+ start_step = matching_tours[0].at(-1);
+ if (typeof start_step != "number") {
+ start_step = 0;
+ } else {
+ start_step += 1;
+ }
+ }
+ if (!tour_name) return;
+ if (frappe.ui.currentTourInstance) {
+ frappe.ui.currentTourInstance.driver_steps = [];
+ frappe.ui.currentTourInstance.driver.reset(true);
+ frappe.ui.currentTourInstance.update_driver_steps();
+ }
+ const tour = (frappe.ui.currentTourInstance = new frappe.ui.OnboardingTour());
+ // wait for workspace and/or data to load.
+ const wait_for_data = setInterval(() => {
+ if (cur_page?.page.querySelector(".workspace-sidebar-skeleton")) return;
+ if (cur_page?.page.querySelector(".workspace-skeleton")) return;
+ if (document.body.getAttribute("data-ajax-state") === "complete") {
+ frappe.utils.sleep(500).then(() => {
+ tour.init({
+ tour_name,
+ start_step
+ });
+ clearInterval(wait_for_data);
+ });
+ }
+ }, 100);
+});
From d4997160e892f4547da85cea272b0a9ca662b6f8 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 12:25:53 +0530
Subject: [PATCH 010/203] fix(minor): use matching next_tour if found
---
frappe/public/js/onboarding.bundle.js | 21 ++++++++++-----------
1 file changed, 10 insertions(+), 11 deletions(-)
diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding.bundle.js
index d96b350a70..bd68b5facf 100644
--- a/frappe/public/js/onboarding.bundle.js
+++ b/frappe/public/js/onboarding.bundle.js
@@ -14,7 +14,7 @@ frappe.ui.OnboardingTour = class OnboardingTour {
keyboardControl: true,
nextBtnText: "Next",
prevBtnText: "Previous",
- opacity: 0.25,
+ opacity: 0.5,
onHighlighted: step => {
frappe.ui.next_form_tour = step.options.step_info?.next_form_tour;
const wait_for_node = setInterval(() => {
@@ -281,7 +281,6 @@ frappe.router.on("change", () => {
matching_tours.push(tour);
});
}
- console.log(matching_tours);
matching_tours = matching_tours.filter(tour => {
if (frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true) return false;
return true;
@@ -296,21 +295,21 @@ frappe.router.on("change", () => {
let current_tour = matching_tours.find(
tour => tour[0] == frappe.ui.currentTourInstance?.tour.name
);
+ let next_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour);
if (current_tour) {
tour_name = current_tour[0];
start_step = current_tour.at(-1);
if (typeof start_step != "number") {
start_step = 0;
}
- } else if (frappe.ui.next_form_tour) {
- let current_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour);
- tour_name = current_tour[0];
- start_step = current_tour.at(-1);
- if (typeof start_step != "number") {
- start_step = 0;
- } else {
- start_step += 1;
- }
+ } else if (next_tour) {
+ tour_name = next_tour[0];
+ start_step = next_tour.at(-1);
+ if (typeof start_step != "number") {
+ start_step = 0;
+ } else {
+ start_step += 1;
+ }
frappe.ui.next_form_tour = undefined;
} else {
tour_name = matching_tours[0][0];
From 76c118bac77fc0f159575097bacd1e38ed425194 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 12:27:37 +0530
Subject: [PATCH 011/203] feat: frappe framework basic UI tours.
---
.../main_workspace_tour.json | 80 +++++++++
.../todo_list_tour/todo_list_tour.json | 159 ++++++++++++++++++
.../tools_workspace_tour.json | 79 +++++++++
3 files changed, 318 insertions(+)
create mode 100644 frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
create mode 100644 frappe/desk/form_tour/todo_list_tour/todo_list_tour.json
create mode 100644 frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json
diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
new file mode 100644
index 0000000000..62888ebdac
--- /dev/null
+++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
@@ -0,0 +1,80 @@
+{
+ "creation": "2023-05-18 12:08:23.196462",
+ "dashboard_name": "",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "list_name": "",
+ "modified": "2023-05-18 12:21:54.389743",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Main Workspace Tour",
+ "new_document_form": 0,
+ "owner": "Administrator",
+ "page_name": "",
+ "page_route": "[\"Workspaces\",\"Build\"]",
+ "reference_doctype": "",
+ "report_name": "",
+ "reset_tours": 0,
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "You can access different things like report, settings, documents (any doctypes), and modules. It saves you time by eliminating the need to navigate through menus. ",
+ "element_selector": "#navbar-search",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "parent_element_selector": ".input-group.search-bar",
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Awesomebar",
+ "ui_tour": 1
+ },
+ {
+ "description": "Workspaces can be used to quickly access various modules and features. It organizes the available functionalities into logical groups. ",
+ "element_selector": ".col-lg-2.layout-side-section",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Right",
+ "title": "Workspace List",
+ "ui_tour": 1
+ },
+ {
+ "description": "Click to visit the Workspace ",
+ "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Tools\"]",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 1,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_form_tour": "New Tools Tour",
+ "next_on_click": 1,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Right",
+ "title": "Tools Workspace",
+ "ui_tour": 1
+ }
+ ],
+ "title": "Main Workspace Tour",
+ "track_steps": 1,
+ "ui_tour": 1,
+ "view_name": "Workspaces",
+ "workspace_name": "Build"
+}
\ No newline at end of file
diff --git a/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json b/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json
new file mode 100644
index 0000000000..6a561e6f51
--- /dev/null
+++ b/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json
@@ -0,0 +1,159 @@
+{
+ "creation": "2023-05-18 12:12:01.839494",
+ "dashboard_name": "",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "list_name": "List",
+ "modified": "2023-05-18 12:22:07.306556",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Todo List Tour",
+ "new_document_form": 0,
+ "owner": "Administrator",
+ "page_name": "",
+ "page_route": "[\"List\",\"ToDo\",\"List\"]",
+ "reference_doctype": "ToDo",
+ "report_name": "",
+ "reset_tours": 0,
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "List View ",
+ "element_selector": ".layout-main-section.frappe-card",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Left",
+ "title": "TODO",
+ "ui_tour": 1
+ },
+ {
+ "description": "List View as the name suggest is used to see documents/records in list format. ",
+ "element_selector": ".frappe-list",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Top Center",
+ "title": "TODO List",
+ "ui_tour": 1
+ },
+ {
+ "description": "Using Quick filter you can refine and narrow down the displayed data by applying specific criteria or conditions ",
+ "element_selector": ".list-sidebar.overlay-sidebar.hidden-xs.hidden-sm",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Right",
+ "title": "Sidebar",
+ "ui_tour": 1
+ },
+ {
+ "description": "You can also filter using this inputs ",
+ "element_selector": ".standard-filter-section.flex",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 1,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Bottom",
+ "title": "Input Filters",
+ "ui_tour": 1
+ },
+ {
+ "description": "Click on the Filter button ",
+ "element_selector": ".filter-selector > .btn-group",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 1,
+ "offset_x": 0,
+ "offset_y": 0,
+ "ondemand_description": "Aou can add multiple filters and hit apply to refine results ",
+ "popover_element": 1,
+ "position": "Left",
+ "title": "Advanced Filters",
+ "ui_tour": 1
+ },
+ {
+ "description": "Click here to remove all filter ",
+ "element_selector": ".filter-selector > .btn-group > .filter-x-button",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 1,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Clear Filters",
+ "ui_tour": 1
+ },
+ {
+ "description": "You can arrange data in ascending or descending order based on selected attributes.\n\n \n Click on Last Updated On \n \n Select the Attribute based on which you want to sort \n \n ",
+ "element_selector": ".sort-selector",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": -20,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Sort By",
+ "ui_tour": 1
+ },
+ {
+ "description": "Click to change ascending or descending order.\n ",
+ "element_selector": ".sort-selector > .btn-group > .btn-order",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": -20,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Sort By",
+ "ui_tour": 1
+ }
+ ],
+ "title": "Todo List Tour",
+ "track_steps": 1,
+ "ui_tour": 1,
+ "view_name": "List",
+ "workspace_name": ""
+}
\ No newline at end of file
diff --git a/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json b/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json
new file mode 100644
index 0000000000..6f10c69328
--- /dev/null
+++ b/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json
@@ -0,0 +1,79 @@
+{
+ "creation": "2023-05-18 12:09:40.792239",
+ "dashboard_name": "",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "list_name": "",
+ "modified": "2023-05-18 12:22:01.208707",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Tools Workspace Tour",
+ "new_document_form": 0,
+ "owner": "Administrator",
+ "page_name": "",
+ "page_route": "[\"Workspaces\",\"Tools\"]",
+ "reference_doctype": "",
+ "report_name": "",
+ "reset_tours": 0,
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "This is Tools Workspace ",
+ "element_selector": ".codex-editor",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Workspace",
+ "ui_tour": 1
+ },
+ {
+ "description": "Workspace have cards that serve as links to different modules and features. For instance, the Email List card provides easy access to related components like Newsletter and Email Group ",
+ "element_selector": "[card_name=\"Email\"]",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Right",
+ "title": "Email Card",
+ "ui_tour": 1
+ },
+ {
+ "description": "Shortcuts are a set of clickable links that serve as direct links to frequently accessed modules and features ",
+ "element_selector": "[shortcut_name=\"ToDo\"]",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 1,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_form_tour": "Todo List Tour",
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Right",
+ "title": "Todo Shortcut",
+ "ui_tour": 1
+ }
+ ],
+ "title": "Tools Workspace Tour",
+ "track_steps": 1,
+ "ui_tour": 1,
+ "view_name": "Workspaces",
+ "workspace_name": "Tools"
+}
\ No newline at end of file
From 20fe275ba2965e860d12c62934ab5d9606599433 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 18 May 2023 12:43:10 +0530
Subject: [PATCH 012/203] fix: set default for user onboarding_status
---
frappe/core/doctype/user/user.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 683063700f..35a58a5851 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -701,6 +701,7 @@
"label": "Onboarding Tours"
},
{
+ "default": "{}",
"fieldname": "onboarding_status",
"fieldtype": "JSON",
"hidden": 1,
@@ -767,7 +768,7 @@
"link_fieldname": "user"
}
],
- "modified": "2023-05-17 22:56:28.260931",
+ "modified": "2023-05-18 12:41:36.765029",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
From b4387086b4cf34b45f5d78852bcec7d8b0c179be Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Fri, 19 May 2023 11:26:03 +0530
Subject: [PATCH 013/203] fix: linter for onboarding.bundle.js
---
frappe/public/js/onboarding.bundle.js | 64 +++++++++++++--------------
1 file changed, 32 insertions(+), 32 deletions(-)
diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding.bundle.js
index bd68b5facf..28419700b6 100644
--- a/frappe/public/js/onboarding.bundle.js
+++ b/frappe/public/js/onboarding.bundle.js
@@ -15,17 +15,19 @@ frappe.ui.OnboardingTour = class OnboardingTour {
nextBtnText: "Next",
prevBtnText: "Previous",
opacity: 0.5,
- onHighlighted: step => {
+ onHighlighted: (step) => {
frappe.ui.next_form_tour = step.options.step_info?.next_form_tour;
const wait_for_node = setInterval(() => {
if (!step.popover.node) return;
if (step.options.step_info?.offset_x) {
- step.popover.node.style.left = `${step.popover.node.offsetLeft +
- step.options.step_info.offset_x}px`;
+ step.popover.node.style.left = `${
+ step.popover.node.offsetLeft + step.options.step_info.offset_x
+ }px`;
}
if (step.options.step_info?.offset_y) {
- step.popover.node.style.top = `${step.popover.node.offsetTop +
- step.options.step_info.offset_y}px`;
+ step.popover.node.style.top = `${
+ step.popover.node.offsetTop + step.options.step_info.offset_y
+ }px`;
}
if (step.popover.closeBtnNode) {
step.popover.closeBtnNode.onclick = () => {
@@ -54,11 +56,9 @@ frappe.ui.OnboardingTour = class OnboardingTour {
// focus on first input.
// TODO : later add option to select which input to focus as well.
- const $input = $(step.node)
- .find("input")
- .get(0);
+ const $input = $(step.node).find("input").get(0);
if ($input) frappe.utils.sleep(200).then(() => $input.focus());
- }
+ },
});
}
@@ -75,12 +75,12 @@ frappe.ui.OnboardingTour = class OnboardingTour {
build_steps() {
this.driver_steps = [];
- this.tour.steps.forEach(step => {
- const on_next = async el => {
+ this.tour.steps.forEach((step) => {
+ const on_next = async (el) => {
const step_index = this.driver.steps.indexOf(el);
if (step_index == -1 || this.last_step_saved?.name == step.name) return;
frappe.boot.user.onboarding_status[this.tour.name] = {
- steps_complete: step_index
+ steps_complete: step_index,
};
if (!this.driver.hasNextStep()) {
this.on_finish && this.on_finish();
@@ -115,7 +115,7 @@ frappe.ui.OnboardingTour = class OnboardingTour {
hide_buttons,
next_on_click,
popover_element,
- modal_trigger
+ modal_trigger,
} = step_info;
let element = cur_page?.page.querySelector(element_selector);
!element && (element = document.querySelector(element_selector));
@@ -165,7 +165,7 @@ frappe.ui.OnboardingTour = class OnboardingTour {
if (this.driver_steps[step_index + 1]?.element.id == popover_id) return;
this.driver_steps = this.driver_steps.filter(
- step => !step.element.id?.startsWith("popover")
+ (step) => !step.element.id?.startsWith("popover")
);
let new_step = { ...this.driver_steps[step_index] };
@@ -178,7 +178,7 @@ frappe.ui.OnboardingTour = class OnboardingTour {
this.driver.moveNext();
this.driver.overlay.refresh();
- $(popover).one("hide.bs.popover", e => {
+ $(popover).one("hide.bs.popover", (e) => {
this.driver_steps.splice(this.driver.currentStep, 1);
this.driver_steps[this.driver.currentStep - 1].showButtons = true;
new_step.popover.description = description;
@@ -199,11 +199,11 @@ frappe.ui.OnboardingTour = class OnboardingTour {
popover: {
title,
description,
- position: frappe.router.slug(position || "Bottom")
+ position: frappe.router.slug(position || "Bottom"),
},
onNext: on_next,
step_info: step_info,
- showButtons
+ showButtons,
};
}
@@ -232,8 +232,8 @@ frappe.ui.OnboardingTour = class OnboardingTour {
title,
description,
position: "left-center",
- doneBtnText: __("Next")
- }
+ doneBtnText: __("Next"),
+ },
};
this.driver_steps.splice(step + 1, 0, attach_dialog_step);
this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM
@@ -267,7 +267,7 @@ frappe.router.on("change", () => {
}
if (route[0] != "dashboard-view") {
frappe.boot.onboarding_tours &&
- frappe.boot.onboarding_tours.forEach(tour => {
+ frappe.boot.onboarding_tours.forEach((tour) => {
let tour_route = tour[1];
length = Math.min(route.length, tour_route.length);
if (length >= 1 && route[0] != tour_route[0]) return;
@@ -281,11 +281,11 @@ frappe.router.on("change", () => {
matching_tours.push(tour);
});
}
- matching_tours = matching_tours.filter(tour => {
+ matching_tours = matching_tours.filter((tour) => {
if (frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true) return false;
return true;
});
- matching_tours = matching_tours.map(tour => {
+ matching_tours = matching_tours.map((tour) => {
if (frappe.boot.user.onboarding_status[tour[0]]?.steps_complete != undefined) {
tour.push(frappe.boot.user.onboarding_status[tour[0]].steps_complete);
}
@@ -293,9 +293,9 @@ frappe.router.on("change", () => {
});
if (matching_tours.length == 0) return;
let current_tour = matching_tours.find(
- tour => tour[0] == frappe.ui.currentTourInstance?.tour.name
+ (tour) => tour[0] == frappe.ui.currentTourInstance?.tour.name
);
- let next_tour = matching_tours.find(tour => tour[0] == frappe.ui.next_form_tour);
+ let next_tour = matching_tours.find((tour) => tour[0] == frappe.ui.next_form_tour);
if (current_tour) {
tour_name = current_tour[0];
start_step = current_tour.at(-1);
@@ -303,13 +303,13 @@ frappe.router.on("change", () => {
start_step = 0;
}
} else if (next_tour) {
- tour_name = next_tour[0];
- start_step = next_tour.at(-1);
- if (typeof start_step != "number") {
- start_step = 0;
- } else {
- start_step += 1;
- }
+ tour_name = next_tour[0];
+ start_step = next_tour.at(-1);
+ if (typeof start_step != "number") {
+ start_step = 0;
+ } else {
+ start_step += 1;
+ }
frappe.ui.next_form_tour = undefined;
} else {
tour_name = matching_tours[0][0];
@@ -335,7 +335,7 @@ frappe.router.on("change", () => {
frappe.utils.sleep(500).then(() => {
tour.init({
tour_name,
- start_step
+ start_step,
});
clearInterval(wait_for_data);
});
From 5f6a6a025a3029a59b220c3f5556e67a646e7096 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 19 May 2023 16:56:54 +0530
Subject: [PATCH 014/203] fix: Load map libraries at build time to avoid async
issues during geolocation render
- When gelocation control is built, it awaits loading of libraries
- Withi the control everything is await but a layer above where multiple controls are sequentially built this breaks
- Form render goes ahead without waiting for the gelocation control
- The `onload_post_render` in `Location` cannot find the map wrapper as the control is still being built
- Therefore, on a hard load, the control does not show up and appears on soft reload.
---
.../public/js/frappe/form/controls/geolocation.js | 14 --------------
.../lib/leaflet_control_locate/L.Control.Locate.js | 2 +-
frappe/public/js/libs.bundle.js | 4 ++++
frappe/public/scss/desk.bundle.scss | 5 +++++
4 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 42534a3c39..aa75a2282b 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -4,7 +4,6 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
static horizontal = false;
async make() {
- await frappe.require(this.required_libs);
super.make();
}
@@ -216,17 +215,4 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.editableLayers.removeLayer(l);
});
}
-
- get required_libs() {
- return [
- "assets/frappe/js/lib/leaflet_easy_button/easy-button.css",
- "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.css",
- "assets/frappe/js/lib/leaflet_draw/leaflet.draw.css",
- "assets/frappe/js/lib/leaflet/leaflet.css",
- "assets/frappe/js/lib/leaflet/leaflet.js",
- "assets/frappe/js/lib/leaflet_easy_button/easy-button.js",
- "assets/frappe/js/lib/leaflet_draw/leaflet.draw.js",
- "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.js",
- ];
- }
};
diff --git a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js
index 8544e17a04..8ea44ce00c 100644
--- a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js
+++ b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js
@@ -17,7 +17,7 @@ You can find the project at: https://github.com/domoritz/leaflet-locatecontrol
if (typeof window !== 'undefined' && window.L) {
module.exports = factory(L);
} else {
- module.exports = factory(require('leaflet'));
+ module.exports = factory(require('../leaflet/leaflet.js'));
}
}
diff --git a/frappe/public/js/libs.bundle.js b/frappe/public/js/libs.bundle.js
index e4e172c1b4..77704bb173 100644
--- a/frappe/public/js/libs.bundle.js
+++ b/frappe/public/js/libs.bundle.js
@@ -1,5 +1,9 @@
import "./jquery-bootstrap";
import "./lib/moment";
+import "../js/lib/leaflet/leaflet.js";
+import "../js/lib/leaflet_easy_button/easy-button.js";
+import "../js/lib/leaflet_draw/leaflet.draw.js";
+import "../js/lib/leaflet_control_locate/L.Control.Locate.js";
import Sortable from "sortablejs";
window.SetVueGlobals = (app) => {
diff --git a/frappe/public/scss/desk.bundle.scss b/frappe/public/scss/desk.bundle.scss
index 10fd116d6c..6b192bb3ff 100644
--- a/frappe/public/scss/desk.bundle.scss
+++ b/frappe/public/scss/desk.bundle.scss
@@ -4,3 +4,8 @@
@import "~frappe-charts/dist/frappe-charts.min";
@import "~plyr/dist/plyr";
@import "./desk/index";
+
+@import "frappe/public/js/lib/leaflet/leaflet.css";
+@import "frappe/public/js/lib/leaflet_easy_button/easy-button.css";
+@import "frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.css";
+@import "frappe/public/js/lib/leaflet_draw/leaflet.draw.css";
\ No newline at end of file
From 77e73ccea4ade94b6b37972328643f69394fba41 Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sun, 21 May 2023 16:35:57 +0530
Subject: [PATCH 015/203] feat: support for dynamic URL in webhook
Closes #11921
---
frappe/integrations/doctype/webhook/webhook.json | 12 ++++++++++--
frappe/integrations/doctype/webhook/webhook.py | 15 ++++++++++-----
2 files changed, 20 insertions(+), 7 deletions(-)
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index a21e460659..9b402c6a27 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -18,8 +18,9 @@
"html_condition",
"sb_webhook",
"request_url",
- "request_method",
+ "is_dynamic_url",
"cb_webhook",
+ "request_method",
"request_structure",
"sb_security",
"enable_security",
@@ -200,10 +201,17 @@
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "description": "On checking this option, URL will be treated like a jinja template string",
+ "fieldname": "is_dynamic_url",
+ "fieldtype": "Check",
+ "label": "Is Dynamic URL?"
}
],
"links": [],
- "modified": "2022-07-11 08:54:10.740512",
+ "modified": "2023-05-21 16:30:10.740512",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 7d168c659f..1b56a1b129 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -115,29 +115,34 @@ def enqueue_webhook(doc, webhook) -> None:
webhook: Webhook = frappe.get_doc("Webhook", webhook.get("name"))
headers = get_webhook_headers(doc, webhook)
data = get_webhook_data(doc, webhook)
- r = None
+ if webhook.is_dynamic_url:
+ request_url = frappe.render_template(webhook.request_url, get_context(doc))
+ else:
+ request_url = webhook.request_url
+
+ r = None
for i in range(3):
try:
r = requests.request(
method=webhook.request_method,
- url=webhook.request_url,
+ url=request_url,
data=json.dumps(data, default=str),
headers=headers,
timeout=5,
)
r.raise_for_status()
frappe.logger().debug({"webhook_success": r.text})
- log_request(webhook.name, doc.name, webhook.request_url, headers, data, r)
+ log_request(webhook.name, doc.name, request_url, headers, data, r)
break
except requests.exceptions.ReadTimeout as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
- log_request(webhook.name, doc.name, webhook.request_url, headers, data)
+ log_request(webhook.name, doc.name, request_url, headers, data)
except Exception as e:
frappe.logger().debug({"webhook_error": e, "try": i + 1})
- log_request(webhook.name, doc.name, webhook.request_url, headers, data, r)
+ log_request(webhook.name, doc.name, request_url, headers, data, r)
sleep(3 * i + 1)
if i != 2:
continue
From 8f8c6b51e87f7659b87ac9c6af34932b35098da0 Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sun, 21 May 2023 16:36:08 +0530
Subject: [PATCH 016/203] test: tests for dynamic URL
---
.../doctype/webhook/test_webhook.py | 54 +++++++++++++++++++
1 file changed, 54 insertions(+)
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index 8284db7fd3..1092d2a253 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -206,3 +206,57 @@ class TestWebhook(FrappeTestCase):
enqueue_webhook(doc, wh)
log = frappe.get_last_doc("Webhook Request Log")
self.assertEqual(len(json.loads(log.response)["json"]), 3)
+
+ def test_webhook_with_dynamic_url_enabled(self):
+ wh_config = {
+ "doctype": "Webhook",
+ "webhook_doctype": "Note",
+ "webhook_docevent": "after_insert",
+ "enabled": 1,
+ "request_url": "https://httpbin.org/anything/{{ doc.doctype }}",
+ "is_dynamic_url": 1,
+ "request_method": "POST",
+ "request_structure": "JSON",
+ "webhook_json": '{}',
+ "meets_condition": "Yes",
+ "webhook_headers": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ }
+ ],
+ }
+
+ with get_test_webhook(wh_config) as wh:
+ doc = frappe.new_doc("Note")
+ doc.title = "Test Webhook Note"
+ enqueue_webhook(doc, wh)
+ log = frappe.get_last_doc("Webhook Request Log")
+ self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/Note")
+
+ def test_webhook_with_dynamic_url_disabled(self):
+ wh_config = {
+ "doctype": "Webhook",
+ "webhook_doctype": "Note",
+ "webhook_docevent": "after_insert",
+ "enabled": 1,
+ "request_url": "https://httpbin.org/anything/{{doc.doctype}}",
+ "is_dynamic_url": 0,
+ "request_method": "POST",
+ "request_structure": "JSON",
+ "webhook_json": '{}',
+ "meets_condition": "Yes",
+ "webhook_headers": [
+ {
+ "key": "Content-Type",
+ "value": "application/json",
+ }
+ ],
+ }
+
+ with get_test_webhook(wh_config) as wh:
+ doc = frappe.new_doc("Note")
+ doc.title = "Test Webhook Note"
+ enqueue_webhook(doc, wh)
+ log = frappe.get_last_doc("Webhook Request Log")
+ self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}")
From 3f08e80d888b279b3a7ad1ebdb852e1d7c8b340a Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Sun, 21 May 2023 16:44:34 +0530
Subject: [PATCH 017/203] style: lint test file
---
frappe/integrations/doctype/webhook/test_webhook.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index 1092d2a253..7235447662 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -217,7 +217,7 @@ class TestWebhook(FrappeTestCase):
"is_dynamic_url": 1,
"request_method": "POST",
"request_structure": "JSON",
- "webhook_json": '{}',
+ "webhook_json": "{}",
"meets_condition": "Yes",
"webhook_headers": [
{
@@ -244,7 +244,7 @@ class TestWebhook(FrappeTestCase):
"is_dynamic_url": 0,
"request_method": "POST",
"request_structure": "JSON",
- "webhook_json": '{}',
+ "webhook_json": "{}",
"meets_condition": "Yes",
"webhook_headers": [
{
@@ -259,4 +259,6 @@ class TestWebhook(FrappeTestCase):
doc.title = "Test Webhook Note"
enqueue_webhook(doc, wh)
log = frappe.get_last_doc("Webhook Request Log")
- self.assertEqual(json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}")
+ self.assertEqual(
+ json.loads(log.response)["url"], "https://httpbin.org/anything/{{doc.doctype}}"
+ )
From 9f83ead7a2ac42b51e45c91456bfb383ba67e118 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 22 May 2023 14:48:19 +0530
Subject: [PATCH 018/203] fix: remove setup_complete events
This can trigger along side actual setup complete event causing two
parallel requests.
---
frappe/desk/page/setup_wizard/setup_wizard.js | 2 --
1 file changed, 2 deletions(-)
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index 56cb61696c..3f902689f8 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -122,8 +122,6 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
show_slide(id) {
if (id === this.slides.length) {
- // show_slide called on last slide
- this.action_on_complete();
return;
}
super.show_slide(id);
From c643d4a33cc58d10c0a3d81c9ed9df9bf6cc0776 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 22 May 2023 16:15:53 +0530
Subject: [PATCH 019/203] chore: capitalization
---
frappe/core/doctype/system_settings/system_settings.json | 2 +-
frappe/desk/page/setup_wizard/setup_wizard.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 4249f250b7..091dc1df1e 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -548,7 +548,7 @@
"default": "1",
"fieldname": "enable_telemetry",
"fieldtype": "Check",
- "label": "Allow Sending Usage Data for Improving applications"
+ "label": "Allow Sending Usage Data for Improving Applications"
}
],
"icon": "fa fa-cog",
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index 3f902689f8..862ac8c14d 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -401,7 +401,7 @@ frappe.setup.slides_settings = [
},
{
fieldname: "enable_telemetry",
- label: __("Allow Sending Usage Data for Improving applications"),
+ label: __("Allow Sending Usage Data for Improving Applications"),
fieldtype: "Check",
default: 1,
},
From 892150eb7397bf229d132b3621fafa81f568bc61 Mon Sep 17 00:00:00 2001
From: marination
Date: Fri, 19 May 2023 16:56:54 +0530
Subject: [PATCH 020/203] fix: Load map libraries at build time to avoid async
issues during geolocation render
- When gelocation control is built, it awaits loading of libraries
- Withi the control everything is await but a layer above where multiple controls are sequentially built this breaks
- Form render goes ahead without waiting for the gelocation control
- The `onload_post_render` in `Location` cannot find the map wrapper as the control is still being built
- Therefore, on a hard load, the control does not show up and appears on soft reload.
---
.../public/js/frappe/form/controls/geolocation.js | 14 --------------
.../lib/leaflet_control_locate/L.Control.Locate.js | 2 +-
frappe/public/js/libs.bundle.js | 4 ++++
frappe/public/scss/desk.bundle.scss | 5 +++++
4 files changed, 10 insertions(+), 15 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index c2ce336266..026457b856 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -4,7 +4,6 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
static horizontal = false;
async make() {
- await frappe.require(this.required_libs);
super.make();
$(this.input_area).addClass("hidden");
}
@@ -200,17 +199,4 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.editableLayers.removeLayer(l);
});
}
-
- get required_libs() {
- return [
- "assets/frappe/js/lib/leaflet_easy_button/easy-button.css",
- "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.css",
- "assets/frappe/js/lib/leaflet_draw/leaflet.draw.css",
- "assets/frappe/js/lib/leaflet/leaflet.css",
- "assets/frappe/js/lib/leaflet/leaflet.js",
- "assets/frappe/js/lib/leaflet_easy_button/easy-button.js",
- "assets/frappe/js/lib/leaflet_draw/leaflet.draw.js",
- "assets/frappe/js/lib/leaflet_control_locate/L.Control.Locate.js",
- ];
- }
};
diff --git a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js
index 8544e17a04..8ea44ce00c 100644
--- a/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js
+++ b/frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.js
@@ -17,7 +17,7 @@ You can find the project at: https://github.com/domoritz/leaflet-locatecontrol
if (typeof window !== 'undefined' && window.L) {
module.exports = factory(L);
} else {
- module.exports = factory(require('leaflet'));
+ module.exports = factory(require('../leaflet/leaflet.js'));
}
}
diff --git a/frappe/public/js/libs.bundle.js b/frappe/public/js/libs.bundle.js
index e4e172c1b4..77704bb173 100644
--- a/frappe/public/js/libs.bundle.js
+++ b/frappe/public/js/libs.bundle.js
@@ -1,5 +1,9 @@
import "./jquery-bootstrap";
import "./lib/moment";
+import "../js/lib/leaflet/leaflet.js";
+import "../js/lib/leaflet_easy_button/easy-button.js";
+import "../js/lib/leaflet_draw/leaflet.draw.js";
+import "../js/lib/leaflet_control_locate/L.Control.Locate.js";
import Sortable from "sortablejs";
window.SetVueGlobals = (app) => {
diff --git a/frappe/public/scss/desk.bundle.scss b/frappe/public/scss/desk.bundle.scss
index 10fd116d6c..6b192bb3ff 100644
--- a/frappe/public/scss/desk.bundle.scss
+++ b/frappe/public/scss/desk.bundle.scss
@@ -4,3 +4,8 @@
@import "~frappe-charts/dist/frappe-charts.min";
@import "~plyr/dist/plyr";
@import "./desk/index";
+
+@import "frappe/public/js/lib/leaflet/leaflet.css";
+@import "frappe/public/js/lib/leaflet_easy_button/easy-button.css";
+@import "frappe/public/js/lib/leaflet_control_locate/L.Control.Locate.css";
+@import "frappe/public/js/lib/leaflet_draw/leaflet.draw.css";
\ No newline at end of file
From d6edc1530ec8705b41e57318f3e8ed719542dc2b Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 22 May 2023 15:08:26 +0200
Subject: [PATCH 021/203] refactor: const instead of var, indentation
---
.../js/frappe/form/controls/geolocation.js | 63 ++++++++++---------
1 file changed, 33 insertions(+), 30 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 026457b856..c886864059 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -10,14 +10,14 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
set_disp_area(value) {
// Create the elements for map area
- if (!this.disp_area) return;
-
+ if (!this.disp_area) {
+ return;
+ }
+
this.map_id = frappe.dom.get_unique_id();
this.map_area = $(
``
);
@@ -46,7 +46,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.map.zoomControl.remove();
} else {
this.bind_leaflet_draw_control();
- this.bind_leaflet_event_listeners();
+ this.bind_leaflet_event_listeners();
this.bind_leaflet_locate_control();
this.bind_leaflet_data(value);
}
@@ -54,29 +54,30 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
bind_leaflet_data(value) {
/* render raw value from db into map */
- if (!this.map || !value) return;
+ if (!this.map || !value) {
+ return;
+ }
this.clear_editable_layers();
- var data_layers = new L.FeatureGroup()
- .addLayer(L.geoJson(JSON.parse(value),{
- pointToLayer: function(geoJsonPoint, latlng) {
- if (geoJsonPoint.properties.point_type == "circle"){
- return L.circle(latlng, {radius: geoJsonPoint.properties.radius});
+ const data_layers = new L.FeatureGroup().addLayer(
+ L.geoJson(JSON.parse(value), {
+ pointToLayer: function (geoJsonPoint, latlng) {
+ if (geoJsonPoint.properties.point_type == "circle") {
+ return L.circle(latlng, { radius: geoJsonPoint.properties.radius });
} else if (geoJsonPoint.properties.point_type == "circlemarker") {
- return L.circleMarker(latlng, {radius: geoJsonPoint.properties.radius});
- }
- else {
+ return L.circleMarker(latlng, { radius: geoJsonPoint.properties.radius });
+ } else {
return L.marker(latlng);
}
- }
- }));
+ },
+ })
+ );
this.add_non_group_layers(data_layers, this.editableLayers);
try {
this.map.fitBounds(this.editableLayers.getBounds(), {
- padding: [50,50]
+ padding: [50, 50],
});
- }
- catch(err) {
+ } catch (err) {
// suppress error if layer has a point.
}
this.editableLayers.addTo(this.map);
@@ -84,10 +85,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
}
bind_leaflet_map() {
- var circleToGeoJSON = L.Circle.prototype.toGeoJSON;
+ const circleToGeoJSON = L.Circle.prototype.toGeoJSON;
L.Circle.include({
toGeoJSON: function () {
- var feature = circleToGeoJSON.call(this);
+ const feature = circleToGeoJSON.call(this);
feature.properties = {
point_type: "circle",
radius: this.getRadius(),
@@ -98,7 +99,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
L.CircleMarker.include({
toGeoJSON: function () {
- var feature = circleToGeoJSON.call(this);
+ const feature = circleToGeoJSON.call(this);
feature.properties = {
point_type: "circlemarker",
radius: this.getRadius(),
@@ -114,8 +115,8 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
L.tileLayer(frappe.utils.map_defaults.tiles, frappe.utils.map_defaults.options).addTo(
this.map
);
-
- this.editableLayers = new L.FeatureGroup();
+
+ this.editableLayers = new L.FeatureGroup();
}
bind_leaflet_locate_control() {
@@ -125,7 +126,9 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
}
bind_leaflet_draw_control() {
- if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, 'write', this.doc)) return;
+ if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc)) {
+ return;
+ }
this.map.addControl(this.get_leaflet_controls());
}
@@ -159,13 +162,13 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
},
edit: {
featureGroup: this.editableLayers, //REQUIRED!!
- remove: true
- }
+ remove: true,
+ },
});
}
bind_leaflet_event_listeners() {
- this.map.on('draw:created', (e) => {
+ this.map.on("draw:created", (e) => {
var type = e.layerType,
layer = e.layer;
if (type === "marker") {
@@ -176,7 +179,7 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
});
this.map.on("draw:deleted draw:edited", (e) => {
- var layer = e.layer;
+ const { layer } = e;
this.editableLayers.removeLayer(layer);
this.set_value(JSON.stringify(this.editableLayers.toGeoJSON()));
});
From 50b15be1c26dee5fa65bde64863323ede2460051 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Mon, 22 May 2023 15:09:01 +0200
Subject: [PATCH 022/203] fix: handle read only property
---
frappe/public/js/frappe/form/controls/geolocation.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index c886864059..37d799e96d 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -126,7 +126,10 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
}
bind_leaflet_draw_control() {
- if (!frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc)) {
+ if (
+ !frappe.perm.has_perm(this.doctype, this.df.permlevel, "write", this.doc) ||
+ this.df.read_only
+ ) {
return;
}
From 253b9bb1f5a8e1fa1e6289dd9d7fa87f6bd90c82 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 22 May 2023 21:36:59 +0530
Subject: [PATCH 023/203] fix(UX): activate next step automatically
---
frappe/public/js/frappe/widgets/onboarding_widget.js | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index daacfb79c6..1994bc7e73 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -438,6 +438,7 @@ export default class OnboardingWidget extends Widget {
};
this.update_step_status(step, "is_complete", 1, callback);
+ this.activate_next_step(step);
}
skip_step(step) {
@@ -451,6 +452,16 @@ export default class OnboardingWidget extends Widget {
};
this.update_step_status(step, "is_skipped", 1, callback);
+ this.activate_next_step(step);
+ }
+
+ activate_next_step(step) {
+ let current_step_index = this.steps.findIndex((s) => s == step);
+ let next_step = this.steps[current_step_index + 1];
+
+ if (!next_step) return;
+
+ this.show_step(next_step);
}
update_step_status(step, status, value, callback) {
From baa7c9dd40b60a6e4e531d95040c02fc2b89cf7a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 22 May 2023 21:41:13 +0530
Subject: [PATCH 024/203] chore: track dismissed onboardings
---
frappe/public/js/frappe/widgets/onboarding_widget.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 1994bc7e73..3d7dbdf49f 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -563,6 +563,7 @@ export default class OnboardingWidget extends Widget {
localStorage.setItem("dismissed-onboarding", JSON.stringify(dismissed));
this.delete(true, true);
this.widget.closest(".ce-block").hide();
+ frappe.telemetry.capture("dismissed_" + frappe.scrub(this.title), "frappe_onboarding");
});
dismiss.appendTo(this.action_area);
}
From 98d56d2412c3b63c3688646593eebba922dada37 Mon Sep 17 00:00:00 2001
From: Christian Werner
Date: Mon, 22 May 2023 17:42:41 +0200
Subject: [PATCH 025/203] Update de.csv
Update Translation
'No' means 'Nein' in German and not 'nothing' 'Kein'
---
frappe/translations/de.csv | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv
index 1876b80720..d1f503be7c 100644
--- a/frappe/translations/de.csv
+++ b/frappe/translations/de.csv
@@ -3920,7 +3920,7 @@ Javascript,Javascript,
Ldap settings,LDAP Einstellungen,
Mobile number,Handynummer,
Mx,Mx,
-No,Kein,
+No,Nein,
Not found,Nicht gefunden,
Notes:,Anmerkungen:,
Notify by email,Per E-Mail benachrichtigen,
From 9fa2655bd995ffb75e9a244f7b4c6c4103b2287b Mon Sep 17 00:00:00 2001
From: Hussain Nagaria
Date: Mon, 22 May 2023 23:11:21 +0530
Subject: [PATCH 026/203] fix: bring back modified field in json file
---
frappe/integrations/doctype/webhook/webhook.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index d30a605f06..cfb2a2e01c 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -218,6 +218,7 @@
"link_fieldname": "webhook"
}
],
+ "modified": "2023-05-22 16:30:10.740512",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
From 5b881636bbd10a272d4b3d8b4068c346dcc3983f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 23 May 2023 10:40:28 +0530
Subject: [PATCH 027/203] chore: track route views for first few days
Identifying where users drop off is tricky without knowing what they are
doing in system. Tracking first few days routes will likely give some
insights.
---
frappe/public/js/telemetry/index.js | 12 ++++++++++++
frappe/utils/telemetry.py | 12 ++++++++++++
2 files changed, 24 insertions(+)
diff --git a/frappe/public/js/telemetry/index.js b/frappe/public/js/telemetry/index.js
index f7e2c9646a..48afaa5258 100644
--- a/frappe/public/js/telemetry/index.js
+++ b/frappe/public/js/telemetry/index.js
@@ -6,6 +6,7 @@ class TelemetryManager {
this.project_id = frappe.boot.posthog_project_id;
this.telemetry_host = frappe.boot.posthog_host;
+ this.site_age = frappe.boot.telemetry_site_age;
if (cint(frappe.boot.enable_telemetry) && this.project_id && this.telemetry_host) {
this.enabled = true;
@@ -24,6 +25,7 @@ class TelemetryManager {
});
posthog.identify(frappe.boot.sitename);
this.send_heartbeat();
+ this.register_pageview_handler();
} catch (e) {
console.trace("Failed to initialize telemetry", e);
this.enabled = false;
@@ -50,6 +52,16 @@ class TelemetryManager {
this.capture("heartbeat", "frappe");
}
}
+
+ register_pageview_handler() {
+ if (this.site_age && this.site_age > 5) {
+ return;
+ }
+
+ frappe.router.on("change", () => {
+ posthog.capture("$pageview");
+ });
+ }
}
frappe.telemetry = new TelemetryManager();
diff --git a/frappe/utils/telemetry.py b/frappe/utils/telemetry.py
index b5bc13dd57..ba06afbf83 100644
--- a/frappe/utils/telemetry.py
+++ b/frappe/utils/telemetry.py
@@ -8,6 +8,8 @@ from contextlib import suppress
from posthog import Posthog
import frappe
+from frappe.utils import getdate
+from frappe.utils.caching import site_cache
POSTHOG_PROJECT_FIELD = "posthog_project_id"
POSTHOG_HOST_FIELD = "posthog_host"
@@ -20,6 +22,16 @@ def add_bootinfo(bootinfo):
bootinfo.posthog_host = frappe.conf.get(POSTHOG_HOST_FIELD)
bootinfo.posthog_project_id = frappe.conf.get(POSTHOG_PROJECT_FIELD)
bootinfo.enable_telemetry = True
+ bootinfo.telemetry_site_age = site_age()
+
+
+@site_cache(ttl=60 * 60 * 12)
+def site_age():
+ try:
+ est_creation = frappe.db.get_value("User", "Administrator", "creation")
+ return (getdate() - getdate(est_creation)).days
+ except Exception:
+ pass
def init_telemetry():
From 05b1793fcd73cc89e3ef133cd256c067ad639803 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 12:42:12 +0530
Subject: [PATCH 028/203] fix: load onboarding_tours async
loads onboarding_tours if enable_onboarding is checked and form tours are available for user.
---
frappe/boot.py | 6 +-
frappe/hooks.py | 1 -
frappe/public/js/frappe/desk.js | 13 ++++
frappe/public/js/onboarding_tours.bundle.js | 1 +
.../onboarding_tours.js} | 60 +++++++++----------
5 files changed, 47 insertions(+), 34 deletions(-)
create mode 100644 frappe/public/js/onboarding_tours.bundle.js
rename frappe/public/js/{onboarding.bundle.js => onboarding_tours/onboarding_tours.js} (89%)
diff --git a/frappe/boot.py b/frappe/boot.py
index 7f4677c249..2e8cb56f33 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -68,8 +68,10 @@ def get_bootinfo():
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
bootinfo.notification_settings = get_notification_settings()
- bootinfo.onboarding_tours = frappe.parse_json(
- frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]"
+ bootinfo.onboarding_tours = (
+ frappe.parse_json(frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]")
+ if frappe.get_system_settings("enable_onboarding")
+ else "[]"
)
set_time_zone(bootinfo)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 5eb9426f2e..6d8c00d483 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -30,7 +30,6 @@ app_include_js = [
"controls.bundle.js",
"report.bundle.js",
"telemetry.bundle.js",
- "onboarding.bundle.js",
]
app_include_css = [
"desk.bundle.css",
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index a299e7c5ae..27bfe9ae87 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -74,6 +74,19 @@ frappe.Application = class Application {
// page container
this.make_page_container();
+ if (!window.Cypress) {
+ let pending_tours =
+ frappe.boot.onboarding_tours.findIndex((tour) => {
+ frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true;
+ }) == -1;
+ if (pending_tours && frappe.boot.onboarding_tours.length > 0) {
+ frappe.require("onboarding_tours.bundle.js", () => {
+ frappe.utils.sleep(1000).then(() => {
+ frappe.ui.init_onboarding_tour();
+ });
+ });
+ }
+ }
this.set_route();
// trigger app startup
diff --git a/frappe/public/js/onboarding_tours.bundle.js b/frappe/public/js/onboarding_tours.bundle.js
new file mode 100644
index 0000000000..90788cedfc
--- /dev/null
+++ b/frappe/public/js/onboarding_tours.bundle.js
@@ -0,0 +1 @@
+import "./onboarding_tours/onboarding_tours.js";
\ No newline at end of file
diff --git a/frappe/public/js/onboarding.bundle.js b/frappe/public/js/onboarding_tours/onboarding_tours.js
similarity index 89%
rename from frappe/public/js/onboarding.bundle.js
rename to frappe/public/js/onboarding_tours/onboarding_tours.js
index 28419700b6..16e457ee71 100644
--- a/frappe/public/js/onboarding.bundle.js
+++ b/frappe/public/js/onboarding_tours/onboarding_tours.js
@@ -31,24 +31,23 @@ frappe.ui.OnboardingTour = class OnboardingTour {
}
if (step.popover.closeBtnNode) {
step.popover.closeBtnNode.onclick = () => {
+ this.on_finish && this.on_finish();
+ !frappe.boot.user.onboarding_status[this.tour.name] &&
+ (frappe.boot.user.onboarding_status[this.tour.name] = {});
+ frappe.boot.user.onboarding_status[this.tour.name].is_complete = true;
if (!this.driver.hasNextStep()) {
- this.on_finish && this.on_finish();
- !frappe.boot.user.onboarding_status[this.tour.name] &&
- (frappe.boot.user.onboarding_status[this.tour.name] = {});
frappe.boot.user.onboarding_status[
this.tour.name
- ].is_complete = true;
- frappe.utils.debounce(
- () =>
- frappe.db.set_value(
- "User",
- frappe.boot.user.name,
- "onboarding_status",
- JSON.stringify(frappe.boot.user.onboarding_status)
- ),
- 1000
- )();
+ ].all_steps_completed = true;
}
+
+ frappe.call({
+ method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status",
+ args: {
+ value: JSON.stringify(frappe.boot.user.onboarding_status),
+ step: JSON.stringify(step.options.step_info),
+ },
+ });
};
}
clearInterval(wait_for_node);
@@ -87,16 +86,13 @@ frappe.ui.OnboardingTour = class OnboardingTour {
frappe.boot.user.onboarding_status[this.tour.name].is_complete = true;
}
this.last_step_saved = step;
- frappe.utils.debounce(
- () =>
- frappe.db.set_value(
- "User",
- frappe.boot.user.name,
- "onboarding_status",
- JSON.stringify(frappe.boot.user.onboarding_status)
- ),
- 1000
- )();
+ frappe.call({
+ method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status",
+ args: {
+ value: JSON.stringify(frappe.boot.user.onboarding_status),
+ step: JSON.stringify(step),
+ },
+ });
};
const driver_step = this.get_step(step, on_next);
driver_step.element && this.driver_steps.push(driver_step);
@@ -250,13 +246,9 @@ frappe.ui.OnboardingTour = class OnboardingTour {
}, 500);
}
};
-// As of now Tours are only for desktop as it is annoying on mobile.
-// Also lot of elements are hidden on mobile so until we find a better way to do it.
-if (window.matchMedia("(max-device-width: 992px)").matches) return;
-frappe.router.on("change", () => {
+frappe.ui.init_onboarding_tour = () => {
let route = frappe.router.current_route;
-
if (route[0] === "") return;
let tour_name;
@@ -293,7 +285,7 @@ frappe.router.on("change", () => {
});
if (matching_tours.length == 0) return;
let current_tour = matching_tours.find(
- (tour) => tour[0] == frappe.ui.currentTourInstance?.tour.name
+ (tour) => tour[0] == frappe.ui.currentTourInstance?.tour?.name
);
let next_tour = matching_tours.find((tour) => tour[0] == frappe.ui.next_form_tour);
if (current_tour) {
@@ -341,4 +333,10 @@ frappe.router.on("change", () => {
});
}
}, 100);
-});
+};
+// As of now Tours are only for desktop as it is annoying on mobile.
+// Also lot of elements are hidden on mobile so until we find a better way to do it.
+window.matchMedia("(min-device-width: 992px)").matches &&
+ frappe.router.on("change", () => {
+ frappe.ui.init_onboarding_tour();
+ });
From 028508c947dcc8dd7e4df85465acaabf6786256f Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 13:25:37 +0530
Subject: [PATCH 029/203] fix: remove from settings from core module.
---
.../doctype/form_tour_settings/__init__.py | 0
.../form_tour_settings/form_tour_settings.js | 8 ---
.../form_tour_settings.json | 51 ----------------
.../form_tour_settings/form_tour_settings.py | 15 -----
.../test_form_tour_settings.py | 9 ---
.../form_tour_settings_item/__init__.py | 0
.../form_tour_settings_item.json | 61 -------------------
.../form_tour_settings_item.py | 9 ---
8 files changed, 153 deletions(-)
delete mode 100644 frappe/core/doctype/form_tour_settings/__init__.py
delete mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.js
delete mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.json
delete mode 100644 frappe/core/doctype/form_tour_settings/form_tour_settings.py
delete mode 100644 frappe/core/doctype/form_tour_settings/test_form_tour_settings.py
delete mode 100644 frappe/core/doctype/form_tour_settings_item/__init__.py
delete mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json
delete mode 100644 frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py
diff --git a/frappe/core/doctype/form_tour_settings/__init__.py b/frappe/core/doctype/form_tour_settings/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.js b/frappe/core/doctype/form_tour_settings/form_tour_settings.js
deleted file mode 100644
index 123e51dbf9..0000000000
--- a/frappe/core/doctype/form_tour_settings/form_tour_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2023, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-// frappe.ui.form.on("Form Tour Settings", {
-// refresh(frm) {
-
-// },
-// });
diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.json b/frappe/core/doctype/form_tour_settings/form_tour_settings.json
deleted file mode 100644
index 15795edde5..0000000000
--- a/frappe/core/doctype/form_tour_settings/form_tour_settings.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "creation": "2023-05-11 18:07:26.879273",
- "default_view": "List",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "onboarding_tours",
- "form_tours"
- ],
- "fields": [
- {
- "fieldname": "form_tours",
- "fieldtype": "Table",
- "label": "Form Tours",
- "options": "Form Tour Settings Item"
- },
- {
- "default": "\"[]\"",
- "fieldname": "onboarding_tours",
- "fieldtype": "JSON",
- "hidden": 1,
- "label": "Onboarding Tours"
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2023-05-17 16:45:21.362524",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Form Tour Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": []
-}
\ No newline at end of file
diff --git a/frappe/core/doctype/form_tour_settings/form_tour_settings.py b/frappe/core/doctype/form_tour_settings/form_tour_settings.py
deleted file mode 100644
index 52b68286d7..0000000000
--- a/frappe/core/doctype/form_tour_settings/form_tour_settings.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# Copyright (c) 2023, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-import json
-
-import frappe
-from frappe.model.document import Document
-
-
-class FormTourSettings(Document):
- def on_update(self):
- onboarding_tours = [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours]
- frappe.db.set_single_value(
- "Form Tour Settings", "onboarding_tours", json.dumps(onboarding_tours)
- )
diff --git a/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py
deleted file mode 100644
index 95838ecb97..0000000000
--- a/frappe/core/doctype/form_tour_settings/test_form_tour_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2023, Frappe Technologies and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests.utils import FrappeTestCase
-
-
-class TestFormTourSettings(FrappeTestCase):
- pass
diff --git a/frappe/core/doctype/form_tour_settings_item/__init__.py b/frappe/core/doctype/form_tour_settings_item/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json
deleted file mode 100644
index 54ab61da21..0000000000
--- a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.json
+++ /dev/null
@@ -1,61 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "creation": "2023-05-11 18:10:15.194034",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "form_tour",
- "view",
- "list_view",
- "page_route"
- ],
- "fields": [
- {
- "fieldname": "form_tour",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Form Tour",
- "options": "Form Tour",
- "read_only": 1
- },
- {
- "fetch_from": "form_tour.view_name",
- "fieldname": "view",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "View",
- "read_only": 1
- },
- {
- "fetch_from": "form_tour.list_name",
- "fieldname": "list_view",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "List View",
- "read_only": 1
- },
- {
- "fetch_from": "form_tour.page_route",
- "fieldname": "page_route",
- "fieldtype": "Data",
- "hidden": 1,
- "in_list_view": 1,
- "label": "Page Route",
- "read_only": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2023-05-17 22:22:58.507769",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Form Tour Settings Item",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": []
-}
\ No newline at end of file
diff --git a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py
deleted file mode 100644
index 0958b000ad..0000000000
--- a/frappe/core/doctype/form_tour_settings_item/form_tour_settings_item.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2023, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class FormTourSettingsItem(Document):
- pass
From 4e74051732c98449fee63baf190a5a9c199378b3 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 15:18:15 +0530
Subject: [PATCH 030/203] fix: add form tour settings to desk module.
---
.../doctype/form_tour_settings/__init__.py | 0
.../form_tour_settings/form_tour_settings.js | 12 ++++
.../form_tour_settings.json | 51 ++++++++++++++++
.../form_tour_settings/form_tour_settings.py | 33 ++++++++++
.../test_form_tour_settings.py | 9 +++
.../form_tour_settings_item/__init__.py | 0
.../form_tour_settings_item.json | 61 +++++++++++++++++++
.../form_tour_settings_item.py | 9 +++
8 files changed, 175 insertions(+)
create mode 100644 frappe/desk/doctype/form_tour_settings/__init__.py
create mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.js
create mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.json
create mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.py
create mode 100644 frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py
create mode 100644 frappe/desk/doctype/form_tour_settings_item/__init__.py
create mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
create mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py
diff --git a/frappe/desk/doctype/form_tour_settings/__init__.py b/frappe/desk/doctype/form_tour_settings/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.js b/frappe/desk/doctype/form_tour_settings/form_tour_settings.js
new file mode 100644
index 0000000000..a9d7f62890
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.js
@@ -0,0 +1,12 @@
+// Copyright (c) 2023, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Form Tour Settings", {
+ refresh(frm) {
+ frm.dashboard.add_comment(
+ "This page is used to set priority for the UI form tours. If there are more than 1 matching tours found for the page, the tour with the highest priority will run.",
+ "blue",
+ true
+ );
+ },
+});
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
new file mode 100644
index 0000000000..15795edde5
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-05-11 18:07:26.879273",
+ "default_view": "List",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "onboarding_tours",
+ "form_tours"
+ ],
+ "fields": [
+ {
+ "fieldname": "form_tours",
+ "fieldtype": "Table",
+ "label": "Form Tours",
+ "options": "Form Tour Settings Item"
+ },
+ {
+ "default": "\"[]\"",
+ "fieldname": "onboarding_tours",
+ "fieldtype": "JSON",
+ "hidden": 1,
+ "label": "Onboarding Tours"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2023-05-17 16:45:21.362524",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Form Tour Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
new file mode 100644
index 0000000000..7d5e38fe37
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+import json
+
+import frappe
+from frappe.model.document import Document
+
+
+class FormTourSettings(Document):
+ def before_save(self):
+ self.onboarding_tours = json.dumps(
+ [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours]
+ )
+
+
+@frappe.whitelist()
+def update_user_status(value, step):
+ from frappe.utils.telemetry import capture
+
+ step = frappe.parse_json(step)
+ tour = frappe.parse_json(value)
+ # from frappe.utils.telemetry import capture
+ capture(
+ frappe.scrub(f"{step.parent}_{step.title}"),
+ app="frappe_ui_tours",
+ properties={
+ "is_completed": tour.is_completed
+ },
+ )
+ frappe.db.set_value(
+ "User", frappe.session.user, "onboarding_status", value, update_modified=False
+ )
diff --git a/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py
new file mode 100644
index 0000000000..95838ecb97
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and Contributors
+# See license.txt
+
+# import frappe
+from frappe.tests.utils import FrappeTestCase
+
+
+class TestFormTourSettings(FrappeTestCase):
+ pass
diff --git a/frappe/desk/doctype/form_tour_settings_item/__init__.py b/frappe/desk/doctype/form_tour_settings_item/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
new file mode 100644
index 0000000000..54ab61da21
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
@@ -0,0 +1,61 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-05-11 18:10:15.194034",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "form_tour",
+ "view",
+ "list_view",
+ "page_route"
+ ],
+ "fields": [
+ {
+ "fieldname": "form_tour",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Form Tour",
+ "options": "Form Tour",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "form_tour.view_name",
+ "fieldname": "view",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "View",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "form_tour.list_name",
+ "fieldname": "list_view",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "List View",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "form_tour.page_route",
+ "fieldname": "page_route",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Page Route",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-05-17 22:22:58.507769",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Form Tour Settings Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py
new file mode 100644
index 0000000000..0958b000ad
--- /dev/null
+++ b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class FormTourSettingsItem(Document):
+ pass
From 20d0e2809202e230c42276b6bccf62a21add3816 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 15:20:35 +0530
Subject: [PATCH 031/203] fix: remove onboarding_tours_section
---
frappe/core/doctype/user/user.json | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 35a58a5851..26940ac9d9 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -92,7 +92,6 @@
"generate_keys",
"column_break_65",
"api_secret",
- "onboarding_tours_section",
"onboarding_status",
"connections_tab"
],
@@ -694,12 +693,6 @@
"fieldtype": "Section Break",
"label": "Desk Settings"
},
- {
- "fieldname": "onboarding_tours_section",
- "fieldtype": "Section Break",
- "hidden": 1,
- "label": "Onboarding Tours"
- },
{
"default": "{}",
"fieldname": "onboarding_status",
@@ -768,7 +761,7 @@
"link_fieldname": "user"
}
],
- "modified": "2023-05-18 12:41:36.765029",
+ "modified": "2023-05-22 09:29:35.277539",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
From 2451d8d263eded576e06e2731eeee4c0bf92e1cc Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 15:22:27 +0530
Subject: [PATCH 032/203] fix: remove always required for element_selector.
---
frappe/desk/doctype/form_tour_step/form_tour_step.json | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
index f0cb8751b7..006bb53b7e 100644
--- a/frappe/desk/doctype/form_tour_step/form_tour_step.json
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -141,8 +141,7 @@
"fieldname": "element_selector",
"fieldtype": "Data",
"label": "Element Selector",
- "mandatory_depends_on": "eval:(doc.ui_tour)",
- "reqd": 1
+ "mandatory_depends_on": "eval:(doc.ui_tour)"
},
{
"depends_on": "eval:(doc.ui_tour)",
@@ -217,7 +216,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2023-05-18 01:55:44.245357",
+ "modified": "2023-05-19 16:35:14.424275",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Step",
From 4557a7b66469254f2b0e7adbf4fc0043ab4a3bab Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 15:23:58 +0530
Subject: [PATCH 033/203] fix: minor changes as per review.
---
frappe/desk/doctype/form_tour/form_tour.js | 160 ++++++++++---------
frappe/desk/doctype/form_tour/form_tour.json | 15 +-
frappe/desk/doctype/form_tour/form_tour.py | 31 ++--
3 files changed, 112 insertions(+), 94 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 8c14e72f56..34536de317 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -6,44 +6,23 @@ frappe.ui.form.on("Form Tour", {
if (frm.doc.is_standard && !frappe.boot.developer_mode) {
frm.trigger("disable_form");
}
- frm.fields_dict["report_name"].get_query = function (doc) {
- if (doc.reference_doctype) {
+ frm.set_query("reference_doctype", () => {
+ return { filters: { istable: 0 } };
+ });
+ frm.set_query("report_name", () => {
+ if (frm.doc.reference_doctype) {
return {
filters: {
- ref_doctype: doc.reference_doctype,
- },
+ ref_doctype: frm.doc.reference_doctype
+ }
};
}
return {};
- };
- frm.fields_dict["reference_doctype"].get_query = function (doc) {
- return {
- filters: {
- istable: 0,
- },
- };
- };
- !frm.doc.ui_tour &&
- frm.add_custom_button(__("Show Tour"), async () => {
- const issingle = await check_if_single(frm.doc.reference_doctype);
- let route_changed = null;
-
- if (issingle) {
- route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
- } else if (frm.doc.first_document) {
- const name = await get_first_document(frm.doc.reference_doctype);
- route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
- } else {
- route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
- }
- route_changed.then(() => {
- const tour_name = frm.doc.name;
- cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
- });
- });
+ });
+ !frm.is_new() && add_custom_button(frm);
},
async report_name(frm) {
- if (!frm.doc.report_name) return;
+ if (!frm.doc.ui_tour || !frm.doc.report_name) return;
let { message } = await frappe.db.get_value("Report", frm.doc.report_name, "ref_doctype");
frm.set_value("reference_doctype", message?.ref_doctype || "");
},
@@ -58,13 +37,13 @@ frappe.ui.form.on("Form Tour", {
"Referance Doctype and Dashboard Name both can't be used at the same time."
);
}
- frm.doc.page_route = JSON.stringify(await get_path(frm));
+ frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm)));
},
- disable_form: function (frm) {
+ disable_form: function(frm) {
frm.set_read_only();
frm.fields
- .filter((field) => field.has_input)
- .forEach((field) => {
+ .filter(field => field.has_input)
+ .forEach(field => {
frm.set_df_property(field.df.fieldname, "read_only", "1");
});
frm.disable_save();
@@ -73,8 +52,8 @@ frappe.ui.form.on("Form Tour", {
reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;
- frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, (df) => !df.hidden).then(
- (options) => {
+ frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, df => !df.hidden).then(
+ options => {
frm.fields_dict.steps.grid.update_docfield_property(
"fieldname",
"options",
@@ -86,34 +65,73 @@ frappe.ui.form.on("Form Tour", {
frm.set_fields_as_options(
"parent_fieldname",
frm.doc.reference_doctype,
- (df) => df.fieldtype == "Table" && !df.hidden
- ).then((options) => {
+ df => df.fieldtype == "Table" && !df.hidden
+ ).then(options => {
frm.fields_dict.steps.grid.update_docfield_property(
"parent_fieldname",
"options",
[""].concat(options)
);
});
- // remove report name if reference doctype is changed and report name is not valid.
- frappe.db
- .get_list(
- "Report",
- {
- filters: {
- ref_doctype: frm.doc.reference_doctype,
+ if (!frm.doc.ui_tour) {
+ // remove report name if reference doctype is changed and report name is not valid.
+ frappe.db
+ .get_list(
+ "Report",
+ {
+ filters: {
+ ref_doctype: frm.doc.reference_doctype
+ }
},
- },
- { fields: ["name"] }
- )
- .then((reports) => {
- if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) {
- frm.set_value("report_name", "");
- frm.refresh_field("report_name");
- }
- });
- },
+ { fields: ["name"] }
+ )
+ .then(reports => {
+ if (reports.findIndex(r => r.name == frm.doc.report_name) == -1) {
+ frm.set_value("report_name", "");
+ frm.refresh_field("report_name");
+ }
+ });
+ }
+ }
});
+add_custom_button = frm => {
+ if (frm.doc.ui_tour) {
+ frm.add_custom_button(__("Reset"), function() {
+ frappe.confirm(
+ __("This will reset this tour and show it to all users. Are you sure?"),
+ function() {
+ frappe.call({
+ method: "frappe.desk.doctype.form_tour.form_tour.reset_tour",
+ args: {
+ tour_name: frm.doc.name
+ }
+ });
+ },
+ delete frappe.boot.user.onboarding_status[frm.doc.name]
+ );
+ });
+ } else {
+ frm.add_custom_button(__("Show Tour"), async () => {
+ const issingle = await check_if_single(frm.doc.reference_doctype);
+ let route_changed = null;
+
+ if (issingle) {
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype);
+ } else if (frm.doc.first_document) {
+ const name = await get_first_document(frm.doc.reference_doctype);
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype, name);
+ } else {
+ route_changed = frappe.set_route("Form", frm.doc.reference_doctype, "new");
+ }
+ route_changed.then(() => {
+ const tour_name = frm.doc.name;
+ cur_frm.tour.init({ tour_name }).then(() => cur_frm.tour.start());
+ });
+ });
+ }
+};
+
frappe.ui.form.on("Form Tour Step", {
form_render(frm, cdt, cdn) {
if (locals[cdt][cdn].is_table_field) {
@@ -125,23 +143,21 @@ frappe.ui.form.on("Form Tour Step", {
const parent_fieldname_df = frappe
.get_meta(frm.doc.reference_doctype)
- .fields.find((df) => df.fieldname == child_row.parent_fieldname);
+ .fields.find(df => df.fieldname == child_row.parent_fieldname);
- frm.set_fields_as_options(
- "fieldname",
- parent_fieldname_df.options,
- (df) => !df.hidden
- ).then((options) => {
- frm.fields_dict.steps.grid.update_docfield_property(
- "fieldname",
- "options",
- [""].concat(options)
- );
- if (child_row.fieldname) {
- frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname);
+ frm.set_fields_as_options("fieldname", parent_fieldname_df.options, df => !df.hidden).then(
+ options => {
+ frm.fields_dict.steps.grid.update_docfield_property(
+ "fieldname",
+ "options",
+ [""].concat(options)
+ );
+ if (child_row.fieldname) {
+ frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname);
+ }
}
- });
- },
+ );
+ }
});
async function check_if_single(doctype) {
@@ -156,7 +172,7 @@ async function check_if_private_workspace(name) {
async function get_first_document(doctype) {
let docname;
- await frappe.db.get_list(doctype, { order_by: "creation" }).then((res) => {
+ await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
if (Array.isArray(res) && res.length) docname = res[0].name;
});
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index 9cc3f65b96..48a8a5cd2f 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -19,7 +19,6 @@
"column_break_6",
"ui_tour",
"track_steps",
- "reset_tours",
"is_standard",
"save_on_complete",
"first_document",
@@ -111,14 +110,6 @@
"hidden": 1,
"label": "Page Route"
},
- {
- "default": "0",
- "depends_on": "ui_tour",
- "description": "Please check this if you want to reset this tour and show it to all users.",
- "fieldname": "reset_tours",
- "fieldtype": "Check",
- "label": "Reset Tours"
- },
{
"depends_on": "eval:(doc.ui_tour && doc.view_name == \"List\" && doc.list_name == \"Dashboard\")",
"fetch_from": ".",
@@ -188,7 +179,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-05-18 02:47:03.528693",
+ "modified": "2023-05-23 11:35:14.195031",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
@@ -206,6 +197,10 @@
"role": "System Manager",
"share": 1,
"write": 1
+ },
+ {
+ "read": 1,
+ "role": "All"
}
],
"sort_field": "modified",
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 48b2bd1f2d..6b5b6885b0 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -14,7 +14,6 @@ class FormTour(Document):
if step.is_table_field and step.parent_fieldname:
parent_field_df = meta.get_field(step.parent_fieldname)
step.child_doctype = parent_field_df.options
-
field_df = frappe.get_meta(step.child_doctype).get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
@@ -22,19 +21,8 @@ class FormTour(Document):
field_df = meta.get_field(step.fieldname)
step.label = field_df.label
step.fieldtype = field_df.fieldtype
- elif self.reset_tours:
- self.reset_tours = 0
- for user in frappe.get_all("User"):
- user_doc = frappe.get_doc("User", user.name)
- onboarding_status = frappe.parse_json(user_doc.onboarding_status)
- if self.name in onboarding_status:
- del onboarding_status[self.name]
- user_doc.onboarding_status = frappe.as_json(onboarding_status)
- user_doc.save()
def on_update(self):
- if frappe.conf.developer_mode and self.is_standard:
- export_to_files([["Form Tour", self.name]], self.module)
if self.ui_tour:
form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
in_settings = False
@@ -59,3 +47,22 @@ class FormTour(Document):
child.save()
form_tour_settings.form_tours.insert(child_index, child)
form_tour_settings.save()
+ if frappe.conf.developer_mode and self.is_standard:
+ export_to_files([["Form Tour", self.name]], self.module)
+
+ def on_trash(self):
+ if self.ui_tour:
+ form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
+ for tour in form_tour_settings.form_tours:
+ if tour.form_tour == self.name:
+ form_tour_settings.remove(tour);
+ form_tour_settings.save()
+
+@frappe.whitelist()
+def reset_tour(tour_name):
+ for user in frappe.get_all("User"):
+ user_doc = frappe.get_doc("User", user.name)
+ onboarding_status = frappe.parse_json(user_doc.onboarding_status)
+ onboarding_status.pop(tour_name, None)
+ user_doc.onboarding_status = frappe.as_json(onboarding_status)
+ user_doc.save()
\ No newline at end of file
From 69705ec0868a75a5d76568c9d71207e278615daf Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Tue, 23 May 2023 15:36:59 +0530
Subject: [PATCH 034/203] fix: update module in json to Desk
---
frappe/desk/doctype/form_tour_settings/form_tour_settings.json | 2 +-
.../form_tour_settings_item/form_tour_settings_item.json | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
index 15795edde5..ff1e2df518 100644
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
@@ -30,7 +30,7 @@
"links": [],
"modified": "2023-05-17 16:45:21.362524",
"modified_by": "Administrator",
- "module": "Core",
+ "module": "Desk",
"name": "Form Tour Settings",
"owner": "Administrator",
"permissions": [
diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
index 54ab61da21..01f05393a3 100644
--- a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
+++ b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
@@ -51,7 +51,7 @@
"links": [],
"modified": "2023-05-17 22:22:58.507769",
"modified_by": "Administrator",
- "module": "Core",
+ "module": "Desk",
"name": "Form Tour Settings Item",
"owner": "Administrator",
"permissions": [],
From 119313810e64b044337e05ec31fdd772251283fa Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 23 May 2023 15:39:00 +0530
Subject: [PATCH 035/203] fix: misc onboarding fixes (#21078)
* fix: show fields without label too
* fix: make form tour step editable
* fix: hide duplicate save buttons
[skip ci]
---
frappe/desk/doctype/form_tour_step/form_tour_step.json | 3 ++-
frappe/public/js/frappe/form/form.js | 3 ++-
frappe/public/js/frappe/form/form_tour.js | 4 ++--
3 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/frappe/desk/doctype/form_tour_step/form_tour_step.json b/frappe/desk/doctype/form_tour_step/form_tour_step.json
index 7eb6eab223..d0c57ba1e6 100644
--- a/frappe/desk/doctype/form_tour_step/form_tour_step.json
+++ b/frappe/desk/doctype/form_tour_step/form_tour_step.json
@@ -2,6 +2,7 @@
"actions": [],
"creation": "2021-05-21 23:05:45.342114",
"doctype": "DocType",
+ "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"is_table_field",
@@ -115,7 +116,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2022-01-27 15:18:36.481801",
+ "modified": "2023-05-23 13:09:15.923043",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Step",
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 47917422b5..4622c58155 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -1994,7 +1994,8 @@ frappe.ui.form.Form = class FrappeForm {
return new Promise((resolve) => {
frappe.model.with_doctype(reference_doctype, () => {
frappe.get_meta(reference_doctype).fields.map((df) => {
- filter_function(df) && options.push({ label: df.label, value: df.fieldname });
+ filter_function(df) &&
+ options.push({ label: df.label || df.fieldname, value: df.fieldname });
});
options &&
this.set_df_property(
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index dbdd673aea..f7953c08b2 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -261,10 +261,10 @@ frappe.ui.form.FormTour = class FormTour {
allowClose: false,
overlayClickNext: false,
popover: {
- title: __("Save"),
+ title: __("Save the document."),
description: "",
position: "left",
- doneBtnText: __("Save"),
+ showButtons: false,
},
onNext: () => {
this.frm.save();
From 22e3ec8bf4b49c6bb4cef017652e54354bd4cc5d Mon Sep 17 00:00:00 2001
From: Aditya Hase
Date: Tue, 23 May 2023 15:47:13 +0530
Subject: [PATCH 036/203] fix(build): Propogate exit code from yarn build to
bench build (#21084)
When `yarn build` fails `bench build` ignores the error and exits with exit code 0
---
esbuild/esbuild.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 3c5c305665..4804f0e25f 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -87,7 +87,10 @@ const NODE_PATHS = [].concat(
execute()
.then(() => RUN_BUILD_COMMAND && run_build_command_for_apps(APPS))
- .catch((e) => console.error(e));
+ .catch((e) => {
+ console.error(e);
+ throw e;
+ });
if (WATCH_MODE) {
// listen for open files in editor event
From a182414610698effb018f2fc7f33a2956169b880 Mon Sep 17 00:00:00 2001
From: PeterG
Date: Wed, 24 May 2023 11:50:37 +0545
Subject: [PATCH 037/203] fix(workflow): populate doc from db in apply_workflow
(#21068)
---
frappe/model/workflow.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index 8338157996..d61d2b3a2b 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -102,6 +102,7 @@ def is_transition_condition_satisfied(transition, doc) -> bool:
def apply_workflow(doc, action):
"""Allow workflow action on the current doc"""
doc = frappe.get_doc(frappe.parse_json(doc))
+ doc.load_from_db()
workflow = get_workflow(doc.doctype)
transitions = get_transitions(doc, workflow)
user = frappe.session.user
From 9fec2fb499631e23d7278259619ea4a67bcf1a71 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Wed, 24 May 2023 11:42:35 +0530
Subject: [PATCH 038/203] fix: change onboarding_status type to long text
json type breaks unrelated tests and in mariadb json is alias for long text.
---
frappe/core/doctype/user/user.json | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 26940ac9d9..20e7f05fab 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -696,7 +696,7 @@
{
"default": "{}",
"fieldname": "onboarding_status",
- "fieldtype": "JSON",
+ "fieldtype": "Long Text",
"hidden": 1,
"label": "Onboarding Status"
}
@@ -761,7 +761,7 @@
"link_fieldname": "user"
}
],
- "modified": "2023-05-22 09:29:35.277539",
+ "modified": "2023-05-24 11:25:27.040415",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
From 19c4b396ae91869a6a62b06f67904fe22f43a4ec Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Wed, 24 May 2023 11:44:36 +0530
Subject: [PATCH 039/203] chore: fix linters
---
frappe/desk/doctype/form_tour/form_tour.js | 68 ++++++++++---------
.../form_tour_settings/form_tour_settings.py | 4 +-
frappe/public/js/frappe/desk.js | 2 +-
3 files changed, 37 insertions(+), 37 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 34536de317..b9010fff0c 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -13,8 +13,8 @@ frappe.ui.form.on("Form Tour", {
if (frm.doc.reference_doctype) {
return {
filters: {
- ref_doctype: frm.doc.reference_doctype
- }
+ ref_doctype: frm.doc.reference_doctype,
+ },
};
}
return {};
@@ -39,11 +39,11 @@ frappe.ui.form.on("Form Tour", {
}
frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm)));
},
- disable_form: function(frm) {
+ disable_form: function (frm) {
frm.set_read_only();
frm.fields
- .filter(field => field.has_input)
- .forEach(field => {
+ .filter((field) => field.has_input)
+ .forEach((field) => {
frm.set_df_property(field.df.fieldname, "read_only", "1");
});
frm.disable_save();
@@ -52,8 +52,8 @@ frappe.ui.form.on("Form Tour", {
reference_doctype(frm) {
if (!frm.doc.reference_doctype) return;
- frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, df => !df.hidden).then(
- options => {
+ frm.set_fields_as_options("fieldname", frm.doc.reference_doctype, (df) => !df.hidden).then(
+ (options) => {
frm.fields_dict.steps.grid.update_docfield_property(
"fieldname",
"options",
@@ -65,8 +65,8 @@ frappe.ui.form.on("Form Tour", {
frm.set_fields_as_options(
"parent_fieldname",
frm.doc.reference_doctype,
- df => df.fieldtype == "Table" && !df.hidden
- ).then(options => {
+ (df) => df.fieldtype == "Table" && !df.hidden
+ ).then((options) => {
frm.fields_dict.steps.grid.update_docfield_property(
"parent_fieldname",
"options",
@@ -80,32 +80,32 @@ frappe.ui.form.on("Form Tour", {
"Report",
{
filters: {
- ref_doctype: frm.doc.reference_doctype
- }
+ ref_doctype: frm.doc.reference_doctype,
+ },
},
{ fields: ["name"] }
)
- .then(reports => {
- if (reports.findIndex(r => r.name == frm.doc.report_name) == -1) {
+ .then((reports) => {
+ if (reports.findIndex((r) => r.name == frm.doc.report_name) == -1) {
frm.set_value("report_name", "");
frm.refresh_field("report_name");
}
});
}
- }
+ },
});
-add_custom_button = frm => {
+add_custom_button = (frm) => {
if (frm.doc.ui_tour) {
- frm.add_custom_button(__("Reset"), function() {
+ frm.add_custom_button(__("Reset"), function () {
frappe.confirm(
__("This will reset this tour and show it to all users. Are you sure?"),
- function() {
+ function () {
frappe.call({
method: "frappe.desk.doctype.form_tour.form_tour.reset_tour",
args: {
- tour_name: frm.doc.name
- }
+ tour_name: frm.doc.name,
+ },
});
},
delete frappe.boot.user.onboarding_status[frm.doc.name]
@@ -143,21 +143,23 @@ frappe.ui.form.on("Form Tour Step", {
const parent_fieldname_df = frappe
.get_meta(frm.doc.reference_doctype)
- .fields.find(df => df.fieldname == child_row.parent_fieldname);
+ .fields.find((df) => df.fieldname == child_row.parent_fieldname);
- frm.set_fields_as_options("fieldname", parent_fieldname_df.options, df => !df.hidden).then(
- options => {
- frm.fields_dict.steps.grid.update_docfield_property(
- "fieldname",
- "options",
- [""].concat(options)
- );
- if (child_row.fieldname) {
- frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname);
- }
+ frm.set_fields_as_options(
+ "fieldname",
+ parent_fieldname_df.options,
+ (df) => !df.hidden
+ ).then((options) => {
+ frm.fields_dict.steps.grid.update_docfield_property(
+ "fieldname",
+ "options",
+ [""].concat(options)
+ );
+ if (child_row.fieldname) {
+ frappe.model.set_value(cdt, cdn, "fieldname", child_row.fieldname);
}
- );
- }
+ });
+ },
});
async function check_if_single(doctype) {
@@ -172,7 +174,7 @@ async function check_if_private_workspace(name) {
async function get_first_document(doctype) {
let docname;
- await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
+ await frappe.db.get_list(doctype, { order_by: "creation" }).then((res) => {
if (Array.isArray(res) && res.length) docname = res[0].name;
});
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
index 7d5e38fe37..57de8d66ee 100644
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
@@ -24,9 +24,7 @@ def update_user_status(value, step):
capture(
frappe.scrub(f"{step.parent}_{step.title}"),
app="frappe_ui_tours",
- properties={
- "is_completed": tour.is_completed
- },
+ properties={"is_completed": tour.is_completed},
)
frappe.db.set_value(
"User", frappe.session.user, "onboarding_status", value, update_modified=False
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 27bfe9ae87..720f19c56e 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -82,7 +82,7 @@ frappe.Application = class Application {
if (pending_tours && frappe.boot.onboarding_tours.length > 0) {
frappe.require("onboarding_tours.bundle.js", () => {
frappe.utils.sleep(1000).then(() => {
- frappe.ui.init_onboarding_tour();
+ frappe.ui.init_onboarding_tour();
});
});
}
From 78dc9fc9acf9c873afe28fd5e06e5c4bf6f5e620 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 12:06:04 +0530
Subject: [PATCH 040/203] fix: fieldtype json -> text
---
frappe/desk/doctype/form_tour/form_tour.json | 4 ++--
.../desk/doctype/form_tour_settings/form_tour_settings.json | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index 48a8a5cd2f..1eeda5598e 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -106,7 +106,7 @@
{
"depends_on": "is_ui_tour",
"fieldname": "page_route",
- "fieldtype": "JSON",
+ "fieldtype": "Small Text",
"hidden": 1,
"label": "Page Route"
},
@@ -179,7 +179,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-05-23 11:35:14.195031",
+ "modified": "2023-05-24 12:05:25.223405",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
index ff1e2df518..755c347264 100644
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
@@ -20,7 +20,7 @@
{
"default": "\"[]\"",
"fieldname": "onboarding_tours",
- "fieldtype": "JSON",
+ "fieldtype": "Small Text",
"hidden": 1,
"label": "Onboarding Tours"
}
@@ -28,7 +28,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2023-05-17 16:45:21.362524",
+ "modified": "2023-05-24 12:05:52.676242",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour Settings",
From 8d63e2f18385d1059b9c87a90c3c3f7bcabde0cb Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 12:31:21 +0530
Subject: [PATCH 041/203] fix: clear boot cache on updating form tour
---
frappe/desk/doctype/form_tour/form_tour.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 6b5b6885b0..0e8bf05472 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -23,6 +23,7 @@ class FormTour(Document):
step.fieldtype = field_df.fieldtype
def on_update(self):
+ frappe.cache().delete_key("bootinfo")
if self.ui_tour:
form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
in_settings = False
@@ -55,9 +56,10 @@ class FormTour(Document):
form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
for tour in form_tour_settings.form_tours:
if tour.form_tour == self.name:
- form_tour_settings.remove(tour);
+ form_tour_settings.remove(tour)
form_tour_settings.save()
-
+
+
@frappe.whitelist()
def reset_tour(tour_name):
for user in frappe.get_all("User"):
@@ -65,4 +67,4 @@ def reset_tour(tour_name):
onboarding_status = frappe.parse_json(user_doc.onboarding_status)
onboarding_status.pop(tour_name, None)
user_doc.onboarding_status = frappe.as_json(onboarding_status)
- user_doc.save()
\ No newline at end of file
+ user_doc.save()
From 09ce5f7a94a08302fbaca136ced1c059fea14f8e Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 12:13:38 +0530
Subject: [PATCH 042/203] chore: fix main workspace tour copy
- fix copy
- make it run on any workspace
---
.../main_workspace_tour/main_workspace_tour.json | 15 +++++++--------
1 file changed, 7 insertions(+), 8 deletions(-)
diff --git a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
index 62888ebdac..afd0583cfb 100644
--- a/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
+++ b/frappe/desk/form_tour/main_workspace_tour/main_workspace_tour.json
@@ -8,21 +8,20 @@
"include_name_field": 0,
"is_standard": 1,
"list_name": "",
- "modified": "2023-05-18 12:21:54.389743",
+ "modified": "2023-05-24 12:43:43.741781",
"modified_by": "Administrator",
"module": "Desk",
"name": "Main Workspace Tour",
"new_document_form": 0,
"owner": "Administrator",
"page_name": "",
- "page_route": "[\"Workspaces\",\"Build\"]",
+ "page_route": "[\"Workspaces\",\"*\"]",
"reference_doctype": "",
"report_name": "",
- "reset_tours": 0,
"save_on_complete": 0,
"steps": [
{
- "description": "You can access different things like report, settings, documents (any doctypes), and modules. It saves you time by eliminating the need to navigate through menus. ",
+ "description": "This is Awesomebar, it helps you to navigate anywhere in the system, find documents, reports, settings, create new records and many more things.",
"element_selector": "#navbar-search",
"fieldtype": "0",
"has_next_condition": 0,
@@ -39,7 +38,7 @@
"ui_tour": 1
},
{
- "description": "Workspaces can be used to quickly access various modules and features. It organizes the available functionalities into logical groups. ",
+ "description": "These are workspaces. Each module workspace provides insightful information and shortcuts on one page. \n\n \n\nTip: You can build custom workspaces for your needs.",
"element_selector": ".col-lg-2.layout-side-section",
"fieldtype": "0",
"has_next_condition": 0,
@@ -56,7 +55,7 @@
},
{
"description": "Click to visit the Workspace ",
- "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Tools\"]",
+ "element_selector": ".desk-sidebar-item.standard-sidebar-item > [title=\"Users\"]",
"fieldtype": "0",
"has_next_condition": 0,
"hide_buttons": 1,
@@ -68,7 +67,7 @@
"offset_y": 0,
"popover_element": 0,
"position": "Right",
- "title": "Tools Workspace",
+ "title": "Users Workspace",
"ui_tour": 1
}
],
@@ -76,5 +75,5 @@
"track_steps": 1,
"ui_tour": 1,
"view_name": "Workspaces",
- "workspace_name": "Build"
+ "workspace_name": ""
}
\ No newline at end of file
From 657c2ba3b221635408f22aa492d4601e88ec6300 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 12:48:07 +0530
Subject: [PATCH 043/203] fix: clear cached onboarding status on update
---
frappe/desk/doctype/form_tour_settings/form_tour_settings.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
index 57de8d66ee..5c711f207b 100644
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
+++ b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
@@ -29,3 +29,4 @@ def update_user_status(value, step):
frappe.db.set_value(
"User", frappe.session.user, "onboarding_status", value, update_modified=False
)
+ frappe.cache().hdel("bootinfo", frappe.session.user)
From 4376ebb3dc45d0d17eae720e4a60619e19fcfa5d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 12:51:23 +0530
Subject: [PATCH 044/203] refactor: replace todo tour with user tour
adding more users is most likely to improve adoption. so better place to
show tours is user doctype?
---
.../user_list_tour/user_list_tour.json | 95 +++++++++++
.../todo_list_tour/todo_list_tour.json | 159 ------------------
.../tools_workspace_tour.json | 79 ---------
.../users_workspace_tour.json | 62 +++++++
4 files changed, 157 insertions(+), 238 deletions(-)
create mode 100644 frappe/core/form_tour/user_list_tour/user_list_tour.json
delete mode 100644 frappe/desk/form_tour/todo_list_tour/todo_list_tour.json
delete mode 100644 frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json
create mode 100644 frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json
diff --git a/frappe/core/form_tour/user_list_tour/user_list_tour.json b/frappe/core/form_tour/user_list_tour/user_list_tour.json
new file mode 100644
index 0000000000..83ae481d25
--- /dev/null
+++ b/frappe/core/form_tour/user_list_tour/user_list_tour.json
@@ -0,0 +1,95 @@
+{
+ "creation": "2023-05-24 12:53:02.844582",
+ "dashboard_name": "",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "list_name": "List",
+ "modified": "2023-05-24 13:21:29.552864",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "User List Tour",
+ "new_document_form": 0,
+ "owner": "Administrator",
+ "page_name": "",
+ "page_route": "[\"List\",\"User\",\"List\"]",
+ "reference_doctype": "User",
+ "report_name": "",
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "List view shows all the documents for a particular DocType. Here you can see all the current enabled users in the system. ",
+ "element_selector": ".frappe-list",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Top Center",
+ "title": "Users List",
+ "ui_tour": 1
+ },
+ {
+ "description": "These are filters. You can use them to narrow down list of records.",
+ "element_selector": ".standard-filter-section.flex",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 1,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Bottom",
+ "title": "Filters",
+ "ui_tour": 1
+ },
+ {
+ "description": "When standard filters are not enough you can use advance filters. ",
+ "element_selector": ".filter-selector > .btn-group",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "ondemand_description": "Advance filters are applied on fields with different operators. \n \nClick on \"Apply Filters\" to continue.",
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Advanced Filters",
+ "ui_tour": 1
+ },
+ {
+ "description": "Let's create a new user.",
+ "element_selector": ".btn-primary.primary-action",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 1,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 1,
+ "offset_x": 0,
+ "offset_y": 0,
+ "parent_element_selector": "",
+ "popover_element": 0,
+ "position": "Bottom",
+ "title": "New User",
+ "ui_tour": 1
+ }
+ ],
+ "title": "User List Tour",
+ "track_steps": 1,
+ "ui_tour": 1,
+ "view_name": "List",
+ "workspace_name": ""
+}
\ No newline at end of file
diff --git a/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json b/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json
deleted file mode 100644
index 6a561e6f51..0000000000
--- a/frappe/desk/form_tour/todo_list_tour/todo_list_tour.json
+++ /dev/null
@@ -1,159 +0,0 @@
-{
- "creation": "2023-05-18 12:12:01.839494",
- "dashboard_name": "",
- "docstatus": 0,
- "doctype": "Form Tour",
- "first_document": 0,
- "idx": 0,
- "include_name_field": 0,
- "is_standard": 1,
- "list_name": "List",
- "modified": "2023-05-18 12:22:07.306556",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Todo List Tour",
- "new_document_form": 0,
- "owner": "Administrator",
- "page_name": "",
- "page_route": "[\"List\",\"ToDo\",\"List\"]",
- "reference_doctype": "ToDo",
- "report_name": "",
- "reset_tours": 0,
- "save_on_complete": 0,
- "steps": [
- {
- "description": "List View ",
- "element_selector": ".layout-main-section.frappe-card",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Left",
- "title": "TODO",
- "ui_tour": 1
- },
- {
- "description": "List View as the name suggest is used to see documents/records in list format. ",
- "element_selector": ".frappe-list",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Top Center",
- "title": "TODO List",
- "ui_tour": 1
- },
- {
- "description": "Using Quick filter you can refine and narrow down the displayed data by applying specific criteria or conditions ",
- "element_selector": ".list-sidebar.overlay-sidebar.hidden-xs.hidden-sm",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Right",
- "title": "Sidebar",
- "ui_tour": 1
- },
- {
- "description": "You can also filter using this inputs ",
- "element_selector": ".standard-filter-section.flex",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 1,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Bottom",
- "title": "Input Filters",
- "ui_tour": 1
- },
- {
- "description": "Click on the Filter button ",
- "element_selector": ".filter-selector > .btn-group",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 1,
- "offset_x": 0,
- "offset_y": 0,
- "ondemand_description": "Aou can add multiple filters and hit apply to refine results ",
- "popover_element": 1,
- "position": "Left",
- "title": "Advanced Filters",
- "ui_tour": 1
- },
- {
- "description": "Click here to remove all filter ",
- "element_selector": ".filter-selector > .btn-group > .filter-x-button",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 1,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Left",
- "title": "Clear Filters",
- "ui_tour": 1
- },
- {
- "description": "You can arrange data in ascending or descending order based on selected attributes.\n\n \n Click on Last Updated On \n \n Select the Attribute based on which you want to sort \n \n ",
- "element_selector": ".sort-selector",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": -20,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Left",
- "title": "Sort By",
- "ui_tour": 1
- },
- {
- "description": "Click to change ascending or descending order.\n ",
- "element_selector": ".sort-selector > .btn-group > .btn-order",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": -20,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Left",
- "title": "Sort By",
- "ui_tour": 1
- }
- ],
- "title": "Todo List Tour",
- "track_steps": 1,
- "ui_tour": 1,
- "view_name": "List",
- "workspace_name": ""
-}
\ No newline at end of file
diff --git a/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json b/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json
deleted file mode 100644
index 6f10c69328..0000000000
--- a/frappe/desk/form_tour/tools_workspace_tour/tools_workspace_tour.json
+++ /dev/null
@@ -1,79 +0,0 @@
-{
- "creation": "2023-05-18 12:09:40.792239",
- "dashboard_name": "",
- "docstatus": 0,
- "doctype": "Form Tour",
- "first_document": 0,
- "idx": 0,
- "include_name_field": 0,
- "is_standard": 1,
- "list_name": "",
- "modified": "2023-05-18 12:22:01.208707",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Tools Workspace Tour",
- "new_document_form": 0,
- "owner": "Administrator",
- "page_name": "",
- "page_route": "[\"Workspaces\",\"Tools\"]",
- "reference_doctype": "",
- "report_name": "",
- "reset_tours": 0,
- "save_on_complete": 0,
- "steps": [
- {
- "description": "This is Tools Workspace ",
- "element_selector": ".codex-editor",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Left",
- "title": "Workspace",
- "ui_tour": 1
- },
- {
- "description": "Workspace have cards that serve as links to different modules and features. For instance, the Email List card provides easy access to related components like Newsletter and Email Group ",
- "element_selector": "[card_name=\"Email\"]",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 0,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_on_click": 0,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Right",
- "title": "Email Card",
- "ui_tour": 1
- },
- {
- "description": "Shortcuts are a set of clickable links that serve as direct links to frequently accessed modules and features ",
- "element_selector": "[shortcut_name=\"ToDo\"]",
- "fieldtype": "0",
- "has_next_condition": 0,
- "hide_buttons": 1,
- "is_table_field": 0,
- "modal_trigger": 0,
- "next_form_tour": "Todo List Tour",
- "next_on_click": 0,
- "offset_x": 0,
- "offset_y": 0,
- "popover_element": 0,
- "position": "Right",
- "title": "Todo Shortcut",
- "ui_tour": 1
- }
- ],
- "title": "Tools Workspace Tour",
- "track_steps": 1,
- "ui_tour": 1,
- "view_name": "Workspaces",
- "workspace_name": "Tools"
-}
\ No newline at end of file
diff --git a/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json b/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json
new file mode 100644
index 0000000000..97159ba6e3
--- /dev/null
+++ b/frappe/desk/form_tour/users_workspace_tour/users_workspace_tour.json
@@ -0,0 +1,62 @@
+{
+ "creation": "2023-05-24 12:50:23.740052",
+ "dashboard_name": "",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "first_document": 0,
+ "idx": 0,
+ "include_name_field": 0,
+ "is_standard": 1,
+ "list_name": "",
+ "modified": "2023-05-24 13:01:56.539128",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Users Workspace Tour",
+ "new_document_form": 0,
+ "owner": "Administrator",
+ "page_name": "",
+ "page_route": "[\"Workspaces\",\"Users\"]",
+ "reference_doctype": "",
+ "report_name": "",
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "This is Users Workspace. You'll find all shortcuts for user, roles and permission management here.",
+ "element_selector": ".codex-editor",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 0,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Left",
+ "title": "Workspace",
+ "ui_tour": 1
+ },
+ {
+ "description": "This is a shortcut to User DocType. \n \n\nLet's Click on the User shortcut to explore all users in System.",
+ "element_selector": "[shortcut_name=\"User\"]",
+ "fieldtype": "0",
+ "has_next_condition": 0,
+ "hide_buttons": 1,
+ "is_table_field": 0,
+ "modal_trigger": 0,
+ "next_form_tour": "User List Tour",
+ "next_on_click": 0,
+ "offset_x": 0,
+ "offset_y": 0,
+ "popover_element": 0,
+ "position": "Right",
+ "title": "Users Shortcut",
+ "ui_tour": 1
+ }
+ ],
+ "title": "Users Workspace Tour",
+ "track_steps": 1,
+ "ui_tour": 1,
+ "view_name": "Workspaces",
+ "workspace_name": "Users"
+}
\ No newline at end of file
From a39d478e2c819d10cf8f21d5c6efadb0440f9395 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 13:27:53 +0530
Subject: [PATCH 045/203] fix: email notification
---
frappe/core/doctype/user/user.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 14266e4cd8..8e00aa7f0f 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -282,6 +282,10 @@ class User(Document):
self.email_new_password(new_password)
except frappe.OutgoingEmailError:
+ frappe.clear_last_message()
+ frappe.msgprint(
+ _("Please setup default outgoing Email Account from Settings > Email Account"), alert=True
+ )
# email server not set, don't send email
self.log_error("Unable to send new password notification")
From 49fe6e0c985dccc30d08d5677683eb27b6f39682 Mon Sep 17 00:00:00 2001
From: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Date: Wed, 24 May 2023 14:47:06 +0530
Subject: [PATCH 046/203] feat: patches.txt template added by default for new
apps #21046 (#21070)
* Added patches.txt template in boilerplate
* test: new app patches.txt
* style: formatting
---------
Co-authored-by: Gursheen Anand
Co-authored-by: Ankush Menat
---
frappe/modules/patch_handler.py | 51 +++++++++++++++++---------------
frappe/tests/test_boilerplate.py | 7 ++++-
frappe/utils/boilerplate.py | 10 ++++++-
3 files changed, 42 insertions(+), 26 deletions(-)
diff --git a/frappe/modules/patch_handler.py b/frappe/modules/patch_handler.py
index 8b25ffcb8e..bf0bd3d869 100644
--- a/frappe/modules/patch_handler.py
+++ b/frappe/modules/patch_handler.py
@@ -101,40 +101,43 @@ def get_patches_from_app(app: str, patch_type: PatchType | None = None) -> list[
2. plain text file with each line representing a patch.
"""
- patches_txt = frappe.get_pymodule_path(app, "patches.txt")
+ patches_file = frappe.get_pymodule_path(app, "patches.txt")
try:
- # Attempt to parse as ini file with pre/post patches
- # allow_no_value: patches are not key value pairs
- # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter
- parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n")
- # preserve case
- parser.optionxform = str
- parser.read(patches_txt)
-
- # empty file
- if not parser.sections():
- return []
-
- if not patch_type:
- return [patch for patch in parser[PatchType.pre_model_sync.value]] + [
- patch for patch in parser[PatchType.post_model_sync.value]
- ]
-
- if patch_type.value in parser.sections():
- return [patch for patch in parser[patch_type.value]]
- else:
- frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type))
-
+ return parse_as_configfile(patches_file, patch_type)
except configparser.MissingSectionHeaderError:
# treat as old format with each line representing a single patch
# backward compatbility with old patches.txt format
if not patch_type or patch_type == PatchType.pre_model_sync:
- return frappe.get_file_items(patches_txt)
+ return frappe.get_file_items(patches_file)
return []
+def parse_as_configfile(patches_file: str, patch_type: PatchType | None = None) -> list[str]:
+ # Attempt to parse as ini file with pre/post patches
+ # allow_no_value: patches are not key value pairs
+ # delimiters = '\n' to avoid treating default `:` and `=` in execute as k:v delimiter
+ parser = configparser.ConfigParser(allow_no_value=True, delimiters="\n")
+ # preserve case
+ parser.optionxform = str
+ parser.read(patches_file)
+
+ # empty file
+ if not parser.sections():
+ return []
+
+ if not patch_type:
+ return [patch for patch in parser[PatchType.pre_model_sync.value]] + [
+ patch for patch in parser[PatchType.post_model_sync.value]
+ ]
+
+ if patch_type.value in parser.sections():
+ return [patch for patch in parser[patch_type.value]]
+ else:
+ frappe.throw(frappe._("Patch type {} not found in patches.txt").format(patch_type))
+
+
def reload_doc(args):
import frappe.modules
diff --git a/frappe/tests/test_boilerplate.py b/frappe/tests/test_boilerplate.py
index 0f58e84df4..717fdc7ab8 100644
--- a/frappe/tests/test_boilerplate.py
+++ b/frappe/tests/test_boilerplate.py
@@ -12,7 +12,7 @@ import git
import yaml
import frappe
-from frappe.modules.patch_handler import get_all_patches
+from frappe.modules.patch_handler import get_all_patches, parse_as_configfile
from frappe.utils.boilerplate import (
PatchCreator,
_create_app_boilerplate,
@@ -138,6 +138,11 @@ class TestBoilerPlate(unittest.TestCase):
app_repo = git.Repo(new_app_dir)
self.assertEqual(app_repo.active_branch.name, "develop")
+ patches_file = os.path.join(new_app_dir, app_name, "patches.txt")
+ self.assertTrue(os.path.exists(patches_file), msg=f"{patches_file} not found")
+
+ self.assertEqual(parse_as_configfile(patches_file), [])
+
def test_create_app_without_git_init(self):
app_name = "test_app_no_git"
diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py
index 2e8a5088ed..0d786972fb 100644
--- a/frappe/utils/boilerplate.py
+++ b/frappe/utils/boilerplate.py
@@ -138,7 +138,8 @@ def _create_app_boilerplate(dest, hooks, no_git=False):
with open(os.path.join(dest, hooks.app_name, hooks.app_name, "hooks.py"), "w") as f:
f.write(frappe.as_unicode(hooks_template.format(**hooks)))
- 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, "patches.txt"), "w") as f:
+ f.write(frappe.as_unicode(patches_template.format(**hooks)))
app_directory = os.path.join(dest, hooks.app_name)
@@ -631,3 +632,10 @@ jobs:
env:
TYPE: server
"""
+
+patches_template = """[pre_model_sync]
+# Patches added in this section will be executed before doctypes are migrated
+# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
+
+[post_model_sync]
+# Patches added in this section will be executed after doctypes are migrated"""
From 6065179080e69a1fe82b7fce4390ee7bba7979f5 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Wed, 24 May 2023 15:05:50 +0530
Subject: [PATCH 047/203] chore: bump `requests`, `chardet`, and `dropbox` to
latest versions (#21080)
* chore: bump `requests` and `chardet`
* chore: bump `dropbox` to `11.36.0`, ignore `GHSA-4xqq-73wg-5mjp` during pip-audit
---
.github/workflows/linters.yml | 2 +-
pyproject.toml | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index be343c1254..c563f9e43f 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -97,4 +97,4 @@ jobs:
pip install pip-audit
cd ${GITHUB_WORKSPACE}
sed -i '/dropbox/d' pyproject.toml # Remove dropbox temporarily https://github.com/dropbox/dropbox-sdk-python/pull/456
- pip-audit --desc on .
+ pip-audit --desc on --ignore-vuln GHSA-4xqq-73wg-5mjp .
diff --git a/pyproject.toml b/pyproject.toml
index f2688e97ed..aa89eed928 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,7 +30,7 @@ dependencies = [
"bleach-allowlist~=1.0.3",
"bleach~=3.3.0",
"cairocffi==1.2.0",
- "chardet~=4.0.0",
+ "chardet~=5.1.0",
"croniter~=1.3.5",
"cryptography~=39.0.1",
"email-reply-parser~=0.5.12",
@@ -61,7 +61,7 @@ dependencies = [
"redis~=4.5.4",
"hiredis~=2.0.0",
"requests-oauthlib~=1.3.0",
- "requests~=2.27.1",
+ "requests~=2.31.0",
"rq~=1.11.1",
"rsa>=4.1",
"semantic-version~=2.10.0",
@@ -75,7 +75,7 @@ dependencies = [
# integration dependencies
"boto3~=1.18.49",
- "dropbox~=11.7.0",
+ "dropbox~=11.36.0",
"google-api-python-client~=2.2.0",
"google-auth-oauthlib~=0.4.4",
"google-auth~=1.29.0",
From d155c3f843c798ce760a916457a794456e95a07f Mon Sep 17 00:00:00 2001
From: Marica
Date: Wed, 24 May 2023 15:07:02 +0530
Subject: [PATCH 048/203] fix: Exclude Geolocation from "hide empty read-only
field" (#21088)
---
frappe/public/js/frappe/form/controls/base_control.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js
index 2188f29e94..fe665cee06 100644
--- a/frappe/public/js/frappe/form/controls/base_control.js
+++ b/frappe/public/js/frappe/form/controls/base_control.js
@@ -125,7 +125,7 @@ frappe.ui.form.Control = class BaseControl {
status === "Read" &&
!this.only_input &&
is_null(value) &&
- !in_list(["HTML", "Image", "Button"], this.df.fieldtype)
+ !in_list(["HTML", "Image", "Button", "Geolocation"], this.df.fieldtype)
) {
// eslint-disable-next-line
if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console
From 5290bbb1c647b6b4c5f1ea2b3334cb2a0d83963c Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Wed, 24 May 2023 15:40:48 +0530
Subject: [PATCH 049/203] fix: reload form_tour_settings
reload form_tour_settings and form_tour_settings_item before form_tour because form_tour on_update have get_doc for form_tour_settings.
---
frappe/utils/install.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frappe/utils/install.py b/frappe/utils/install.py
index df918c27e0..50feab0475 100644
--- a/frappe/utils/install.py
+++ b/frappe/utils/install.py
@@ -14,6 +14,8 @@ def before_install():
frappe.reload_doc("core", "doctype", "doctype_action")
frappe.reload_doc("core", "doctype", "doctype_link")
frappe.reload_doc("desk", "doctype", "form_tour_step")
+ frappe.reload_doc("desk", "doctype", "form_tour_settings")
+ frappe.reload_doc("desk", "doctype", "form_tour_settings_item")
frappe.reload_doc("desk", "doctype", "form_tour")
frappe.reload_doc("core", "doctype", "doctype")
From f612d84247a6d8298900f72ec2af20e5291669c6 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Wed, 24 May 2023 15:42:16 +0530
Subject: [PATCH 050/203] fix: change onboarding_status fieldtype
change onboarding_status to small text and handle null cases.
---
frappe/boot.py | 2 +-
frappe/core/doctype/user/user.json | 4 ++--
frappe/public/js/frappe/desk.js | 6 +++++-
frappe/public/js/onboarding_tours/onboarding_tours.js | 5 ++++-
4 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/frappe/boot.py b/frappe/boot.py
index 2e8cb56f33..a54e6e2f07 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -71,7 +71,7 @@ def get_bootinfo():
bootinfo.onboarding_tours = (
frappe.parse_json(frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]")
if frappe.get_system_settings("enable_onboarding")
- else "[]"
+ else []
)
set_time_zone(bootinfo)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 20e7f05fab..654f20936e 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -696,7 +696,7 @@
{
"default": "{}",
"fieldname": "onboarding_status",
- "fieldtype": "Long Text",
+ "fieldtype": "Small Text",
"hidden": 1,
"label": "Onboarding Status"
}
@@ -761,7 +761,7 @@
"link_fieldname": "user"
}
],
- "modified": "2023-05-24 11:25:27.040415",
+ "modified": "2023-05-24 15:20:06.434506",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 720f19c56e..f92bb3a1bf 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -74,7 +74,11 @@ frappe.Application = class Application {
// page container
this.make_page_container();
- if (!window.Cypress) {
+ if (
+ !window.Cypress &&
+ frappe.boot.onboarding_tours &&
+ frappe.boot.user.onboarding_status != null
+ ) {
let pending_tours =
frappe.boot.onboarding_tours.findIndex((tour) => {
frappe.boot.user.onboarding_status[tour[0]]?.is_complete == true;
diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js
index 16e457ee71..63ecd6d670 100644
--- a/frappe/public/js/onboarding_tours/onboarding_tours.js
+++ b/frappe/public/js/onboarding_tours/onboarding_tours.js
@@ -248,6 +248,9 @@ frappe.ui.OnboardingTour = class OnboardingTour {
};
frappe.ui.init_onboarding_tour = () => {
+ typeof frappe.boot.onboarding_tours == "undefined" && frappe.boot.onboarding_tours == [];
+ typeof frappe.boot.user.onboarding_status == "undefined" &&
+ frappe.boot.user.onboarding_status == {};
let route = frappe.router.current_route;
if (route[0] === "") return;
@@ -313,7 +316,7 @@ frappe.ui.init_onboarding_tour = () => {
}
}
if (!tour_name) return;
- if (frappe.ui.currentTourInstance) {
+ if (frappe.ui.currentTourInstance?.driver) {
frappe.ui.currentTourInstance.driver_steps = [];
frappe.ui.currentTourInstance.driver.reset(true);
frappe.ui.currentTourInstance.update_driver_steps();
From 6354a018de798c49c69de28c3493b04fa43bf96c Mon Sep 17 00:00:00 2001
From: Richard Case <110036763+casesolved-co-uk@users.noreply.github.com>
Date: Wed, 24 May 2023 11:30:32 +0100
Subject: [PATCH 051/203] feat: wkhtmltopdf logging (#19935)
* feat: wkhtmltopdf logging
* fix: must supply output function
---
frappe/utils/logger.py | 28 ++++++++++++++++++++++++++++
frappe/utils/pdf.py | 12 ++++++++++--
2 files changed, 38 insertions(+), 2 deletions(-)
diff --git a/frappe/utils/logger.py b/frappe/utils/logger.py
index ddb81f3d79..8976130a7c 100755
--- a/frappe/utils/logger.py
+++ b/frappe/utils/logger.py
@@ -1,6 +1,8 @@
# imports - standard imports
import logging
import os
+import sys
+from contextlib import contextmanager
from copy import deepcopy
from logging.handlers import RotatingFileHandler
from typing import Literal
@@ -123,3 +125,29 @@ def sanitized_dict(form_dict):
if secret_kw in k:
sanitized_dict[k] = "********"
return sanitized_dict
+
+
+@contextmanager
+def pipe_to_log(logger_fn, stream=None):
+ "Pass an existing logger function e.g. logger.info. Stream defaults to stdout"
+ # late bind source
+ if stream is None:
+ stream = sys.stdout
+
+ stream_int = stream.fileno()
+ r_int, w_int = os.pipe()
+
+ # copy stream_fd before it is overwritten
+ with os.fdopen(os.dup(stream_int), "wb") as copied:
+ stream.flush()
+ os.dup2(w_int, stream_int) # $ exec >&pipe
+ try:
+ with os.fdopen(w_int, "wb"):
+ yield stream
+ finally:
+ # restore stream to its previous value
+ stream.flush()
+ os.dup2(copied.fileno(), stream_int) # $ exec >&copied
+ with os.fdopen(r_int, newline="") as r:
+ text = r.read()
+ logger_fn(text)
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index 0c273854f7..a0ba4a6de1 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -14,6 +14,7 @@ import frappe
from frappe import _
from frappe.utils import scrub_urls
from frappe.utils.jinja_globals import bundled_asset, is_rtl
+from frappe.utils.logger import pipe_to_log
PDF_CONTENT_ERRORS = [
"ContentNotFoundError",
@@ -22,6 +23,9 @@ PDF_CONTENT_ERRORS = [
"RemoteHostClosedError",
]
+logger = frappe.logger("wkhtmltopdf", max_size=100000, file_count=3)
+logger.setLevel("INFO")
+
def pdf_header_html(soup, head, content, styles, html_id, css):
return frappe.render_template(
@@ -59,8 +63,13 @@ def get_pdf(html, options=None, output: PdfWriter | None = None):
options.update({"disable-smart-shrinking": ""})
try:
+ # wkhtmltopdf writes the pdf to stdout and errors to stderr
+ # pdfkit v1.0.0 writes the pdf to file or returns it
+ # stderr is written to sys.stdout if verbose=True is supplied
# Set filename property to false, so no file is actually created
- filedata = pdfkit.from_string(html, options=options or {}, verbose=True)
+ # defaults to redirecting stdout
+ with pipe_to_log(logger.info):
+ filedata = pdfkit.from_string(html, False, options=options or {}, verbose=True)
# create in-memory binary streams from filedata and create a PdfReader object
reader = PdfReader(io.BytesIO(filedata))
@@ -118,7 +127,6 @@ def prepare_options(html, options):
"print-media-type": None,
"background": None,
"images": None,
- "quiet": None,
# 'no-outline': None,
"encoding": "UTF-8",
# 'load-error-handling': 'ignore'
From 4ab98b998defa244419bf826e5a183799bb01b36 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 17:43:51 +0530
Subject: [PATCH 052/203] fix: setup wizard recursion in routing (#21101)
- setup wizard shows slide from index in route
- if you visit `/app/setup-wizard` then index in route is `undefined`
- because of hacky code we do `id + ''` so `undefined` becomes `"undefined"` a truthy value
The recursion of undefined > 0 > undefined > 0 continues until browser
intervenes and stops further routing.
---
frappe/desk/page/setup_wizard/setup_wizard.js | 11 +++--------
1 file changed, 3 insertions(+), 8 deletions(-)
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index 862ac8c14d..7d68fd683c 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -49,19 +49,14 @@ frappe.pages["setup-wizard"].on_page_load = function (wrapper) {
};
frappe.wizard = new frappe.setup.SetupWizard(wizard_settings);
frappe.setup.run_event("after_load");
- let route = frappe.get_route();
- if (route) {
- frappe.wizard.show_slide(route[1]);
- }
+ frappe.wizard.show_slide(cint(frappe.get_route()[1]));
},
});
});
};
frappe.pages["setup-wizard"].on_page_show = function () {
- if (frappe.get_route()[1]) {
- frappe.wizard && frappe.wizard.show_slide(frappe.get_route()[1]);
- }
+ frappe.wizard && frappe.wizard.show_slide(cint(frappe.get_route()[1]));
};
frappe.setup.on("before_load", function () {
@@ -125,7 +120,7 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
return;
}
super.show_slide(id);
- frappe.set_route(this.page_name, id + "");
+ frappe.set_route(this.page_name, cstr(id));
}
show_hide_prev_next(id) {
From af3213a44582b1d6c1bb1ecc396241c386bfe61c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 24 May 2023 18:13:16 +0530
Subject: [PATCH 053/203] fix: offset log cleanup to avoid deadlocks (#21105)
- Email queue is cleared at midnight
- Another worker at same time might be email Email
This causes random failures almost daily
---
frappe/hooks.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 6d8c00d483..edf572b642 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -195,6 +195,14 @@ scheduler_events = {
"0/10 * * * *": [
"frappe.email.doctype.email_account.email_account.pull",
],
+ # Hourly but offset by 30 minutes
+ # "30 * * * *": [
+ #
+ # ],
+ # Daily but offset by 45 minutes
+ "45 0 * * *": [
+ "frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
+ ],
},
"all": [
"frappe.email.queue.flush",
@@ -227,7 +235,6 @@ scheduler_events = {
"frappe.automation.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
"frappe.automation.doctype.auto_repeat.auto_repeat.set_auto_repeat_as_completed",
"frappe.email.doctype.unhandled_email.unhandled_email.remove_old_unhandled_emails",
- "frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
From 32396c7fe5f19ec3342250f5af321f99edf51e19 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 25 May 2023 11:42:24 +0530
Subject: [PATCH 054/203] fix: guess module if not set in form tour
if unable to guess save in Desk module.
---
frappe/desk/doctype/form_tour/form_tour.json | 5 ++---
frappe/desk/doctype/form_tour/form_tour.py | 8 ++++++++
2 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index 1eeda5598e..95ec270f2c 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -70,10 +70,10 @@
"label": "Is Standard"
},
{
+ "depends_on": "eval: doc.ui_tour && doc.is_standard",
"fetch_from": "reference_doctype.module",
"fieldname": "module",
"fieldtype": "Link",
- "hidden": 1,
"label": "Module",
"options": "Module Def",
"read_only": 1
@@ -104,7 +104,6 @@
"set_only_once": 1
},
{
- "depends_on": "is_ui_tour",
"fieldname": "page_route",
"fieldtype": "Small Text",
"hidden": 1,
@@ -179,7 +178,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-05-24 12:05:25.223405",
+ "modified": "2023-05-25 11:30:44.396248",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 0e8bf05472..2271563e71 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -8,6 +8,14 @@ from frappe.modules.export_file import export_to_files
class FormTour(Document):
def before_save(self):
+ if self.is_standard and not self.module:
+ if self.workspace_name:
+ self.module = frappe.db.get_value("Workspace", self.workspace_name, "module")
+ elif self.dashboard_name:
+ dashboard_doctype = frappe.db.get_value("Dashboard", self.dashboard_name, "module")
+ self.module = frappe.db.get_value("DocType", dashboard_doctype, "module")
+ else:
+ self.module = "Desk"
if not self.ui_tour:
meta = frappe.get_meta(self.reference_doctype)
for step in self.steps:
From 40b78692fea0a3a88444b0a454721c5fd5e2ba83 Mon Sep 17 00:00:00 2001
From: Maharshi Patel
Date: Thu, 25 May 2023 11:44:19 +0530
Subject: [PATCH 055/203] chore: fix linters
missing translate function and newline at end of file.
---
frappe/desk/doctype/form_tour/form_tour.js | 2 +-
frappe/public/js/onboarding_tours.bundle.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index b9010fff0c..8a65cc1619 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -34,7 +34,7 @@ frappe.ui.form.on("Form Tour", {
frm.doc.reference_doctype
) {
frappe.throw(
- "Referance Doctype and Dashboard Name both can't be used at the same time."
+ __("Referance Doctype and Dashboard Name both can't be used at the same time.")
);
}
frm.doc.ui_tour && (frm.doc.page_route = JSON.stringify(await get_path(frm)));
diff --git a/frappe/public/js/onboarding_tours.bundle.js b/frappe/public/js/onboarding_tours.bundle.js
index 90788cedfc..6ed7934b78 100644
--- a/frappe/public/js/onboarding_tours.bundle.js
+++ b/frappe/public/js/onboarding_tours.bundle.js
@@ -1 +1 @@
-import "./onboarding_tours/onboarding_tours.js";
\ No newline at end of file
+import "./onboarding_tours/onboarding_tours.js";
From c5e62cac261d0c6bb42f2a8ca59272f44ae0d9d2 Mon Sep 17 00:00:00 2001
From: Ritwik Puri
Date: Thu, 25 May 2023 12:29:20 +0530
Subject: [PATCH 056/203] fix: allow setting default in longtext and text
columns (#21089)
---
frappe/database/schema.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 7a8330595e..11948eda66 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -205,7 +205,6 @@ class DbColumn:
self.default
and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
and not cstr(self.default).startswith(":")
- and column_def not in ("text", "longtext")
):
column_def += f" default {frappe.db.escape(self.default)}"
From 76d7e6e3791bcaeb8346576f105d12e47dd31bf5 Mon Sep 17 00:00:00 2001
From: Yash Jane
Date: Thu, 25 May 2023 13:19:07 +0530
Subject: [PATCH 057/203] feat: added email template customization option for
welcome and password reset emails
---
.../system_settings/system_settings.json | 16 +++++++++++++-
frappe/core/doctype/user/user.py | 21 +++++++++++++++----
2 files changed, 32 insertions(+), 5 deletions(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 091dc1df1e..5efe87da25 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -72,6 +72,8 @@
"disable_standard_email_footer",
"hide_footer_in_auto_email_reports",
"attach_view_link",
+ "welcome_email_template",
+ "reset_password_template",
"prepared_report_section",
"max_auto_email_report_per_user",
"system_updates_section",
@@ -549,12 +551,24 @@
"fieldname": "enable_telemetry",
"fieldtype": "Check",
"label": "Allow Sending Usage Data for Improving Applications"
+ },
+ {
+ "fieldname": "welcome_email_template",
+ "fieldtype": "Link",
+ "label": "Welcome Email Template",
+ "options": "Email Template"
+ },
+ {
+ "fieldname": "reset_password_template",
+ "fieldtype": "Link",
+ "label": "Reset Password Template",
+ "options": "Email Template"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2023-04-23 11:14:59.302851",
+ "modified": "2023-05-25 13:02:54.808773",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 14266e4cd8..de49b00bbd 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -325,7 +325,10 @@ class User(Document):
return (self.first_name or "") + (self.first_name and " " or "") + (self.last_name or "")
def password_reset_mail(self, link):
- self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True)
+
+ reset_password_template = frappe.db.get_system_setting("reset_password_template")
+
+ self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True, custom_template=reset_password_template)
def send_welcome_mail_to_user(self):
from frappe.utils import get_url
@@ -342,16 +345,19 @@ class User(Document):
else:
subject = _("Complete Registration")
+ welcome_email_template = frappe.db.get_system_setting("welcome_email_template")
+
self.send_login_mail(
subject,
"new_user",
- dict(
+ dict(
link=link,
site_url=get_url(),
),
+ custom_template=welcome_email_template,
)
- def send_login_mail(self, subject, template, add_args, now=None):
+ def send_login_mail(self, subject, template, add_args, now=None, custom_template=None):
"""send mail with login details"""
from frappe.utils import get_url
from frappe.utils.user import get_user_fullname
@@ -374,11 +380,18 @@ class User(Document):
frappe.session.user not in STANDARD_USERS and get_formatted_email(frappe.session.user) or None
)
+ if custom_template:
+ from frappe.email.doctype.email_template.email_template import get_email_template
+ email_template = get_email_template(custom_template, args)
+ subject = email_template.get("subject")
+ content = email_template.get("message")
+
frappe.sendmail(
recipients=self.email,
sender=sender,
subject=subject,
- template=template,
+ template=template if not custom_template else None,
+ content=content if custom_template else None,
args=args,
header=[subject, "green"],
delayed=(not now) if now is not None else self.flags.delay_emails,
From 81c103a7414dc48ccd3396ec5534eb0217dc242e Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 25 May 2023 14:07:19 +0530
Subject: [PATCH 058/203] refactor: remove form tour settings
---
frappe/boot.py | 7 +--
frappe/desk/doctype/form_tour/form_tour.py | 62 ++++++++++---------
.../doctype/form_tour_settings/__init__.py | 0
.../form_tour_settings/form_tour_settings.js | 12 ----
.../form_tour_settings.json | 51 ---------------
.../form_tour_settings/form_tour_settings.py | 32 ----------
.../test_form_tour_settings.py | 9 ---
.../form_tour_settings_item/__init__.py | 0
.../form_tour_settings_item.json | 61 ------------------
.../form_tour_settings_item.py | 9 ---
.../js/onboarding_tours/onboarding_tours.js | 4 +-
frappe/utils/install.py | 2 -
12 files changed, 36 insertions(+), 213 deletions(-)
delete mode 100644 frappe/desk/doctype/form_tour_settings/__init__.py
delete mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.js
delete mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.json
delete mode 100644 frappe/desk/doctype/form_tour_settings/form_tour_settings.py
delete mode 100644 frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py
delete mode 100644 frappe/desk/doctype/form_tour_settings_item/__init__.py
delete mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
delete mode 100644 frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py
diff --git a/frappe/boot.py b/frappe/boot.py
index a54e6e2f07..37d89365c4 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -8,6 +8,7 @@ import frappe
import frappe.defaults
import frappe.desk.desk_page
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo, get_navbar_settings
+from frappe.desk.doctype.form_tour.form_tour import get_onboarding_ui_tours
from frappe.desk.doctype.route_history.route_history import frequently_visited_links
from frappe.desk.form.load import get_meta_bundle
from frappe.email.inbox import get_email_accounts
@@ -68,11 +69,7 @@ def get_bootinfo():
bootinfo.home_folder = frappe.db.get_value("File", {"is_home_folder": 1})
bootinfo.navbar_settings = get_navbar_settings()
bootinfo.notification_settings = get_notification_settings()
- bootinfo.onboarding_tours = (
- frappe.parse_json(frappe.db.get_single_value("Form Tour Settings", "onboarding_tours") or "[]")
- if frappe.get_system_settings("enable_onboarding")
- else []
- )
+ bootinfo.onboarding_tours = get_onboarding_ui_tours()
set_time_zone(bootinfo)
# ipinfo
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 2271563e71..0b1a22f64c 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -1,6 +1,8 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
+import json
+
import frappe
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
@@ -32,40 +34,12 @@ class FormTour(Document):
def on_update(self):
frappe.cache().delete_key("bootinfo")
- if self.ui_tour:
- form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
- in_settings = False
- child_index = 0
- for tour in form_tour_settings.form_tours:
- if tour.form_tour == self.name:
- in_settings = True
- child_index = tour.idx
- form_tour_settings.remove(tour)
- if not in_settings:
- child_index = len(form_tour_settings.form_tours) + 1
- child = frappe.new_doc("Form Tour Settings Item")
- child.update(
- {
- "idx": child_index,
- "form_tour": self.name,
- "parent": "Form Tour Settings",
- "parentfield": "form_tours",
- "parenttype": "Form Tour Settings",
- }
- )
- child.save()
- form_tour_settings.form_tours.insert(child_index, child)
- form_tour_settings.save()
+
if frappe.conf.developer_mode and self.is_standard:
export_to_files([["Form Tour", self.name]], self.module)
def on_trash(self):
- if self.ui_tour:
- form_tour_settings = frappe.get_doc("Form Tour Settings", "Form Tour Settings")
- for tour in form_tour_settings.form_tours:
- if tour.form_tour == self.name:
- form_tour_settings.remove(tour)
- form_tour_settings.save()
+ frappe.cache().delete_key("bootinfo")
@frappe.whitelist()
@@ -76,3 +50,31 @@ def reset_tour(tour_name):
onboarding_status.pop(tour_name, None)
user_doc.onboarding_status = frappe.as_json(onboarding_status)
user_doc.save()
+
+
+@frappe.whitelist()
+def update_user_status(value, step):
+ from frappe.utils.telemetry import capture
+
+ step = frappe.parse_json(step)
+ tour = frappe.parse_json(value)
+
+ capture(
+ frappe.scrub(f"{step.parent}_{step.title}"),
+ app="frappe_ui_tours",
+ properties={"is_completed": tour.is_completed},
+ )
+ frappe.db.set_value(
+ "User", frappe.session.user, "onboarding_status", value, update_modified=False
+ )
+
+ frappe.cache().hdel("bootinfo", frappe.session.user)
+
+
+def get_onboarding_ui_tours():
+ if not frappe.get_system_settings("enable_onboarding"):
+ return []
+
+ ui_tours = frappe.get_all("Form Tour", filters={"ui_tour": 1}, fields=["page_route", "name"])
+
+ return [[tour.name, json.loads(tour.page_route)] for tour in ui_tours]
diff --git a/frappe/desk/doctype/form_tour_settings/__init__.py b/frappe/desk/doctype/form_tour_settings/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.js b/frappe/desk/doctype/form_tour_settings/form_tour_settings.js
deleted file mode 100644
index a9d7f62890..0000000000
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.js
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2023, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on("Form Tour Settings", {
- refresh(frm) {
- frm.dashboard.add_comment(
- "This page is used to set priority for the UI form tours. If there are more than 1 matching tours found for the page, the tour with the highest priority will run.",
- "blue",
- true
- );
- },
-});
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json b/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
deleted file mode 100644
index 755c347264..0000000000
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.json
+++ /dev/null
@@ -1,51 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "creation": "2023-05-11 18:07:26.879273",
- "default_view": "List",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "onboarding_tours",
- "form_tours"
- ],
- "fields": [
- {
- "fieldname": "form_tours",
- "fieldtype": "Table",
- "label": "Form Tours",
- "options": "Form Tour Settings Item"
- },
- {
- "default": "\"[]\"",
- "fieldname": "onboarding_tours",
- "fieldtype": "Small Text",
- "hidden": 1,
- "label": "Onboarding Tours"
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2023-05-24 12:05:52.676242",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Form Tour Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": []
-}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
deleted file mode 100644
index 5c711f207b..0000000000
--- a/frappe/desk/doctype/form_tour_settings/form_tour_settings.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (c) 2023, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-import json
-
-import frappe
-from frappe.model.document import Document
-
-
-class FormTourSettings(Document):
- def before_save(self):
- self.onboarding_tours = json.dumps(
- [[tour.form_tour, json.loads(tour.page_route)] for tour in self.form_tours]
- )
-
-
-@frappe.whitelist()
-def update_user_status(value, step):
- from frappe.utils.telemetry import capture
-
- step = frappe.parse_json(step)
- tour = frappe.parse_json(value)
- # from frappe.utils.telemetry import capture
- capture(
- frappe.scrub(f"{step.parent}_{step.title}"),
- app="frappe_ui_tours",
- properties={"is_completed": tour.is_completed},
- )
- frappe.db.set_value(
- "User", frappe.session.user, "onboarding_status", value, update_modified=False
- )
- frappe.cache().hdel("bootinfo", frappe.session.user)
diff --git a/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py b/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py
deleted file mode 100644
index 95838ecb97..0000000000
--- a/frappe/desk/doctype/form_tour_settings/test_form_tour_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2023, Frappe Technologies and Contributors
-# See license.txt
-
-# import frappe
-from frappe.tests.utils import FrappeTestCase
-
-
-class TestFormTourSettings(FrappeTestCase):
- pass
diff --git a/frappe/desk/doctype/form_tour_settings_item/__init__.py b/frappe/desk/doctype/form_tour_settings_item/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
deleted file mode 100644
index 01f05393a3..0000000000
--- a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.json
+++ /dev/null
@@ -1,61 +0,0 @@
-{
- "actions": [],
- "allow_rename": 1,
- "creation": "2023-05-11 18:10:15.194034",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "form_tour",
- "view",
- "list_view",
- "page_route"
- ],
- "fields": [
- {
- "fieldname": "form_tour",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Form Tour",
- "options": "Form Tour",
- "read_only": 1
- },
- {
- "fetch_from": "form_tour.view_name",
- "fieldname": "view",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "View",
- "read_only": 1
- },
- {
- "fetch_from": "form_tour.list_name",
- "fieldname": "list_view",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "List View",
- "read_only": 1
- },
- {
- "fetch_from": "form_tour.page_route",
- "fieldname": "page_route",
- "fieldtype": "Data",
- "hidden": 1,
- "in_list_view": 1,
- "label": "Page Route",
- "read_only": 1
- }
- ],
- "index_web_pages_for_search": 1,
- "istable": 1,
- "links": [],
- "modified": "2023-05-17 22:22:58.507769",
- "modified_by": "Administrator",
- "module": "Desk",
- "name": "Form Tour Settings Item",
- "owner": "Administrator",
- "permissions": [],
- "sort_field": "modified",
- "sort_order": "DESC",
- "states": []
-}
\ No newline at end of file
diff --git a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py b/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py
deleted file mode 100644
index 0958b000ad..0000000000
--- a/frappe/desk/doctype/form_tour_settings_item/form_tour_settings_item.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2023, Frappe Technologies and contributors
-# For license information, please see license.txt
-
-# import frappe
-from frappe.model.document import Document
-
-
-class FormTourSettingsItem(Document):
- pass
diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js
index 63ecd6d670..63215d3659 100644
--- a/frappe/public/js/onboarding_tours/onboarding_tours.js
+++ b/frappe/public/js/onboarding_tours/onboarding_tours.js
@@ -42,7 +42,7 @@ frappe.ui.OnboardingTour = class OnboardingTour {
}
frappe.call({
- method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status",
+ method: "frappe.desk.doctype.form_tour.form_tour.update_user_status",
args: {
value: JSON.stringify(frappe.boot.user.onboarding_status),
step: JSON.stringify(step.options.step_info),
@@ -87,7 +87,7 @@ frappe.ui.OnboardingTour = class OnboardingTour {
}
this.last_step_saved = step;
frappe.call({
- method: "frappe.desk.doctype.form_tour_settings.form_tour_settings.update_user_status",
+ method: "frappe.desk.doctype.form_tour.form_tour.update_user_status",
args: {
value: JSON.stringify(frappe.boot.user.onboarding_status),
step: JSON.stringify(step),
diff --git a/frappe/utils/install.py b/frappe/utils/install.py
index 50feab0475..df918c27e0 100644
--- a/frappe/utils/install.py
+++ b/frappe/utils/install.py
@@ -14,8 +14,6 @@ def before_install():
frappe.reload_doc("core", "doctype", "doctype_action")
frappe.reload_doc("core", "doctype", "doctype_link")
frappe.reload_doc("desk", "doctype", "form_tour_step")
- frappe.reload_doc("desk", "doctype", "form_tour_settings")
- frappe.reload_doc("desk", "doctype", "form_tour_settings_item")
frappe.reload_doc("desk", "doctype", "form_tour")
frappe.reload_doc("core", "doctype", "doctype")
From 7c82876305e37233d50b4f8a6f3361f78d5b4c0f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 25 May 2023 14:33:12 +0530
Subject: [PATCH 059/203] fix: patch existing users/site
---
frappe/desk/doctype/form_tour/patches/__init__.py | 0
.../doctype/form_tour/patches/introduce_ui_tours.py | 13 +++++++++++++
frappe/patches.txt | 4 +++-
3 files changed, 16 insertions(+), 1 deletion(-)
create mode 100644 frappe/desk/doctype/form_tour/patches/__init__.py
create mode 100644 frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py
diff --git a/frappe/desk/doctype/form_tour/patches/__init__.py b/frappe/desk/doctype/form_tour/patches/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py b/frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py
new file mode 100644
index 0000000000..2ca981dae7
--- /dev/null
+++ b/frappe/desk/doctype/form_tour/patches/introduce_ui_tours.py
@@ -0,0 +1,13 @@
+import json
+
+import frappe
+
+
+def execute():
+ """Handle introduction of UI tours"""
+ completed = {}
+ for tour in frappe.get_all("Form Tour", {"ui_tour": 1}, pluck="name"):
+ completed[tour] = {"is_complete": True}
+
+ User = frappe.qb.DocType("User")
+ frappe.qb.update(User).set("onboarding_status", json.dumps(completed)).run()
diff --git a/frappe/patches.txt b/frappe/patches.txt
index fa9d884386..a4eb40a36f 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -196,6 +196,7 @@ frappe.patches.v14_0.update_webforms
frappe.patches.v14_0.delete_payment_gateways
frappe.patches.v15_0.remove_event_streaming
frappe.patches.v15_0.copy_disable_prepared_report_to_prepared_report
+execute:frappe.reload_doc("desk", "doctype", "Form Tour")
[post_model_sync]
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
@@ -223,4 +224,5 @@ frappe.patches.v14_0.disable_email_accounts_with_oauth
execute:frappe.delete_doc("Page", "translation-tool", force=1)
frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
frappe.patches.v14_0.remove_manage_subscriptions_from_navbar
-frappe.patches.v15_0.remove_background_jobs_from_dropdown
\ No newline at end of file
+frappe.patches.v15_0.remove_background_jobs_from_dropdown
+frappe.desk.doctype.form_tour.patches.introduce_ui_tours
From 1634ee0eb2399e8845f4c72ab6a7be566ab10525 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 25 May 2023 15:49:39 +0530
Subject: [PATCH 060/203] chore: update readme
remove manual install and setup production
---
README.md | 13 +++++++++----
1 file changed, 9 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 562437d5d1..d3b76648a2 100644
--- a/README.md
+++ b/README.md
@@ -56,10 +56,15 @@ Full-stack web application framework that uses Python and MariaDB on the server
## Installation
-* [Install via Docker](https://github.com/frappe/frappe_docker)
-* [Install via Frappe Bench](https://github.com/frappe/bench)
-* [Offical Documentation](https://frappeframework.com/docs/user/en/installation)
-* [Managed Hosting on Frappe Cloud](https://frappecloud.com/frappe/signup)
+### Production
+* [Managed Hosting on Frappe Cloud](https://frappecloud.com/)
+* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script)
+* [Manual install using Docker images](https://github.com/frappe/frappe_docker)
+
+### Development
+* [Easy install script using Docker images](https://github.com/frappe/bench/tree/develop#easy-install-script)
+* [Development installlation on bare metal](https://frappeframework.com/docs/user/en/installation)
+
## Contributing
From 1eabd21bb8719e5f23989d8bf4865fc83cb168a7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 25 May 2023 17:10:43 +0530
Subject: [PATCH 061/203] chore!: remove mixpanel code (#21112)
---
frappe/public/js/frappe/desk.js | 13 -------------
.../includes/app_analytics/mixpanel_analytics.html | 6 ------
frappe/www/app.html | 1 -
frappe/www/app.py | 1 -
4 files changed, 21 deletions(-)
delete mode 100644 frappe/templates/includes/app_analytics/mixpanel_analytics.html
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index f92bb3a1bf..fba4678cbb 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -38,7 +38,6 @@ frappe.Application = class Application {
this.load_user_permissions();
this.make_nav_bar();
this.set_favicon();
- this.setup_analytics();
this.set_fullwidth_if_enabled();
this.add_browser_class();
this.setup_energy_point_listeners();
@@ -512,18 +511,6 @@ frappe.Application = class Application {
});
}
- setup_analytics() {
- if (window.mixpanel) {
- window.mixpanel.identify(frappe.session.user);
- window.mixpanel.people.set({
- $first_name: frappe.boot.user.first_name,
- $last_name: frappe.boot.user.last_name,
- $created: frappe.boot.user.creation,
- $email: frappe.session.user,
- });
- }
- }
-
add_browser_class() {
$("html").addClass(frappe.utils.get_browser().name.toLowerCase());
}
diff --git a/frappe/templates/includes/app_analytics/mixpanel_analytics.html b/frappe/templates/includes/app_analytics/mixpanel_analytics.html
deleted file mode 100644
index 286593be04..0000000000
--- a/frappe/templates/includes/app_analytics/mixpanel_analytics.html
+++ /dev/null
@@ -1,6 +0,0 @@
-{% if mixpanel_id %}
-
-{% endif %}
\ No newline at end of file
diff --git a/frappe/www/app.html b/frappe/www/app.html
index a7468cfc30..ceceaf3219 100644
--- a/frappe/www/app.html
+++ b/frappe/www/app.html
@@ -52,7 +52,6 @@
{% endfor %}
{% include "templates/includes/app_analytics/google_analytics.html" %}
- {% include "templates/includes/app_analytics/mixpanel_analytics.html" %}
{% for sound in (sounds or []) %}
diff --git a/frappe/www/app.py b/frappe/www/app.py
index dcb326af36..4f18a50a0a 100644
--- a/frappe/www/app.py
+++ b/frappe/www/app.py
@@ -60,7 +60,6 @@ def get_context(context):
"csrf_token": csrf_token,
"google_analytics_id": frappe.conf.get("google_analytics_id"),
"google_analytics_anonymize_ip": frappe.conf.get("google_analytics_anonymize_ip"),
- "mixpanel_id": frappe.conf.get("mixpanel_id"),
"app_name": (
frappe.get_website_settings("app_name") or frappe.get_system_settings("app_name") or "Frappe"
),
From 2eca7b483760020d276cd1674b6cae6bec390959 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Thu, 25 May 2023 18:47:25 +0530
Subject: [PATCH 062/203] chore: more pythonic code
[skip ci]
---
frappe/model/document.py | 8 +++++---
1 file changed, 5 insertions(+), 3 deletions(-)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 8477d35418..75c3a005c9 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -967,9 +967,11 @@ class Document(BaseDocument):
return
def _evaluate_alert(alert):
- if not alert.name in self.flags.notifications_executed:
- evaluate_alert(self, alert.name, alert.event)
- self.flags.notifications_executed.append(alert.name)
+ if alert.name in self.flags.notifications_executed:
+ return
+
+ evaluate_alert(self, alert.name, alert.event)
+ self.flags.notifications_executed.append(alert.name)
event_map = {
"on_update": "Save",
From 70de5d05f2d00b9799db482af352fa4a4dd5ae87 Mon Sep 17 00:00:00 2001
From: ljain112
Date: Thu, 25 May 2023 17:57:19 +0530
Subject: [PATCH 063/203] fix: don't mutate notification when getting cc and
bcc
---
.../doctype/notification/notification.py | 23 ++++++++-----------
1 file changed, 10 insertions(+), 13 deletions(-)
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index 2efbf597ec..aee68aa4e5 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -282,19 +282,8 @@ def get_context(context):
email_ids = email_ids_value.replace(",", "\n")
recipients = recipients + email_ids.split("\n")
- if recipient.cc and "{" in recipient.cc:
- recipient.cc = frappe.render_template(recipient.cc, context)
-
- if recipient.cc:
- recipient.cc = recipient.cc.replace(",", "\n")
- cc = cc + recipient.cc.split("\n")
-
- if recipient.bcc and "{" in recipient.bcc:
- recipient.bcc = frappe.render_template(recipient.bcc, context)
-
- if recipient.bcc:
- recipient.bcc = recipient.bcc.replace(",", "\n")
- bcc = bcc + recipient.bcc.split("\n")
+ cc.extend(get_emails_from_template(recipient.cc, context))
+ bcc.extend(get_emails_from_template(recipient.bcc, context))
# For sending emails to specified role
if recipient.receiver_by_role:
@@ -485,3 +474,11 @@ def get_assignees(doc):
recipients = [d.allocated_to for d in assignees]
return recipients
+
+
+def get_emails_from_template(template, context):
+ if not template:
+ return ()
+
+ emails = frappe.render_template(template, context) if "{" in template else template
+ return filter(None, emails.replace(",", "\n").split("\n"))
From e277b947142a052b127c4c30cb5ad6e7c49461fd Mon Sep 17 00:00:00 2001
From: Ritwik Puri
Date: Fri, 26 May 2023 11:09:28 +0530
Subject: [PATCH 064/203] fix: allow default for text and long text when
altering table (#21109)
---
frappe/database/schema.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index 11948eda66..e65d7b980b 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -247,7 +247,6 @@ class DbColumn:
self.default_changed(current_def)
and (self.default not in frappe.db.DEFAULT_SHORTCUTS)
and not cstr(self.default).startswith(":")
- and not (column_type in ["text", "longtext"])
):
self.table.set_default.append(self)
From 3f0b03c80893f037c7e06b0bef29ad206fe3731a Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Fri, 26 May 2023 13:23:21 +0530
Subject: [PATCH 065/203] feat: Round QB function
---
frappe/query_builder/functions.py | 5 +++++
frappe/tests/test_query_builder.py | 19 +++++++++++++++++++
2 files changed, 24 insertions(+)
diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py
index 512df8835c..054e33c31d 100644
--- a/frappe/query_builder/functions.py
+++ b/frappe/query_builder/functions.py
@@ -41,6 +41,11 @@ class Timestamp(Function):
super().__init__("TIMESTAMP", term, alias=alias)
+class Round(Function):
+ def __init__(self, term, decimal=0, **kwargs):
+ super().__init__("ROUND", term, decimal, **kwargs)
+
+
GroupConcat = ImportMapper({db_type_is.MARIADB: GROUP_CONCAT, db_type_is.POSTGRES: STRING_AGG})
Match = ImportMapper({db_type_is.MARIADB: MATCH, db_type_is.POSTGRES: TO_TSVECTOR})
diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py
index a16c2a23ae..6d6937038a 100644
--- a/frappe/tests/test_query_builder.py
+++ b/frappe/tests/test_query_builder.py
@@ -13,6 +13,7 @@ from frappe.query_builder.functions import (
Date,
GroupConcat,
Match,
+ Round,
UnixTimestamp,
)
from frappe.query_builder.utils import db_type_is
@@ -153,6 +154,15 @@ class TestCustomFunctionsMariaDB(FrappeTestCase):
"SELECT `tabred`.`other`,CONCAT(`tabNote`.`name`,'') FROM `tabred`,`tabNote`",
)
+ def test_round(self):
+ note = frappe.qb.DocType("Note")
+
+ query = frappe.qb.from_(note).select(Round(note.price))
+ self.assertEqual("select round(`price`,0) from `tabnote`", str(query).lower())
+
+ query = frappe.qb.from_(note).select(Round(note.price, 3))
+ self.assertEqual("select round(`price`,3) from `tabnote`", str(query).lower())
+
@run_only_if(db_type_is.POSTGRES)
class TestCustomFunctionsPostgres(FrappeTestCase):
@@ -283,6 +293,15 @@ class TestCustomFunctionsPostgres(FrappeTestCase):
'SELECT "tabred"."other",CAST("tabNote"."name" AS VARCHAR) FROM "tabred","tabNote"',
)
+ def test_round(self):
+ note = frappe.qb.DocType("Note")
+
+ query = frappe.qb.from_(note).select(Round(note.price))
+ self.assertEqual('select round("price",0) from "tabnote"', str(query).lower())
+
+ query = frappe.qb.from_(note).select(Round(note.price, 3))
+ self.assertEqual('select round("price",3) from "tabnote"', str(query).lower())
+
class TestBuilderBase:
def test_adding_tabs(self):
From a43ad15eab731863548f201292fc73b8524584a3 Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Fri, 26 May 2023 13:23:47 +0530
Subject: [PATCH 066/203] feat: Truncate QB function
---
frappe/query_builder/functions.py | 5 +++++
frappe/tests/test_query_builder.py | 11 +++++++++++
2 files changed, 16 insertions(+)
diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py
index 054e33c31d..aa25fa1215 100644
--- a/frappe/query_builder/functions.py
+++ b/frappe/query_builder/functions.py
@@ -46,6 +46,11 @@ class Round(Function):
super().__init__("ROUND", term, decimal, **kwargs)
+class Truncate(Function):
+ def __init__(self, term, decimal, **kwargs):
+ super().__init__("TRUNCATE", term, decimal, **kwargs)
+
+
GroupConcat = ImportMapper({db_type_is.MARIADB: GROUP_CONCAT, db_type_is.POSTGRES: STRING_AGG})
Match = ImportMapper({db_type_is.MARIADB: MATCH, db_type_is.POSTGRES: TO_TSVECTOR})
diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py
index 6d6937038a..e3ca63abf1 100644
--- a/frappe/tests/test_query_builder.py
+++ b/frappe/tests/test_query_builder.py
@@ -14,6 +14,7 @@ from frappe.query_builder.functions import (
GroupConcat,
Match,
Round,
+ Truncate,
UnixTimestamp,
)
from frappe.query_builder.utils import db_type_is
@@ -163,6 +164,11 @@ class TestCustomFunctionsMariaDB(FrappeTestCase):
query = frappe.qb.from_(note).select(Round(note.price, 3))
self.assertEqual("select round(`price`,3) from `tabnote`", str(query).lower())
+ def test_truncate(self):
+ note = frappe.qb.DocType("Note")
+ query = frappe.qb.from_(note).select(Truncate(note.price, 3))
+ self.assertEqual("select truncate(`price`,3) from `tabnote`", str(query).lower())
+
@run_only_if(db_type_is.POSTGRES)
class TestCustomFunctionsPostgres(FrappeTestCase):
@@ -302,6 +308,11 @@ class TestCustomFunctionsPostgres(FrappeTestCase):
query = frappe.qb.from_(note).select(Round(note.price, 3))
self.assertEqual('select round("price",3) from "tabnote"', str(query).lower())
+ def test_truncate(self):
+ note = frappe.qb.DocType("Note")
+ query = frappe.qb.from_(note).select(Truncate(note.price, 3))
+ self.assertEqual('select truncate("price",3) from "tabnote"', str(query).lower())
+
class TestBuilderBase:
def test_adding_tabs(self):
From af7502bba7be1e4b88fddc0782e6828cc8fa37e5 Mon Sep 17 00:00:00 2001
From: Nabin Hait
Date: Fri, 26 May 2023 14:42:31 +0530
Subject: [PATCH 067/203] refactor: Workspace cleanup (#21100)
* refactor: Workspace cleanup
* fix: Resolved conflict
---
frappe/automation/workspace/tools/tools.json | 82 ++++++--
.../permitted_documents_for_user.py | 5 +-
frappe/core/workspace/build/build.json | 194 +++++++++++++-----
frappe/core/workspace/settings/settings.json | 9 +-
frappe/core/workspace/users/users.json | 8 +-
.../customization/customization.json | 171 ---------------
.../workspace/integrations/integrations.json | 7 +-
frappe/patches.txt | 1 +
.../public/js/frappe/widgets/widget_dialog.js | 1 +
frappe/website/workspace/website/website.json | 8 +-
10 files changed, 237 insertions(+), 249 deletions(-)
delete mode 100644 frappe/custom/workspace/customization/customization.json
diff --git a/frappe/automation/workspace/tools/tools.json b/frappe/automation/workspace/tools/tools.json
index d0e2c4fcfd..c3de151282 100644
--- a/frappe/automation/workspace/tools/tools.json
+++ b/frappe/automation/workspace/tools/tools.json
@@ -1,13 +1,15 @@
{
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Note\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Auto Repeat\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]",
+ "content": "[{\"id\":\"-P-RG1wVHg\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"id\":\"sR-UFcO7II\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Import Data\",\"col\":3}},{\"id\":\"IkcVmgWb3z\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"ToDo\",\"col\":3}},{\"id\":\"6wir-jZFRE\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"File\",\"col\":3}},{\"id\":\"45a1jzQkTm\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Assignment Rule\",\"col\":3}},{\"id\":\"LdZrgvxxo7\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yNSSTIaDWZ\",\"type\":\"header\",\"data\":{\"text\":\"Documents \",\"col\":12}},{\"id\":\"0yceBIfhHM\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"42WbBA9rpj\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"7_U7_xCOos\",\"type\":\"card\",\"data\":{\"card_name\":\"Email\",\"col\":4}},{\"id\":\"SlYKJZj5r3\",\"type\":\"card\",\"data\":{\"card_name\":\"Automation\",\"col\":4}}]",
"creation": "2020-03-02 14:53:24.980279",
+ "custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
+ "is_hidden": 0,
"label": "Tools",
"links": [
{
@@ -132,28 +134,89 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Data",
+ "link_count": 5,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Import Data",
+ "link_count": 0,
+ "link_to": "Data Import",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Export Data",
+ "link_count": 0,
+ "link_to": "Data Export",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bulk Update",
+ "link_count": 0,
+ "link_to": "Bulk Update",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Download Backups",
+ "link_count": 0,
+ "link_to": "backups",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Deleted Documents",
+ "link_count": 0,
+ "link_to": "Deleted Document",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2022-12-12 14:58:44.733393",
+ "modified": "2023-05-24 14:47:24.740856",
"modified_by": "Administrator",
"module": "Automation",
"name": "Tools",
+ "number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 26.0,
+ "sequence_id": 17.0,
"shortcuts": [
{
- "label": "ToDo",
- "link_to": "ToDo",
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Import Data",
+ "link_to": "Data Import",
"type": "DocType"
},
{
- "label": "Note",
- "link_to": "Note",
+ "label": "ToDo",
+ "link_to": "ToDo",
"type": "DocType"
},
{
@@ -165,11 +228,6 @@
"label": "Assignment Rule",
"link_to": "Assignment Rule",
"type": "DocType"
- },
- {
- "label": "Auto Repeat",
- "link_to": "Auto Repeat",
- "type": "DocType"
}
],
"title": "Tools"
diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
index 2c92a72ab3..4b455e0ab4 100644
--- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
+++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py
@@ -52,12 +52,15 @@ def query_doctypes(doctype, txt, searchfield, start, page_len, filters):
user_perms = frappe.utils.user.UserPermissions(user)
user_perms.build_permissions()
can_read = user_perms.can_read # Does not include child tables
+ include_single_doctypes = filters.get("include_single_doctypes")
single_doctypes = [d[0] for d in frappe.db.get_values("DocType", {"issingle": 1})]
out = []
for dt in can_read:
- if txt.lower().replace("%", "") in dt.lower() and dt not in single_doctypes:
+ if txt.lower().replace("%", "") in dt.lower() and (
+ include_single_doctypes or dt not in single_doctypes
+ ):
out.append([dt])
return out
diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json
index 67dfae650f..b917f88e27 100644
--- a/frappe/core/workspace/build/build.json
+++ b/frappe/core/workspace/build/build.json
@@ -1,13 +1,15 @@
{
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Workspace\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Elements \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]",
+ "content": "[{\"id\":\"5nnLaQeoFa\",\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"id\":\"HXRmktXYHy\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"DocType\",\"col\":3}},{\"id\":\"pYALX3MwBW\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"id\":\"XC78DuYB65\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Report\",\"col\":3}},{\"id\":\"XPm50Ppq3J\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"id\":\"yoU6nWiT83\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"id\":\"5UgFESBY0N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Format Builder\",\"col\":3}},{\"id\":\"62hseENHbd\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"tOCrOgLW1G\",\"type\":\"header\",\"data\":{\"text\":\"Elements \",\"col\":12}},{\"id\":\"BIHjudL0T_\",\"type\":\"card\",\"data\":{\"card_name\":\"Modules\",\"col\":4}},{\"id\":\"cJ6CVsa8qW\",\"type\":\"card\",\"data\":{\"card_name\":\"Models\",\"col\":4}},{\"id\":\"MmEJpjEdGR\",\"type\":\"card\",\"data\":{\"card_name\":\"Views\",\"col\":4}},{\"id\":\"2ZdtgxQZqq\",\"type\":\"card\",\"data\":{\"card_name\":\"Customization\",\"col\":4}},{\"id\":\"NPFolijIcb\",\"type\":\"card\",\"data\":{\"card_name\":\"Scripting\",\"col\":4}},{\"id\":\"iK3JQ9RXJE\",\"type\":\"card\",\"data\":{\"card_name\":\"Packages\",\"col\":4}},{\"id\":\"TiO9FCUUeC\",\"type\":\"card\",\"data\":{\"card_name\":\"System Logs\",\"col\":4}}]",
"creation": "2021-01-02 10:51:16.579957",
+ "custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "tool",
"idx": 0,
+ "is_hidden": 0,
"label": "Build",
"links": [
{
@@ -153,57 +155,6 @@
"onboard": 0,
"type": "Link"
},
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Views",
- "link_count": 4,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Report",
- "link_count": 0,
- "link_to": "Report",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Print Format",
- "link_count": 0,
- "link_to": "Print Format",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Dashboard",
- "link_count": 0,
- "link_to": "Dashboard",
- "link_type": "DocType",
- "onboard": 0,
- "only_for": "",
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Workspace",
- "link_count": 0,
- "link_to": "Workspace",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
{
"hidden": 0,
"is_query_report": 0,
@@ -271,20 +222,144 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Views",
+ "link_count": 5,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Report",
+ "link_count": 0,
+ "link_to": "Report",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Print Format",
+ "link_count": 0,
+ "link_to": "Print Format",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workspace",
+ "link_count": 0,
+ "link_to": "Workspace",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Dashboard",
+ "link_count": 0,
+ "link_to": "Dashboard",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "",
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Dashboard Chart",
+ "link_count": 0,
+ "link_to": "Dashboard Chart",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customization",
+ "link_count": 4,
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customize Form",
+ "link_count": 0,
+ "link_to": "Customize Form",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Custom Field",
+ "link_count": 0,
+ "link_to": "Custom Field",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Custom Translation",
+ "link_count": 0,
+ "link_to": "Translation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Navbar Settings",
+ "link_count": 0,
+ "link_to": "Navbar Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2022-09-11 06:41:31.095300",
+ "modified": "2023-05-24 14:47:24.395259",
"modified_by": "Administrator",
"module": "Core",
"name": "Build",
+ "number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 5.0,
+ "sequence_id": 16.0,
"shortcuts": [
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Print Format Builder",
+ "link_to": "print-format-builder",
+ "type": "Page"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Client Script",
+ "link_to": "Client Script",
+ "type": "DocType"
+ },
{
"doc_view": "",
"label": "DocType",
@@ -293,8 +368,15 @@
},
{
"doc_view": "",
- "label": "Workspace",
- "link_to": "Workspace",
+ "label": "Customize Form",
+ "link_to": "Customize Form",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Server Script",
+ "link_to": "Server Script",
"type": "DocType"
},
{
diff --git a/frappe/core/workspace/settings/settings.json b/frappe/core/workspace/settings/settings.json
index 1469892bd8..24e534ce19 100644
--- a/frappe/core/workspace/settings/settings.json
+++ b/frappe/core/workspace/settings/settings.json
@@ -1,13 +1,15 @@
{
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Settings \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
+ "content": "[{\"id\":\"bc3WecV0uU\",\"type\":\"header\",\"data\":{\"text\":\"Settings \",\"col\":12}},{\"id\":\"_6Jxax2I11\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"System Settings\",\"col\":3}},{\"id\":\"rbf1Om8zJG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Print Settings\",\"col\":3}},{\"id\":\"xMytWpIImZ\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"id\":\"Q9DPlmrPpX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"oVwctUh0gf\",\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"id\":\"hC0b24aSJG\",\"type\":\"card\",\"data\":{\"card_name\":\"Data\",\"col\":4}},{\"id\":\"JA_iI4Z0yI\",\"type\":\"card\",\"data\":{\"card_name\":\"Email / Notifications\",\"col\":4}},{\"id\":\"F1GxSqFKy9\",\"type\":\"card\",\"data\":{\"card_name\":\"Website\",\"col\":4}},{\"id\":\"vugObM_K_T\",\"type\":\"card\",\"data\":{\"card_name\":\"Core\",\"col\":4}},{\"id\":\"XwKthiuAAW\",\"type\":\"card\",\"data\":{\"card_name\":\"Printing\",\"col\":4}},{\"id\":\"EQY7Sfmdxn\",\"type\":\"card\",\"data\":{\"card_name\":\"Workflow\",\"col\":4}}]",
"creation": "2020-03-02 15:09:40.527211",
+ "custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
+ "is_hidden": 0,
"label": "Settings",
"links": [
{
@@ -345,17 +347,18 @@
"type": "Link"
}
],
- "modified": "2022-08-28 21:41:28.065190",
+ "modified": "2023-05-24 14:58:44.010999",
"modified_by": "Administrator",
"module": "Core",
"name": "Settings",
+ "number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 29.0,
+ "sequence_id": 18.0,
"shortcuts": [
{
"icon": "setting",
diff --git a/frappe/core/workspace/users/users.json b/frappe/core/workspace/users/users.json
index 5741c54eeb..53ba10c0f9 100644
--- a/frappe/core/workspace/users/users.json
+++ b/frappe/core/workspace/users/users.json
@@ -2,12 +2,14 @@
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Permission Manager\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Profile\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"User Type\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Users\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Logs\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Permissions\",\"col\":4}}]",
"creation": "2020-03-02 15:12:16.754449",
+ "custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "users",
"idx": 0,
+ "is_hidden": 0,
"label": "Users",
"links": [
{
@@ -145,16 +147,18 @@
"type": "Link"
}
],
- "modified": "2022-01-13 17:49:08.912772",
+ "modified": "2023-05-24 14:47:23.619182",
"modified_by": "Administrator",
"module": "Core",
"name": "Users",
+ "number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
+ "quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 27.0,
+ "sequence_id": 13.0,
"shortcuts": [
{
"label": "User",
diff --git a/frappe/custom/workspace/customization/customization.json b/frappe/custom/workspace/customization/customization.json
deleted file mode 100644
index 8985bf54ed..0000000000
--- a/frappe/custom/workspace/customization/customization.json
+++ /dev/null
@@ -1,171 +0,0 @@
-{
- "charts": [],
- "content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Customization\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customize Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Custom Role\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Client Script\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Server Script\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Dashboards\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Form Customization\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other\",\"col\":4}}]",
- "creation": "2020-03-02 15:15:03.839594",
- "docstatus": 0,
- "doctype": "Workspace",
- "for_user": "",
- "hide_custom": 0,
- "icon": "customization",
- "idx": 0,
- "label": "Customization",
- "links": [
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Dashboards",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Dashboard",
- "link_count": 0,
- "link_to": "Dashboard",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Dashboard Chart",
- "link_count": 0,
- "link_to": "Dashboard Chart",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Dashboard Chart Source",
- "link_count": 0,
- "link_to": "Dashboard Chart Source",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Form Customization",
- "link_count": 0,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Customize Form",
- "link_count": 0,
- "link_to": "Customize Form",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Custom Field",
- "link_count": 0,
- "link_to": "Custom Field",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Client Script",
- "link_count": 0,
- "link_to": "Client Script",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "DocType",
- "link_count": 0,
- "link_to": "DocType",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Other",
- "link_count": 2,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Custom Translations",
- "link_count": 0,
- "link_to": "Translation",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Navbar Settings",
- "link_count": 0,
- "link_to": "Navbar Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
- }
- ],
- "modified": "2022-08-28 20:56:24.980719",
- "modified_by": "Administrator",
- "module": "Custom",
- "name": "Customization",
- "owner": "Administrator",
- "parent_page": "",
- "public": 1,
- "quick_lists": [],
- "restrict_to_domain": "",
- "roles": [],
- "sequence_id": 8.0,
- "shortcuts": [
- {
- "label": "Customize Form",
- "link_to": "Customize Form",
- "type": "DocType"
- },
- {
- "label": "Custom Role",
- "link_to": "Custom Role",
- "type": "DocType"
- },
- {
- "label": "Client Script",
- "link_to": "Client Script",
- "type": "DocType"
- },
- {
- "doc_view": "",
- "label": "Server Script",
- "link_to": "Server Script",
- "type": "DocType"
- }
- ],
- "title": "Customization"
-}
\ No newline at end of file
diff --git a/frappe/integrations/workspace/integrations/integrations.json b/frappe/integrations/workspace/integrations/integrations.json
index 8d1dfd64af..73a1a393a5 100644
--- a/frappe/integrations/workspace/integrations/integrations.json
+++ b/frappe/integrations/workspace/integrations/integrations.json
@@ -2,12 +2,14 @@
"charts": [],
"content": "[{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 15:16:18.714190",
+ "custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
+ "is_hidden": 0,
"label": "Integrations",
"links": [
{
@@ -197,17 +199,18 @@
"type": "Link"
}
],
- "modified": "2022-07-23 18:00:28.805405",
+ "modified": "2023-05-24 14:58:55.910408",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Integrations",
+ "number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 15.0,
+ "sequence_id": 20.0,
"shortcuts": [],
"title": "Integrations"
}
\ No newline at end of file
diff --git a/frappe/patches.txt b/frappe/patches.txt
index a4eb40a36f..d3d5e3ee15 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -226,3 +226,4 @@ frappe.patches.v15_0.remove_prepared_report_settings_from_system_settings
frappe.patches.v14_0.remove_manage_subscriptions_from_navbar
frappe.patches.v15_0.remove_background_jobs_from_dropdown
frappe.desk.doctype.form_tour.patches.introduce_ui_tours
+execute:frappe.delete_doc_if_exists("Workspace", "Customization")
\ No newline at end of file
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index ba5270c27a..4f200f390f 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -358,6 +358,7 @@ class ShortcutDialog extends WidgetDialog {
query: "frappe.core.report.permitted_documents_for_user.permitted_documents_for_user.query_doctypes",
filters: {
user: frappe.session.user,
+ include_single_doctypes: true,
},
};
};
diff --git a/frappe/website/workspace/website/website.json b/frappe/website/workspace/website/website.json
index a0d9a817d4..61911c0b6b 100644
--- a/frappe/website/workspace/website/website.json
+++ b/frappe/website/workspace/website/website.json
@@ -2,12 +2,14 @@
"charts": [],
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Website\",\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts \",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Blog Post\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Blogger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Web Page\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Web Form\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Website Settings\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters \",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Setup\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Blog\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Web Site\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Portal\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Knowledge Base\",\"col\":4}}]",
"creation": "2020-03-02 14:13:51.089373",
+ "custom_blocks": [],
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "website",
"idx": 0,
+ "is_hidden": 0,
"label": "Website",
"links": [
{
@@ -232,16 +234,18 @@
"type": "Link"
}
],
- "modified": "2022-01-13 17:49:41.527194",
+ "modified": "2023-05-24 14:47:23.879036",
"modified_by": "Administrator",
"module": "Website",
"name": "Website",
+ "number_cards": [],
"owner": "Administrator",
"parent_page": "",
"public": 1,
+ "quick_lists": [],
"restrict_to_domain": "",
"roles": [],
- "sequence_id": 28.0,
+ "sequence_id": 14.0,
"shortcuts": [
{
"color": "Green",
From 43714825b0eb3dbac86845bfec3a74d2e04f416d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 26 May 2023 15:58:26 +0530
Subject: [PATCH 068/203] fix: sync importable doctype before documents
(#21131)
---
frappe/model/sync.py | 51 ++++++++++++++++++++++++--------------------
1 file changed, 28 insertions(+), 23 deletions(-)
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index 6272c9cb7d..0b344b892a 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -11,6 +11,28 @@ from frappe.modules.import_file import import_file_by_path
from frappe.modules.patch_handler import _patch_mode
from frappe.utils import update_progress_bar
+IMPORTABLE_DOCTYPES = [
+ ("core", "doctype"),
+ ("core", "page"),
+ ("core", "report"),
+ ("desk", "dashboard_chart_source"),
+ ("printing", "print_format"),
+ ("website", "web_page"),
+ ("website", "website_theme"),
+ ("website", "web_form"),
+ ("website", "web_template"),
+ ("email", "notification"),
+ ("printing", "print_style"),
+ ("desk", "workspace"),
+ ("desk", "onboarding_step"),
+ ("desk", "module_onboarding"),
+ ("desk", "form_tour"),
+ ("custom", "client_script"),
+ ("core", "server_script"),
+ ("custom", "custom_field"),
+ ("custom", "property_setter"),
+]
+
def sync_all(force=0, reset_permissions=False):
_patch_mode(True)
@@ -71,6 +93,11 @@ def sync_for(app_name, force=0, reset_permissions=False):
]:
files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))
+ for module_name, document_type in IMPORTABLE_DOCTYPES:
+ file = os.path.join(FRAPPE_PATH, module_name, "doctype", document_type, f"{document_type}.json")
+ if file not in files:
+ files.append(file)
+
for module_name in frappe.local.app_modules.get(app_name) or []:
folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__)
files = get_doc_files(files=files, start_path=folder)
@@ -97,29 +124,7 @@ def get_doc_files(files, start_path):
files = files or []
- # load in sequence - warning for devs
- document_types = [
- "doctype",
- "page",
- "report",
- "dashboard_chart_source",
- "print_format",
- "web_page",
- "website_theme",
- "web_form",
- "web_template",
- "notification",
- "print_style",
- "workspace",
- "onboarding_step",
- "module_onboarding",
- "form_tour",
- "client_script",
- "server_script",
- "custom_field",
- "property_setter",
- ]
- for doctype in document_types:
+ for _module, doctype in IMPORTABLE_DOCTYPES:
doctype_path = os.path.join(start_path, doctype)
if os.path.exists(doctype_path):
for docname in os.listdir(doctype_path):
From d099c9376b86c000535fb5b3434663352276c741 Mon Sep 17 00:00:00 2001
From: Ritwik Puri
Date: Fri, 26 May 2023 16:03:28 +0530
Subject: [PATCH 069/203] feat(minor): db.get_column_type for postgres (#21125)
---
frappe/database/database.py | 15 ---------------
frappe/database/mariadb/database.py | 15 +++++++++++++++
frappe/database/postgres/database.py | 15 +++++++++++++++
3 files changed, 30 insertions(+), 15 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 2d38a6dea8..59c514991a 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1119,21 +1119,6 @@ class Database:
"""Returns True if column exists in database."""
return column in self.get_table_columns(doctype)
- def get_column_type(self, doctype, column):
- """Returns column type from database."""
- information_schema = frappe.qb.Schema("information_schema")
- table = get_table_name(doctype)
-
- return (
- frappe.qb.from_(information_schema.columns)
- .select(information_schema.columns.column_type)
- .where(
- (information_schema.columns.table_name == table)
- & (information_schema.columns.column_name == column)
- )
- .run(pluck=True)[0]
- )
-
def has_index(self, table_name, index_name):
raise NotImplementedError
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 43540956e0..8e52cc7ffd 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -318,6 +318,21 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
as_dict=1,
)
+ def get_column_type(self, doctype, column):
+ """Returns column type from database."""
+ information_schema = frappe.qb.Schema("information_schema")
+ table = get_table_name(doctype)
+
+ return (
+ frappe.qb.from_(information_schema.columns)
+ .select(information_schema.columns.column_type)
+ .where(
+ (information_schema.columns.table_name == table)
+ & (information_schema.columns.column_name == column)
+ )
+ .run(pluck=True)[0]
+ )
+
def has_index(self, table_name, index_name):
return self.sql(
"""SHOW INDEX FROM `{table_name}`
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index d082afceaf..836a689251 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -394,6 +394,21 @@ class PostgresDatabase(PostgresExceptionUtil, Database):
as_dict=1,
)
+ def get_column_type(self, doctype, column):
+ """Returns column type from database."""
+ information_schema = frappe.qb.Schema("information_schema")
+ table = get_table_name(doctype)
+
+ return (
+ frappe.qb.from_(information_schema.columns)
+ .select(information_schema.columns.data_type)
+ .where(
+ (information_schema.columns.table_name == table)
+ & (information_schema.columns.column_name == column)
+ )
+ .run(pluck=True)[0]
+ )
+
def get_database_list(self):
return self.sql("SELECT datname FROM pg_database", pluck=True)
From 4b2730642d012b48e13ed21d9f5af759605e61cf Mon Sep 17 00:00:00 2001
From: Doridel Cahanap Mendez
Date: Fri, 12 May 2023 05:26:19 +0000
Subject: [PATCH 070/203] refactor: attach txt file in received emails
(cherry picked from commit a36d6a9df02e9a86e3701fcc6861c9c64bd40c66)
---
frappe/email/receive.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 382fd2ac99..525703c8a2 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -459,6 +459,10 @@ class Email:
if content_type == "text/plain":
self.text_content += self.get_payload(part)
+ # attach txt file from received email as well aside from saving to text_content if it has filename
+ if part.get_filename():
+ self.get_attachment(part)
+
elif content_type == "text/html":
self.html_content += self.get_payload(part)
From 28f6ef74a6e7075451bd91c9856fb57851c23a5e Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 18:12:30 +0530
Subject: [PATCH 071/203] feat: select group button
---
frappe/public/js/frappe/utils/utils.js | 60 ++++++++++++++++++++++++++
frappe/public/scss/desk/global.scss | 14 ++++++
2 files changed, 74 insertions(+)
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index ac9a18785b..7c05ccc535 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1440,6 +1440,66 @@ Object.assign(frappe.utils, {
prepend && wrapper.prepend(button);
},
+ add_select_group_button(actions, btn_type, wrapper, prepend) {
+ // actions = [{
+ // label: "Action 1",
+ // description: "Description 1", (optional)
+ // action: () => {},
+ // },
+ // {
+ // label: "Action 2",
+ // description: "Description 2", (optional)
+ // action: () => {},
+ // }]
+ let selected_action = actions[0];
+
+ let $select_group_button = $(`
+
+
+ ${selected_action.label}
+
+
+
+ ${frappe.utils.icon("down", "xs")}
+
+
+
+
+ `);
+
+ actions.forEach((action) => {
+ $(`
+
+ ${frappe.utils.icon("check", "xs")}
+
+
${action.label}
+
${action.description || ""}
+
+
+ `)
+ .appendTo($select_group_button.find(".dropdown-menu"))
+ .click((e) => {
+ selected_action = action;
+ $select_group_button.find(".selected-button").text(action.label);
+ $select_group_button.find(".tick-icon").addClass("selected");
+
+ $(e.currentTarget).siblings().find(".tick-icon").removeClass("selected");
+ });
+ });
+
+ $select_group_button.find(".dropdown-menu li:first-child .tick-icon").addClass("selected");
+
+ $select_group_button.find(".selected-button").click((event) => {
+ event.stopPropagation();
+ selected_action.action && selected_action.action(event);
+ });
+
+ !prepend && $select_group_button.appendTo(wrapper);
+ prepend && wrapper.prepend($select_group_button);
+
+ return $select_group_button;
+ },
+
sleep(time) {
return new Promise((resolve) => setTimeout(resolve, time));
},
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index 765e51cab9..80e4e3c9bc 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -249,6 +249,20 @@ h2 {
}
}
+.select-group-btn {
+ .dropdown-toggle-split::after {
+ display: none;
+ }
+
+ .dropdown-item .tick-icon {
+ visibility: hidden;
+
+ &.selected {
+ visibility: visible;
+ }
+ }
+}
+
.btn-xs {
@extend .btn-sm;
line-height: 1.2;
From b1b05ee05b3bec5e9c8664d1a7e2b991e611b290 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 18:18:52 +0530
Subject: [PATCH 072/203] fix: clear & add email template using select group
btn
---
.../public/js/frappe/views/communication.js | 40 ++++++++++++++++---
frappe/public/scss/common/modal.scss | 18 +++++++++
2 files changed, 52 insertions(+), 6 deletions(-)
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 3505199d3f..21a42988eb 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -77,12 +77,22 @@ frappe.views.CommunicationComposer = class {
fieldtype: "MultiSelect",
fieldname: "bcc",
},
+ {
+ fieldtype: "Section Break",
+ fieldname: "email_template_section_break",
+ hidden: 1,
+ },
{
label: __("Email Template"),
fieldtype: "Link",
options: "Email Template",
fieldname: "email_template",
},
+ {
+ fieldtype: "HTML",
+ label: __("Clear & Add template"),
+ fieldname: "clear_and_add_template",
+ },
{ fieldtype: "Section Break" },
{
label: __("Subject"),
@@ -170,6 +180,7 @@ frappe.views.CommunicationComposer = class {
toggle_more_options(show_options) {
show_options = show_options || this.dialog.fields_dict.more_options.df.hidden;
this.dialog.set_df_property("more_options", "hidden", !show_options);
+ this.dialog.set_df_property("email_template_section_break", "hidden", !show_options);
const label = frappe.utils.icon(show_options ? "up-line" : "down");
this.dialog.get_field("option_toggle_button").set_label(label);
@@ -266,13 +277,14 @@ frappe.views.CommunicationComposer = class {
setup_email_template() {
const me = this;
- this.dialog.fields_dict["email_template"].df.onchange = () => {
+ const fields = this.dialog.fields_dict;
+ const clear_and_add_template = $(fields.clear_and_add_template.wrapper);
+
+ function add_template() {
const email_template = me.dialog.fields_dict.email_template.get_value();
if (!email_template) return;
function prepend_reply(reply) {
- if (me.reply_added === email_template) return;
-
const content_field = me.dialog.fields_dict.content;
const subject_field = me.dialog.fields_dict.subject;
@@ -280,8 +292,6 @@ frappe.views.CommunicationComposer = class {
content_field.set_value(`${reply.message} ${content}`);
subject_field.set_value(reply.subject);
-
- me.reply_added = email_template;
}
frappe.call({
@@ -294,7 +304,25 @@ frappe.views.CommunicationComposer = class {
prepend_reply(r.message);
},
});
- };
+ }
+
+ let email_template_actions = [
+ {
+ label: __("Add Template"),
+ description: __("Prepend the template to the email message"),
+ action: () => add_template(),
+ },
+ {
+ label: __("Clear & Add Template"),
+ description: __("Clear the email message and add the template"),
+ action: () => {
+ me.dialog.fields_dict.content.set_value("");
+ add_template();
+ },
+ },
+ ];
+
+ frappe.utils.add_select_group_button(email_template_actions, "", clear_and_add_template);
}
setup_last_edited_communication() {
diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss
index 8e69a956e5..6909967cdb 100644
--- a/frappe/public/scss/common/modal.scss
+++ b/frappe/public/scss/common/modal.scss
@@ -228,6 +228,24 @@ body.modal-open[style^="padding-right"] {
}
}
+.modal [data-fieldname="email_template_section_break"] {
+ form {
+ display: flex;
+ align-items: center;
+
+ .frappe-control:first-child {
+ &[data-fieldname="email_template"] {
+ margin-right: 10px;
+ }
+ flex: 1;
+ }
+
+ .frappe-control:last-child {
+ margin-bottom: -8px;
+ }
+ }
+}
+
// modal is xs (for grids)
.modal .hidden-xs {
display: none !important;
From f06449adaf46e015382506bc0c3c3e561411e4d0 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 18:20:05 +0530
Subject: [PATCH 073/203] fix: make option toggle button smaller
---
frappe/public/js/frappe/views/communication.js | 4 ++--
frappe/public/scss/common/modal.scss | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 21a42988eb..7b861fe161 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -56,7 +56,7 @@ frappe.views.CommunicationComposer = class {
},
{
fieldtype: "Button",
- label: frappe.utils.icon("down"),
+ label: frappe.utils.icon("down", "xs"),
fieldname: "option_toggle_button",
click: () => {
this.toggle_more_options();
@@ -182,7 +182,7 @@ frappe.views.CommunicationComposer = class {
this.dialog.set_df_property("more_options", "hidden", !show_options);
this.dialog.set_df_property("email_template_section_break", "hidden", !show_options);
- const label = frappe.utils.icon(show_options ? "up-line" : "down");
+ const label = frappe.utils.icon(show_options ? "up-line" : "down", "xs");
this.dialog.get_field("option_toggle_button").set_label(label);
}
diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss
index 6909967cdb..4b7f028c79 100644
--- a/frappe/public/scss/common/modal.scss
+++ b/frappe/public/scss/common/modal.scss
@@ -222,7 +222,7 @@ body.modal-open[style^="padding-right"] {
margin-bottom: -24px;
button {
// same as form-control input
- height: calc(1.5em + .75rem + 2px);
+ height: calc(1.5em + .7rem);
}
}
}
From 61dcd09172f36b37c7a23807e7cb2e8ab332b9d8 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 18:35:08 +0530
Subject: [PATCH 074/203] fix: add selected on current dropdown-item
---
frappe/public/js/frappe/utils/utils.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 7c05ccc535..b613d895d3 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1481,8 +1481,8 @@ Object.assign(frappe.utils, {
.click((e) => {
selected_action = action;
$select_group_button.find(".selected-button").text(action.label);
- $select_group_button.find(".tick-icon").addClass("selected");
+ $(e.currentTarget).find(".tick-icon").addClass("selected");
$(e.currentTarget).siblings().find(".tick-icon").removeClass("selected");
});
});
From 78f3d9c2419054abfca13a0fdc61405e2619c5e0 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 19:55:06 +0530
Subject: [PATCH 075/203] fix: make dropdown item label bold
---
frappe/public/js/frappe/utils/utils.js | 4 ++--
frappe/public/scss/desk/global.scss | 14 ++++++++++----
2 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index b613d895d3..90a19fca40 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1472,8 +1472,8 @@ Object.assign(frappe.utils, {
${frappe.utils.icon("check", "xs")}
-
${action.label}
-
${action.description || ""}
+
${action.label}
+
${action.description || ""}
`)
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index 80e4e3c9bc..fa27ef99ad 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -254,11 +254,17 @@ h2 {
display: none;
}
- .dropdown-item .tick-icon {
- visibility: hidden;
+ .dropdown-item {
+ .tick-icon {
+ visibility: hidden;
- &.selected {
- visibility: visible;
+ &.selected {
+ visibility: visible;
+ }
+ }
+
+ .item-label {
+ font-weight: 500;
}
}
}
From f91bc1bde6a517a2867089dd91af4f5c0fa7efcc Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 19:56:12 +0530
Subject: [PATCH 076/203] fix: added left icon option
---
frappe/public/js/frappe/utils/utils.js | 7 ++++---
frappe/public/js/frappe/views/communication.js | 2 +-
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 90a19fca40..095b04c931 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1440,7 +1440,7 @@ Object.assign(frappe.utils, {
prepend && wrapper.prepend(button);
},
- add_select_group_button(actions, btn_type, wrapper, prepend) {
+ add_select_group_button(wrapper, actions, btn_type, icon = "", prepend) {
// actions = [{
// label: "Action 1",
// description: "Description 1", (optional)
@@ -1456,7 +1456,8 @@ Object.assign(frappe.utils, {
let $select_group_button = $(`
- ${selected_action.label}
+ ${icon && frappe.utils.icon(icon, "xs")}
+ ${selected_action.label}
@@ -1480,7 +1481,7 @@ Object.assign(frappe.utils, {
.appendTo($select_group_button.find(".dropdown-menu"))
.click((e) => {
selected_action = action;
- $select_group_button.find(".selected-button").text(action.label);
+ $select_group_button.find(".selected-button .label").text(action.label);
$(e.currentTarget).find(".tick-icon").addClass("selected");
$(e.currentTarget).siblings().find(".tick-icon").removeClass("selected");
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index 7b861fe161..c3e998788d 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -322,7 +322,7 @@ frappe.views.CommunicationComposer = class {
},
];
- frappe.utils.add_select_group_button(email_template_actions, "", clear_and_add_template);
+ frappe.utils.add_select_group_button(clear_and_add_template, email_template_actions);
}
setup_last_edited_communication() {
From 17f25aab93139babc85e084eb79ad62d4fef98aa Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 20:38:43 +0530
Subject: [PATCH 077/203] fix: form-builder/new-doctype opens separate dialog
refactored form_builder.js
---
frappe/desk/page/form_builder/form_builder.js | 319 +++++++++---------
1 file changed, 164 insertions(+), 155 deletions(-)
diff --git a/frappe/desk/page/form_builder/form_builder.js b/frappe/desk/page/form_builder/form_builder.js
index cc29084b69..d2c6c8f3ab 100644
--- a/frappe/desk/page/form_builder/form_builder.js
+++ b/frappe/desk/page/form_builder/form_builder.js
@@ -19,7 +19,10 @@ frappe.pages["form-builder"].on_page_show = function (wrapper) {
function load_form_builder(wrapper) {
let route = frappe.get_route();
route = route.filter((a) => a);
- if (route.length > 1) {
+
+ if (route.length > 1 && route[1] === "new-doctype") {
+ new_doctype(route[2]);
+ } else if (route.length > 1) {
let doctype = route[1];
let is_customize_form = route[2] === "customize";
@@ -44,159 +47,165 @@ function load_form_builder(wrapper) {
});
});
} else {
- let d = new frappe.ui.Dialog({
- title: __("Select DocType"),
- fields: [
- {
- label: __("Select DocType"),
- fieldname: "doctype",
- fieldtype: "Link",
- options: "DocType",
- only_select: 1,
- },
- {
- label: __("Customize"),
- fieldname: "customize",
- fieldtype: "Check",
- },
- ],
- primary_action_label: __("Edit"),
- primary_action({ doctype, customize }) {
- if (customize) {
- frappe.model.with_doctype(doctype).then(() => {
- let meta = frappe.get_meta(doctype);
- if (in_list(frappe.model.core_doctypes_list, this.doctype))
- frappe.throw(__("Core DocTypes cannot be customized."));
-
- if (meta.issingle)
- frappe.throw(__("Single DocTypes cannot be customized."));
-
- if (meta.custom)
- frappe.throw(
- __(
- "Only standard DocTypes are allowed to be customized from Customize Form."
- )
- );
- frappe.set_route("form-builder", doctype, "customize");
- });
- } else {
- frappe.set_route("form-builder", doctype);
- }
- },
- secondary_action_label: __("Create New DocType"),
- secondary_action() {
- let doctype = d.get_value("doctype") || "";
- let non_developer =
- frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
- d.hide();
- let new_d = new frappe.ui.Dialog({
- title: __("Create New DocType"),
- fields: [
- {
- label: __("DocType Name"),
- fieldname: "doctype_name",
- fieldtype: "Data",
- default: doctype,
- reqd: 1,
- },
- { fieldtype: "Column Break" },
- {
- label: __("Module"),
- fieldname: "module",
- fieldtype: "Link",
- options: "Module Def",
- reqd: 1,
- },
- { fieldtype: "Section Break" },
- {
- label: __("Is Submittable"),
- fieldname: "is_submittable",
- fieldtype: "Check",
- description: __(
- "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
- ),
- depends_on: "eval:!doc.istable && !doc.issingle",
- },
- {
- label: __("Is Child Table"),
- fieldname: "istable",
- fieldtype: "Check",
- description: __("Child Tables are shown as a Grid in other DocTypes"),
- depends_on: "eval:!doc.is_submittable && !doc.issingle",
- },
- {
- label: __("Editable Grid"),
- fieldname: "editable_grid",
- fieldtype: "Check",
- depends_on: "istable",
- default: 1,
- },
- {
- label: __("Is Single"),
- fieldname: "issingle",
- fieldtype: "Check",
- description: __(
- "Single Types have only one record no tables associated. Values are stored in tabSingles"
- ),
- depends_on: "eval:!doc.istable && !doc.is_submittable",
- },
- {
- label: __("Custom?"),
- fieldname: "custom",
- fieldtype: "Check",
- default: non_developer,
- read_only: non_developer,
- },
- ],
- primary_action_label: __("Create & Continue"),
- primary_action(values) {
- if (!values.istable) values.editable_grid = 0;
- frappe.db
- .insert({
- doctype: "DocType",
- name: values.doctype_name,
- module: values.module,
- istable: values.istable,
- editable_grid: values.editable_grid,
- issingle: values.issingle,
- custom: values.custom,
- is_submittable: values.is_submittable,
- permissions: [
- {
- create: 1,
- delete: 1,
- email: 1,
- export: 1,
- print: 1,
- read: 1,
- report: 1,
- role: "System Manager",
- share: 1,
- write: 1,
- },
- ],
- fields: [
- {
- label: "Title",
- fieldname: "title",
- fieldtype: "Data",
- },
- ],
- })
- .then((doc) => {
- frappe.set_route("form-builder", doc.name);
- });
- },
- secondary_action_label: __("Back"),
- secondary_action() {
- new_d.hide();
- d.show();
- },
- });
- new_d.show();
- },
- });
-
- d.show();
+ select_doctype();
}
}
+
+function select_doctype() {
+ let d = new frappe.ui.Dialog({
+ title: __("Select DocType"),
+ fields: [
+ {
+ label: __("Select DocType"),
+ fieldname: "doctype",
+ fieldtype: "Link",
+ options: "DocType",
+ only_select: 1,
+ },
+ {
+ label: __("Customize"),
+ fieldname: "customize",
+ fieldtype: "Check",
+ },
+ ],
+ primary_action_label: __("Edit"),
+ primary_action({ doctype, customize }) {
+ if (customize) {
+ frappe.model.with_doctype(doctype).then(() => {
+ let meta = frappe.get_meta(doctype);
+ if (in_list(frappe.model.core_doctypes_list, this.doctype))
+ frappe.throw(__("Core DocTypes cannot be customized."));
+
+ if (meta.issingle) frappe.throw(__("Single DocTypes cannot be customized."));
+
+ if (meta.custom)
+ frappe.throw(
+ __(
+ "Only standard DocTypes are allowed to be customized from Customize Form."
+ )
+ );
+ frappe.set_route("form-builder", doctype, "customize");
+ });
+ } else {
+ frappe.set_route("form-builder", doctype);
+ }
+ },
+ secondary_action_label: __("Create New DocType"),
+ secondary_action() {
+ let doctype = d.get_value("doctype") || "";
+ d.hide();
+ frappe.set_route("form-builder", "new-doctype", doctype);
+ },
+ });
+
+ d.show();
+}
+
+function new_doctype(doctype) {
+ let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
+ let new_d = new frappe.ui.Dialog({
+ title: __("Create New DocType"),
+ fields: [
+ {
+ label: __("DocType Name"),
+ fieldname: "doctype_name",
+ fieldtype: "Data",
+ default: doctype,
+ reqd: 1,
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Module"),
+ fieldname: "module",
+ fieldtype: "Link",
+ options: "Module Def",
+ reqd: 1,
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Is Submittable"),
+ fieldname: "is_submittable",
+ fieldtype: "Check",
+ description: __(
+ "Once submitted, submittable documents cannot be changed. They can only be Cancelled and Amended."
+ ),
+ depends_on: "eval:!doc.istable && !doc.issingle",
+ },
+ {
+ label: __("Is Child Table"),
+ fieldname: "istable",
+ fieldtype: "Check",
+ description: __("Child Tables are shown as a Grid in other DocTypes"),
+ depends_on: "eval:!doc.is_submittable && !doc.issingle",
+ },
+ {
+ label: __("Editable Grid"),
+ fieldname: "editable_grid",
+ fieldtype: "Check",
+ depends_on: "istable",
+ default: 1,
+ },
+ {
+ label: __("Is Single"),
+ fieldname: "issingle",
+ fieldtype: "Check",
+ description: __(
+ "Single Types have only one record no tables associated. Values are stored in tabSingles"
+ ),
+ depends_on: "eval:!doc.istable && !doc.is_submittable",
+ },
+ {
+ label: __("Custom?"),
+ fieldname: "custom",
+ fieldtype: "Check",
+ default: non_developer,
+ read_only: non_developer,
+ },
+ ],
+ primary_action_label: __("Create & Continue"),
+ primary_action(values) {
+ if (!values.istable) values.editable_grid = 0;
+ frappe.db
+ .insert({
+ doctype: "DocType",
+ name: values.doctype_name,
+ module: values.module,
+ istable: values.istable,
+ editable_grid: values.editable_grid,
+ issingle: values.issingle,
+ custom: values.custom,
+ is_submittable: values.is_submittable,
+ permissions: [
+ {
+ create: 1,
+ delete: 1,
+ email: 1,
+ export: 1,
+ print: 1,
+ read: 1,
+ report: 1,
+ role: "System Manager",
+ share: 1,
+ write: 1,
+ },
+ ],
+ fields: [
+ {
+ label: "Title",
+ fieldname: "title",
+ fieldtype: "Data",
+ },
+ ],
+ })
+ .then((doc) => {
+ frappe.set_route("form-builder", doc.name);
+ });
+ },
+ secondary_action_label: __("Back"),
+ secondary_action() {
+ new_d.hide();
+ frappe.set_route("form-builder");
+ },
+ });
+ new_d.show();
+}
From 2af7e74eb94d152b56765b9d439c49cbf20ec288 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 20:41:28 +0530
Subject: [PATCH 078/203] fix: made select_doctype accessable gobally which can
be used to change doctype from form-builder
---
frappe/desk/page/form_builder/form_builder.js | 12 ++++-----
frappe/public/js/form_builder/FormBuilder.vue | 26 +------------------
2 files changed, 7 insertions(+), 31 deletions(-)
diff --git a/frappe/desk/page/form_builder/form_builder.js b/frappe/desk/page/form_builder/form_builder.js
index d2c6c8f3ab..0f8b1b9eae 100644
--- a/frappe/desk/page/form_builder/form_builder.js
+++ b/frappe/desk/page/form_builder/form_builder.js
@@ -21,7 +21,7 @@ function load_form_builder(wrapper) {
route = route.filter((a) => a);
if (route.length > 1 && route[1] === "new-doctype") {
- new_doctype(route[2]);
+ frappe.pages["form-builder"].new_doctype(route[2]);
} else if (route.length > 1) {
let doctype = route[1];
let is_customize_form = route[2] === "customize";
@@ -47,11 +47,11 @@ function load_form_builder(wrapper) {
});
});
} else {
- select_doctype();
+ frappe.pages["form-builder"].select_doctype();
}
}
-function select_doctype() {
+frappe.pages["form-builder"].select_doctype = function () {
let d = new frappe.ui.Dialog({
title: __("Select DocType"),
fields: [
@@ -99,9 +99,9 @@ function select_doctype() {
});
d.show();
-}
+};
-function new_doctype(doctype) {
+frappe.pages["form-builder"].new_doctype = function (doctype) {
let non_developer = frappe.session.user !== "Administrator" || !frappe.boot.developer_mode;
let new_d = new frappe.ui.Dialog({
title: __("Create New DocType"),
@@ -208,4 +208,4 @@ function new_doctype(doctype) {
},
});
new_d.show();
-}
+};
diff --git a/frappe/public/js/form_builder/FormBuilder.vue b/frappe/public/js/form_builder/FormBuilder.vue
index fe7eaaa682..2a1441c51a 100644
--- a/frappe/public/js/form_builder/FormBuilder.vue
+++ b/frappe/public/js/form_builder/FormBuilder.vue
@@ -24,31 +24,7 @@ whenever(() => meta_s.value || ctrl_s.value, () => {
function setup_change_doctype_dialog() {
store.page.$title_area.on("click", () => {
- let dialog = new frappe.ui.Dialog({
- title: __("Change DocType"),
- fields: [
- {
- label: __("Select DocType"),
- fieldname: "doctype",
- fieldtype: "Link",
- options: "DocType",
- default: store.doctype || null
- },
- {
- label: __("Customize"),
- fieldname: "customize",
- fieldtype: "Check",
- default: store.is_customize_form
- }
- ],
- primary_action_label: __("Change"),
- primary_action({ doctype }) {
- dialog.hide();
- let customize = dialog.get_value("customize") ? "customize" : "";
- frappe.set_route("form-builder", doctype, customize);
- }
- });
- dialog.show();
+ frappe.pages["form-builder"].select_doctype();
});
}
From ac3fb737021110317e2118fca5d9705f5f256751 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 20:42:43 +0530
Subject: [PATCH 079/203] fix: replaced add doctype button with select group
button
---
frappe/core/doctype/doctype/doctype_list.js | 25 +++++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 frappe/core/doctype/doctype/doctype_list.js
diff --git a/frappe/core/doctype/doctype/doctype_list.js b/frappe/core/doctype/doctype/doctype_list.js
new file mode 100644
index 0000000000..f30e0b7229
--- /dev/null
+++ b/frappe/core/doctype/doctype/doctype_list.js
@@ -0,0 +1,25 @@
+frappe.listview_settings["DocType"] = {
+ onload: function (me) {
+ me.page.btn_primary.addClass("hidden");
+
+ let actions = [
+ {
+ label: __("Add DocType"),
+ description: __("Create a new DocType"),
+ action: () => frappe.new_doc("DocType"),
+ },
+ {
+ label: __("Add DocType (Form Builder)"),
+ description: __("Use the form builder to create a new DocType"),
+ action: () => frappe.set_route("form-builder", "new-doctype"),
+ },
+ ];
+
+ frappe.utils.add_select_group_button(
+ me.page.btn_primary.parent(),
+ actions,
+ "btn-primary",
+ "add"
+ );
+ },
+};
From 3baaea0086574e6b787ded154c324229e663b07a Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 21:02:37 +0530
Subject: [PATCH 080/203] fix: css fix for select group button
---
frappe/public/scss/desk/global.scss | 10 ++++++++--
1 file changed, 8 insertions(+), 2 deletions(-)
diff --git a/frappe/public/scss/desk/global.scss b/frappe/public/scss/desk/global.scss
index fa27ef99ad..b355dbdec2 100644
--- a/frappe/public/scss/desk/global.scss
+++ b/frappe/public/scss/desk/global.scss
@@ -250,8 +250,14 @@ h2 {
}
.select-group-btn {
- .dropdown-toggle-split::after {
- display: none;
+ .dropdown-toggle-split {
+ padding-left: 0.375rem !important;
+ padding-right: 0.375rem !important;
+ min-width: 0 !important;
+
+ &::after {
+ display: none;
+ }
}
.dropdown-item {
From 25a243aa6d0921628ba159e7f52110113f270283 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 21:03:52 +0530
Subject: [PATCH 081/203] fix: replaced add doctype button with select group
button
---
frappe/core/doctype/doctype/doctype_list.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/frappe/core/doctype/doctype/doctype_list.js b/frappe/core/doctype/doctype/doctype_list.js
index f30e0b7229..c66edf1e21 100644
--- a/frappe/core/doctype/doctype/doctype_list.js
+++ b/frappe/core/doctype/doctype/doctype_list.js
@@ -1,7 +1,10 @@
frappe.listview_settings["DocType"] = {
onload: function (me) {
me.page.btn_primary.addClass("hidden");
+ this.setup_select_primary_button(me);
+ },
+ setup_select_primary_button: function (me) {
let actions = [
{
label: __("Add DocType"),
From 0c90d1748f560e86cf5ccaa37d28785a721cabd6 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 26 May 2023 21:12:05 +0530
Subject: [PATCH 082/203] fix: go back instead of opening select_doctype dialog
---
frappe/desk/page/form_builder/form_builder.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/desk/page/form_builder/form_builder.js b/frappe/desk/page/form_builder/form_builder.js
index 0f8b1b9eae..a7a25b5c6c 100644
--- a/frappe/desk/page/form_builder/form_builder.js
+++ b/frappe/desk/page/form_builder/form_builder.js
@@ -204,7 +204,7 @@ frappe.pages["form-builder"].new_doctype = function (doctype) {
secondary_action_label: __("Back"),
secondary_action() {
new_d.hide();
- frappe.set_route("form-builder");
+ window.history.back();
},
});
new_d.show();
From b308e000ef409782657600a1f17261ff52d37149 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Sat, 27 May 2023 13:00:31 +0530
Subject: [PATCH 083/203] fix: init doctype to trigger doctype_list events on
redirecting
---
frappe/public/js/form_builder/store.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js
index d69f32eeb1..fe7da984fe 100644
--- a/frappe/public/js/form_builder/store.js
+++ b/frappe/public/js/form_builder/store.js
@@ -122,6 +122,7 @@ export const useStore = defineStore("form-builder-store", () => {
}
function setup_breadcrumbs() {
+ frappe.model.init_doctype("DocType");
let breadcrumbs = `
${__("DocType")}
${__(doctype.value)}
From 1e47282d86269d9ba4d696c9b6c91bdcb7fdc7e2 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Sat, 27 May 2023 14:46:43 +0530
Subject: [PATCH 084/203] fix: only init doctype for doctype form
---
frappe/public/js/form_builder/store.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/form_builder/store.js b/frappe/public/js/form_builder/store.js
index fe7da984fe..b60bdc7919 100644
--- a/frappe/public/js/form_builder/store.js
+++ b/frappe/public/js/form_builder/store.js
@@ -122,7 +122,7 @@ export const useStore = defineStore("form-builder-store", () => {
}
function setup_breadcrumbs() {
- frappe.model.init_doctype("DocType");
+ !is_customize_form.value && frappe.model.init_doctype("DocType");
let breadcrumbs = `
${__("DocType")}
${__(doctype.value)}
From 7fdc6007ba76b745362b0f683a8593f72514edd5 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Sat, 27 May 2023 14:47:52 +0530
Subject: [PATCH 085/203] refactor: show/hide form builder action buttons
---
.../js/form_builder/form_builder.bundle.js | 43 +++++++++++++------
1 file changed, 29 insertions(+), 14 deletions(-)
diff --git a/frappe/public/js/form_builder/form_builder.bundle.js b/frappe/public/js/form_builder/form_builder.bundle.js
index 0e54bdd80b..b1c04584be 100644
--- a/frappe/public/js/form_builder/form_builder.bundle.js
+++ b/frappe/public/js/form_builder/form_builder.bundle.js
@@ -57,10 +57,10 @@ class FormBuilder {
}
);
- this.customize_form_btn = this.page.add_menu_item(__("Switch to Customize Form"), () => {
+ this.customize_form_btn = this.page.add_menu_item(__("Switch to Customize"), () => {
frappe.set_route("form-builder", this.doctype, "customize");
});
- this.doctype_form_btn = this.page.add_menu_item(__("Switch to DocType Form"), () => {
+ this.doctype_form_btn = this.page.add_menu_item(__("Switch to DocType"), () => {
frappe.set_route("form-builder", this.doctype);
});
@@ -106,21 +106,36 @@ class FormBuilder {
this.reset_changes_btn.hide();
}
- // toggle doctype / customize form btn based on url
- this.customize_form_btn.toggle(!this.store.is_customize_form);
- this.doctype_form_btn.toggle(this.store.is_customize_form);
+ // hide all buttons
+ this.go_to_doctype_list_btn.hide();
+ this.customize_form_btn.hide();
+ this.doctype_form_btn.hide();
+ this.go_to_doctype_btn.hide();
+ this.go_to_customize_form_btn.hide();
- // hide customize form & Go to customize form btn
+ // show customize form & Go to customize form btn
if (
this.store.doc &&
- (this.store.doc.custom ||
- this.store.doc.issingle ||
- in_list(frappe.model.core_doctypes_list, this.doctype))
+ !this.store.doc.custom &&
+ !this.store.doc.issingle &&
+ !this.store.is_customize_form &&
+ !in_list(frappe.model.core_doctypes_list, this.doctype)
) {
- this.customize_form_btn.hide();
- if (this.doctype != "Customize Form") {
- this.go_to_customize_form_btn.hide();
- }
+ this.customize_form_btn.show();
+ this.go_to_customize_form_btn.show();
+ hide_menu = false;
+ }
+
+ // show doctype form & Go to doctype form btn
+ if (
+ this.store.doc &&
+ !this.store.doc.custom &&
+ !this.store.doc.issingle &&
+ this.store.is_customize_form
+ ) {
+ this.doctype_form_btn.show();
+ this.go_to_doctype_btn.show();
+ hide_menu = false;
}
// show Go to {0} List or Go to {0} button
@@ -129,7 +144,7 @@ class FormBuilder {
? __("Go to {0}", [__(this.doctype)])
: __("Go to {0} List", [__(this.doctype)]);
- this.go_to_doctype_list_btn.text(label);
+ this.go_to_doctype_list_btn.text(label).show();
}
// toggle preview btn text
From 064caaa3b40e8bebf7d88b33b205c692ef60ede5 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Sat, 27 May 2023 14:59:58 +0530
Subject: [PATCH 086/203] fix: only show menu button if it contains action
items
---
frappe/public/js/form_builder/form_builder.bundle.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/frappe/public/js/form_builder/form_builder.bundle.js b/frappe/public/js/form_builder/form_builder.bundle.js
index b1c04584be..87767d6961 100644
--- a/frappe/public/js/form_builder/form_builder.bundle.js
+++ b/frappe/public/js/form_builder/form_builder.bundle.js
@@ -113,6 +113,9 @@ class FormBuilder {
this.go_to_doctype_btn.hide();
this.go_to_customize_form_btn.hide();
+ this.page.menu_btn_group.show();
+ let hide_menu = true;
+
// show customize form & Go to customize form btn
if (
this.store.doc &&
@@ -147,6 +150,10 @@ class FormBuilder {
this.go_to_doctype_list_btn.text(label).show();
}
+ if (hide_menu && window.matchMedia("(min-device-width: 992px)").matches) {
+ this.page.menu_btn_group.hide();
+ }
+
// toggle preview btn text
this.preview_btn.text(this.store.preview ? __("Hide Preview") : __("Show Preview"));
From a556cd6a589a51e7b0749b7608c3fc2858c05d18 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Sat, 27 May 2023 15:16:46 +0530
Subject: [PATCH 087/203] test: fixed failing UI test
---
cypress/integration/form_builder.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/cypress/integration/form_builder.js b/cypress/integration/form_builder.js
index 968f6aaaf0..b298abdbe7 100644
--- a/cypress/integration/form_builder.js
+++ b/cypress/integration/form_builder.js
@@ -26,11 +26,11 @@ context("Form Builder", () => {
cy.get(".page-title").click();
cy.get(".frappe-control[data-fieldname='doctype'] input").click().as("input");
- cy.get("@input").type("{rightArrow} Field", { delay: 200 });
+ cy.get("@input").type("{rightArrow}Web Form Field", { delay: 200 });
cy.wait("@search_link");
cy.get("@input").type("{enter}").blur();
- cy.click_modal_primary_button("Change");
+ cy.click_modal_primary_button("Edit");
cy.get(".page-title .title-text").should("have.text", "Web Form Field");
});
From b00aac92ba741bd3f25cb2302bd2018d1a49f61b Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Sat, 27 May 2023 16:49:13 +0530
Subject: [PATCH 088/203] feat: Allow setting cron as Server Script frequency
---
frappe/core/doctype/server_script/server_script.json | 11 +++++++++--
frappe/core/doctype/server_script/server_script.py | 8 ++++++--
2 files changed, 15 insertions(+), 4 deletions(-)
diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json
index 3aedd4f542..67cb6e75ea 100644
--- a/frappe/core/doctype/server_script/server_script.json
+++ b/frappe/core/doctype/server_script/server_script.json
@@ -9,6 +9,7 @@
"script_type",
"reference_doctype",
"event_frequency",
+ "cron_format",
"doctype_event",
"api_method",
"allow_guest",
@@ -99,7 +100,7 @@
"fieldtype": "Select",
"label": "Event Frequency",
"mandatory_depends_on": "eval:doc.script_type == \"Scheduler Event\"",
- "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long"
+ "options": "All\nHourly\nDaily\nWeekly\nMonthly\nYearly\nHourly Long\nDaily Long\nWeekly Long\nMonthly Long\nCron"
},
{
"fieldname": "module",
@@ -132,6 +133,12 @@
"fieldname": "rate_limit_seconds",
"fieldtype": "Int",
"label": "Time Window (Seconds)"
+ },
+ {
+ "depends_on": "eval:doc.event_frequency==='Cron'",
+ "fieldname": "cron_format",
+ "fieldtype": "Data",
+ "label": "Cron Format"
}
],
"index_web_pages_for_search": 1,
@@ -141,7 +148,7 @@
"link_fieldname": "server_script"
}
],
- "modified": "2023-05-16 11:03:58.282680",
+ "modified": "2023-05-27 16:33:16.595424",
"modified_by": "Administrator",
"module": "Core",
"name": "Server Script",
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index a9b870e240..50f13fedaa 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -52,7 +52,9 @@ class ServerScript(Document):
def sync_scheduler_events(self):
"""Create or update Scheduled Job Type documents for Scheduler Event Server Scripts"""
if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event":
- setup_scheduler_events(script_name=self.name, frequency=self.event_frequency)
+ setup_scheduler_events(
+ script_name=self.name, frequency=self.event_frequency, cron_format=self.cron_format
+ )
def clear_scheduled_events(self):
"""Deletes existing scheduled jobs by Server Script if self.event_frequency has changed"""
@@ -171,7 +173,7 @@ class ServerScript(Document):
return items
-def setup_scheduler_events(script_name, frequency):
+def setup_scheduler_events(script_name: str, frequency: str, cron_format: str | None = None):
"""Creates or Updates Scheduled Job Type documents based on the specified script name and frequency
Args:
@@ -188,6 +190,7 @@ def setup_scheduler_events(script_name, frequency):
"method": method,
"frequency": frequency,
"server_script": script_name,
+ "cron_format": cron_format,
}
).insert()
@@ -200,6 +203,7 @@ def setup_scheduler_events(script_name, frequency):
return
doc.frequency = frequency
+ doc.cron_format = cron_format
doc.save()
frappe.msgprint(_("Scheduled execution for script {0} has updated").format(script_name))
From d6d5d488a22877b6626e14f92c51bf5476fac05f Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Mon, 29 May 2023 09:59:45 +0530
Subject: [PATCH 089/203] fix: ensure correct return value for
`get_values_from_single`
---
frappe/database/database.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 59c514991a..8b077ce4f7 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -617,10 +617,10 @@ class Database:
return []
if as_dict:
- return values and [values] or []
+ return [values] if values else []
if isinstance(fields, list):
- return [map(values.get, fields)]
+ return [list(map(values.get, fields))]
else:
r = frappe.qb.get_query(
From 38c1207abe78c123dae66f35849052202f9f435d Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Mon, 29 May 2023 10:48:26 +0530
Subject: [PATCH 090/203] fix: multiple fixes to `Engine.get_query` (#21135)
---
frappe/database/query.py | 27 +++++++++++++++++----------
1 file changed, 17 insertions(+), 10 deletions(-)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 595bd5a3ff..25fa6a0528 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -25,6 +25,10 @@ BRACKETS_PATTERN = re.compile(r"\(.*?\)|$")
SQL_FUNCTIONS = [sql_function.value for sql_function in SqlFunctions]
COMMA_PATTERN = re.compile(r",\s*(?![^()]*\))")
+# less restrictive version of frappe.core.doctype.doctype.doctype.START_WITH_LETTERS_PATTERN
+# to allow table names like __Auth
+TABLE_NAME_PATTERN = re.compile(r"^[\w -]*$", flags=re.ASCII)
+
class Engine:
def get_query(
@@ -50,6 +54,7 @@ class Engine:
self.doctype = get_doctype_name(table.get_sql())
else:
self.doctype = table
+ self.validate_doctype()
self.table = frappe.qb.DocType(table)
if update:
@@ -82,6 +87,10 @@ class Engine:
return self.query
+ def validate_doctype(self):
+ if not TABLE_NAME_PATTERN.match(self.doctype):
+ frappe.throw(_("Invalid DocType: {0}").format(self.doctype))
+
def apply_fields(self, fields):
# add fields
self.fields = self.parse_fields(fields)
@@ -135,9 +144,12 @@ class Engine:
self._apply_filter(field, value, operator, doctype)
def apply_dict_filters(self, filters: dict[str, str | int | list]):
- for key in filters:
- value = filters.get(key)
- self._apply_filter(key, value)
+ for field, value in filters.items():
+ operator = "="
+ if isinstance(value, (list, tuple)):
+ operator, value = value
+
+ self._apply_filter(field, value, operator)
def _apply_filter(
self, field: str, value: str | int | list | None, operator: str = "=", doctype: str | None = None
@@ -168,15 +180,10 @@ class Engine:
(table.parent == self.table.name) & (table.parenttype == self.doctype)
)
- if isinstance(_value, (list, tuple)):
- _operator, _value = _value
- elif isinstance(_value, bool):
+ if isinstance(_value, bool):
_value = int(_value)
- if isinstance(_value, str) and has_function(_value):
- _value = self.get_function_object(_value)
-
- if isinstance(_value, (list, tuple)) and not _value:
+ elif not _value and isinstance(_value, (list, tuple)):
_value = ("",)
# Nested set
From b66d8e8a40f17625b783188c841a57bbbdcc106b Mon Sep 17 00:00:00 2001
From: Raphael Krupinski
Date: Mon, 29 May 2023 07:31:51 +0200
Subject: [PATCH 091/203] chore(DX): add type hints to Document, BaseDocument
and get_doc (#21060)
* chore: add type hints to Document, BaseDocument and get_doc
* refactor: better type hints
get_doc has multiple ways to use it, added all known ways
---------
Co-authored-by: Raphael Krupinski <10319569-mattesilver@users.noreply.gitlab.com>
Co-authored-by: Ankush Menat
---
frappe/__init__.py | 39 +++++++++++++++++++++++++++++++++++++--
1 file changed, 37 insertions(+), 2 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 5a03438a9e..5efdfd8ce9 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -17,7 +17,7 @@ import json
import os
import re
import warnings
-from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, overload
+from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, TypeAlias, overload
import click
from werkzeug.local import Local, release_local
@@ -1142,7 +1142,42 @@ def get_cached_value(
return values
-def get_doc(*args, **kwargs) -> "Document":
+_SingleDocument: TypeAlias = "Document"
+_NewDocument: TypeAlias = "Document"
+
+
+@overload
+def get_doc(document: "Document", /) -> "Document":
+ pass
+
+
+@overload
+def get_doc(doctype: str, /) -> _SingleDocument:
+ """Retrieve Single DocType from DB, doctype must be positional argument."""
+ pass
+
+
+@overload
+def get_doc(doctype: str, name: str, /, for_update: bool | None = None) -> "Document":
+ """Retrieve DocType from DB, doctype and name must be positional argument."""
+ pass
+
+
+@overload
+def get_doc(**kwargs: dict) -> "_NewDocument":
+ """Initialize document from kwargs.
+ Not recommended. Use `frappe.new_doc` instead."""
+ pass
+
+
+@overload
+def get_doc(documentdict: dict) -> "_NewDocument":
+ """Create document from dict.
+ Not recommended. Use `frappe.new_doc` instead."""
+ pass
+
+
+def get_doc(*args, **kwargs):
"""Return a `frappe.model.document.Document` object of the given type and name.
:param arg1: DocType name as string **or** document JSON.
From b1433af3d7c1e0428229f21e59c82988a1f287e5 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 12:43:55 +0530
Subject: [PATCH 092/203] fix: Dont show save step if frm is not dirty
Use case: single doctype review, in all other cases document will be
dirty and hence save step will be added
---
frappe/public/js/frappe/form/form_tour.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index f7953c08b2..87de149d07 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -83,7 +83,7 @@ frappe.ui.form.FormTour = class FormTour {
if (step.fieldtype == "Attach Image") this.handle_attach_image_steps(step);
});
- if (this.tour.save_on_complete) {
+ if (this.tour.save_on_complete && this.frm.is_dirty()) {
this.add_step_to_save();
}
}
From a2a84215b1ac02e456d59922f9587a056219b302 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 12:53:25 +0530
Subject: [PATCH 093/203] fix: render first incomplete step instead of first
step
---
frappe/public/js/frappe/widgets/onboarding_widget.js | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 3d7dbdf49f..378c054c44 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -33,7 +33,13 @@ export default class OnboardingWidget extends Widget {
this.add_step(step, index);
});
- this.show_step(this.steps[0]);
+ let first_incomplete_step = this.steps.findIndex((s) => !s.is_skipped && !s.is_complete);
+
+ if (first_incomplete_step == -1) {
+ first_incomplete_step = 0;
+ }
+
+ this.show_step(this.steps[first_incomplete_step]);
}
add_step(step, index) {
From 2ed2f8747d508a7db0bc1cc56f6b3286252e8eb0 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 13:27:15 +0530
Subject: [PATCH 094/203] chore: track doc creation
This is alternate to "Activation" level that already exists, activation
level will be phased out over time.
---
frappe/desk/form/save.py | 2 ++
frappe/utils/telemetry.py | 10 ++++++++++
2 files changed, 12 insertions(+)
diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py
index 9ee2541a90..75335cb1ce 100644
--- a/frappe/desk/form/save.py
+++ b/frappe/desk/form/save.py
@@ -9,12 +9,14 @@ from frappe.desk.form.load import run_onload
from frappe.model.docstatus import DocStatus
from frappe.monitor import add_data_to_monitor
from frappe.utils.scheduler import is_scheduler_inactive
+from frappe.utils.telemetry import capture_doc
@frappe.whitelist()
def savedocs(doc, action):
"""save / submit / update doclist"""
doc = frappe.get_doc(json.loads(doc))
+ capture_doc(doc)
set_local_name(doc)
# action
diff --git a/frappe/utils/telemetry.py b/frappe/utils/telemetry.py
index ba06afbf83..e15146c71d 100644
--- a/frappe/utils/telemetry.py
+++ b/frappe/utils/telemetry.py
@@ -57,3 +57,13 @@ def capture(event, app, **kwargs):
ph: Posthog = getattr(frappe.local, "posthog", None)
with suppress(Exception):
ph and ph.capture(distinct_id=frappe.local.site, event=f"{app}_{event}", **kwargs)
+
+
+def capture_doc(doc):
+ with suppress(Exception):
+ age = site_age()
+ if not age or age > 15:
+ return
+
+ if doc.get("__islocal") or not doc.get("name"):
+ capture("document_created", "frappe", properties={"doctype": doc.doctype})
From 30c866106327acbcb92e361bde96aaeed3b845de Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 15:05:46 +0530
Subject: [PATCH 095/203] fix(UX): add title for documentation link
---
frappe/public/js/frappe/form/controls/base_input.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 7831a9e9b6..5aacc193d1 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -178,7 +178,7 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
let $help = this.$wrapper.find("span.help");
$help.empty();
- $(`
+ $(`
${frappe.utils.icon("help", "sm")}
`).appendTo($help);
}
From e5878b0c68145082ed3a1d01cec4464238e8297d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 15:35:38 +0530
Subject: [PATCH 096/203] test: reduce duplication
---
frappe/tests/test_nestedset.py | 68 ++++++++++++++++------------------
1 file changed, 32 insertions(+), 36 deletions(-)
diff --git a/frappe/tests/test_nestedset.py b/frappe/tests/test_nestedset.py
index ef63fb66c2..07f674a478 100644
--- a/frappe/tests/test_nestedset.py
+++ b/frappe/tests/test_nestedset.py
@@ -51,35 +51,35 @@ records = [
},
]
+TEST_DOCTYPE = "Test Tree DocType"
+
class NestedSetTestUtil:
def setup_test_doctype(self):
- frappe.db.sql("delete from `tabDocType` where `name` = 'Test Tree DocType'")
- frappe.db.sql_ddl("drop table if exists `tabTest Tree DocType`")
+ frappe.db.delete("DocType", TEST_DOCTYPE)
+ frappe.db.sql_ddl(f"drop table if exists `{TEST_DOCTYPE}`")
- self.tree_doctype = new_doctype(
- "Test Tree DocType", is_tree=True, autoname="field:some_fieldname"
- )
+ self.tree_doctype = new_doctype(TEST_DOCTYPE, is_tree=True, autoname="field:some_fieldname")
self.tree_doctype.insert()
for record in records:
- d = frappe.new_doc("Test Tree DocType")
+ d = frappe.new_doc(TEST_DOCTYPE)
d.update(record)
d.insert()
def teardown_test_doctype(self):
self.tree_doctype.delete()
- frappe.db.sql_ddl("drop table if exists `tabTest Tree DocType`")
+ frappe.db.sql_ddl(f"drop table if exists `{TEST_DOCTYPE}`")
def move_it_back(self):
- parent_1 = frappe.get_doc("Test Tree DocType", "Parent 1")
+ parent_1 = frappe.get_doc(TEST_DOCTYPE, "Parent 1")
parent_1.parent_test_tree_doctype = "Root Node"
parent_1.save()
def get_no_of_children(self, record_name: str) -> int:
if not record_name:
- return frappe.db.count("Test Tree DocType")
- return len(get_descendants_of("Test Tree DocType", record_name, ignore_permissions=True))
+ return frappe.db.count(TEST_DOCTYPE)
+ return len(get_descendants_of(TEST_DOCTYPE, record_name, ignore_permissions=True))
class TestNestedSet(FrappeTestCase):
@@ -101,18 +101,18 @@ class TestNestedSet(FrappeTestCase):
global records
min_lft = 1
- max_rgt = frappe.qb.from_("Test Tree DocType").select(Max(Field("rgt"))).run(pluck=True)[0]
+ max_rgt = frappe.qb.from_(TEST_DOCTYPE).select(Max(Field("rgt"))).run(pluck=True)[0]
for record in records:
lft, rgt, parent_test_tree_doctype = frappe.db.get_value(
- "Test Tree DocType",
+ TEST_DOCTYPE,
record["some_fieldname"],
["lft", "rgt", "parent_test_tree_doctype"],
)
if parent_test_tree_doctype:
parent_lft, parent_rgt = frappe.db.get_value(
- "Test Tree DocType", parent_test_tree_doctype, ["lft", "rgt"]
+ TEST_DOCTYPE, parent_test_tree_doctype, ["lft", "rgt"]
)
else:
# root
@@ -138,19 +138,19 @@ class TestNestedSet(FrappeTestCase):
self.assertTrue(parent_rgt == (parent_lft + 1 + (2 * no_of_children)))
def test_recursion(self):
- leaf_node = frappe.get_doc("Test Tree DocType", {"some_fieldname": "Parent 2"})
+ leaf_node = frappe.get_doc(TEST_DOCTYPE, {"some_fieldname": "Parent 2"})
leaf_node.parent_test_tree_doctype = "Child 3"
self.assertRaises(NestedSetRecursionError, leaf_node.save)
leaf_node.reload()
def test_rebuild_tree(self):
- rebuild_tree("Test Tree DocType", "parent_test_tree_doctype")
+ rebuild_tree(TEST_DOCTYPE, "parent_test_tree_doctype")
self.test_basic_tree()
def test_move_group_into_another(self):
- old_lft, old_rgt = frappe.db.get_value("Test Tree DocType", "Parent 2", ["lft", "rgt"])
+ old_lft, old_rgt = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
- parent_1 = frappe.get_doc("Test Tree DocType", "Parent 1")
+ parent_1 = frappe.get_doc(TEST_DOCTYPE, "Parent 1")
lft, rgt = parent_1.lft, parent_1.rgt
parent_1.parent_test_tree_doctype = "Parent 2"
@@ -158,7 +158,7 @@ class TestNestedSet(FrappeTestCase):
self.test_basic_tree()
# after move
- new_lft, new_rgt = frappe.db.get_value("Test Tree DocType", "Parent 2", ["lft", "rgt"])
+ new_lft, new_rgt = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
# lft should reduce
self.assertEqual(old_lft - new_lft, rgt - lft + 1)
@@ -170,12 +170,10 @@ class TestNestedSet(FrappeTestCase):
self.test_basic_tree()
def test_move_leaf_into_another_group(self):
- child_2 = frappe.get_doc("Test Tree DocType", "Child 2")
+ child_2 = frappe.get_doc(TEST_DOCTYPE, "Child 2")
# assert that child 2 is not already under parent 1
- parent_lft_old, parent_rgt_old = frappe.db.get_value(
- "Test Tree DocType", "Parent 2", ["lft", "rgt"]
- )
+ parent_lft_old, parent_rgt_old = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
self.assertTrue((parent_lft_old > child_2.lft) and (parent_rgt_old > child_2.rgt))
child_2.parent_test_tree_doctype = "Parent 2"
@@ -183,22 +181,20 @@ class TestNestedSet(FrappeTestCase):
self.test_basic_tree()
# assert that child 2 is under parent 1
- parent_lft_new, parent_rgt_new = frappe.db.get_value(
- "Test Tree DocType", "Parent 2", ["lft", "rgt"]
- )
+ parent_lft_new, parent_rgt_new = frappe.db.get_value(TEST_DOCTYPE, "Parent 2", ["lft", "rgt"])
self.assertFalse((parent_lft_new > child_2.lft) and (parent_rgt_new > child_2.rgt))
def test_delete_leaf(self):
global records
el = {"some_fieldname": "Child 1", "parent_test_tree_doctype": "Parent 1", "is_group": 0}
- child_1 = frappe.get_doc("Test Tree DocType", "Child 1")
+ child_1 = frappe.get_doc(TEST_DOCTYPE, "Child 1")
child_1.delete()
records.remove(el)
self.test_basic_tree()
- n = frappe.new_doc("Test Tree DocType")
+ n = frappe.new_doc(TEST_DOCTYPE)
n.update(el)
n.insert()
records.append(el)
@@ -208,10 +204,10 @@ class TestNestedSet(FrappeTestCase):
def test_delete_group(self):
# cannot delete group with child, but can delete leaf
with self.assertRaises(NestedSetChildExistsError):
- frappe.delete_doc("Test Tree DocType", "Parent 1")
+ frappe.delete_doc(TEST_DOCTYPE, "Parent 1")
def test_remove_subtree(self):
- remove_subtree("Test Tree DocType", "Parent 2")
+ remove_subtree(TEST_DOCTYPE, "Parent 2")
self.test_basic_tree()
def test_rename_nestedset(self):
@@ -223,7 +219,7 @@ class TestNestedSet(FrappeTestCase):
def test_merge_groups(self):
global records
el = {"some_fieldname": "Parent 2", "parent_test_tree_doctype": "Root Node", "is_group": 1}
- frappe.rename_doc("Test Tree DocType", "Parent 2", "Parent 1", merge=True)
+ frappe.rename_doc(TEST_DOCTYPE, "Parent 2", "Parent 1", merge=True)
records.remove(el)
self.test_basic_tree()
@@ -232,7 +228,7 @@ class TestNestedSet(FrappeTestCase):
el = {"some_fieldname": "Child 3", "parent_test_tree_doctype": "Parent 2", "is_group": 0}
frappe.rename_doc(
- "Test Tree DocType",
+ TEST_DOCTYPE,
"Child 3",
"Child 2",
merge=True,
@@ -242,17 +238,17 @@ class TestNestedSet(FrappeTestCase):
def test_merge_leaf_into_group(self):
with self.assertRaises(NestedSetInvalidMergeError):
- frappe.rename_doc("Test Tree DocType", "Child 1", "Parent 1", merge=True)
+ frappe.rename_doc(TEST_DOCTYPE, "Child 1", "Parent 1", merge=True)
def test_merge_group_into_leaf(self):
with self.assertRaises(NestedSetInvalidMergeError):
- frappe.rename_doc("Test Tree DocType", "Parent 1", "Child 1", merge=True)
+ frappe.rename_doc(TEST_DOCTYPE, "Parent 1", "Child 1", merge=True)
def test_root_deletion(self):
for doc in ["Child 3", "Child 2", "Child 1", "Parent 2", "Parent 1"]:
- frappe.delete_doc("Test Tree DocType", doc)
+ frappe.delete_doc(TEST_DOCTYPE, doc)
- root_node = frappe.get_doc("Test Tree DocType", "Root Node")
+ root_node = frappe.get_doc(TEST_DOCTYPE, "Root Node")
# root deletion with allow_root_deletion
# patched as delete_doc create a new instance of Root Node (using get_doc)
@@ -263,4 +259,4 @@ class TestNestedSet(FrappeTestCase):
# root deletion without allow_root_deletion
root_node.delete()
- self.assertFalse(frappe.db.exists("Test Tree DocType", "Root Node"))
+ self.assertFalse(frappe.db.exists(TEST_DOCTYPE, "Root Node"))
From 08f0f8beb142719e77cc8c62266eec1177f2a398 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 29 May 2023 15:56:38 +0530
Subject: [PATCH 097/203] fix: make desk css available i shadow html
---
frappe/desk/doctype/custom_html_block/custom_html_block.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.js b/frappe/desk/doctype/custom_html_block/custom_html_block.js
index 727a73c92f..0cd9a2700e 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.js
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.js
@@ -21,6 +21,11 @@ function render_custom_html_block(frm) {
let div = document.createElement("div");
div.innerHTML = frappe.dom.remove_script_and_style(frm.doc.html);
+ // link global desk css
+ let link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.href = frappe.assets.bundled_asset("desk.bundle.css");
+
// css
let style = document.createElement("style");
style.textContent = frm.doc.style;
@@ -36,6 +41,7 @@ function render_custom_html_block(frm) {
`;
this.attachShadow({ mode: "open" });
+ this.shadowRoot?.appendChild(link);
this.shadowRoot?.appendChild(div);
this.shadowRoot?.appendChild(style);
this.shadowRoot?.appendChild(script);
From 62565b51919b5493fb91b8bd7bb1b2a372d20394 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 29 May 2023 15:57:58 +0530
Subject: [PATCH 098/203] fix: made a util to create shadow element
---
.../custom_html_block/custom_html_block.js | 42 +-----------------
frappe/public/js/frappe/dom.js | 44 +++++++++++++++++++
.../js/frappe/widgets/custom_block_widget.js | 44 +++----------------
3 files changed, 51 insertions(+), 79 deletions(-)
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.js b/frappe/desk/doctype/custom_html_block/custom_html_block.js
index 0cd9a2700e..3875117103 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.js
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.js
@@ -11,45 +11,5 @@ function render_custom_html_block(frm) {
let wrapper = frm.fields_dict["preview"].wrapper;
wrapper.classList.add("mb-3");
- let random_id = "custom-block-" + frappe.utils.get_random(5).toLowerCase();
-
- class CustomBlock extends HTMLElement {
- constructor() {
- super();
-
- // html
- let div = document.createElement("div");
- div.innerHTML = frappe.dom.remove_script_and_style(frm.doc.html);
-
- // link global desk css
- let link = document.createElement("link");
- link.rel = "stylesheet";
- link.href = frappe.assets.bundled_asset("desk.bundle.css");
-
- // css
- let style = document.createElement("style");
- style.textContent = frm.doc.style;
-
- // javascript
- let script = document.createElement("script");
- script.textContent = `
- (function() {
- let cname = ${JSON.stringify(random_id)};
- let root_element = document.querySelector(cname).shadowRoot;
- ${frm.doc.script}
- })();
- `;
-
- this.attachShadow({ mode: "open" });
- this.shadowRoot?.appendChild(link);
- this.shadowRoot?.appendChild(div);
- this.shadowRoot?.appendChild(style);
- this.shadowRoot?.appendChild(script);
- }
- }
-
- if (!customElements.get(random_id)) {
- customElements.define(random_id, CustomBlock);
- }
- wrapper.innerHTML = `<${random_id}>${random_id}>`;
+ frappe.create_shadow_element(wrapper, frm.doc.html, frm.doc.style, frm.doc.script);
}
diff --git a/frappe/public/js/frappe/dom.js b/frappe/public/js/frappe/dom.js
index 2e4fec371c..a12e56d0d7 100644
--- a/frappe/public/js/frappe/dom.js
+++ b/frappe/public/js/frappe/dom.js
@@ -359,6 +359,50 @@ frappe.is_online = function () {
return true;
};
+frappe.create_shadow_element = function (wrapper, html, css, js) {
+ let random_id = "custom-block-" + frappe.utils.get_random(5).toLowerCase();
+
+ class CustomBlock extends HTMLElement {
+ constructor() {
+ super();
+
+ // html
+ let div = document.createElement("div");
+ div.innerHTML = frappe.dom.remove_script_and_style(html);
+
+ // link global desk css
+ let link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.href = frappe.assets.bundled_asset("desk.bundle.css");
+
+ // css
+ let style = document.createElement("style");
+ style.textContent = css;
+
+ // javascript
+ let script = document.createElement("script");
+ script.textContent = `
+ (function() {
+ let cname = ${JSON.stringify(random_id)};
+ let root_element = document.querySelector(cname).shadowRoot;
+ ${js}
+ })();
+ `;
+
+ this.attachShadow({ mode: "open" });
+ this.shadowRoot?.appendChild(link);
+ this.shadowRoot?.appendChild(div);
+ this.shadowRoot?.appendChild(style);
+ this.shadowRoot?.appendChild(script);
+ }
+ }
+
+ if (!customElements.get(random_id)) {
+ customElements.define(random_id, CustomBlock);
+ }
+ wrapper.innerHTML = `<${random_id}>${random_id}>`;
+};
+
// bind online/offline events
$(window).on("online", function () {
frappe.show_alert({
diff --git a/frappe/public/js/frappe/widgets/custom_block_widget.js b/frappe/public/js/frappe/widgets/custom_block_widget.js
index c64c2340a3..2ee9f06b06 100644
--- a/frappe/public/js/frappe/widgets/custom_block_widget.js
+++ b/frappe/public/js/frappe/widgets/custom_block_widget.js
@@ -27,44 +27,12 @@ export default class CustomBlockWidget extends Widget {
await this.get_custom_block_data();
this.body.empty();
- this.random_id = "custom-block-" + frappe.utils.get_random(5).toLowerCase();
-
- let me = this;
-
- class CustomBlock extends HTMLElement {
- constructor() {
- super();
-
- // html
- let div = document.createElement("div");
- div.innerHTML = frappe.dom.remove_script_and_style(me.custom_block_doc.html);
-
- // css
- let style = document.createElement("style");
- style.textContent = me.custom_block_doc.style;
-
- // js
- let script = document.createElement("script");
- script.textContent = `
- (function() {
- let cname = ${JSON.stringify(me.random_id)};
- let root_element = document.querySelector(cname).shadowRoot;
- ${me.custom_block_doc.script}
- })();
- `;
-
- this.attachShadow({ mode: "open" });
- this.shadowRoot?.appendChild(div);
- this.shadowRoot?.appendChild(style);
- this.shadowRoot?.appendChild(script);
- }
- }
-
- if (!customElements.get(this.random_id)) {
- customElements.define(this.random_id, CustomBlock);
- }
-
- this.body.append(`<${this.random_id}>${this.random_id}>`);
+ frappe.create_shadow_element(
+ this.body[0],
+ this.custom_block_doc.html,
+ this.custom_block_doc.style,
+ this.custom_block_doc.script
+ );
}
async get_custom_block_data() {
From 2a97e3b19ffef94779488cad3d143c1a4200bc63 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 29 May 2023 15:59:41 +0530
Subject: [PATCH 099/203] fix: removed html message since we no longer have no
ristrictions
---
.../doctype/custom_html_block/custom_html_block.json | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.json b/frappe/desk/doctype/custom_html_block/custom_html_block.json
index 6c3d80fba9..3fab5d7902 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.json
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.json
@@ -9,7 +9,6 @@
"preview_section",
"preview",
"html_section",
- "html_message",
"html",
"javascript_section",
"js_message",
@@ -71,12 +70,6 @@
"label": "JS Message",
"options": "To interact with above HTML you will have to use `root_element` as a parent selector.
For example:
// here root_element is provided by default\nlet some_class_element = root_element.querySelector('.some-class');\nsome_class_element.textContent = \"New content\";\n "
},
- {
- "fieldname": "html_message",
- "fieldtype": "HTML",
- "label": "HTML Message",
- "options": "You cannot use global class on elements. The css for those classes will not be applied on this HTML, you will have to rewrite styles again in CSS field
For Example:
\n// style for class m-3 will not work\n <div class=\"m-3\"></div> \n // You will have to add style of m-3 in CSS field below like\n .m-3 {\n margin: 14px!important\n }\n "
- },
{
"fieldname": "roles_section",
"fieldtype": "Section Break",
@@ -91,7 +84,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-05-17 17:17:04.232519",
+ "modified": "2023-05-29 18:28:28.326843",
"modified_by": "Administrator",
"module": "Desk",
"name": "Custom HTML Block",
From d00e41d725ded5ec8bbb019676c73c91e28c8ba8 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 29 May 2023 16:21:00 +0530
Subject: [PATCH 100/203] refactor: rearranged code
---
.../doctype/custom_html_block/custom_html_block.js | 12 ++++--------
1 file changed, 4 insertions(+), 8 deletions(-)
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.js b/frappe/desk/doctype/custom_html_block/custom_html_block.js
index 3875117103..64b794ec44 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.js
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.js
@@ -3,13 +3,9 @@
frappe.ui.form.on("Custom HTML Block", {
refresh(frm) {
- render_custom_html_block(frm);
+ let wrapper = frm.fields_dict["preview"].wrapper;
+ wrapper.classList.add("mb-3");
+
+ frappe.create_shadow_element(wrapper, frm.doc.html, frm.doc.style, frm.doc.script);
},
});
-
-function render_custom_html_block(frm) {
- let wrapper = frm.fields_dict["preview"].wrapper;
- wrapper.classList.add("mb-3");
-
- frappe.create_shadow_element(wrapper, frm.doc.html, frm.doc.style, frm.doc.script);
-}
From 3df13ca3922c68c49090a37a87ec79d223be3d17 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 15:36:38 +0530
Subject: [PATCH 101/203] feat: new operator - `descendants of (inclusive)`
Co-Authored-By: Faris Ansari
---
frappe/database/utils.py | 1 +
frappe/model/db_query.py | 24 +++++++++++++++-----
frappe/public/js/frappe/ui/filters/filter.js | 2 ++
frappe/utils/data.py | 7 ++----
4 files changed, 23 insertions(+), 11 deletions(-)
diff --git a/frappe/database/utils.py b/frappe/database/utils.py
index d1030ca6d7..61dd0016c5 100644
--- a/frappe/database/utils.py
+++ b/frappe/database/utils.py
@@ -23,6 +23,7 @@ NestedSetHierarchy = (
"descendants of",
"not ancestors of",
"not descendants of",
+ "descendants of (inclusive)",
)
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 340b3a97f4..ca1969abcf 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -730,18 +730,30 @@ class DatabaseQuery:
lft, rgt = frappe.db.get_value(ref_doctype, f.value, ["lft", "rgt"])
# Get descendants elements of a DocType with a tree structure
- if f.operator.lower() in ("descendants of", "not descendants of"):
- result = frappe.get_all(
- ref_doctype, filters={"lft": [">", lft], "rgt": ["<", rgt]}, order_by="`lft` ASC"
+ if f.operator.lower() in (
+ "descendants of",
+ "not descendants of",
+ "descendants of (inclusive)",
+ ):
+ nodes = frappe.get_all(
+ ref_doctype,
+ filters={"lft": [">", lft], "rgt": ["<", rgt]},
+ order_by="`lft` ASC",
+ pluck="name",
)
+ if f.operator.lower() == "descendants of (inclusive)":
+ nodes += [f.value]
else:
# Get ancestor elements of a DocType with a tree structure
- result = frappe.get_all(
- ref_doctype, filters={"lft": ["<", lft], "rgt": [">", rgt]}, order_by="`lft` DESC"
+ nodes = frappe.get_all(
+ ref_doctype,
+ filters={"lft": ["<", lft], "rgt": [">", rgt]},
+ order_by="`lft` DESC",
+ pluck="name",
)
fallback = "''"
- value = [frappe.db.escape((cstr(v.name) or "").strip(), percent=False) for v in result]
+ value = [frappe.db.escape((cstr(v)).strip(), percent=False) for v in nodes]
if len(value):
value = f"({', '.join(value)})"
else:
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js
index 5902c136bd..dd8ce7f6dd 100644
--- a/frappe/public/js/frappe/ui/filters/filter.js
+++ b/frappe/public/js/frappe/ui/filters/filter.js
@@ -30,6 +30,7 @@ frappe.ui.Filter = class {
this.nested_set_conditions = [
["descendants of", __("Descendants Of")],
+ ["descendants of (inclusive)", __("Descendants Of (inclusive)")],
["not descendants of", __("Not Descendants Of")],
["ancestors of", __("Ancestors Of")],
["not ancestors of", __("Not Ancestors Of")],
@@ -524,6 +525,7 @@ frappe.ui.filter_utils = {
"=",
"!=",
"descendants of",
+ "descendants of (inclusive)",
"ancestors of",
"not descendants of",
"not ancestors of",
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index a51cdee04a..3fe854bbf6 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -1766,6 +1766,7 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr
"fieldtype":
}
"""
+ from frappe.database.utils import NestedSetHierarchy
from frappe.model import child_table_fields, default_fields, optional_fields
if isinstance(f, dict):
@@ -1805,14 +1806,10 @@ def get_filter(doctype: str, f: dict | list | tuple, filters_config=None) -> "fr
"not in",
"is",
"between",
- "descendants of",
- "ancestors of",
- "not descendants of",
- "not ancestors of",
"timespan",
"previous",
"next",
- )
+ ) + NestedSetHierarchy
if filters_config:
additional_operators = []
From f858c41137a1cc8b0d1a4df890c7d0d2952ba886 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 15:47:56 +0530
Subject: [PATCH 102/203] fix!: consider list view filters inclusive by default
It rarely makes sense to match treeview directly against internal nodes.
- In case of leaf node: behaviour is same, though SLIGHTLY slower
- In case of internal nodes: all child nodes are added in results.
Co-Authored-By: Faris Ansari
---
frappe/public/js/frappe/list/base_list.js | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index e6ed0df7f4..da47417942 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -678,7 +678,9 @@ class FilterArea {
if (
fields_dict[fieldname] &&
(condition === "=" ||
- (condition === "like" && fields_dict[fieldname]?.df?.fieldtype != "Link"))
+ (condition === "like" && fields_dict[fieldname]?.df?.fieldtype != "Link") ||
+ (condition === "descendants of (inclusive)" &&
+ fields_dict[fieldname]?.df?.fieldtype == "Link"))
) {
// standard filter
out.promise = out.promise.then(() => fields_dict[fieldname].set_value(value));
@@ -788,6 +790,13 @@ class FilterArea {
options = options.join("\n");
}
}
+ if (
+ df.fieldtype == "Link" &&
+ df.options &&
+ frappe.boot.treeviews.includes(df.options)
+ ) {
+ condition = "descendants of (inclusive)";
+ }
return {
fieldtype: fieldtype,
From 842195ce29abb7d70e99ac76cdb29ba154312907 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 16:28:37 +0530
Subject: [PATCH 103/203] fix: simplify plucking
no idea why itertools are used for this dumb operation
---
frappe/database/query.py | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 25fa6a0528..dba0cd4bbe 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -1,4 +1,3 @@
-import itertools
import re
from ast import literal_eval
from types import BuiltinFunctionType
@@ -197,7 +196,6 @@ class Engine:
else OPERATOR_MAP["in"]
)
if result:
- result = list(itertools.chain.from_iterable(result))
self.query = self.query.where(operator_fn(_field, result))
else:
self.query = self.query.where(operator_fn(_field, ("",)))
@@ -513,7 +511,7 @@ def has_function(field):
return True
-def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str):
+def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str) -> list[str]:
table = frappe.qb.DocType(doctype)
try:
lft, rgt = frappe.qb.from_(table).select("lft", "rgt").where(table.name == name).run()[0]
@@ -527,7 +525,7 @@ def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str):
.where(table.lft > lft)
.where(table.rgt < rgt)
.orderby(table.lft, order=Order.asc)
- .run()
+ .run(pluck=True)
)
else:
# Get ancestor elements of a DocType with a tree structure
@@ -537,6 +535,6 @@ def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str):
.where(table.lft < lft)
.where(table.rgt > rgt)
.orderby(table.lft, order=Order.desc)
- .run()
+ .run(pluck=True)
)
return result
From a1c40d9158a3e6ab046765bce5391e2c449f4c6b Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 16:24:03 +0530
Subject: [PATCH 104/203] feat: support tree-link-fields filtering in QB
---
frappe/database/query.py | 15 ++++++++++----
frappe/tests/test_nestedset.py | 38 +++++++++++++++++++++++++++++++++-
2 files changed, 48 insertions(+), 5 deletions(-)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index dba0cd4bbe..02beff9afc 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -189,14 +189,18 @@ class Engine:
if _operator in OPERATOR_MAP["nested_set"]:
hierarchy = _operator
docname = _value
- result = get_nested_set_hierarchy_result(self.doctype, docname, hierarchy)
+
+ _df = frappe.get_meta(self.doctype).get_field(field)
+ ref_doctype = _df.options if _df else self.doctype
+
+ nodes = get_nested_set_hierarchy_result(ref_doctype, docname, hierarchy)
operator_fn = (
OPERATOR_MAP["not in"]
if hierarchy in ("not ancestors of", "not descendants of")
else OPERATOR_MAP["in"]
)
- if result:
- self.query = self.query.where(operator_fn(_field, result))
+ if nodes:
+ self.query = self.query.where(operator_fn(_field, nodes))
else:
self.query = self.query.where(operator_fn(_field, ("",)))
return
@@ -512,13 +516,14 @@ def has_function(field):
def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str) -> list[str]:
+ """Get matching nodes based on operator."""
table = frappe.qb.DocType(doctype)
try:
lft, rgt = frappe.qb.from_(table).select("lft", "rgt").where(table.name == name).run()[0]
except IndexError:
lft, rgt = None, None
- if hierarchy in ("descendants of", "not descendants of"):
+ if hierarchy in ("descendants of", "not descendants of", "descendants of (inclusive)"):
result = (
frappe.qb.from_(table)
.select(table.name)
@@ -527,6 +532,8 @@ def get_nested_set_hierarchy_result(doctype: str, name: str, hierarchy: str) ->
.orderby(table.lft, order=Order.asc)
.run(pluck=True)
)
+ if hierarchy == "descendants of (inclusive)":
+ result += [name]
else:
# Get ancestor elements of a DocType with a tree structure
result = (
diff --git a/frappe/tests/test_nestedset.py b/frappe/tests/test_nestedset.py
index 07f674a478..340b53bf38 100644
--- a/frappe/tests/test_nestedset.py
+++ b/frappe/tests/test_nestedset.py
@@ -57,7 +57,7 @@ TEST_DOCTYPE = "Test Tree DocType"
class NestedSetTestUtil:
def setup_test_doctype(self):
frappe.db.delete("DocType", TEST_DOCTYPE)
- frappe.db.sql_ddl(f"drop table if exists `{TEST_DOCTYPE}`")
+ frappe.db.sql_ddl(f"drop table if exists `tab{TEST_DOCTYPE}`")
self.tree_doctype = new_doctype(TEST_DOCTYPE, is_tree=True, autoname="field:some_fieldname")
self.tree_doctype.insert()
@@ -260,3 +260,39 @@ class TestNestedSet(FrappeTestCase):
# root deletion without allow_root_deletion
root_node.delete()
self.assertFalse(frappe.db.exists(TEST_DOCTYPE, "Root Node"))
+
+ def test_desc_filters(self):
+
+ linked_doctype = (
+ new_doctype(
+ fields=[
+ {
+ "fieldname": "link_field",
+ "fieldtype": "Link",
+ "options": TEST_DOCTYPE,
+ }
+ ]
+ )
+ .insert()
+ .name
+ )
+
+ record = "Child 1"
+
+ exclusive_filter = {"name": ("descendants of", record)}
+ inclusive_filter = {"name": ("descendants of (inclusive)", record)}
+ exclusive_link = {"link_field": ("descendants of", record)}
+ inclusive_link = {"link_field": ("descendants of (inclusive)", record)}
+
+ # db_query
+ self.assertNotIn(record, frappe.get_all(TEST_DOCTYPE, exclusive_filter, run=0))
+ self.assertIn(record, frappe.get_all(TEST_DOCTYPE, inclusive_filter, run=0))
+ self.assertNotIn(record, frappe.get_all(linked_doctype, exclusive_link, run=0))
+ self.assertIn(record, frappe.get_all(linked_doctype, inclusive_link, run=0))
+
+ # QB
+ self.assertNotIn(record, str(frappe.qb.get_query(TEST_DOCTYPE, filters=exclusive_filter)))
+ self.assertIn(record, str(frappe.qb.get_query(TEST_DOCTYPE, filters=inclusive_filter)))
+
+ self.assertNotIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=exclusive_link)))
+ self.assertIn(record, str(frappe.qb.get_query(table=linked_doctype, filters=inclusive_link)))
From 888209f6d2d11967a7422445a96b737ae9405ece Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 17:42:04 +0530
Subject: [PATCH 105/203] test: cron/scheduled scripts
---
.../server_script/test_server_script.py | 34 +++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 3d11a02ca4..36c36fded8 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -3,6 +3,7 @@
import requests
import frappe
+from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.frappeclient import FrappeClient, FrappeException
from frappe.tests.utils import FrappeTestCase
from frappe.utils import get_site_url
@@ -283,3 +284,36 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
script1.delete()
script2.delete()
frappe.db.commit()
+
+ def test_server_script_scheduled(self):
+ scheduled_script = frappe.get_doc(
+ doctype="Server Script",
+ name="scheduled_script_wo_cron",
+ script_type="Scheduler Event",
+ script="""frappe.flags = {"test": True}""",
+ event_frequency="Hourly",
+ ).insert()
+
+ cron_script = frappe.get_doc(
+ doctype="Server Script",
+ name="scheduled_script_w_cron",
+ script_type="Scheduler Event",
+ script="""frappe.flags = {"test": True}""",
+ event_frequency="Cron",
+ cron_format="0 0 1 1 *", # 1st january
+ ).insert()
+
+ # Ensure that jobs remain in DB after migrate
+ sync_jobs()
+ self.assertTrue(frappe.db.exists("Scheduled Job Type", {"server_script": scheduled_script.name}))
+
+ cron_job_name = frappe.db.get_value("Scheduled Job Type", {"server_script": cron_script.name})
+ self.assertTrue(cron_job_name)
+
+ cron_job = frappe.get_doc("Scheduled Job Type", cron_job_name)
+ self.assertEqual(cron_job.next_execution.day, 1)
+ self.assertEqual(cron_job.next_execution.month, 1)
+
+ cron_script.cron_format = "0 0 2 1 *" # 2nd january
+ cron_job.reload()
+ self.assertEqual(cron_job.next_execution.day, 2)
From a1b16792f96aa97d9525fd731445b378ce7390dd Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Mon, 29 May 2023 18:16:23 +0530
Subject: [PATCH 106/203] fix: Pass cron format only if frequency is Cron
---
frappe/core/doctype/server_script/server_script.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 50f13fedaa..2d8a95da4a 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -52,8 +52,9 @@ class ServerScript(Document):
def sync_scheduler_events(self):
"""Create or update Scheduled Job Type documents for Scheduler Event Server Scripts"""
if not self.disabled and self.event_frequency and self.script_type == "Scheduler Event":
+ cron_format = self.cron_format if self.event_frequency == "Cron" else None
setup_scheduler_events(
- script_name=self.name, frequency=self.event_frequency, cron_format=self.cron_format
+ script_name=self.name, frequency=self.event_frequency, cron_format=cron_format
)
def clear_scheduled_events(self):
From 9f53b56bdb29401a85a52f98c35d8684526cc2eb Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 18:30:17 +0530
Subject: [PATCH 107/203] test: save server script
---
frappe/core/doctype/server_script/test_server_script.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 36c36fded8..4371806b32 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -315,5 +315,6 @@ frappe.qb.from_(todo).select(todo.name).where(todo.name == "{todo.name}").run()
self.assertEqual(cron_job.next_execution.month, 1)
cron_script.cron_format = "0 0 2 1 *" # 2nd january
+ cron_script.save()
cron_job.reload()
self.assertEqual(cron_job.next_execution.day, 2)
From cdef2ccdd6a48d333c00d0dd651510fd84b185fe Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Mon, 29 May 2023 18:34:39 +0530
Subject: [PATCH 108/203] fix: Clear scheduled events if cron_format is changed
---
frappe/core/doctype/server_script/server_script.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 2d8a95da4a..07808d619b 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -58,8 +58,10 @@ class ServerScript(Document):
)
def clear_scheduled_events(self):
- """Deletes existing scheduled jobs by Server Script if self.event_frequency has changed"""
- if self.script_type == "Scheduler Event" and self.has_value_changed("event_frequency"):
+ """Deletes existing scheduled jobs by Server Script if self.event_frequency or self.cron_format has changed"""
+ if self.script_type == "Scheduler Event" and (
+ self.has_value_changed("event_frequency") or self.has_value_changed("cron_format")
+ ):
for scheduled_job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", scheduled_job.name)
From 50a8c4423e4baf8de4657fc83e369564f8ea2b3b Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 29 May 2023 21:52:16 +0530
Subject: [PATCH 109/203] fix: remove hacky/broken like/unlike toggle code
---
frappe/public/js/frappe/ui/like.js | 16 +---------------
1 file changed, 1 insertion(+), 15 deletions(-)
diff --git a/frappe/public/js/frappe/ui/like.js b/frappe/public/js/frappe/ui/like.js
index aa007cf138..6828c77dfd 100644
--- a/frappe/public/js/frappe/ui/like.js
+++ b/frappe/public/js/frappe/ui/like.js
@@ -89,7 +89,7 @@ frappe.ui.click_toggle_like = function () {
return false;
};
-frappe.ui.setup_like_popover = ($parent, selector, check_not_liked = true) => {
+frappe.ui.setup_like_popover = ($parent, selector) => {
if (frappe.dom.is_touchscreen()) {
return;
}
@@ -109,20 +109,6 @@ frappe.ui.setup_like_popover = ($parent, selector, check_not_liked = true) => {
liked_by = liked_by ? decodeURI(liked_by) : "[]";
liked_by = JSON.parse(liked_by);
- const user = frappe.session.user;
- // hack
- if (check_not_liked) {
- if (target_element.parents(".liked-by").find(".not-liked").length) {
- if (liked_by.indexOf(user) !== -1) {
- liked_by.splice(liked_by.indexOf(user), 1);
- }
- } else {
- if (liked_by.indexOf(user) === -1) {
- liked_by.push(user);
- }
- }
- }
-
if (!liked_by.length) {
return "";
}
From 66eb3774920ac5c8107354c25db0a2a8fd98514f Mon Sep 17 00:00:00 2001
From: Corentin Flr <10946971+cogk@users.noreply.github.com>
Date: Tue, 30 May 2023 07:07:26 +0200
Subject: [PATCH 110/203] fix(role): Set desk properties (e.g. search_bar) to 1
for roles with desk access (#21162)
Replace get_doc with new_doc, because default values are not retrieved when using get_doc. Some roles with desk_access had no access to most features of the desk, such as the search bar or the form's sidebar.
---
frappe/core/doctype/doctype/doctype.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 9a0613e6ca..91a317dbff 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -1679,7 +1679,9 @@ def make_module_and_roles(doc, perm_fieldname="permissions"):
for role in list(set(roles)):
if frappe.db.table_exists("Role", cached=False) and not frappe.db.exists("Role", role):
- r = frappe.get_doc(dict(doctype="Role", role_name=role, desk_access=1))
+ r = frappe.new_doc("Role")
+ r.role_name = role
+ r.desk_access = 1
r.flags.ignore_mandatory = r.flags.ignore_permissions = True
r.insert()
except frappe.DoesNotExistError as e:
From 07781eefe955df2df9d92eaf3afb16e914fe78f8 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Tue, 30 May 2023 12:07:43 +0530
Subject: [PATCH 111/203] fix: create private custom html block
---
.../doctype/custom_html_block/custom_html_block.js | 12 ++++++++++++
.../doctype/custom_html_block/custom_html_block.json | 11 ++++++++++-
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.js b/frappe/desk/doctype/custom_html_block/custom_html_block.js
index 64b794ec44..49f50a72be 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.js
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.js
@@ -3,6 +3,18 @@
frappe.ui.form.on("Custom HTML Block", {
refresh(frm) {
+ if (
+ !has_common(frappe.user_roles, [
+ "Administrator",
+ "System Manager",
+ "Workspace Manager",
+ ])
+ ) {
+ frm.set_value("private", true);
+ } else {
+ frm.set_df_property("private", "read_only", false);
+ }
+
let wrapper = frm.fields_dict["preview"].wrapper;
wrapper.classList.add("mb-3");
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.json b/frappe/desk/doctype/custom_html_block/custom_html_block.json
index 3fab5d7902..8fb06003ce 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.json
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.json
@@ -6,6 +6,7 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "private",
"preview_section",
"preview",
"html_section",
@@ -80,11 +81,19 @@
"fieldtype": "Table",
"label": "Roles",
"options": "Has Role"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.private || doc.__unsaved",
+ "fieldname": "private",
+ "fieldtype": "Check",
+ "label": "Private",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2023-05-29 18:28:28.326843",
+ "modified": "2023-05-30 14:33:31.994738",
"modified_by": "Administrator",
"module": "Desk",
"name": "Custom HTML Block",
From 474186c2645a77994ae9ebfef43fa29461adc36d Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Tue, 30 May 2023 12:08:43 +0530
Subject: [PATCH 112/203] fix: only show public and user's private blocks in
dropdown
---
.../custom_html_block/custom_html_block.py | 18 +++++++++++++++++-
.../public/js/frappe/widgets/widget_dialog.js | 5 +++++
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/frappe/desk/doctype/custom_html_block/custom_html_block.py b/frappe/desk/doctype/custom_html_block/custom_html_block.py
index 7f85c2db5f..2b65ceeaf4 100644
--- a/frappe/desk/doctype/custom_html_block/custom_html_block.py
+++ b/frappe/desk/doctype/custom_html_block/custom_html_block.py
@@ -1,9 +1,25 @@
# Copyright (c) 2023, Frappe Technologies and contributors
# For license information, please see license.txt
-# import frappe
+import frappe
from frappe.model.document import Document
+from frappe.query_builder.utils import DocType
class CustomHTMLBlock(Document):
pass
+
+
+@frappe.whitelist()
+def get_custom_blocks_for_user(doctype, txt, searchfield, start, page_len, filters):
+ # return logged in users private blocks and all public blocks
+ customHTMLBlock = DocType("Custom HTML Block")
+
+ condition_query = frappe.qb.get_query(customHTMLBlock)
+
+ return (
+ condition_query.select(customHTMLBlock.name).where(
+ (customHTMLBlock.private == 0)
+ | ((customHTMLBlock.owner == frappe.session.user) & (customHTMLBlock.private == 1))
+ )
+ ).run()
diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js
index 4f200f390f..51138a7dc4 100644
--- a/frappe/public/js/frappe/widgets/widget_dialog.js
+++ b/frappe/public/js/frappe/widgets/widget_dialog.js
@@ -715,6 +715,11 @@ class CustomBlockDialog extends WidgetDialog {
label: "Custom Block Name",
options: "Custom HTML Block",
reqd: 1,
+ get_query: () => {
+ return {
+ query: "frappe.desk.doctype.custom_html_block.custom_html_block.get_custom_blocks_for_user",
+ };
+ },
},
];
}
From e6e59cbe07b9801c56bf640181f8d144c535707c Mon Sep 17 00:00:00 2001
From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com>
Date: Tue, 30 May 2023 12:16:27 +0530
Subject: [PATCH 113/203] fix(ux): re routes based on setup_complete (#21118)
* fix(ux): re routes based on setup_complete
If Setup Wizard is not complete re_route any route to setup_wizard and if it complete re_route setup-wizard to app
* fix: only re_route if not on setup-wizard.
* fix: remove hardcoded slide number
---------
Co-authored-by: Sagar Sharma
---
frappe/public/js/frappe/router.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/frappe/public/js/frappe/router.js b/frappe/public/js/frappe/router.js
index c03bfc9b95..def04cf37e 100644
--- a/frappe/public/js/frappe/router.js
+++ b/frappe/public/js/frappe/router.js
@@ -139,6 +139,12 @@ frappe.router = {
if (!frappe.app) return;
let sub_path = this.get_sub_path();
+ if (frappe.boot.setup_complete) {
+ !frappe.re_route["setup-wizard"] && (frappe.re_route["setup-wizard"] = "app");
+ } else if (!sub_path.startsWith("setup-wizard")) {
+ frappe.re_route["setup-wizard"] && delete frappe.re_route["setup-wizard"];
+ frappe.set_route(["setup-wizard"]);
+ }
if (this.re_route(sub_path)) return;
this.current_sub_path = sub_path;
From abcc7b18709129838997f098099c90a3b2694c25 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Tue, 30 May 2023 13:05:20 +0530
Subject: [PATCH 114/203] style: make text smaller and reduced some spacings
for webform for mobile view
---
frappe/public/scss/website/web_form.scss | 42 ++++++++++++++++++++++++
1 file changed, 42 insertions(+)
diff --git a/frappe/public/scss/website/web_form.scss b/frappe/public/scss/website/web_form.scss
index f1889a7b8e..d606b38719 100644
--- a/frappe/public/scss/website/web_form.scss
+++ b/frappe/public/scss/website/web_form.scss
@@ -28,6 +28,10 @@
margin-top: 0;
margin-bottom: 0;
padding-bottom: 2px;
+
+ @include media-breakpoint-down(sm) {
+ font-size: 1.25rem;
+ }
}
.web-form-header {
@@ -38,6 +42,10 @@
background-color: var(--fg-color);
padding: 2rem 2rem 0;
+ @include media-breakpoint-down(sm) {
+ padding: 1.5rem 1.5rem 0;
+ }
+
.breadcrumb-container {
padding: 0px;
margin: 0 0 2rem;
@@ -83,6 +91,10 @@
p {
color: var(--text-muted);
+
+ @include media-breakpoint-down(sm) {
+ font-size: var(--text-xs);
+ }
}
}
}
@@ -96,10 +108,18 @@
border-bottom-left-radius: var(--border-radius);
border-bottom-right-radius: var(--border-radius);
+ @include media-breakpoint-down(sm) {
+ padding: 1rem 1.5rem 1.5rem;
+ }
+
.web-form-wrapper {
.form-control {
color: var(--text-color);
background-color: var(--control-bg);
+
+ @include media-breakpoint-down(sm) {
+ font-size: var(--text-sm);
+ }
}
.form-section {
@@ -113,9 +133,19 @@
.form-column {
padding: 0 var(--padding-sm);
+ .form-group {
+ @include media-breakpoint-down(sm) {
+ margin-bottom: 0.5rem;
+ }
+ }
+
.frappe-control {
position: relative;
+ @include media-breakpoint-down(sm) {
+ font-size: var(--text-sm);
+ }
+
&[data-fieldtype="Rating"] {
.like-disabled-input {
background-color: unset;
@@ -194,6 +224,10 @@
.web-form-footer {
margin-top: 1rem;
+ @include media-breakpoint-down(sm) {
+ margin-top: 0.5rem;
+ }
+
.web-form-actions {
display: flex;
justify-content: space-between;
@@ -201,6 +235,10 @@
.btn {
font-size: var(--text-base);
+
+ @include media-breakpoint-down(sm) {
+ font-size: var(--text-sm);
+ }
}
.btn-link {
@@ -294,6 +332,10 @@
width: 100%;
justify-content: center;
margin-bottom: 1.5rem;
+
+ &:empty {
+ margin: 0;
+ }
}
}
From 7727f294e51625b6264c5d65d5f10de470d09b72 Mon Sep 17 00:00:00 2001
From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com>
Date: Tue, 30 May 2023 13:17:10 +0530
Subject: [PATCH 115/203] fix: form tour set parent tab active. (#21163)
form tours were broken due to tabs i have added support for it by setting tab active on next and previous.
---
frappe/public/js/frappe/form/form_tour.js | 27 +++++++++++++++++++++--
1 file changed, 25 insertions(+), 2 deletions(-)
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 87de149d07..1069ecdb13 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -73,9 +73,31 @@ frappe.ui.form.FormTour = class FormTour {
if (!this.driver.hasNextStep()) {
this.on_finish && this.on_finish();
}
+ let field = this.get_next_step()?.options.element.fieldobj;
+ if (field?.tab && !field.tab.is_active()) {
+ field.tab.set_active();
+ this.driver.reset(true);
+ frappe.utils.sleep(200).then(() => {
+ this.start(step.idx);
+ this.driver.overlay.refresh();
+ });
+ }
+ };
+ const on_prev = () => {
+ if (!this.driver.hasPreviousStep()) return;
+ let field =
+ this.driver.steps[this.driver.currentStep - 1]?.options.element.fieldobj;
+ if (field?.tab && !field.tab.is_active()) {
+ field.tab.set_active();
+ this.driver.reset(true);
+ frappe.utils.sleep(200).then(() => {
+ this.start(step.idx - 2);
+ this.driver.overlay.refresh();
+ });
+ }
};
- const driver_step = this.get_step(step, on_next);
+ const driver_step = this.get_step(step, on_next, on_prev);
this.driver_steps.push(driver_step);
if (step.fieldtype == "Table") this.handle_table_step(step);
@@ -93,7 +115,7 @@ frappe.ui.form.FormTour = class FormTour {
return form.layout.evaluate_depends_on_value(step.next_step_condition || true);
}
- get_step(step_info, on_next) {
+ get_step(step_info, on_next, on_prev) {
const { name, fieldname, title, description, position, is_table_field } = step_info;
let element = `.frappe-control[data-fieldname='${fieldname}']`;
@@ -113,6 +135,7 @@ frappe.ui.form.FormTour = class FormTour {
name,
popover: { title, description, position: frappe.router.slug(position || "Bottom") },
onNext: on_next,
+ onPrevious: on_prev,
};
}
From 1e0daa6edcb502a98091151f0c4b76d3b2ced059 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 30 May 2023 15:21:12 +0530
Subject: [PATCH 116/203] fix: correct abbreviation for lakh (L)
[skip ci]
---
frappe/public/js/frappe/utils/number_systems.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/utils/number_systems.js b/frappe/public/js/frappe/utils/number_systems.js
index e27f5d98fd..8224f11d37 100644
--- a/frappe/public/js/frappe/utils/number_systems.js
+++ b/frappe/public/js/frappe/utils/number_systems.js
@@ -24,7 +24,7 @@ export default {
},
{
divisor: 1.0e5,
- symbol: __("Lakh", null, "Number system"),
+ symbol: __("L", null, "Number system"),
},
{
divisor: 1.0e3,
From 77db84a2be239c76fc686cd876ec60677536d16f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 30 May 2023 15:39:03 +0530
Subject: [PATCH 117/203] fix: reset tour
[skip ci]
---
frappe/desk/doctype/form_tour/form_tour.py | 14 +++++++++-----
1 file changed, 9 insertions(+), 5 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 0b1a22f64c..6838f15d8f 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -4,6 +4,7 @@
import json
import frappe
+from frappe import _
from frappe.model.document import Document
from frappe.modules.export_file import export_to_files
@@ -44,12 +45,15 @@ class FormTour(Document):
@frappe.whitelist()
def reset_tour(tour_name):
- for user in frappe.get_all("User"):
- user_doc = frappe.get_doc("User", user.name)
- onboarding_status = frappe.parse_json(user_doc.onboarding_status)
+ for user in frappe.get_all("User", pluck="name"):
+ onboarding_status = frappe.parse_json(frappe.db.get_value("User", user, "onboarding_status"))
onboarding_status.pop(tour_name, None)
- user_doc.onboarding_status = frappe.as_json(onboarding_status)
- user_doc.save()
+ frappe.db.set_value(
+ "User", user, "onboarding_status", frappe.as_json(onboarding_status), update_modified=False
+ )
+ frappe.cache().hdel("bootinfo", user)
+
+ frappe.msgprint(_("Successfully reset onboarding status for all users."), alert=True)
@frappe.whitelist()
From ca85524f91be8877dce6ddb69c83153c9f6b1fed Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 30 May 2023 16:28:47 +0530
Subject: [PATCH 118/203] fix: skip form tours on mobile (#21180)
[skip ci]
---
.../public/js/onboarding_tours/onboarding_tours.js | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js
index 63215d3659..df3e3c3894 100644
--- a/frappe/public/js/onboarding_tours/onboarding_tours.js
+++ b/frappe/public/js/onboarding_tours/onboarding_tours.js
@@ -248,6 +248,10 @@ frappe.ui.OnboardingTour = class OnboardingTour {
};
frappe.ui.init_onboarding_tour = () => {
+ // As of now Tours are only for desktop as it is annoying on mobile.
+ // Also lot of elements are hidden on mobile so until we find a better way to do it.
+ if (!window.matchMedia("(min-device-width: 992px)").matches) return;
+
typeof frappe.boot.onboarding_tours == "undefined" && frappe.boot.onboarding_tours == [];
typeof frappe.boot.user.onboarding_status == "undefined" &&
frappe.boot.user.onboarding_status == {};
@@ -337,9 +341,7 @@ frappe.ui.init_onboarding_tour = () => {
}
}, 100);
};
-// As of now Tours are only for desktop as it is annoying on mobile.
-// Also lot of elements are hidden on mobile so until we find a better way to do it.
-window.matchMedia("(min-device-width: 992px)").matches &&
- frappe.router.on("change", () => {
- frappe.ui.init_onboarding_tour();
- });
+
+frappe.router.on("change", () => {
+ frappe.ui.init_onboarding_tour();
+});
From a13592a66a94b8bdd11f5c3d2c0cd9d69edc19f8 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 31 May 2023 08:55:59 +0530
Subject: [PATCH 119/203] fix: Warn users if "Repeat Header and Footer" is
disabled
- and if the user still sets Letterhead
Also, add label to the print options
---
frappe/printing/page/print/print.js | 23 ++++++++++++++++++----
frappe/public/scss/desk/print_preview.scss | 3 ---
2 files changed, 19 insertions(+), 7 deletions(-)
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 8e5e165c78..14770c2d26 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -91,7 +91,7 @@ frappe.ui.form.PrintView = class {
fieldtype: "Link",
fieldname: "print_format",
options: "Print Format",
- placeholder: __("Print Format"),
+ label: __("Print Format"),
get_query: () => {
return { filters: { doc_type: this.frm.doctype } };
},
@@ -101,7 +101,7 @@ frappe.ui.form.PrintView = class {
this.language_selector = this.add_sidebar_item({
fieldtype: "Link",
fieldname: "language",
- placeholder: __("Language"),
+ label: __("Language"),
options: "Language",
change: () => {
this.set_user_lang();
@@ -109,12 +109,27 @@ frappe.ui.form.PrintView = class {
},
}).$input;
+ let description = "";
+ if (!cint(this.print_settings.repeat_header_footer)) {
+ description =
+ "" +
+ __("Footer might not be visible as {0} option is disabled
", [
+ `${__(
+ "Repeat Header and Footer"
+ )} `,
+ ]);
+ }
+ const print_view = this;
this.letterhead_selector = this.add_sidebar_item({
fieldtype: "Link",
fieldname: "letterhead",
options: "Letter Head",
- placeholder: __("Letter Head"),
- change: () => this.preview(),
+ label: __("Letter Head"),
+ description: description,
+ change: function () {
+ this.set_description(this.get_value() ? description : "");
+ print_view.preview();
+ },
}).$input;
this.sidebar_dynamic_section = $(`
`).appendTo(
this.sidebar
diff --git a/frappe/public/scss/desk/print_preview.scss b/frappe/public/scss/desk/print_preview.scss
index 468b37fe5a..ed85f8b933 100644
--- a/frappe/public/scss/desk/print_preview.scss
+++ b/frappe/public/scss/desk/print_preview.scss
@@ -45,9 +45,6 @@
.layout-side-section.print-preview-sidebar {
padding-right: var(--padding-md);
- .clearfix {
- display: none;
- }
.label-area {
white-space: nowrap;
From 55455cef4e2b4bc73c80d588246d41013f396795 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 31 May 2023 09:03:07 +0530
Subject: [PATCH 120/203] fix: Update label
"in PDF" is not required as it is already under PDF settings.
---
frappe/printing/doctype/print_settings/print_settings.json | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json
index f45de7637d..a67440b54e 100644
--- a/frappe/printing/doctype/print_settings/print_settings.json
+++ b/frappe/printing/doctype/print_settings/print_settings.json
@@ -47,7 +47,7 @@
"default": "1",
"fieldname": "repeat_header_footer",
"fieldtype": "Check",
- "label": "Repeat Header and Footer in PDF"
+ "label": "Repeat Header and Footer"
},
{
"fieldname": "column_break_4",
@@ -176,7 +176,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-09-17 12:59:14.783694",
+ "modified": "2023-05-30 14:55:25.740691",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Settings",
@@ -193,5 +193,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
From abb479f89fdca712170dccb20215febbbdb44e3c Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 31 May 2023 09:55:39 +0530
Subject: [PATCH 121/203] fix: Check if signature already exist
---
frappe/public/js/frappe/views/communication.js | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js
index c3e998788d..6baf4893e9 100755
--- a/frappe/public/js/frappe/views/communication.js
+++ b/frappe/public/js/frappe/views/communication.js
@@ -747,7 +747,10 @@ frappe.views.CommunicationComposer = class {
this.content_set = true;
}
- message += await this.get_signature(sender_email || null);
+ const signature = await this.get_signature(sender_email || "");
+ if (!this.content_set || !strip_html(message).includes(strip_html(signature))) {
+ message += signature;
+ }
if (this.is_a_reply && !this.reply_set) {
message += this.get_earlier_reply();
From aabaab0fd21c402d713e246034e1a9d5d616addb Mon Sep 17 00:00:00 2001
From: Saurabh
Date: Wed, 31 May 2023 11:15:33 +0530
Subject: [PATCH 122/203] feat: used cached version of document in mapper
(#21186)
* fix: in get_mapped_doc return cached version if available
* feat: specify explicitly cached version of doc in doc_mapper
[skip ci]
---
frappe/model/mapper.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py
index 9df79ef276..84d9c6c095 100644
--- a/frappe/model/mapper.py
+++ b/frappe/model/mapper.py
@@ -62,6 +62,7 @@ def get_mapped_doc(
postprocess=None,
ignore_permissions=False,
ignore_child_tables=False,
+ cached=False,
):
apply_strict_user_permissions = frappe.get_system_settings("apply_strict_user_permissions")
@@ -79,7 +80,10 @@ def get_mapped_doc(
):
target_doc.raise_no_permission_to("create")
- source_doc = frappe.get_doc(from_doctype, from_docname)
+ if cached:
+ source_doc = frappe.get_cached_doc(from_doctype, from_docname)
+ else:
+ source_doc = frappe.get_doc(from_doctype, from_docname)
if not ignore_permissions:
if not source_doc.has_permission("read"):
From 4a81d9f8e307261ca21a3f86e7efa4904db4d093 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 12:29:31 +0530
Subject: [PATCH 123/203] feat!: populate fields from kwargs in
`frappe.new_doc` (#21190)
This makes it similar to `get_doc` API BUT still signifies intent that
it's a "NEW" document.
Minor Breaking Change: positional arguments are now forcefully keyword
arguments. Only seems to be used internally from what I can tell.
https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/frappe/.*+/frappe.new_doc%5C%28.*%3F%2C.*%3F%5C%29/+lang:python+&patternType=regexp&case=yes&sm=0&groupBy=repo
---
frappe/__init__.py | 12 ++++++++++--
frappe/model/mapper.py | 4 +++-
.../v11_0/apply_customization_to_custom_doctype.py | 2 +-
frappe/tests/test_document.py | 4 ++++
4 files changed, 18 insertions(+), 4 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 5efdfd8ce9..e5a0b9c4aa 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1049,18 +1049,26 @@ def reset_metadata_version():
def new_doc(
doctype: str,
+ *,
parent_doc: Optional["Document"] = None,
parentfield: str | None = None,
as_dict: bool = False,
+ **kwargs,
) -> "Document":
"""Returns a new document of the given DocType with defaults set.
:param doctype: DocType of the new document.
:param parent_doc: [optional] add to parent document.
- :param parentfield: [optional] add against this `parentfield`."""
+ :param parentfield: [optional] add against this `parentfield`.
+ :param as_dict: [optional] return as dictionary instead of Document.
+ :param kwargs: [optional] You can specify fields as field=value pairs in function call.
+ """
+
from frappe.model.create_new import get_new_doc
- return get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict)
+ new_doc = get_new_doc(doctype, parent_doc, parentfield, as_dict=as_dict)
+
+ return new_doc.update(kwargs)
def set_value(doctype, docname, fieldname, value=None):
diff --git a/frappe/model/mapper.py b/frappe/model/mapper.py
index 84d9c6c095..4b9051f59c 100644
--- a/frappe/model/mapper.py
+++ b/frappe/model/mapper.py
@@ -259,7 +259,9 @@ def map_fetch_fields(target_doc, df, no_copy_fields):
def map_child_doc(source_d, target_parent, table_map, source_parent=None):
target_child_doctype = table_map["doctype"]
target_parentfield = target_parent.get_parentfield_of_doctype(target_child_doctype)
- target_d = frappe.new_doc(target_child_doctype, target_parent, target_parentfield)
+ target_d = frappe.new_doc(
+ target_child_doctype, parent_doc=target_parent, parentfield=target_parentfield
+ )
map_doc(source_d, target_d, table_map, source_parent)
diff --git a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
index d652efcef7..90986e065a 100644
--- a/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
+++ b/frappe/patches/v11_0/apply_customization_to_custom_doctype.py
@@ -44,7 +44,7 @@ def execute():
if field:
field.update(cf)
else:
- df = frappe.new_doc("DocField", meta, "fields")
+ df = frappe.new_doc("DocField", parent_doc=meta, parentfield="fields")
df.update(cf)
meta.fields.append(df)
frappe.db.delete("Custom Field", {"name": cf.name})
diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py
index 474971c935..4e575528ab 100644
--- a/frappe/tests/test_document.py
+++ b/frappe/tests/test_document.py
@@ -169,6 +169,10 @@ class TestDocument(FrappeTestCase):
with self.assertQueryCount(0):
user.db_set("user_type", "Magical Wizard")
+ def test_new_doc_with_fields(self):
+ user = frappe.new_doc("User", first_name="wizard")
+ self.assertEqual(user.first_name, "wizard")
+
def test_update_after_submit(self):
d = self.test_insert()
d.starts_on = "2014-09-09"
From fd232805fe27b5da95178f2b61668ef05d0c2251 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 12:34:06 +0530
Subject: [PATCH 124/203] chore: capture frappe version in heartbeat
---
frappe/public/js/telemetry/index.js | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/frappe/public/js/telemetry/index.js b/frappe/public/js/telemetry/index.js
index 48afaa5258..b9dee3be1c 100644
--- a/frappe/public/js/telemetry/index.js
+++ b/frappe/public/js/telemetry/index.js
@@ -32,9 +32,9 @@ class TelemetryManager {
}
}
- capture(event, app) {
+ capture(event, app, props) {
if (!this.enabled) return;
- posthog.capture(`${app}_${event}`);
+ posthog.capture(`${app}_${event}`, props);
}
disable() {
@@ -49,7 +49,7 @@ class TelemetryManager {
if (!last || moment(now).diff(moment(last), "hours") > 12) {
localStorage.setItem(KEY, now.toISOString());
- this.capture("heartbeat", "frappe");
+ this.capture("heartbeat", "frappe", { frappe_version: frappe.boot?.versions?.frappe });
}
}
From 9f5a994f709898d157f55783cfa96c5702e0634a Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Tue, 30 May 2023 13:51:18 +0530
Subject: [PATCH 125/203] fix!: improved filter validation in
`Engine.get_query`
---
frappe/database/database.py | 26 +++++++++++++++----
frappe/database/query.py | 15 ++++++++---
.../desk/doctype/number_card/number_card.py | 6 ++++-
frappe/desk/listview.py | 7 ++++-
frappe/tests/test_query.py | 7 -----
frappe/utils/goal.py | 1 +
6 files changed, 44 insertions(+), 18 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 8b077ce4f7..aa30cd9ae9 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -812,6 +812,7 @@ class Database:
fields=fields,
distinct=distinct,
limit=limit,
+ validate_filters=True,
)
if isinstance(fields, str) and fields == "*":
as_dict = True
@@ -840,6 +841,7 @@ class Database:
order_by=order_by,
distinct=distinct,
limit=limit,
+ validate_filters=True,
).run(debug=debug, run=run, as_dict=as_dict, pluck=pluck)
return {}
@@ -889,7 +891,12 @@ class Database:
field, val, modified=modified, modified_by=modified_by, update_modified=update_modified
)
- query = frappe.qb.get_query(table=dt, filters=dn, update=True)
+ query = frappe.qb.get_query(
+ table=dt,
+ filters=dn,
+ update=True,
+ validate_filters=True,
+ )
if isinstance(dn, str):
frappe.clear_document_cache(dt, dn)
@@ -1057,9 +1064,13 @@ class Database:
cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
if cache_count is not None:
return cache_count
- count = frappe.qb.get_query(table=dt, filters=filters, fields=Count("*"), distinct=distinct).run(
- debug=debug
- )[0][0]
+ count = frappe.qb.get_query(
+ table=dt,
+ filters=filters,
+ fields=Count("*"),
+ distinct=distinct,
+ validate_filters=True,
+ ).run(debug=debug)[0][0]
if not filters and cache:
frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
return count
@@ -1179,7 +1190,12 @@ class Database:
Doctype name can be passed directly, it will be pre-pended with `tab`.
"""
filters = filters or kwargs.get("conditions")
- query = frappe.qb.get_query(table=doctype, filters=filters, delete=True)
+ query = frappe.qb.get_query(
+ table=doctype,
+ filters=filters,
+ delete=True,
+ validate_filters=True,
+ )
if "debug" not in kwargs:
kwargs["debug"] = debug
return query.run(**kwargs)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 02beff9afc..eed52329d2 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -9,6 +9,7 @@ from pypika.queries import QueryBuilder, Table
import frappe
from frappe import _
from frappe.database.operator_map import OPERATOR_MAP
+from frappe.database.schema import SPECIAL_CHAR_PATTERN
from frappe.database.utils import DefaultOrderBy, get_doctype_name
from frappe.query_builder import Criterion, Field, Order, functions
from frappe.query_builder.functions import Function, SqlFunctions
@@ -44,6 +45,8 @@ class Engine:
update: bool = False,
into: bool = False,
delete: bool = False,
+ *,
+ validate_filters: bool = False,
) -> QueryBuilder:
self.is_mariadb = frappe.db.db_type == "mariadb"
self.is_postgres = frappe.db.db_type == "postgres"
@@ -56,6 +59,8 @@ class Engine:
self.validate_doctype()
self.table = frappe.qb.DocType(table)
+ self.validate_filters = validate_filters
+
if update:
self.query = frappe.qb.update(self.table)
elif into:
@@ -157,14 +162,16 @@ class Engine:
_value = value
_operator = operator
- if isinstance(_field, Field):
+ if not isinstance(_field, str):
pass
- elif dynamic_field := DynamicTableField.parse(field, self.doctype):
+ elif not self.validate_filters and (
+ dynamic_field := DynamicTableField.parse(field, self.doctype)
+ ):
# apply implicit join if link field's field is referenced
self.query = dynamic_field.apply_join(self.query)
_field = dynamic_field.field
- elif has_function(field):
- _field = self.get_function_object(field)
+ elif self.validate_filters and SPECIAL_CHAR_PATTERN.search(_field):
+ frappe.throw(_("Invalid filter: {0}").format(_field))
elif not doctype or doctype == self.doctype:
_field = self.table[field]
elif doctype:
diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py
index 451dc699fe..a8e4841953 100644
--- a/frappe/desk/doctype/number_card/number_card.py
+++ b/frappe/desk/doctype/number_card/number_card.py
@@ -202,7 +202,11 @@ def get_cards_for_user(doctype, txt, searchfield, start, page_len, filters):
if txt:
search_conditions = [numberCard[field].like(f"%{txt}%") for field in searchfields]
- condition_query = frappe.qb.get_query(doctype, filters=filters)
+ condition_query = frappe.qb.get_query(
+ doctype,
+ filters=filters,
+ validate_filters=True,
+ )
return (
condition_query.select(numberCard.name, numberCard.label, numberCard.document_type)
diff --git a/frappe/desk/listview.py b/frappe/desk/listview.py
index 05d45ad9ac..a1db82810e 100644
--- a/frappe/desk/listview.py
+++ b/frappe/desk/listview.py
@@ -36,7 +36,12 @@ def get_group_by_count(doctype: str, current_filters: str, field: str) -> list[d
ToDo = DocType("ToDo")
User = DocType("User")
count = Count("*").as_("count")
- filtered_records = frappe.qb.get_query(doctype, filters=current_filters, fields=["name"])
+ filtered_records = frappe.qb.get_query(
+ doctype,
+ filters=current_filters,
+ fields=["name"],
+ validate_filters=True,
+ )
return (
frappe.qb.from_(ToDo)
diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py
index dfebf5e890..b3270c7c13 100644
--- a/frappe/tests/test_query.py
+++ b/frappe/tests/test_query.py
@@ -218,13 +218,6 @@ class TestQuery(FrappeTestCase):
@run_only_if(db_type_is.MARIADB)
def test_filters(self):
- self.assertEqual(
- frappe.qb.get_query(
- "User", filters={"IfNull(name, " ")": ("<", Now())}, fields=["Max(name)"]
- ).run(),
- frappe.qb.from_("User").select(Max(Field("name"))).where(Ifnull("name", "") < Now()).run(),
- )
-
self.assertEqual(
frappe.qb.get_query(
"DocType",
diff --git a/frappe/utils/goal.py b/frappe/utils/goal.py
index 709fdc1644..01cd9d835e 100644
--- a/frappe/utils/goal.py
+++ b/frappe/utils/goal.py
@@ -31,6 +31,7 @@ def get_monthly_results(
Function(aggregation, goal_field),
],
filters=filters,
+ validate_filters=True,
)
.groupby("month_year")
.run()
From 81d5160ac1e61681e3c8a4c1737e036023919ec3 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Tue, 30 May 2023 17:49:51 +0530
Subject: [PATCH 126/203] test: ensure stricter filters when `validate_filters`
is passed
---
frappe/tests/test_query.py | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/frappe/tests/test_query.py b/frappe/tests/test_query.py
index b3270c7c13..9242630104 100644
--- a/frappe/tests/test_query.py
+++ b/frappe/tests/test_query.py
@@ -251,6 +251,17 @@ class TestQuery(FrappeTestCase):
),
)
+ self.assertRaisesRegex(
+ frappe.ValidationError,
+ "Invalid filter",
+ lambda: frappe.qb.get_query(
+ "DocType",
+ fields=["name"],
+ filters={"permissions.role": "System Manager"},
+ validate_filters=True,
+ ),
+ )
+
self.assertEqual(
frappe.qb.get_query(
"DocType",
From 1b2d1dd5678afca852661aa52406173925c73f7f Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Wed, 31 May 2023 14:20:26 +0530
Subject: [PATCH 127/203] chore: move statement to set `validate_filters`
property
---
frappe/database/query.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index eed52329d2..06295d33a6 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -50,6 +50,7 @@ class Engine:
) -> QueryBuilder:
self.is_mariadb = frappe.db.db_type == "mariadb"
self.is_postgres = frappe.db.db_type == "postgres"
+ self.validate_filters = validate_filters
if isinstance(table, Table):
self.table = table
@@ -59,8 +60,6 @@ class Engine:
self.validate_doctype()
self.table = frappe.qb.DocType(table)
- self.validate_filters = validate_filters
-
if update:
self.query = frappe.qb.update(self.table)
elif into:
From 1fd84f6b09efc1ed892a1716f5e4abccce0de7bd Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 31 May 2023 15:42:50 +0530
Subject: [PATCH 128/203] fix: Remove field_id from URL
- scroll to field is not yet supported
---
frappe/printing/page/print/print.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js
index 14770c2d26..f930359b58 100644
--- a/frappe/printing/page/print/print.js
+++ b/frappe/printing/page/print/print.js
@@ -114,7 +114,7 @@ frappe.ui.form.PrintView = class {
description =
"" +
__("Footer might not be visible as {0} option is disabled
", [
- `${__(
+ ` ${__(
"Repeat Header and Footer"
)} `,
]);
From eda8be74ca93a49b70a4d8af63f8bc6ba08343d4 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 31 May 2023 15:44:05 +0530
Subject: [PATCH 129/203] fix: Hide font-size from print format
- Since it is only used in new print format builder
and it can be set via new print format builder's interface
---
frappe/printing/doctype/print_format/print_format.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json
index 7f4408c950..664692ec45 100644
--- a/frappe/printing/doctype/print_format/print_format.json
+++ b/frappe/printing/doctype/print_format/print_format.json
@@ -245,6 +245,7 @@
"default": "14",
"fieldname": "font_size",
"fieldtype": "Int",
+ "hidden": 1,
"label": "Font Size"
},
{
@@ -258,7 +259,7 @@
"icon": "fa fa-print",
"idx": 1,
"links": [],
- "modified": "2022-11-09 15:29:46.709305",
+ "modified": "2023-05-31 15:40:52.919029",
"modified_by": "Administrator",
"module": "Printing",
"name": "Print Format",
From 48539dc0a338d619c69ded8c49da1c9e2c39903a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 16:00:59 +0530
Subject: [PATCH 130/203] feat: log all DDL queries (#21107)
---
frappe/database/database.py | 21 ++++++++++++++++++---
frappe/database/mariadb/database.py | 4 ++--
2 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 8b077ce4f7..0a978b2d87 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -4,6 +4,7 @@
import datetime
import itertools
import json
+import logging
import random
import re
import string
@@ -105,6 +106,8 @@ class Database:
self.password = password or frappe.conf.db_password
self.value_cache = {}
+ self.logger = frappe.logger("database")
+ self.logger.setLevel("INFO")
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@@ -122,7 +125,7 @@ class Database:
if execution_timeout := get_query_execution_timeout():
self.set_execution_timeout(execution_timeout)
except Exception as e:
- frappe.logger("database").warning(f"Couldn't set execution timeout {e}")
+ self.logger.warning(f"Couldn't set execution timeout {e}")
def set_execution_timeout(self, seconds: int):
"""Set session speicifc timeout on exeuction of statements.
@@ -285,7 +288,13 @@ class Database:
return self.convert_to_lists(self.last_result)
return self.last_result
- def _log_query(self, mogrified_query: str, debug: bool = False, explain: bool = False) -> None:
+ def _log_query(
+ self,
+ mogrified_query: str,
+ debug: bool = False,
+ explain: bool = False,
+ unmogrified_query: str = "",
+ ) -> None:
"""Takes the query and logs it to various interfaces according to the settings."""
_query = None
@@ -303,6 +312,12 @@ class Database:
_query = _query or str(mogrified_query)
frappe.log(f"<<<< query\n{_query}\n>>>>")
+ if unmogrified_query and is_query_type(
+ unmogrified_query, ("alter", "drop", "select", "create", "truncate", "rename")
+ ):
+ _query = _query or str(mogrified_query)
+ self.logger.info("DDL Query made:\n" + _query)
+
if frappe.flags.in_migrate:
_query = _query or str(mogrified_query)
self.log_touched_tables(_query)
@@ -314,7 +329,7 @@ class Database:
# like cursor._transformed_statement from the cursor object. We can also avoid setting
# mogrified_query if we don't need to log it.
mogrified_query = self.lazy_mogrify(query, values)
- self._log_query(mogrified_query, debug, explain)
+ self._log_query(mogrified_query, debug, explain, unmogrified_query=query)
return mogrified_query
def mogrify(self, query: Query, values: QueryValues):
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 8e52cc7ffd..8465751ca4 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -200,8 +200,8 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
return db_size[0].get("database_size")
def log_query(self, query, values, debug, explain):
- self.last_query = query = self._cursor._executed
- self._log_query(query, debug, explain)
+ self.last_query = self._cursor._executed
+ self._log_query(self.last_query, debug, explain, query)
return self.last_query
@staticmethod
From 950277a88de97590aa690dfa02e5511915e1cbd8 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 16:04:56 +0530
Subject: [PATCH 131/203] chore: increase logging level
---
frappe/database/database.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 0a978b2d87..faf65b02c1 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -4,7 +4,6 @@
import datetime
import itertools
import json
-import logging
import random
import re
import string
@@ -107,7 +106,7 @@ class Database:
self.password = password or frappe.conf.db_password
self.value_cache = {}
self.logger = frappe.logger("database")
- self.logger.setLevel("INFO")
+ self.logger.setLevel("WARNING")
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@@ -316,7 +315,7 @@ class Database:
unmogrified_query, ("alter", "drop", "select", "create", "truncate", "rename")
):
_query = _query or str(mogrified_query)
- self.logger.info("DDL Query made:\n" + _query)
+ self.logger.warning("DDL Query made to DB:\n" + _query)
if frappe.flags.in_migrate:
_query = _query or str(mogrified_query)
From 4104e7d733dd396def90267d3fb55cd9ad89c04d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 16:28:44 +0530
Subject: [PATCH 132/203] feat: show db table utilization on doctype (#21193)
* feat: show db table utilization on doctype
* fix: nicer error message with docs
---
frappe/core/doctype/doctype/doctype.py | 13 ++++-
.../doctype/customize_form/customize_form.py | 13 ++++-
frappe/database/database.py | 4 ++
frappe/database/mariadb/database.py | 58 +++++++++++++++++++
frappe/database/postgres/database.py | 4 ++
frappe/public/js/frappe/doctype/index.js | 23 ++++++++
6 files changed, 113 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 91a317dbff..12545adb4e 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -33,7 +33,7 @@ from frappe.model.meta import Meta
from frappe.modules import get_doc_path, make_boilerplate
from frappe.modules.import_file import get_file_path
from frappe.query_builder.functions import Concat
-from frappe.utils import cint, random_string
+from frappe.utils import cint, flt, random_string
from frappe.website.utils import clear_cache
if TYPE_CHECKING:
@@ -1751,3 +1751,14 @@ def get_field(doc, fieldname):
for field in doc.fields:
if field.fieldname == fieldname:
return field
+
+
+@frappe.whitelist()
+def get_row_size_utilization(doctype: str) -> float:
+ """Get row size utilization in percentage"""
+
+ frappe.has_permission("DocType", throw=True)
+ try:
+ return flt(frappe.db.get_row_size(doctype) / frappe.db.MAX_ROW_SIZE_LIMIT * 100, 2)
+ except Exception:
+ return 0.0
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 42cbf33f4f..9e6b8990d5 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -172,7 +172,18 @@ class CustomizeForm(Document):
check_email_append_to(self)
if self.flags.update_db:
- frappe.db.updatedb(self.doc_type)
+ try:
+ frappe.db.updatedb(self.doc_type)
+ except Exception as e:
+ if frappe.db.is_db_table_size_limit(e):
+ frappe.throw(
+ _("You have hit the row size limit on database table: {0}").format(
+ ""
+ "Maximum Number of Fields in a Form "
+ ),
+ title=_("Database Table Row Size Limit"),
+ )
+ raise
if not hasattr(self, "hide_success") or not self.hide_success:
frappe.msgprint(_("{0} updated").format(_(self.doc_type)), alert=True)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index faf65b02c1..aff9395846 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1283,6 +1283,10 @@ class Database:
return get_next_val(*args, **kwargs)
+ def get_row_size(self, doctype: str) -> int:
+ """Get estimated max row size of any table in bytes."""
+ raise NotImplementedError
+
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import (
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 8465751ca4..f14fce2710 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -76,6 +76,10 @@ class MariaDBExceptionUtil:
def is_data_too_long(e: pymysql.Error) -> bool:
return e.args[0] == ER.DATA_TOO_LONG
+ @staticmethod
+ def is_db_table_size_limit(e: pymysql.Error) -> bool:
+ return e.args[0] == ER.TOO_BIG_ROWSIZE
+
@staticmethod
def is_primary_key_violation(e: pymysql.Error) -> bool:
return (
@@ -145,6 +149,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
UnicodeWithAttrs: escape_string,
}
default_port = "3306"
+ MAX_ROW_SIZE_LIMIT = 65_535 # bytes
def setup_type_map(self):
self.db_type = "mariadb"
@@ -445,3 +450,56 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
frappe.cache().set_value("db_tables", tables)
return tables
+
+ def get_row_size(self, doctype: str) -> int:
+ """Get estimated max row size of any table in bytes."""
+
+ # Query reused from this answer: https://dba.stackexchange.com/a/313889/274503
+ # Modification: get values for particular table instead of full summary.
+ # Reference: https://mariadb.com/kb/en/data-type-storage-requirements/
+
+ est_row_size = frappe.db.sql(
+ """
+ SELECT SUM(col_sizes.col_size) AS EST_MAX_ROW_SIZE
+ FROM (
+ SELECT
+ cols.COLUMN_NAME,
+ CASE cols.DATA_TYPE
+ WHEN 'tinyint' THEN 1
+ WHEN 'smallint' THEN 2
+ WHEN 'mediumint' THEN 3
+ WHEN 'int' THEN 4
+ WHEN 'bigint' THEN 8
+ WHEN 'float' THEN IF(cols.NUMERIC_PRECISION > 24, 8, 4)
+ WHEN 'double' THEN 8
+ WHEN 'decimal' THEN ((cols.NUMERIC_PRECISION - cols.NUMERIC_SCALE) DIV 9)*4 + (cols.NUMERIC_SCALE DIV 9)*4 + CEIL(MOD(cols.NUMERIC_PRECISION - cols.NUMERIC_SCALE,9)/2) + CEIL(MOD(cols.NUMERIC_SCALE,9)/2)
+ WHEN 'bit' THEN (cols.NUMERIC_PRECISION + 7) DIV 8
+ WHEN 'year' THEN 1
+ WHEN 'date' THEN 3
+ WHEN 'time' THEN 3 + CEIL(cols.DATETIME_PRECISION /2)
+ WHEN 'datetime' THEN 5 + CEIL(cols.DATETIME_PRECISION /2)
+ WHEN 'timestamp' THEN 4 + CEIL(cols.DATETIME_PRECISION /2)
+ WHEN 'char' THEN cols.CHARACTER_OCTET_LENGTH
+ WHEN 'binary' THEN cols.CHARACTER_OCTET_LENGTH
+ WHEN 'varchar' THEN IF(cols.CHARACTER_OCTET_LENGTH > 255, 2, 1) + cols.CHARACTER_OCTET_LENGTH
+ WHEN 'varbinary' THEN IF(cols.CHARACTER_OCTET_LENGTH > 255, 2, 1) + cols.CHARACTER_OCTET_LENGTH
+ WHEN 'tinyblob' THEN 9
+ WHEN 'tinytext' THEN 9
+ WHEN 'blob' THEN 10
+ WHEN 'text' THEN 10
+ WHEN 'mediumblob' THEN 11
+ WHEN 'mediumtext' THEN 11
+ WHEN 'longblob' THEN 12
+ WHEN 'longtext' THEN 12
+ WHEN 'enum' THEN 2
+ WHEN 'set' THEN 8
+ ELSE 0
+ END AS col_size
+ FROM INFORMATION_SCHEMA.COLUMNS cols
+ WHERE cols.TABLE_NAME = %s
+ ) AS col_sizes;""",
+ (get_table_name(doctype),),
+ )
+
+ if est_row_size:
+ return int(est_row_size[0][0])
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index 836a689251..2d5b3a893f 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -107,6 +107,10 @@ class PostgresExceptionUtil:
def is_data_too_long(e):
return getattr(e, "pgcode", None) == STRING_DATA_RIGHT_TRUNCATION
+ @staticmethod
+ def is_db_table_size_limit(e) -> bool:
+ return False
+
class PostgresDatabase(PostgresExceptionUtil, Database):
REGEX_CHARACTER = "~"
diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js
index 0dc5fd0a34..a0023164d7 100644
--- a/frappe/public/js/frappe/doctype/index.js
+++ b/frappe/public/js/frappe/doctype/index.js
@@ -22,6 +22,29 @@ frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.
};
}
+ refresh() {
+ this.show_db_utilization();
+ }
+
+ show_db_utilization() {
+ const doctype = this.frm.doc.doc_type || this.frm.doc.name;
+ frappe
+ .xcall("frappe.core.doctype.doctype.doctype.get_row_size_utilization", {
+ doctype,
+ })
+ .then((r) => {
+ if (r < 50.0) return;
+ this.frm.dashboard.show_progress(
+ __("Database Row Size Utilization"),
+ r,
+ __(
+ "Database Table Row Size Utilization: {0}%, this limits number of fields you can add.",
+ [r]
+ )
+ );
+ });
+ }
+
max_attachments() {
if (!this.frm.doc.max_attachments) {
return;
From 3bbe4498a005e38418e489c2a18bcef13d7b7e00 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 16:38:51 +0530
Subject: [PATCH 133/203] feat: allow re-running patches in developer mode
Simpler debugging.
---
frappe/core/doctype/patch_log/patch_log.js | 4 ++++
frappe/core/doctype/patch_log/patch_log.py | 11 ++++++++++-
2 files changed, 14 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/patch_log/patch_log.js b/frappe/core/doctype/patch_log/patch_log.js
index 171a1d3a0f..78580a0cb0 100644
--- a/frappe/core/doctype/patch_log/patch_log.js
+++ b/frappe/core/doctype/patch_log/patch_log.js
@@ -4,5 +4,9 @@
frappe.ui.form.on("Patch Log", {
refresh: function (frm) {
frm.disable_save();
+
+ frm.add_custom_button(__("Re-Run Patch"), () => {
+ frm.call("rerun_patch");
+ });
},
});
diff --git a/frappe/core/doctype/patch_log/patch_log.py b/frappe/core/doctype/patch_log/patch_log.py
index c7d619017e..284a80df35 100644
--- a/frappe/core/doctype/patch_log/patch_log.py
+++ b/frappe/core/doctype/patch_log/patch_log.py
@@ -4,11 +4,20 @@
# License: MIT. See LICENSE
import frappe
+from frappe import _
from frappe.model.document import Document
class PatchLog(Document):
- pass
+ @frappe.whitelist()
+ def rerun_patch(self):
+ from frappe.modules.patch_handler import run_single
+
+ if not frappe.conf.developer_mode:
+ frappe.throw(_("Re-running patch is only allowed in developer mode."))
+
+ run_single(self.patch, force=True)
+ frappe.msgprint(_("Successfully re-ran patch: {0}").format(self.patch), alert=True)
def before_migrate():
From 83e3a20901db227bfa0f0122fa393e0f9a2f02cd Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 31 May 2023 17:50:10 +0530
Subject: [PATCH 134/203] feat: allow clearing web page views
---
frappe/website/doctype/web_page_view/web_page_view.py | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/frappe/website/doctype/web_page_view/web_page_view.py b/frappe/website/doctype/web_page_view/web_page_view.py
index bbf2a394a6..b284dc095c 100644
--- a/frappe/website/doctype/web_page_view/web_page_view.py
+++ b/frappe/website/doctype/web_page_view/web_page_view.py
@@ -9,7 +9,13 @@ from frappe.model.document import Document
class WebPageView(Document):
- pass
+ @staticmethod
+ def clear_old_logs(days=180):
+ from frappe.query_builder import Interval
+ from frappe.query_builder.functions import Now
+
+ table = frappe.qb.DocType("Web Page View")
+ frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
@frappe.whitelist(allow_guest=True)
From efff6ebba7a2c71c3339542ca65251a4d0e134fe Mon Sep 17 00:00:00 2001
From: Dhia' Alhaq Shalabi <30384731+dhiashalabi@users.noreply.github.com>
Date: Thu, 1 Jun 2023 09:39:25 +0300
Subject: [PATCH 135/203] fix: doctype name localization (#21197)
[skip ci]
---
frappe/model/base_document.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 811ba5894c..63188e749d 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -530,7 +530,7 @@ class BaseDocument:
if not ignore_if_duplicate:
frappe.msgprint(
- _("{0} {1} already exists").format(self.doctype, frappe.bold(self.name)),
+ _("{0} {1} already exists").format(_(self.doctype), frappe.bold(self.name)),
title=_("Duplicate Name"),
indicator="red",
)
From f8edb3dc3daab5b5c36ba86d4933fb9821703f7e Mon Sep 17 00:00:00 2001
From: "Jeans K. Real"
Date: Thu, 1 Jun 2023 19:10:29 -0600
Subject: [PATCH 136/203] Closing span tags on form_links.html and list_view
status field indicator. Also Removed extra calls from multicheck.js
---
frappe/public/js/frappe/form/controls/multicheck.js | 2 --
frappe/public/js/frappe/form/templates/form_links.html | 2 +-
frappe/public/js/frappe/list/list_view.js | 2 +-
3 files changed, 2 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/multicheck.js b/frappe/public/js/frappe/form/controls/multicheck.js
index de4c330ff7..7b980299aa 100644
--- a/frappe/public/js/frappe/form/controls/multicheck.js
+++ b/frappe/public/js/frappe/form/controls/multicheck.js
@@ -18,13 +18,11 @@ frappe.ui.form.ControlMultiCheck = class ControlMultiCheck extends frappe.ui.for
this.$checkbox_area = $(`
`).appendTo(
this.wrapper
);
- this.refresh();
}
refresh() {
this.set_options();
this.bind_checkboxes();
- this.refresh_input();
super.refresh();
}
diff --git a/frappe/public/js/frappe/form/templates/form_links.html b/frappe/public/js/frappe/form/templates/form_links.html
index 57edb69a15..cd423bb238 100644
--- a/frappe/public/js/frappe/form/templates/form_links.html
+++ b/frappe/public/js/frappe/form/templates/form_links.html
@@ -5,7 +5,7 @@
{% } %}
- {{ __(transactions[i].label) }}
+ {{ __(transactions[i].label) }}
{% for (let j=0; j < transactions[i].items.length; j++) { %}
{% let doctype = transactions[i].items[j]; %}
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 7fd8d2c55c..f009593f6f 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1029,7 +1029,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
return `
${__(indicator[0])}
- `;
+ `;
}
return "";
}
From 6fd9c44391e9ec6a89cd6beb3e99d82836a8c123 Mon Sep 17 00:00:00 2001
From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com>
Date: Fri, 2 Jun 2023 10:53:05 +0530
Subject: [PATCH 137/203] fix: ui tour popover when outside viewport (#21164)
* fix: ui tour always popover inside viewport
There are times when popover will go outside viewport this updates the style on highlight to make sure it doesn't.
* fix: don't run form ui tours on small screens.
* Revert "fix: don't run form ui tours on small screens."
This reverts commit b11aaf8d182fa07369f17b914e8a0cb3e7327a18.
[skip ci]
---
frappe/public/js/onboarding_tours/onboarding_tours.js | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/frappe/public/js/onboarding_tours/onboarding_tours.js b/frappe/public/js/onboarding_tours/onboarding_tours.js
index df3e3c3894..29790ce5de 100644
--- a/frappe/public/js/onboarding_tours/onboarding_tours.js
+++ b/frappe/public/js/onboarding_tours/onboarding_tours.js
@@ -29,6 +29,13 @@ frappe.ui.OnboardingTour = class OnboardingTour {
step.popover.node.offsetTop + step.options.step_info.offset_y
}px`;
}
+ if (step.popover.node.offsetLeft < 0) {
+ step.popover.node.style.minWidth = "200px";
+ step.popover.node.style.maxWidth = `${
+ 350 + step.popover.node.offsetLeft
+ }px`;
+ step.popover.node.style.left = "0px";
+ }
if (step.popover.closeBtnNode) {
step.popover.closeBtnNode.onclick = () => {
this.on_finish && this.on_finish();
From f223bc02490902dfcc32892058f13f343d51fbaf Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Fri, 2 Jun 2023 14:26:50 +0530
Subject: [PATCH 138/203] chore: fix formatting in `user.py`
---
frappe/core/doctype/user/user.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index c670ec35a6..94ea8b16a0 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -332,7 +332,13 @@ class User(Document):
reset_password_template = frappe.db.get_system_setting("reset_password_template")
- self.send_login_mail(_("Password Reset"), "password_reset", {"link": link}, now=True, custom_template=reset_password_template)
+ self.send_login_mail(
+ _("Password Reset"),
+ "password_reset",
+ {"link": link},
+ now=True,
+ custom_template=reset_password_template,
+ )
def send_welcome_mail_to_user(self):
from frappe.utils import get_url
@@ -354,7 +360,7 @@ class User(Document):
self.send_login_mail(
subject,
"new_user",
- dict(
+ dict(
link=link,
site_url=get_url(),
),
@@ -386,6 +392,7 @@ class User(Document):
if custom_template:
from frappe.email.doctype.email_template.email_template import get_email_template
+
email_template = get_email_template(custom_template, args)
subject = email_template.get("subject")
content = email_template.get("message")
From f65198fba3dd2bc38fb6fe02436fda89f42730c9 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Fri, 2 Jun 2023 14:28:04 +0530
Subject: [PATCH 139/203] chore: ignore formatting commit
[skip ci]
---
.git-blame-ignore-revs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 5a96c3fea8..03efd1d30d 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -34,3 +34,6 @@ c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
# db.get_all -> get_all
2eec621e95564c359ad22da79501a855c1f32b03
+
+# minor formatting fix in `user.py`
+f223bc02490902dfcc32892058f13f343d51fbaf
From cad6f938c164e437dd1d9edbeddc430cc33d658e Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Fri, 2 Jun 2023 14:09:48 +0530
Subject: [PATCH 140/203] feat: formatter for `Attach` and `Attach Image`
fields
---
frappe/public/js/frappe/form/formatters.js | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index f4371f901b..9739eed8bb 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -365,8 +365,14 @@ frappe.form.formatters = {
`
: "";
},
+ Attach: format_attachment_url,
+ AttachImage: format_attachment_url,
};
+function format_attachment_url(url) {
+ return url ? `${url} ` : "";
+}
+
frappe.form.get_formatter = function (fieldtype) {
if (!fieldtype) fieldtype = "Data";
return frappe.form.formatters[fieldtype.replace(/ /g, "")] || frappe.form.formatters.Data;
From ac95b7496be9f770481c0873785c3a224e1695e0 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 2 Jun 2023 16:16:39 +0530
Subject: [PATCH 141/203] fix: do not render custom cards if workspace does not
contain content
---
frappe/public/js/frappe/views/workspace/workspace.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js
index 7bb53c65cd..b5fb0e2e54 100644
--- a/frappe/public/js/frappe/views/workspace/workspace.js
+++ b/frappe/public/js/frappe/views/workspace/workspace.js
@@ -353,7 +353,7 @@ frappe.views.Workspace = class Workspace {
let current_page = pages.filter((p) => p.title == page.name)[0];
this.content = current_page && JSON.parse(current_page.content);
- this.add_custom_cards_in_content();
+ this.content && this.add_custom_cards_in_content();
$(".item-anchor").addClass("disable-click");
From 691cbd6da74d6a17eec62eee42d35970935b0f0a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 16:39:35 +0530
Subject: [PATCH 142/203] chore: remove select from ddl prefix
---
frappe/database/database.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 3d2997a5a3..728d1e9584 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -312,7 +312,7 @@ class Database:
frappe.log(f"<<<< query\n{_query}\n>>>>")
if unmogrified_query and is_query_type(
- unmogrified_query, ("alter", "drop", "select", "create", "truncate", "rename")
+ unmogrified_query, ("alter", "drop", "create", "truncate", "rename")
):
_query = _query or str(mogrified_query)
self.logger.warning("DDL Query made to DB:\n" + _query)
From cb885a86ae3c341f9a66d7a4a4bd5229a5318c86 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 17:32:33 +0530
Subject: [PATCH 143/203] feat: allow clearing view logs
These dont need to exist for eternity. Let site admins decide when to
drop them.
---
frappe/core/doctype/view_log/view_log.py | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/view_log/view_log.py b/frappe/core/doctype/view_log/view_log.py
index 8383af818e..5dde78d007 100644
--- a/frappe/core/doctype/view_log/view_log.py
+++ b/frappe/core/doctype/view_log/view_log.py
@@ -1,8 +1,15 @@
# Copyright (c) 2018, Frappe Technologies and contributors
# License: MIT. See LICENSE
+import frappe
from frappe.model.document import Document
class ViewLog(Document):
- pass
+ @staticmethod
+ def clear_old_logs(days=180):
+ from frappe.query_builder import Interval
+ from frappe.query_builder.functions import Now
+
+ table = frappe.qb.DocType("View Log")
+ frappe.db.delete(table, filters=(table.modified < (Now() - Interval(days=days))))
From 5185374f72681e9e7980f62bf3e6913d29a091ea Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 21:57:58 +0530
Subject: [PATCH 144/203] refactor!: Remove dynamic addition of `_comments`
(#21217)
This isn't required anymore, was added to handle old sites.
---
frappe/app.py | 3 ---
frappe/core/doctype/comment/comment.py | 17 +----------------
2 files changed, 1 insertion(+), 19 deletions(-)
diff --git a/frappe/app.py b/frappe/app.py
index fab8facd3f..55855efaf9 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -19,7 +19,6 @@ import frappe.recorder
import frappe.utils.response
from frappe import _
from frappe.auth import SAFE_HTTP_METHODS, UNSAFE_HTTP_METHODS, HTTPRequest
-from frappe.core.doctype.comment.comment import update_comments_in_parent_after_request
from frappe.middlewares import StaticDataMiddleware
from frappe.utils import cint, get_site_name, sanitize_html
from frappe.utils.error import make_error_snapshot
@@ -351,8 +350,6 @@ def sync_database(rollback: bool) -> bool:
frappe.db.commit()
rollback = False
- update_comments_in_parent_after_request()
-
return rollback
diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py
index dff13e1170..c86c7811ad 100644
--- a/frappe/core/doctype/comment/comment.py
+++ b/frappe/core/doctype/comment/comment.py
@@ -152,14 +152,9 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
except Exception as e:
if frappe.db.is_column_missing(e) and getattr(frappe.local, "request", None):
- # missing column and in request, add column and update after commit
- frappe.local._comments = getattr(frappe.local, "_comments", []) + [
- (reference_doctype, reference_name, _comments)
- ]
-
+ pass
elif frappe.db.is_data_too_long(e):
raise frappe.DataTooLongException
-
else:
raise
else:
@@ -169,13 +164,3 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments):
# Clear route cache
if route := frappe.get_cached_value(reference_doctype, reference_name, "route"):
clear_cache(route)
-
-
-def update_comments_in_parent_after_request():
- """update _comments in parent if _comments column is missing"""
- if hasattr(frappe.local, "_comments"):
- for (reference_doctype, reference_name, _comments) in frappe.local._comments:
- add_column(reference_doctype, "_comments", "Text")
- update_comments_in_parent(reference_doctype, reference_name, _comments)
-
- frappe.db.commit()
From 042595ca926afb523e6faf759af926e3411ceb47 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Sat, 3 Jun 2023 16:52:51 +0530
Subject: [PATCH 145/203] fix: handle multiple webform for same doctype
---
frappe/website/doctype/web_form/web_form.json | 31 +++++++++++++++----
frappe/website/doctype/web_form/web_form.py | 14 ++++++---
2 files changed, 35 insertions(+), 10 deletions(-)
diff --git a/frappe/website/doctype/web_form/web_form.json b/frappe/website/doctype/web_form/web_form.json
index 96749e460d..e0883ba439 100644
--- a/frappe/website/doctype/web_form/web_form.json
+++ b/frappe/website/doctype/web_form/web_form.json
@@ -31,6 +31,10 @@
"allow_incomplete",
"section_break_2",
"max_attachment_size",
+ "section_break_xzqr",
+ "condition",
+ "column_break_tjgl",
+ "condition_description",
"section_break_3",
"list_setting_message",
"show_list",
@@ -279,10 +283,6 @@
"fieldtype": "Tab Break",
"label": "Form"
},
- {
- "fieldname": "column_break_1",
- "fieldtype": "Column Break"
- },
{
"fieldname": "section_break_1",
"fieldtype": "Section Break"
@@ -297,7 +297,6 @@
"fieldtype": "Column Break"
},
{
- "collapsible": 1,
"fieldname": "section_break_2",
"fieldtype": "Section Break"
},
@@ -374,13 +373,33 @@
"fieldname": "anonymous",
"fieldtype": "Check",
"label": "Anonymous"
+ },
+ {
+ "fieldname": "condition",
+ "fieldtype": "Code",
+ "label": "Condition",
+ "max_height": "150px"
+ },
+ {
+ "fieldname": "section_break_xzqr",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_tjgl",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "condition_description",
+ "fieldtype": "HTML",
+ "label": "Condition Description",
+ "options": "Multiple webforms can be created for a single doctype. Write a condition specific to this webform to display correct record after submission.
For Example:
\nIf you create a separate webform every year to capture feedback from employees add a \n field named year in doctype and add a condition doc.year==\"2023\"
\n"
}
],
"has_web_view": 1,
"icon": "icon-edit",
"is_published_field": "published",
"links": [],
- "modified": "2023-04-20 17:24:42.657731",
+ "modified": "2023-06-03 19:18:56.760479",
"modified_by": "Administrator",
"module": "Website",
"name": "Web Form",
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index 3e2705bdbe..81c6001558 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -153,10 +153,16 @@ def get_context(context):
and not frappe.form_dict.name
and not frappe.form_dict.is_list
):
- name = frappe.db.get_value(self.doc_type, {"owner": frappe.session.user}, "name")
- if name:
- context.in_view_mode = True
- frappe.redirect(f"/{self.route}/{name}")
+ names = frappe.db.get_values(self.doc_type, {"owner": frappe.session.user}, pluck="name")
+ for name in names:
+ if self.condition:
+ doc = frappe.get_doc(self.doc_type, name)
+ if frappe.safe_eval(self.condition, None, {"doc": doc.as_dict()}):
+ context.in_view_mode = True
+ frappe.redirect(f"/{self.route}/{name}")
+ else:
+ context.in_view_mode = True
+ frappe.redirect(f"/{self.route}/{name}")
# Show new form when
# - User is Guest
From ba2251219141cc57b714bd3d6aece682d419de1c Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Sat, 3 Jun 2023 14:16:43 +0200
Subject: [PATCH 146/203] refactor: call control's `on_section_collapse()`
This way, the logic can stay in the control itself. Each control can
decide what it needs to do on section collapse/expand.
---
frappe/public/js/frappe/form/controls/signature.js | 3 +++
frappe/public/js/frappe/form/section.js | 7 +------
2 files changed, 4 insertions(+), 6 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/signature.js b/frappe/public/js/frappe/form/controls/signature.js
index 0cbc1f3c26..6ab96012f1 100644
--- a/frappe/public/js/frappe/form/controls/signature.js
+++ b/frappe/public/js/frappe/form/controls/signature.js
@@ -133,4 +133,7 @@ frappe.ui.form.ControlSignature = class ControlSignature extends frappe.ui.form.
this.set_my_value(base64_img);
this.set_image(this.get_value());
}
+ on_section_collapse() {
+ this.refresh();
+ }
};
diff --git a/frappe/public/js/frappe/form/section.js b/frappe/public/js/frappe/form/section.js
index a692cbac0d..b4908e749a 100644
--- a/frappe/public/js/frappe/form/section.js
+++ b/frappe/public/js/frappe/form/section.js
@@ -110,12 +110,7 @@ export default class Section {
this.set_icon(hide);
- // refresh signature fields
- this.fields_list.forEach((f) => {
- if (f.df.fieldtype == "Signature") {
- f.refresh();
- }
- });
+ this.fields_list.forEach((f) => f.on_section_collapse && f.on_section_collapse(hide));
// save state for next reload ('' is falsy)
if (this.df.css_class)
From 79aaf072bde8646d11cc7a5e80d73a0ffa74f2a6 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Sat, 3 Jun 2023 14:17:59 +0200
Subject: [PATCH 147/203] fix: fit and recenter map when section is expanded
---
.../js/frappe/form/controls/geolocation.js | 25 +++++++++++++------
1 file changed, 17 insertions(+), 8 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 37d799e96d..29d36539ec 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -73,15 +73,8 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
})
);
this.add_non_group_layers(data_layers, this.editableLayers);
- try {
- this.map.fitBounds(this.editableLayers.getBounds(), {
- padding: [50, 50],
- });
- } catch (err) {
- // suppress error if layer has a point.
- }
this.editableLayers.addTo(this.map);
- this.map.invalidateSize();
+ this.fit_and_recenter_map();
}
bind_leaflet_map() {
@@ -205,4 +198,20 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.editableLayers.removeLayer(l);
});
}
+
+ fit_and_recenter_map() {
+ // Spread map across the wrapper, recenter and zoom w.r.t bounds
+ try {
+ this.map.invalidateSize();
+ this.map.fitBounds(this.editableLayers.getBounds(), {
+ padding: [50, 50],
+ });
+ } catch (err) {
+ // suppress error if layer has a point.
+ }
+ }
+
+ on_section_collapse(hide) {
+ !hide && this.fit_and_recenter_map();
+ }
};
From f9251f0f5b2a051def991bd1e7828ba83e425a11 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Sat, 3 Jun 2023 14:38:14 +0200
Subject: [PATCH 148/203] refactor: move pointToLayer into a separate method
For better customizability
---
.../js/frappe/form/controls/geolocation.js | 30 ++++++++++++-------
1 file changed, 19 insertions(+), 11 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js
index 29d36539ec..ada729fd66 100644
--- a/frappe/public/js/frappe/form/controls/geolocation.js
+++ b/frappe/public/js/frappe/form/controls/geolocation.js
@@ -60,23 +60,31 @@ frappe.ui.form.ControlGeolocation = class ControlGeolocation extends frappe.ui.f
this.clear_editable_layers();
const data_layers = new L.FeatureGroup().addLayer(
- L.geoJson(JSON.parse(value), {
- pointToLayer: function (geoJsonPoint, latlng) {
- if (geoJsonPoint.properties.point_type == "circle") {
- return L.circle(latlng, { radius: geoJsonPoint.properties.radius });
- } else if (geoJsonPoint.properties.point_type == "circlemarker") {
- return L.circleMarker(latlng, { radius: geoJsonPoint.properties.radius });
- } else {
- return L.marker(latlng);
- }
- },
- })
+ L.geoJson(JSON.parse(value), { pointToLayer: this.point_to_layer })
);
this.add_non_group_layers(data_layers, this.editableLayers);
this.editableLayers.addTo(this.map);
this.fit_and_recenter_map();
}
+ /**
+ * Defines custom rules for how geoJSON data is rendered on the map.
+ *
+ * @param {Object} geoJsonPoint - The geoJSON object to be rendered on the map.
+ * @param {Object} latlng - The latitude and longitude where the geoJSON data should be rendered on the map.
+ * @returns {Object} - Returns the Leaflet layer object to be rendered on the map.
+ */
+ point_to_layer(geoJsonPoint, latlng) {
+ // Custom rules for how geojson data is rendered on the map
+ if (geoJsonPoint.properties.point_type == "circle") {
+ return L.circle(latlng, { radius: geoJsonPoint.properties.radius });
+ } else if (geoJsonPoint.properties.point_type == "circlemarker") {
+ return L.circleMarker(latlng, { radius: geoJsonPoint.properties.radius });
+ } else {
+ return L.marker(latlng);
+ }
+ }
+
bind_leaflet_map() {
const circleToGeoJSON = L.Circle.prototype.toGeoJSON;
L.Circle.include({
From 235e4855c2ed45953c0c1bdb3115901ee7d4618c Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 19:27:06 +0530
Subject: [PATCH 149/203] feat: DBHooks to run things before/after
commit/rollback
This is a common pattern which is implemented in inconsistent and undocumented ways using these:
- `frappe.local.rollback_observers`
- `frappe.flags.enqueue_after_commit`
- `frappe.local.realtime_log`
- `frappe.local.before_commit`
- `flush_local_link_count`
Instead new simple api:
- Simple function call `frappe.db.after_commit.run(function)`
- If you need args just pass partial function `frappe.db.after_commit.run(lambda: frappe.clear_cache(doctype, name)`
---
frappe/database/database.py | 91 ++++++++++++++++++++++++++++++-------
1 file changed, 75 insertions(+), 16 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 728d1e9584..8109a83d43 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -8,9 +8,10 @@ import random
import re
import string
import traceback
+from collections import deque
from contextlib import contextmanager, suppress
from time import time
-from typing import Any, Iterable, Sequence
+from typing import Any, Callable, Iterable, Sequence
from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
from pypika.terms import Criterion, NullValue
@@ -107,6 +108,12 @@ class Database:
self.value_cache = {}
self.logger = frappe.logger("database")
self.logger.setLevel("WARNING")
+
+ self.before_commit = DBHooks()
+ self.after_commit = DBHooks()
+ self.before_rollback = DBHooks()
+ self.after_rollback = DBHooks()
+
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@@ -973,14 +980,45 @@ class Database:
for method in frappe.local.before_commit:
frappe.call(method[0], *(method[1] or []), **(method[2] or {}))
+ # Invalidated by a commit.
+ self.before_rollback.reset()
+ self.after_rollback.reset()
+
+ self.before_commit.run()
+
self.sql("commit")
self.begin() # explicitly start a new transaction
+ self.after_commit.run()
+
frappe.local.rollback_observers = []
self.flush_realtime_log()
enqueue_jobs_after_commit()
flush_local_link_count()
+ def rollback(self, *, save_point=None):
+ """`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
+ if save_point:
+ self.sql(f"rollback to savepoint {save_point}")
+ else:
+ self.before_commit.reset()
+ self.after_commit.reset()
+
+ self.before_rollback.run()
+
+ self.sql("rollback")
+ self.begin()
+
+ self.after_rollback.run()
+
+ for obj in dict.fromkeys(frappe.local.rollback_observers):
+ if hasattr(obj, "on_rollback"):
+ obj.on_rollback()
+ frappe.local.rollback_observers = []
+
+ frappe.local.realtime_log = []
+ frappe.flags.enqueue_after_commit = []
+
def add_before_commit(self, method, args=None, kwargs=None):
frappe.local.before_commit.append([method, args, kwargs])
@@ -1004,21 +1042,6 @@ class Database:
def release_savepoint(self, save_point):
self.sql(f"release savepoint {save_point}")
- def rollback(self, *, save_point=None):
- """`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
- if save_point:
- self.sql(f"rollback to savepoint {save_point}")
- else:
- self.sql("rollback")
- self.begin()
- for obj in dict.fromkeys(frappe.local.rollback_observers):
- if hasattr(obj, "on_rollback"):
- obj.on_rollback()
- frappe.local.rollback_observers = []
-
- frappe.local.realtime_log = []
- frappe.flags.enqueue_after_commit = []
-
def field_exists(self, dt, fn):
"""Return true of field exists."""
return self.exists("DocField", {"fieldname": fn, "parent": dt})
@@ -1304,6 +1327,42 @@ class Database:
raise NotImplementedError
+class DBHooks:
+ """Hooks for database events.
+
+ Primarily used for doing things before/after commit/rollback.
+
+ hook_manager = DBHooks()
+
+ # Put a function call in queue
+ hook_manager.add(func)
+
+ # Run all pending functions in queue
+ hook_manager.run()
+
+ # Reset quue
+ hook_manager.reset()
+ """
+
+ __slots__ = ("_functions",)
+
+ def __init__(self) -> None:
+ self._functions = deque()
+
+ def add(self, func: Callable) -> None:
+ """Add a function to queue, functions are executed in order of addition."""
+ self._functions.append(func)
+
+ def run(self):
+ """Run all functions in queue"""
+ while self._functions:
+ _func = self._functions.popleft()
+ _func()
+
+ def reset(self):
+ self._functions = deque()
+
+
def enqueue_jobs_after_commit():
from frappe.utils.background_jobs import (
RQ_JOB_FAILURE_TTL,
From 7e9ef00bea42fb7b296f4a6ca371d8650511f026 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 19:49:17 +0530
Subject: [PATCH 150/203] refactor!: Remove `frappe.db.add_before_commit`
Not used anywhere, use `frappe.db.before_commit.add()` instead.
---
frappe/__init__.py | 1 -
frappe/database/database.py | 6 ------
frappe/tests/utils.py | 1 -
3 files changed, 8 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index e5a0b9c4aa..68a87e5226 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -209,7 +209,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
)
local.rollback_observers = []
local.locked_documents = []
- local.before_commit = []
local.test_objects = {}
local.site = site
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 8109a83d43..ab752e0a6b 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -977,9 +977,6 @@ class Database:
def commit(self):
"""Commit current transaction. Calls SQL `COMMIT`."""
- for method in frappe.local.before_commit:
- frappe.call(method[0], *(method[1] or []), **(method[2] or {}))
-
# Invalidated by a commit.
self.before_rollback.reset()
self.after_rollback.reset()
@@ -1019,9 +1016,6 @@ class Database:
frappe.local.realtime_log = []
frappe.flags.enqueue_after_commit = []
- def add_before_commit(self, method, args=None, kwargs=None):
- frappe.local.before_commit.append([method, args, kwargs])
-
@staticmethod
def flush_realtime_log():
for args in frappe.local.realtime_log:
diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py
index 2cdcfb5643..e1517e38a0 100644
--- a/frappe/tests/utils.py
+++ b/frappe/tests/utils.py
@@ -101,7 +101,6 @@ def _commit_watcher():
def _rollback_db():
- frappe.local.before_commit = []
frappe.local.rollback_observers = []
frappe.db.value_cache = {}
frappe.db.rollback()
From b3d370a0b18613c3d9cfa8ab36d11e5a137018ab Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 20:14:38 +0530
Subject: [PATCH 151/203] refactor!: remove `rollback_observers`
use `frappe.db.after_rollback.add` instead
---
frappe/__init__.py | 1 -
frappe/commands/utils.py | 2 +-
frappe/core/doctype/file/file.py | 18 ++++++++++++------
frappe/core/doctype/file/test_file.py | 12 +++++++++++-
frappe/database/database.py | 7 -------
frappe/tests/utils.py | 1 -
6 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 68a87e5226..f87807a7cb 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -207,7 +207,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
"read_only": False,
}
)
- local.rollback_observers = []
local.locked_documents = []
local.test_objects = {}
diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py
index 03374986d4..e44009a886 100644
--- a/frappe/commands/utils.py
+++ b/frappe/commands/utils.py
@@ -623,7 +623,7 @@ frappe.db.connect()
def _console_cleanup():
- # Execute rollback_observers on console close
+ # Execute after_rollback on console close
frappe.db.rollback()
frappe.destroy()
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 3728bd0af0..c4cefc7271 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -69,7 +69,7 @@ class File(Document):
else:
self.save_file(content=self.get_content())
self.flags.new_file = True
- frappe.local.rollback_observers.append(self)
+ frappe.db.after_rollback.add(self.on_rollback)
def after_insert(self):
if not self.is_folder:
@@ -121,10 +121,16 @@ class File(Document):
self.add_comment_in_reference_doc("Attachment Removed", _("Removed {0}").format(self.file_name))
def on_rollback(self):
+ rollback_flags = ("new_file", "original_content", "original_path")
+
+ def pop_rollback_flags():
+ for flag in rollback_flags:
+ self.flags.pop(flag, None)
+
# following condition is only executed when an insert has been rolledback
if self.flags.new_file:
self._delete_file_on_disk()
- self.flags.pop("new_file")
+ pop_rollback_flags()
return
# if original_content flag is set, this rollback should revert the file to its original state
@@ -139,14 +145,14 @@ class File(Document):
with open(file_path, mode) as f:
f.write(self.flags.original_content)
os.fsync(f.fileno())
- self.flags.pop("original_content")
+ pop_rollback_flags()
# used in case file path (File.file_url) has been changed
if self.flags.original_path:
target = self.flags.original_path["old"]
source = self.flags.original_path["new"]
shutil.move(source, target)
- self.flags.pop("original_path")
+ pop_rollback_flags()
def get_name_based_on_parent_folder(self) -> str | None:
if self.folder:
@@ -218,7 +224,7 @@ class File(Document):
# Uses os.rename which is an atomic operation
shutil.move(source, target)
self.flags.original_path = {"old": source, "new": target}
- frappe.local.rollback_observers.append(self)
+ frappe.db.after_rollback.add(self.on_rollback)
self.file_url = updated_file_url
update_existing_file_docs(self)
@@ -520,7 +526,7 @@ class File(Document):
f.write(self._content)
os.fsync(f.fileno())
- frappe.local.rollback_observers.append(self)
+ frappe.db.after_rollback.add(self.on_rollback)
return file_path
diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py
index 51e065f710..bbe8bb6d1a 100644
--- a/frappe/core/doctype/file/test_file.py
+++ b/frappe/core/doctype/file/test_file.py
@@ -17,7 +17,7 @@ from frappe.core.api.file import (
move_file,
unzip_file,
)
-from frappe.core.doctype.file.utils import get_extension
+from frappe.core.doctype.file.utils import delete_file, get_extension
from frappe.exceptions import ValidationError
from frappe.tests.utils import FrappeTestCase
from frappe.utils import get_files_path
@@ -77,6 +77,16 @@ class TestSimpleFile(FrappeTestCase):
self.assertEqual(content, self.test_content)
+class TestFSRollbacks(FrappeTestCase):
+ def test_rollback_from_file_system(self):
+ file_name = content = frappe.generate_hash()
+ file = frappe.new_doc("File", file_name=file_name, content=content).insert()
+ self.assertTrue(file.exists_on_disk())
+
+ frappe.db.rollback()
+ self.assertFalse(file.exists_on_disk())
+
+
class TestBase64File(FrappeTestCase):
def setUp(self):
self.attached_to_doctype, self.attached_to_docname = make_test_doc()
diff --git a/frappe/database/database.py b/frappe/database/database.py
index ab752e0a6b..cd00e28554 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -125,7 +125,6 @@ class Database:
self.cur_db_name = self.user
self._conn = self.get_connection()
self._cursor = self._conn.cursor()
- frappe.local.rollback_observers = []
try:
if execution_timeout := get_query_execution_timeout():
@@ -988,7 +987,6 @@ class Database:
self.after_commit.run()
- frappe.local.rollback_observers = []
self.flush_realtime_log()
enqueue_jobs_after_commit()
flush_local_link_count()
@@ -1008,11 +1006,6 @@ class Database:
self.after_rollback.run()
- for obj in dict.fromkeys(frappe.local.rollback_observers):
- if hasattr(obj, "on_rollback"):
- obj.on_rollback()
- frappe.local.rollback_observers = []
-
frappe.local.realtime_log = []
frappe.flags.enqueue_after_commit = []
diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py
index e1517e38a0..df02ee2789 100644
--- a/frappe/tests/utils.py
+++ b/frappe/tests/utils.py
@@ -101,7 +101,6 @@ def _commit_watcher():
def _rollback_db():
- frappe.local.rollback_observers = []
frappe.db.value_cache = {}
frappe.db.rollback()
From 65196510028ba8b19b5595f6ac142c88370de11a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 20:24:53 +0530
Subject: [PATCH 152/203] refactor: change implementation of
enqueue_after_commit
if enqueue_after_commit then pass partial function after commit instead
of storing it in flags. SLIGHTLY less efficient, but uses consistent API.
---
frappe/database/database.py | 24 --------------------
frappe/utils/background_jobs.py | 40 ++++++++++++++-------------------
2 files changed, 17 insertions(+), 47 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index cd00e28554..44876602b7 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -988,7 +988,6 @@ class Database:
self.after_commit.run()
self.flush_realtime_log()
- enqueue_jobs_after_commit()
flush_local_link_count()
def rollback(self, *, save_point=None):
@@ -1007,7 +1006,6 @@ class Database:
self.after_rollback.run()
frappe.local.realtime_log = []
- frappe.flags.enqueue_after_commit = []
@staticmethod
def flush_realtime_log():
@@ -1350,28 +1348,6 @@ class DBHooks:
self._functions = deque()
-def enqueue_jobs_after_commit():
- from frappe.utils.background_jobs import (
- RQ_JOB_FAILURE_TTL,
- RQ_RESULTS_TTL,
- execute_job,
- get_queue,
- )
-
- if frappe.flags.enqueue_after_commit and len(frappe.flags.enqueue_after_commit) > 0:
- for job in frappe.flags.enqueue_after_commit:
- q = get_queue(job.get("queue"), is_async=job.get("is_async"))
- q.enqueue_call(
- execute_job,
- timeout=job.get("timeout"),
- kwargs=job.get("queue_args"),
- failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
- result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
- job_id=job.get("job_id"),
- )
- frappe.flags.enqueue_after_commit = []
-
-
@contextmanager
def savepoint(catch: type | tuple[type, ...] = Exception):
"""Wrapper for wrapping blocks of DB operations in a savepoint.
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index 0fbc9e15ec..93c957639b 100755
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -113,6 +113,7 @@ def enqueue(
if not timeout:
timeout = get_queues_timeout().get(queue) or 300
+
queue_args = {
"site": frappe.local.site,
"user": frappe.session.user,
@@ -122,32 +123,25 @@ def enqueue(
"is_async": is_async,
"kwargs": kwargs,
}
- if enqueue_after_commit:
- if not frappe.flags.enqueue_after_commit:
- frappe.flags.enqueue_after_commit = []
- frappe.flags.enqueue_after_commit.append(
- {
- "queue": queue,
- "is_async": is_async,
- "timeout": timeout,
- "queue_args": queue_args,
- "job_id": job_id,
- }
+ def enqueue_call():
+ return q.enqueue_call(
+ execute_job,
+ on_success=on_success,
+ on_failure=on_failure,
+ timeout=timeout,
+ kwargs=queue_args,
+ at_front=at_front,
+ failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
+ result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
+ job_id=job_id,
)
- return frappe.flags.enqueue_after_commit
- return q.enqueue_call(
- execute_job,
- on_success=on_success,
- on_failure=on_failure,
- timeout=timeout,
- kwargs=queue_args,
- at_front=at_front,
- failure_ttl=frappe.conf.get("rq_job_failure_ttl") or RQ_JOB_FAILURE_TTL,
- result_ttl=frappe.conf.get("rq_results_ttl") or RQ_RESULTS_TTL,
- job_id=job_id,
- )
+ if enqueue_after_commit:
+ frappe.db.after_commit.add(enqueue_call)
+ return
+
+ return enqueue_call()
def enqueue_doc(
From ccc107b41fbd0edac3318d77d45976a474aeb207 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 21:30:56 +0530
Subject: [PATCH 153/203] test: use db.before_commit
---
frappe/database/database.py | 1 -
frappe/tests/utils.py | 2 +-
2 files changed, 1 insertion(+), 2 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 44876602b7..816d91062c 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -976,7 +976,6 @@ class Database:
def commit(self):
"""Commit current transaction. Calls SQL `COMMIT`."""
- # Invalidated by a commit.
self.before_rollback.reset()
self.after_rollback.reset()
diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py
index df02ee2789..33b2a6c478 100644
--- a/frappe/tests/utils.py
+++ b/frappe/tests/utils.py
@@ -32,7 +32,7 @@ class FrappeTestCase(unittest.TestCase):
# flush changes done so far to avoid flake
frappe.db.commit()
if cls.SHOW_TRANSACTION_COMMIT_WARNINGS:
- frappe.db.add_before_commit(_commit_watcher)
+ frappe.db.before_commit.add(_commit_watcher)
# enqueue teardown actions (executed in LIFO order)
cls.addClassCleanup(_restore_thread_locals, copy.deepcopy(frappe.local.flags))
From 3f1c66de1048058da7b71d4e1ce0585a22ffe5f8 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 20:43:03 +0530
Subject: [PATCH 154/203] refactor: move flush_local_link_count to hook
---
frappe/database/database.py | 1 -
frappe/model/utils/link_count.py | 15 ++++++++-------
2 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 816d91062c..e11e2fccbd 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -987,7 +987,6 @@ class Database:
self.after_commit.run()
self.flush_realtime_log()
- flush_local_link_count()
def rollback(self, *, save_point=None):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py
index 9a7694b9f8..3218d8482a 100644
--- a/frappe/model/utils/link_count.py
+++ b/frappe/model/utils/link_count.py
@@ -14,6 +14,8 @@ def notify_link_count(doctype, name):
else:
frappe.local.link_count[(doctype, name)] = 1
+ frappe.db.after_commit.add(flush_local_link_count)
+
def flush_local_link_count():
"""flush from local before ending request"""
@@ -31,6 +33,7 @@ def flush_local_link_count():
link_count[key] = frappe.local.link_count[key]
frappe.cache().set_value("_link_count", link_count)
+ frappe.local.link_count = {}
def update_link_count():
@@ -38,14 +41,12 @@ def update_link_count():
link_count = frappe.cache().get_value("_link_count")
if link_count:
- for key, count in link_count.items():
- if key[0] not in ignore_doctypes:
+ for (doctype, name), count in link_count.items():
+ if doctype not in ignore_doctypes:
try:
- frappe.db.sql(
- f"update `tab{key[0]}` set idx = idx + {count} where name=%s",
- key[1],
- auto_commit=1,
- )
+ table = frappe.qb.DocType(doctype)
+ frappe.qb.update(table).set(table.idx, table.idx + count).where(table.name == name).run()
+ frappe.db.commit()
except Exception as e:
if not frappe.db.is_table_missing(e): # table not found, single
raise e
From 680cf73cba177fad075eac0d7b57dc588ebdd309 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 22:11:21 +0530
Subject: [PATCH 155/203] fix: `link_count`
This didn't work correctly, if link_count is present in cache it would
just read and dump it back in.
This has practically never worked correctly.
---
frappe/__init__.py | 1 -
frappe/model/utils/link_count.py | 31 +++++++++++++++----------------
2 files changed, 15 insertions(+), 17 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index f87807a7cb..563404c570 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -231,7 +231,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.role_permissions = {}
local.valid_columns = {}
local.new_doc_templates = {}
- local.link_count = {}
local.jenv = None
local.jloader = None
diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py
index 3218d8482a..49ed0d5a6c 100644
--- a/frappe/model/utils/link_count.py
+++ b/frappe/model/utils/link_count.py
@@ -1,6 +1,8 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: MIT. See LICENSE
+from collections import defaultdict
+
import frappe
ignore_doctypes = ("DocType", "Print Format", "Role", "Module Def", "Communication", "ToDo")
@@ -8,32 +10,29 @@ ignore_doctypes = ("DocType", "Print Format", "Role", "Module Def", "Communicati
def notify_link_count(doctype, name):
"""updates link count for given document"""
- if hasattr(frappe.local, "link_count"):
- if (doctype, name) in frappe.local.link_count:
- frappe.local.link_count[(doctype, name)] += 1
- else:
- frappe.local.link_count[(doctype, name)] = 1
+ if not hasattr(frappe.local, "_link_count"):
+ frappe.local._link_count = defaultdict(int)
+ frappe.db.after_commit.add(flush_local_link_count)
- frappe.db.after_commit.add(flush_local_link_count)
+ frappe.local._link_count[(doctype, name)] += 1
def flush_local_link_count():
"""flush from local before ending request"""
- if not getattr(frappe.local, "link_count", None):
+ new_links = getattr(frappe.local, "_link_count", None)
+ if not new_links:
return
- link_count = frappe.cache().get_value("_link_count")
- if not link_count:
- link_count = {}
+ link_count = frappe.cache().get_value("_link_count") or {}
- for key, value in frappe.local.link_count.items():
- if key in link_count:
- link_count[key] += frappe.local.link_count[key]
- else:
- link_count[key] = frappe.local.link_count[key]
+ for key, value in new_links.items():
+ if key in link_count:
+ link_count[key] += value
+ else:
+ link_count[key] = value
frappe.cache().set_value("_link_count", link_count)
- frappe.local.link_count = {}
+ new_links.clear()
def update_link_count():
From 6ce7444669c7462e26875bb6772838dab0df3cb5 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 22:29:05 +0530
Subject: [PATCH 156/203] refactor: generic callback manager
---
frappe/database/database.py | 48 +++++--------------------------------
frappe/utils/__init__.py | 42 +++++++++++++++++++++++++++++++-
2 files changed, 47 insertions(+), 43 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index e11e2fccbd..bd99f790f4 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -8,10 +8,9 @@ import random
import re
import string
import traceback
-from collections import deque
from contextlib import contextmanager, suppress
from time import time
-from typing import Any, Callable, Iterable, Sequence
+from typing import Any, Iterable, Sequence
from pypika.dialects import MySQLQueryBuilder, PostgreSQLQueryBuilder
from pypika.terms import Criterion, NullValue
@@ -32,6 +31,7 @@ from frappe.database.utils import (
from frappe.exceptions import DoesNotExistError, ImplicitCommitError
from frappe.model.utils.link_count import flush_local_link_count
from frappe.query_builder.functions import Count
+from frappe.utils import CallbackManager
from frappe.utils import cast as cast_fieldtype
from frappe.utils import cint, get_datetime, get_table_name, getdate, now, sbool
from frappe.utils.deprecations import deprecated, deprecation_warning
@@ -109,10 +109,10 @@ class Database:
self.logger = frappe.logger("database")
self.logger.setLevel("WARNING")
- self.before_commit = DBHooks()
- self.after_commit = DBHooks()
- self.before_rollback = DBHooks()
- self.after_rollback = DBHooks()
+ self.before_commit = CallbackManager()
+ self.after_commit = CallbackManager()
+ self.before_rollback = CallbackManager()
+ self.after_rollback = CallbackManager()
# self.db_type: str
# self.last_query (lazy) attribute of last sql query executed
@@ -1310,42 +1310,6 @@ class Database:
raise NotImplementedError
-class DBHooks:
- """Hooks for database events.
-
- Primarily used for doing things before/after commit/rollback.
-
- hook_manager = DBHooks()
-
- # Put a function call in queue
- hook_manager.add(func)
-
- # Run all pending functions in queue
- hook_manager.run()
-
- # Reset quue
- hook_manager.reset()
- """
-
- __slots__ = ("_functions",)
-
- def __init__(self) -> None:
- self._functions = deque()
-
- def add(self, func: Callable) -> None:
- """Add a function to queue, functions are executed in order of addition."""
- self._functions.append(func)
-
- def run(self):
- """Run all functions in queue"""
- while self._functions:
- _func = self._functions.popleft()
- _func()
-
- def reset(self):
- self._functions = deque()
-
-
@contextmanager
def savepoint(catch: type | tuple[type, ...] = Exception):
"""Wrapper for wrapping blocks of DB operations in a savepoint.
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index ef32ff5653..b7dc565555 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -9,6 +9,7 @@ import os
import re
import sys
import traceback
+from collections import deque
from collections.abc import (
Container,
Generator,
@@ -20,7 +21,7 @@ from collections.abc import (
from email.header import decode_header, make_header
from email.utils import formataddr, parseaddr
from gzip import GzipFile
-from typing import Any, Literal
+from typing import Any, Callable, Literal
from urllib.parse import quote, urlparse
from redis.exceptions import ConnectionError
@@ -1092,3 +1093,42 @@ def is_git_url(url: str) -> bool:
# modified to allow without the tailing .git from https://github.com/jonschlinkert/is-git-url.git
pattern = r"(?:git|ssh|https?|\w*@[-\w.]+):(\/\/)?(.*?)(\.git)?(\/?|\#[-\d\w._]+?)$"
return bool(re.match(pattern, url))
+
+
+class CallbackManager:
+ """Manage callbacks.
+
+ ```
+ # Capture callacks
+ callbacks = CallbackManager()
+
+ # Put a function call in queue
+ callbacks.add(func)
+
+ # Run all pending functions in queue
+ callbacks.run()
+
+ # Reset queue
+ callbacks.reset()
+ ```
+
+ Example usage: frappe.db.after_commit
+ """
+
+ __slots__ = ("_functions",)
+
+ def __init__(self) -> None:
+ self._functions = deque()
+
+ def add(self, func: Callable) -> None:
+ """Add a function to queue, functions are executed in order of addition."""
+ self._functions.append(func)
+
+ def run(self):
+ """Run all functions in queue"""
+ while self._functions:
+ _func = self._functions.popleft()
+ _func()
+
+ def reset(self):
+ self._functions.clear()
From 54ae0c4a21976798b4effab6ebc9c5838d7ee381 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 22:40:12 +0530
Subject: [PATCH 157/203] refactor: move flush_realtime_log to realtime.py
This doesn't have anything to do with databases
---
frappe/__init__.py | 1 -
frappe/database/database.py | 11 -----------
frappe/realtime.py | 20 ++++++++++++++++++--
frappe/tests/utils.py | 1 -
4 files changed, 18 insertions(+), 15 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 563404c570..b13c9230b3 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -190,7 +190,6 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.error_log = []
local.message_log = []
local.debug_log = []
- local.realtime_log = []
local.flags = _dict(
{
"currently_saving": [],
diff --git a/frappe/database/database.py b/frappe/database/database.py
index bd99f790f4..7209ec39da 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -986,8 +986,6 @@ class Database:
self.after_commit.run()
- self.flush_realtime_log()
-
def rollback(self, *, save_point=None):
"""`ROLLBACK` current transaction. Optionally rollback to a known save_point."""
if save_point:
@@ -1003,15 +1001,6 @@ class Database:
self.after_rollback.run()
- frappe.local.realtime_log = []
-
- @staticmethod
- def flush_realtime_log():
- for args in frappe.local.realtime_log:
- frappe.realtime.emit_via_redis(*args)
-
- frappe.local.realtime_log = []
-
def savepoint(self, save_point):
"""Savepoints work as a nested transaction.
diff --git a/frappe/realtime.py b/frappe/realtime.py
index e6980ef917..fdb86546f3 100644
--- a/frappe/realtime.py
+++ b/frappe/realtime.py
@@ -73,13 +73,29 @@ def publish_realtime(
room = get_site_room()
if after_commit:
+ if not hasattr(frappe.local, "_realtime_log"):
+ frappe.local._realtime_log = []
+ frappe.db.after_commit.add(flush_realtime_log)
+ frappe.db.after_rollback.add(clear_realtime_log)
+
params = [event, message, room]
- if params not in frappe.local.realtime_log:
- frappe.local.realtime_log.append(params)
+ if params not in frappe.local._realtime_log:
+ frappe.local._realtime_log.append(params)
else:
emit_via_redis(event, message, room)
+def flush_realtime_log():
+ for args in frappe.local._realtime_log:
+ frappe.realtime.emit_via_redis(*args)
+
+ frappe.local._realtime_log = []
+
+
+def clear_realtime_log():
+ frappe.local._realtime_log = []
+
+
def emit_via_redis(event, message, room):
"""Publish real-time updates via redis
diff --git a/frappe/tests/utils.py b/frappe/tests/utils.py
index 33b2a6c478..07003d3b8c 100644
--- a/frappe/tests/utils.py
+++ b/frappe/tests/utils.py
@@ -110,7 +110,6 @@ def _restore_thread_locals(flags):
frappe.local.error_log = []
frappe.local.message_log = []
frappe.local.debug_log = []
- frappe.local.realtime_log = []
frappe.local.conf = frappe._dict(frappe.get_site_config())
frappe.local.cache = {}
frappe.local.lang = "en"
From 0b9dee47913d314b3e48bc306011b278a62f2f27 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 22:52:03 +0530
Subject: [PATCH 158/203] test: db callbacks
---
frappe/tests/test_db.py | 31 +++++++++++++++++++++++++++++++
1 file changed, 31 insertions(+)
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index ed01af655c..e27d1db0ba 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -593,6 +593,37 @@ class TestDB(FrappeTestCase):
modify_values((23, 23.0, 23.00004345, "wow", [1, 2, 3, "abc"])),
)
+ def test_callbacks(self):
+
+ order_of_execution = []
+
+ def f(val):
+ nonlocal order_of_execution
+ order_of_execution.append(val)
+
+ frappe.db.before_commit.add(lambda: f(0))
+ frappe.db.before_commit.add(lambda: f(1))
+
+ frappe.db.after_commit.add(lambda: f(2))
+ frappe.db.after_commit.add(lambda: f(3))
+
+ frappe.db.before_rollback.add(lambda: f("IGNORED"))
+ frappe.db.before_rollback.add(lambda: f("IGNORED"))
+
+ frappe.db.commit()
+
+ frappe.db.after_commit.add(lambda: f("IGNORED"))
+ frappe.db.after_commit.add(lambda: f("IGNORED"))
+
+ frappe.db.before_rollback.add(lambda: f(4))
+ frappe.db.before_rollback.add(lambda: f(5))
+ frappe.db.after_rollback.add(lambda: f(6))
+ frappe.db.after_rollback.add(lambda: f(7))
+
+ frappe.db.rollback()
+
+ self.assertEqual(order_of_execution, list(range(0, 8)))
+
@run_only_if(db_type_is.MARIADB)
class TestDDLCommandsMaria(FrappeTestCase):
From 07e1d34568a1ba73b110fd791b7d3a314b57e642 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 12:02:39 +0530
Subject: [PATCH 159/203] refactor: RQ enqueue after commit and tests
---
frappe/core/doctype/rq_job/test_rq_job.py | 22 +++++++++++-
frappe/database/database.py | 1 -
frappe/utils/background_jobs.py | 42 +++++++++++------------
3 files changed, 42 insertions(+), 23 deletions(-)
diff --git a/frappe/core/doctype/rq_job/test_rq_job.py b/frappe/core/doctype/rq_job/test_rq_job.py
index 265583fe83..09a90f7445 100644
--- a/frappe/core/doctype/rq_job/test_rq_job.py
+++ b/frappe/core/doctype/rq_job/test_rq_job.py
@@ -11,7 +11,7 @@ import frappe
from frappe.core.doctype.rq_job.rq_job import RQJob, remove_failed_jobs, stop_job
from frappe.tests.utils import FrappeTestCase, timeout
from frappe.utils import cstr, execute_in_shell
-from frappe.utils.background_jobs import is_job_enqueued
+from frappe.utils.background_jobs import get_job_status, is_job_enqueued
class TestRQJob(FrappeTestCase):
@@ -104,6 +104,26 @@ class TestRQJob(FrappeTestCase):
self.check_status(job, "finished")
self.assertFalse(is_job_enqueued(job_id))
+ @timeout(20)
+ def test_enqueue_after_commit(self):
+ job_id = frappe.generate_hash()
+
+ frappe.enqueue(self.BG_JOB, enqueue_after_commit=True, job_id=job_id)
+ self.assertIsNone(get_job_status(job_id))
+
+ frappe.db.commit()
+ self.assertIsNotNone(get_job_status(job_id))
+
+ job_id = frappe.generate_hash()
+ frappe.enqueue(self.BG_JOB, enqueue_after_commit=True, job_id=job_id)
+ self.assertIsNone(get_job_status(job_id))
+
+ frappe.db.rollback()
+ self.assertIsNone(get_job_status(job_id))
+
+ frappe.db.commit()
+ self.assertIsNone(get_job_status(job_id))
+
def test_func(fail=False, sleep=0):
if fail:
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 7209ec39da..fc4f7b3b1b 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -29,7 +29,6 @@ from frappe.database.utils import (
is_query_type,
)
from frappe.exceptions import DoesNotExistError, ImplicitCommitError
-from frappe.model.utils.link_count import flush_local_link_count
from frappe.query_builder.functions import Count
from frappe.utils import CallbackManager
from frappe.utils import cast as cast_fieldtype
diff --git a/frappe/utils/background_jobs.py b/frappe/utils/background_jobs.py
index 93c957639b..6a203f8dc7 100755
--- a/frappe/utils/background_jobs.py
+++ b/frappe/utils/background_jobs.py
@@ -3,13 +3,14 @@ import socket
import time
from collections import defaultdict
from functools import lru_cache
-from typing import TYPE_CHECKING, Any, Literal, NoReturn, Union
+from typing import Any, Callable, Literal, NoReturn
from uuid import uuid4
import redis
from redis.exceptions import BusyLoadingError, ConnectionError
from rq import Connection, Queue, Worker
from rq.exceptions import NoSuchJobError
+from rq.job import Job, JobStatus
from rq.logutils import setup_loghandlers
from rq.worker import RandomWorker, RoundRobinWorker
from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
@@ -22,10 +23,6 @@ from frappe.utils.commands import log
from frappe.utils.deprecations import deprecation_warning
from frappe.utils.redis_queue import RedisQueue
-if TYPE_CHECKING:
- from rq.job import Job
-
-
# TTL to keep RQ job logs in redis for.
RQ_JOB_FAILURE_TTL = 7 * 24 * 60 * 60 # 7 days instead of 1 year (default)
RQ_RESULTS_TTL = 10 * 60
@@ -54,21 +51,21 @@ redis_connection = None
def enqueue(
- method,
- queue="default",
- timeout=None,
- on_success=None,
- on_failure=None,
+ method: str | Callable,
+ queue: str = "default",
+ timeout: int | None = None,
event=None,
- is_async=True,
- job_name=None,
- now=False,
- enqueue_after_commit=False,
+ is_async: bool = True,
+ job_name: str | None = None,
+ now: bool = False,
+ enqueue_after_commit: bool = False,
*,
- at_front=False,
- job_id=None,
+ on_success: Callable = None,
+ on_failure: Callable = None,
+ at_front: bool = False,
+ job_id: str = None,
**kwargs,
-) -> Union["Job", Any]:
+) -> Job | Any:
"""
Enqueue method to be executed using a background worker
@@ -431,12 +428,15 @@ def create_job_id(job_id: str) -> str:
return f"{frappe.local.site}::{job_id}"
-def is_job_enqueued(job_id: str) -> str:
- from rq.job import Job
+def is_job_enqueued(job_id: str) -> bool:
+ return get_job_status(job_id) in (JobStatus.QUEUED, JobStatus.STARTED)
+
+def get_job_status(job_id: str) -> JobStatus | None:
+ """Get RQ job status, returns None if job is not found."""
try:
job = Job.fetch(create_job_id(job_id), connection=get_redis_conn())
except NoSuchJobError:
- return False
+ return None
- return job.get_status() in ("queued", "started")
+ return job.get_status()
From 6717b07ab9e4bc3720211f1dc6b527e6908fedb7 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 22:07:54 +0530
Subject: [PATCH 160/203] perf(customize_form): rebuild global search in bg
---
frappe/custom/doctype/customize_form/customize_form.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 9e6b8990d5..9aa61869d3 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -192,7 +192,9 @@ class CustomizeForm(Document):
if self.flags.rebuild_doctype_for_global_search:
frappe.enqueue(
- "frappe.utils.global_search.rebuild_for_doctype", now=True, doctype=self.doc_type
+ "frappe.utils.global_search.rebuild_for_doctype",
+ doctype=self.doc_type,
+ enqueue_after_commit=True,
)
def set_property_setters(self):
From 339cbf208cf30e0fc544a6fa5796805be8ebc465 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 19:43:15 +0530
Subject: [PATCH 161/203] fix: Cache clearing implementation
---
frappe/model/document.py | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 75c3a005c9..b8053d8076 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1122,8 +1122,15 @@ class Document(BaseDocument):
frappe.flags.currently_saving.remove((self.doctype, self.name))
def clear_cache(self):
+ # TODO: Remove this call after verifying,
+ # after commit call alone should be enough.
frappe.clear_document_cache(self.doctype, self.name)
+ # There's a possibility that another worker might read data after clearing cache and before
+ # changes are commited to DB, in which case stale doc can be read and stored in DB. So
+ # clear cache after commiting.
+ frappe.db.after_commit.add(lambda: frappe.clear_document_cache(self.doctype, self.name))
+
def reset_seen(self):
"""Clear _seen property and set current user as seen"""
if getattr(self.meta, "track_seen", False):
From be1da0dd00bd3fb81ed65327f26390c0458699aa Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 18:53:24 +0530
Subject: [PATCH 162/203] chore: remove duplicate cache clearing
---
frappe/__init__.py | 1 -
frappe/desk/doctype/list_view_settings/list_view_settings.py | 3 +--
frappe/model/document.py | 1 -
3 files changed, 1 insertion(+), 4 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index b13c9230b3..507d174a8b 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1116,7 +1116,6 @@ def get_document_cache_key(doctype: str, name: str):
def clear_document_cache(doctype, name):
- cache().hdel("last_modified", doctype)
cache().hdel("document_cache", get_document_cache_key(doctype, name))
if doctype == "System Settings" and hasattr(local, "system_settings"):
diff --git a/frappe/desk/doctype/list_view_settings/list_view_settings.py b/frappe/desk/doctype/list_view_settings/list_view_settings.py
index 36ebce34d5..e3b6a60a42 100644
--- a/frappe/desk/doctype/list_view_settings/list_view_settings.py
+++ b/frappe/desk/doctype/list_view_settings/list_view_settings.py
@@ -6,8 +6,7 @@ from frappe.model.document import Document
class ListViewSettings(Document):
- def on_update(self):
- frappe.clear_document_cache(self.doctype, self.name)
+ pass
@frappe.whitelist()
diff --git a/frappe/model/document.py b/frappe/model/document.py
index b8053d8076..92379f407c 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1207,7 +1207,6 @@ class Document(BaseDocument):
if notify:
self.notify_update()
- self.clear_cache()
if commit:
frappe.db.commit()
From 106ff1f1ee519701627540f54861a456ef0bc300 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 19:07:41 +0530
Subject: [PATCH 163/203] fix: move cache clearing away from document
Passing lambda function from inside document object would keep reference
to document alive. This means increasing memeory usage in bulk
processing.
Refer https://github.com/frappe/frappe/pull/17061 for example
This also extends it to db.set_value
---
frappe/__init__.py | 7 ++++++-
frappe/model/document.py | 7 -------
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 507d174a8b..80e1cb0235 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1116,7 +1116,12 @@ def get_document_cache_key(doctype: str, name: str):
def clear_document_cache(doctype, name):
- cache().hdel("document_cache", get_document_cache_key(doctype, name))
+ def clear_in_redis():
+ cache().hdel("document_cache", get_document_cache_key(doctype, name))
+
+ clear_in_redis()
+ if hasattr(db, "after_commit"):
+ db.after_commit.add(clear_in_redis)
if doctype == "System Settings" and hasattr(local, "system_settings"):
delattr(local, "system_settings")
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 92379f407c..f944b28a49 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1122,15 +1122,8 @@ class Document(BaseDocument):
frappe.flags.currently_saving.remove((self.doctype, self.name))
def clear_cache(self):
- # TODO: Remove this call after verifying,
- # after commit call alone should be enough.
frappe.clear_document_cache(self.doctype, self.name)
- # There's a possibility that another worker might read data after clearing cache and before
- # changes are commited to DB, in which case stale doc can be read and stored in DB. So
- # clear cache after commiting.
- frappe.db.after_commit.add(lambda: frappe.clear_document_cache(self.doctype, self.name))
-
def reset_seen(self):
"""Clear _seen property and set current user as seen"""
if getattr(self.meta, "track_seen", False):
From 356a2587e2a68a53a0864991b8684a0b1d5a8110 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 19:43:24 +0530
Subject: [PATCH 164/203] refactor: Use plain keys instead of hashes for
caching
- Hashes are supposed to be used for representing complex object, not
multiple documents of same DocType.
- Redis's auto cache clearing wont clear individual key from hashes even if they
are rarely used.
- Keys can have expiry.
---
frappe/__init__.py | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 80e1cb0235..c512de0de6 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1075,7 +1075,7 @@ def set_value(doctype, docname, fieldname, value=None):
def get_cached_doc(*args, **kwargs) -> "Document":
- if (key := can_cache_doc(args)) and (doc := cache().hget("document_cache", key)):
+ if (key := can_cache_doc(args)) and (doc := cache().get_value(key)):
return doc
# Not found in cache, fetch from DB
@@ -1091,7 +1091,7 @@ def get_cached_doc(*args, **kwargs) -> "Document":
def _set_document_in_cache(key: str, doc: "Document") -> None:
- cache().hset("document_cache", key, doc)
+ cache().set_value(key, doc)
def can_cache_doc(args) -> str | None:
@@ -1112,12 +1112,12 @@ def can_cache_doc(args) -> str | None:
def get_document_cache_key(doctype: str, name: str):
- return f"{doctype}::{name}"
+ return f"document_cache::{doctype}::{name}"
def clear_document_cache(doctype, name):
def clear_in_redis():
- cache().hdel("document_cache", get_document_cache_key(doctype, name))
+ cache().delete_value(get_document_cache_key(doctype, name))
clear_in_redis()
if hasattr(db, "after_commit"):
@@ -1206,7 +1206,7 @@ def get_doc(*args, **kwargs):
doc = frappe.model.document.get_doc(*args, **kwargs)
# Replace cache if stale one exists
- if (key := can_cache_doc(args)) and cache().hexists("document_cache", key):
+ if (key := can_cache_doc(args)) and cache().exists(key):
_set_document_in_cache(key, doc)
return doc
From 98b4693dcf869e32632e41e29ed2051e9cec65df Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 19:48:02 +0530
Subject: [PATCH 165/203] perf: finer cache eviction on db.set_value
Instead of nuking everything, just clear matching prefix
---
frappe/__init__.py | 7 +++++--
frappe/cache_manager.py | 3 ++-
frappe/database/database.py | 6 ++----
3 files changed, 9 insertions(+), 7 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index c512de0de6..b31377a563 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1115,9 +1115,12 @@ def get_document_cache_key(doctype: str, name: str):
return f"document_cache::{doctype}::{name}"
-def clear_document_cache(doctype, name):
+def clear_document_cache(doctype: str, name: str | None = None) -> None:
def clear_in_redis():
- cache().delete_value(get_document_cache_key(doctype, name))
+ if name is not None:
+ cache().delete_value(get_document_cache_key(doctype, name))
+ else:
+ cache().delete_keys(get_document_cache_key(doctype, ""))
clear_in_redis()
if hasattr(db, "after_commit"):
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 12e829ff09..eee703bcf9 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -125,8 +125,9 @@ def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
cache = frappe.cache()
- for key in ("is_table", "doctype_modules", "document_cache"):
+ for key in ("is_table", "doctype_modules"):
cache.delete_value(key)
+ cache.delete_keys("document_cache")
def clear_single(dt):
for name in doctype_cache_keys:
diff --git a/frappe/database/database.py b/frappe/database/database.py
index fc4f7b3b1b..2bac5a1ffc 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -920,10 +920,8 @@ class Database:
if isinstance(dn, str):
frappe.clear_document_cache(dt, dn)
else:
- # TODO: Fix this; doesn't work rn - gavin@frappe.io
- # frappe.cache().hdel_keys(dt, "document_cache")
- # Workaround: clear all document caches
- frappe.cache().delete_value("document_cache")
+ # No way to guess which documents are modified, clear all of them
+ frappe.clear_document_cache(dt)
for column, value in to_update.items():
query = query.set(column, value)
From 4193a251a5fe8def2665dc73047a04bc1f25a3aa Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 20:20:38 +0530
Subject: [PATCH 166/203] fix: Invalidate cache on rollback too
Steps:
- Document modified
- Document refetched from cache
- Transaction rolled back
- Cache now contains unmodified changes.
---
frappe/__init__.py | 1 +
frappe/tests/test_caching.py | 47 ++++++++++++++++++++++++++++++++++++
2 files changed, 48 insertions(+)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index b31377a563..8a2a4513d4 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -1125,6 +1125,7 @@ def clear_document_cache(doctype: str, name: str | None = None) -> None:
clear_in_redis()
if hasattr(db, "after_commit"):
db.after_commit.add(clear_in_redis)
+ db.after_rollback.add(clear_in_redis)
if doctype == "System Settings" and hasattr(local, "system_settings"):
delattr(local, "system_settings")
diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py
index 37f1583097..b9f7c66887 100644
--- a/frappe/tests/test_caching.py
+++ b/frappe/tests/test_caching.py
@@ -163,3 +163,50 @@ class TestRedisCache(FrappeAPITestCase):
calculate_area(radius=10)
# kwargs should hit cache too
self.assertEqual(function_call_count, 4)
+
+
+class TestDocumentCache(FrappeAPITestCase):
+ TEST_DOCTYPE = "User"
+ TEST_DOCNAME = "Administrator"
+ TEST_FIELD = "middle_name"
+
+ def setUp(self) -> None:
+ self.test_value = frappe.generate_hash()
+
+ def test_caching(self):
+ doc = frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+
+ with self.assertQueryCount(0):
+ doc = frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+
+ doc.db_set(self.TEST_FIELD, self.test_value)
+ new_doc = frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+
+ self.assertIsNot(doc, new_doc) # Shouldn't be same object from frappe.local
+ self.assertEqual(new_doc.get(self.TEST_FIELD), self.test_value) # Cache invalidated and fetched
+ frappe.db.rollback()
+
+ doc_after_rollback = frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+ self.assertIsNot(new_doc, doc_after_rollback)
+ # Cache invalidated after rollback
+ self.assertNotEqual(doc_after_rollback.get(self.TEST_FIELD), self.test_value)
+
+ with self.assertQueryCount(0):
+ frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+
+ def test_cache_invalidation_set_value(self):
+ doc = frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+
+ frappe.db.set_value(
+ self.TEST_DOCTYPE,
+ {"name": ("like", "%Admin%")},
+ self.TEST_FIELD,
+ self.test_value,
+ )
+
+ new_doc = frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+ self.assertIsNot(doc, new_doc)
+ self.assertEqual(new_doc.get(self.TEST_FIELD), self.test_value)
+
+ with self.assertQueryCount(0):
+ frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
From 7d50ef19d3a366c41cafb752e0d1ff12d69e182a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 21:25:04 +0530
Subject: [PATCH 167/203] perf: Delete multiple keys in O(1) redis calls
Currently we call redis for each key, redis already supports deleting
multiple keys in one go.
---
frappe/tests/test_caching.py | 15 +++++++++++++++
frappe/utils/redis_wrapper.py | 20 +++++++++++---------
2 files changed, 26 insertions(+), 9 deletions(-)
diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py
index b9f7c66887..c71f116649 100644
--- a/frappe/tests/test_caching.py
+++ b/frappe/tests/test_caching.py
@@ -210,3 +210,18 @@ class TestDocumentCache(FrappeAPITestCase):
with self.assertQueryCount(0):
frappe.get_cached_doc(self.TEST_DOCTYPE, self.TEST_DOCNAME)
+
+
+class TestRedisWrapper(FrappeAPITestCase):
+ def test_delete_keys(self):
+
+ c = frappe.cache()
+
+ prefix = "test_del_"
+
+ for i in range(5):
+ c.set_value(f"{prefix}{i}", 1)
+
+ self.assertEqual(len(c.get_keys(prefix)), 5)
+ c.delete_keys(prefix)
+ self.assertEqual(len(c.get_keys(prefix)), 0)
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index 3b335b2c1d..eb3ea3707e 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -127,20 +127,22 @@ class RedisWrapper(redis.Redis):
def delete_value(self, keys, user=None, make_keys=True, shared=False):
"""Delete value, list of values."""
+ if not keys:
+ return
+
if not isinstance(keys, (list, tuple)):
keys = (keys,)
+ if make_keys:
+ keys = [self.make_key(k, shared=shared, user=user) for k in keys]
+
for key in keys:
- if make_keys:
- key = self.make_key(key, shared=shared)
+ frappe.local.cache.pop(key, None)
- if key in frappe.local.cache:
- del frappe.local.cache[key]
-
- try:
- self.delete(key)
- except redis.exceptions.ConnectionError:
- pass
+ try:
+ self.delete(*keys)
+ except redis.exceptions.ConnectionError:
+ pass
def lpush(self, key, value):
super().lpush(self.make_key(key), value)
From 84ef2cc89ce141b12c14a77b5dd3926faee114b6 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 21:54:05 +0530
Subject: [PATCH 168/203] fix: reliable cache clearing for doctype
reclear cache after commit to prevent stale caching.
---
frappe/cache_manager.py | 11 ++++++++++-
1 file changed, 10 insertions(+), 1 deletion(-)
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index eee703bcf9..9c1754148a 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -123,13 +123,21 @@ def clear_defaults_cache(user=None):
def clear_doctype_cache(doctype=None):
clear_controller_cache(doctype)
+
+ _clear_doctype_cache_form_redis()
+ if hasattr(frappe.db, "after_commit"):
+ frappe.db.after_commit.add(_clear_doctype_cache_form_redis)
+ frappe.db.after_rollback.add(_clear_doctype_cache_form_redis)
+
+
+def _clear_doctype_cache_form_redis(doctype: str | None = None):
cache = frappe.cache()
for key in ("is_table", "doctype_modules"):
cache.delete_value(key)
- cache.delete_keys("document_cache")
def clear_single(dt):
+ frappe.clear_document_cache(dt)
for name in doctype_cache_keys:
cache.hdel(name, dt)
@@ -156,6 +164,7 @@ def clear_doctype_cache(doctype=None):
# clear all
for name in doctype_cache_keys:
cache.delete_value(name)
+ cache.delete_keys("document_cache::")
def clear_controller_cache(doctype=None):
From 0d056a3a2bc4f7e1ce9545eeb3ec00fa942ef042 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 22:23:11 +0530
Subject: [PATCH 169/203] test: fix broken tests
Fixture test:
This is broken cause it's trying to find doctype after it has been
deleted (wut?)
It was working so far because cache wasn't cleared correctly so you'd
still find it from cache.
db.set_value test:
converted to use last query instead of patching SQL
---
frappe/tests/test_db.py | 25 ++++++++++++-------------
frappe/tests/test_fixture_import.py | 8 +++++---
2 files changed, 17 insertions(+), 16 deletions(-)
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index e27d1db0ba..067d6c49b0 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -796,21 +796,20 @@ class TestDBSetValue(FrappeTestCase):
def test_set_value(self):
self.todo1.reload()
- with patch.object(Database, "sql") as sql_called:
- frappe.db.set_value(
- self.todo1.doctype,
- self.todo1.name,
- "description",
- f"{self.todo1.description}-edit by `test_for_update`",
- )
- first_query = sql_called.call_args_list[0].args[0]
+ frappe.db.set_value(
+ self.todo1.doctype,
+ self.todo1.name,
+ "description",
+ f"{self.todo1.description}-edit by `test_for_update`",
+ )
+ query = frappe.db.last_query
- if frappe.conf.db_type == "postgres":
- from frappe.database.postgres.database import modify_query
+ if frappe.conf.db_type == "postgres":
+ from frappe.database.postgres.database import modify_query
- self.assertTrue(modify_query("UPDATE `tabToDo` SET") in first_query)
- if frappe.conf.db_type == "mariadb":
- self.assertTrue("UPDATE `tabToDo` SET" in first_query)
+ self.assertTrue(modify_query("UPDATE `tabToDo` SET") in query)
+ if frappe.conf.db_type == "mariadb":
+ self.assertTrue("UPDATE `tabToDo` SET" in query)
def test_cleared_cache(self):
self.todo2.reload()
diff --git a/frappe/tests/test_fixture_import.py b/frappe/tests/test_fixture_import.py
index b9bd4550b2..8e4fa16763 100644
--- a/frappe/tests/test_fixture_import.py
+++ b/frappe/tests/test_fixture_import.py
@@ -69,10 +69,12 @@ class TestFixtureImport(FrappeTestCase):
import_doc(path_to_exported_fixtures)
- delete_doc("DocType", "temp_singles", delete_permanently=True)
- os.remove(path_to_exported_fixtures)
-
data = frappe.db.get_single_value("temp_singles", "member_name")
truncate_query.run()
self.assertEqual(data, dummy_name_list[0])
+
+ delete_doc("DocType", "temp_singles", delete_permanently=True)
+ os.remove(path_to_exported_fixtures)
+
+ frappe.db.commit()
From 26722b1a1cd63a456e23406c6abe3acfee12b406 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 3 Jun 2023 22:31:49 +0530
Subject: [PATCH 170/203] fix: ignore ConnectionError in
`frappe.cache().exists()`
---
frappe/tests/test_db.py | 2 +-
frappe/utils/redis_wrapper.py | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 067d6c49b0..afc24ecf68 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -802,7 +802,7 @@ class TestDBSetValue(FrappeTestCase):
"description",
f"{self.todo1.description}-edit by `test_for_update`",
)
- query = frappe.db.last_query
+ query = str(frappe.db.last_query)
if frappe.conf.db_type == "postgres":
from frappe.database.postgres.database import modify_query
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index eb3ea3707e..70280e4bf7 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -199,7 +199,11 @@ class RedisWrapper(redis.Redis):
def exists(self, *names: str, user=None, shared=None) -> int:
names = [self.make_key(n, user=user, shared=shared) for n in names]
- return super().exists(*names)
+
+ try:
+ return super().exists(*names)
+ except redis.exceptions.ConnectionError:
+ return False
def hgetall(self, name):
value = super().hgetall(self.make_key(name))
From 65a2cdcffc2c4e71d8e0700c741f3da61a97c101 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 4 Jun 2023 08:59:08 +0530
Subject: [PATCH 171/203] fix(safe_eval): Normalize code passed before
validating the code
---
frappe/__init__.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index e5a0b9c4aa..68797d2f06 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -16,6 +16,7 @@ import inspect
import json
import os
import re
+import unicodedata
import warnings
from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, TypeAlias, overload
@@ -2271,6 +2272,7 @@ def bold(text):
def safe_eval(code, eval_globals=None, eval_locals=None):
"""A safer `eval`"""
whitelisted_globals = {"int": int, "float": float, "long": int, "round": round}
+ code = unicodedata.normalize("NFKC", code)
UNSAFE_ATTRIBUTES = {
# Generator Attributes
From e30b823eeb2ff0819460eed9fe81e08159a0a310 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Fri, 2 Jun 2023 17:00:40 +0530
Subject: [PATCH 172/203] fix!: Webhook naming should be prompt
---
.../test_document_naming_settings.py | 31 +++++++++++++++----
.../doctype/webhook/test_webhook.py | 7 ++++-
.../integrations/doctype/webhook/webhook.json | 15 +++------
3 files changed, 35 insertions(+), 18 deletions(-)
diff --git a/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py b/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py
index 98ce9e738b..bcd3197112 100644
--- a/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py
+++ b/frappe/core/doctype/document_naming_settings/test_document_naming_settings.py
@@ -2,6 +2,7 @@
# See license.txt
import frappe
+from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.core.doctype.document_naming_settings.document_naming_settings import (
DocumentNamingSettings,
)
@@ -11,6 +12,25 @@ from frappe.utils import cint
class TestNamingSeries(FrappeTestCase):
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ cls.ns_doctype = (
+ new_doctype(
+ fields=[
+ {
+ "label": "Series",
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "options": f"\n{frappe.generate_hash()}-.###",
+ }
+ ],
+ autoname="naming_series:",
+ )
+ .insert()
+ .name
+ )
+
def setUp(self):
self.dns: DocumentNamingSettings = frappe.get_doc("Document Naming Settings")
@@ -23,7 +43,7 @@ class TestNamingSeries(FrappeTestCase):
return VALID_SERIES + exisiting_series
def test_naming_preview(self):
- self.dns.transaction_type = "Webhook"
+ self.dns.transaction_type = self.ns_doctype
self.dns.try_naming_series = "AXBZ.####"
serieses = self.dns.preview_series().split("\n")
@@ -35,23 +55,22 @@ class TestNamingSeries(FrappeTestCase):
def test_get_transactions(self):
naming_info = self.dns.get_transactions_and_prefixes()
- self.assertIn("Webhook", naming_info["transactions"])
+ self.assertIn(self.ns_doctype, naming_info["transactions"])
- existing_naming_series = frappe.get_meta("Webhook").get_field("naming_series").options
+ existing_naming_series = frappe.get_meta(self.ns_doctype).get_field("naming_series").options
for series in existing_naming_series.split("\n"):
self.assertIn(NamingSeries(series).get_prefix(), naming_info["prefixes"])
def test_default_naming_series(self):
- self.assertIn("HOOK", get_default_naming_series("Webhook"))
self.assertIsNone(get_default_naming_series("DocType"))
def test_updates_naming_options(self):
- self.dns.transaction_type = "Webhook"
+ self.dns.transaction_type = self.ns_doctype
test_series = "KOOHBEW.###"
self.dns.naming_series_options = self.dns.get_options() + "\n" + test_series
self.dns.update_series()
- self.assertIn(test_series, frappe.get_meta("Webhook").get_naming_series_options())
+ self.assertIn(test_series, frappe.get_meta(self.ns_doctype).get_naming_series_options())
def test_update_series_counter(self):
for series in self.get_valid_serieses():
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index 7235447662..2edf2fcf5c 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -14,7 +14,10 @@ from frappe.tests.utils import FrappeTestCase
@contextmanager
def get_test_webhook(config):
- wh = frappe.get_doc(config).insert()
+ wh = frappe.get_doc(config)
+ if not wh.name:
+ wh.name = frappe.generate_hash()
+ wh.insert()
wh.reload()
try:
yield wh
@@ -37,6 +40,7 @@ class TestWebhook(FrappeTestCase):
def create_sample_webhooks(cls):
samples_webhooks_data = [
{
+ "name": frappe.generate_hash(),
"webhook_doctype": "User",
"webhook_docevent": "after_insert",
"request_url": "https://httpbin.org/post",
@@ -44,6 +48,7 @@ class TestWebhook(FrappeTestCase):
"enabled": True,
},
{
+ "name": frappe.generate_hash(),
"webhook_doctype": "User",
"webhook_docevent": "after_insert",
"request_url": "https://httpbin.org/post",
diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json
index cfb2a2e01c..404e0be944 100644
--- a/frappe/integrations/doctype/webhook/webhook.json
+++ b/frappe/integrations/doctype/webhook/webhook.json
@@ -1,13 +1,12 @@
{
"actions": [],
- "autoname": "naming_series:",
+ "autoname": "prompt",
"creation": "2017-09-08 16:16:13.060641",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sb_doc_events",
- "naming_series",
"webhook_doctype",
"cb_doc_events",
"webhook_docevent",
@@ -46,6 +45,7 @@
{
"fieldname": "webhook_doctype",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1,
@@ -136,12 +136,6 @@
"label": "JSON Request Body",
"options": "JSON"
},
- {
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "label": "Naming Series",
- "options": "\nHOOK-.####"
- },
{
"fieldname": "sb_security",
"fieldtype": "Section Break",
@@ -218,11 +212,11 @@
"link_fieldname": "webhook"
}
],
- "modified": "2023-05-22 16:30:10.740512",
+ "modified": "2023-06-02 17:25:12.598232",
"modified_by": "Administrator",
"module": "Integrations",
"name": "Webhook",
- "naming_rule": "By \"Naming Series\" field",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
@@ -241,6 +235,5 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
- "title_field": "webhook_doctype",
"track_changes": 1
}
\ No newline at end of file
From 7c7c11b4541a27a265a6895a0ed6fbde4630a6bd Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 4 Jun 2023 12:31:07 +0530
Subject: [PATCH 173/203] perf: Cache web view routes
Each call to evaluate if route is web view makes N queries where N = #
of web view doctypes. This entire computation can be definitely cached
for short duration.
- Added cache bursting in WebsiteGenerator doctype updates.
- Added 60 minutes TTL in case cache invalidation wasn't done reliably.
---
frappe/__init__.py | 3 ++
.../website/page_renderers/document_page.py | 46 ++++++++++++-------
frappe/website/utils.py | 4 ++
3 files changed, 36 insertions(+), 17 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index e081a65e7b..9c40d56f07 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -876,6 +876,7 @@ def clear_cache(user: str | None = None, doctype: str | None = None):
:param doctype: If doctype is given, only DocType cache is cleared."""
import frappe.cache_manager
import frappe.utils.caching
+ from frappe.website.page_renderers.document_page import clear_routing_cache
if doctype:
frappe.cache_manager.clear_doctype_cache(doctype)
@@ -904,6 +905,8 @@ def clear_cache(user: str | None = None, doctype: str | None = None):
if hasattr(local, "website_settings"):
del local.website_settings
+ clear_routing_cache()
+
def only_has_select_perm(doctype, user=None, ignore_permissions=False):
if ignore_permissions:
diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py
index abfd72ac6f..1d8ce82317 100644
--- a/frappe/website/page_renderers/document_page.py
+++ b/frappe/website/page_renderers/document_page.py
@@ -1,5 +1,6 @@
import frappe
from frappe.model.document import get_controller
+from frappe.utils.caching import redis_cache
from frappe.website.page_renderers.base_template_page import BaseTemplatePage
from frappe.website.router import (
get_doctypes_with_web_view,
@@ -22,22 +23,9 @@ class DocumentPage(BaseTemplatePage):
return False
def search_in_doctypes_with_web_view(self):
- for doctype in get_doctypes_with_web_view():
- filters = dict(route=self.path)
- meta = frappe.get_meta(doctype)
- condition_field = self.get_condition_field(meta)
-
- if condition_field:
- filters[condition_field] = 1
-
- try:
- self.docname = frappe.db.get_value(doctype, filters, "name")
- if self.docname:
- self.doctype = doctype
- return True
- except Exception as e:
- if not frappe.db.is_missing_column(e):
- raise e
+ if document := _find_matching_document_webview(self.path):
+ self.doctype, self.docname = document
+ return True
def search_web_page_dynamic_routes(self):
d = get_page_info_from_web_page_with_dynamic_routes(self.path)
@@ -83,7 +71,8 @@ class DocumentPage(BaseTemplatePage):
if prop not in self.context:
self.context[prop] = getattr(self.doc, prop, False)
- def get_condition_field(self, meta):
+ @staticmethod
+ def get_condition_field(meta):
condition_field = None
if meta.is_published_field:
condition_field = meta.is_published_field
@@ -92,3 +81,26 @@ class DocumentPage(BaseTemplatePage):
condition_field = controller.website.condition_field
return condition_field
+
+
+@redis_cache(ttl=60 * 60)
+def _find_matching_document_webview(route: str) -> tuple[str, str] | None:
+ for doctype in get_doctypes_with_web_view():
+ filters = dict(route=route)
+ meta = frappe.get_meta(doctype)
+ condition_field = DocumentPage.get_condition_field(meta)
+
+ if condition_field:
+ filters[condition_field] = 1
+
+ try:
+ docname = frappe.db.get_value(doctype, filters, "name")
+ if docname:
+ return (doctype, docname)
+ except Exception as e:
+ if not frappe.db.is_missing_column(e):
+ raise e
+
+
+def clear_routing_cache():
+ _find_matching_document_webview.clear_cache()
diff --git a/frappe/website/utils.py b/frappe/website/utils.py
index 71af463c96..66b4198256 100644
--- a/frappe/website/utils.py
+++ b/frappe/website/utils.py
@@ -360,9 +360,13 @@ def get_html_content_based_on_type(doc, fieldname, content_type):
def clear_cache(path=None):
"""Clear website caches
:param path: (optional) for the given path"""
+ from frappe.website.page_renderers.document_page import clear_routing_cache
+
for key in ("website_generator_routes", "website_pages", "website_full_index", "sitemap_routes"):
frappe.cache().delete_value(key)
+ clear_routing_cache()
+
frappe.cache().delete_value("website_404")
if path:
frappe.cache().hdel("website_redirects", path)
From c94c0591c338f41df97a7020e754777ea464b948 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 4 Jun 2023 12:47:09 +0530
Subject: [PATCH 174/203] perf: cache dynamic web pages
---
frappe/website/doctype/web_page/web_page.py | 16 +++++++++++-----
frappe/website/page_renderers/document_page.py | 3 +++
frappe/website/router.py | 7 +++----
3 files changed, 17 insertions(+), 9 deletions(-)
diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py
index 9a16654085..2f89252626 100644
--- a/frappe/website/doctype/web_page/web_page.py
+++ b/frappe/website/doctype/web_page/web_page.py
@@ -8,6 +8,7 @@ from jinja2.exceptions import TemplateSyntaxError
import frappe
from frappe import _
from frappe.utils import get_datetime, now, quoted, strip_html
+from frappe.utils.caching import redis_cache
from frappe.utils.jinja import render_template
from frappe.utils.safe_exec import safe_exec
from frappe.website.doctype.website_slideshow.website_slideshow import get_slideshow
@@ -30,11 +31,9 @@ class WebPage(WebsiteGenerator):
if not self.dynamic_route:
self.route = quoted(self.route)
- def on_update(self):
- super().on_update()
-
- def on_trash(self):
- super().on_trash()
+ def clear_cache(self):
+ super().clear_cache()
+ get_dynamic_web_pages.clear_cache()
def get_context(self, context):
context.main_section = get_html_content_based_on_type(self, "main_section", self.content_type)
@@ -247,3 +246,10 @@ def extract_script_and_style_tags(html):
style.extract()
return str(soup), scripts, styles
+
+
+@redis_cache(ttl=60 * 60)
+def get_dynamic_web_pages() -> dict[str, str]:
+ return frappe.get_all(
+ "Web Page", fields=["name", "route", "modified"], filters=dict(published=1, dynamic_route=1)
+ )
diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py
index 1d8ce82317..6c760fa0b0 100644
--- a/frappe/website/page_renderers/document_page.py
+++ b/frappe/website/page_renderers/document_page.py
@@ -103,4 +103,7 @@ def _find_matching_document_webview(route: str) -> tuple[str, str] | None:
def clear_routing_cache():
+ from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages
+
_find_matching_document_webview.clear_cache()
+ get_dynamic_web_pages.clear_cache()
diff --git a/frappe/website/router.py b/frappe/website/router.py
index 655fcc1357..4493d437b8 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -16,12 +16,11 @@ def get_page_info_from_web_page_with_dynamic_routes(path):
"""
Query Web Page with dynamic_route = 1 and evaluate if any of the routes match
"""
+ from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages
+
rules, page_info = [], {}
- # build rules from all web page with `dynamic_route = 1`
- for d in frappe.get_all(
- "Web Page", fields=["name", "route", "modified"], filters=dict(published=1, dynamic_route=1)
- ):
+ for d in get_dynamic_web_pages():
rules.append(Rule("/" + d.route, endpoint=d.name))
d.doctype = "Web Page"
page_info[d.name] = d
From 392a506a76c0e8c8cf743b937fc3b402b4920dfc Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 4 Jun 2023 12:57:56 +0530
Subject: [PATCH 175/203] perf: Cache published web forms
---
frappe/__init__.py | 2 +-
frappe/website/doctype/web_form/web_form.py | 9 ++++++---
frappe/website/doctype/web_page/web_page.py | 4 ----
frappe/website/page_renderers/document_page.py | 7 -------
frappe/website/router.py | 15 +++++++++++++--
frappe/website/utils.py | 2 +-
6 files changed, 21 insertions(+), 18 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 9c40d56f07..ff2a663d50 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -876,7 +876,7 @@ def clear_cache(user: str | None = None, doctype: str | None = None):
:param doctype: If doctype is given, only DocType cache is cleared."""
import frappe.cache_manager
import frappe.utils.caching
- from frappe.website.page_renderers.document_page import clear_routing_cache
+ from frappe.website.router import clear_routing_cache
if doctype:
frappe.cache_manager.clear_doctype_cache(doctype)
diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py
index 81c6001558..fd9949c45f 100644
--- a/frappe/website/doctype/web_form/web_form.py
+++ b/frappe/website/doctype/web_form/web_form.py
@@ -12,6 +12,7 @@ from frappe.desk.form.meta import get_code_files_via_hooks
from frappe.modules.utils import export_module_json, get_doc_module
from frappe.rate_limiter import rate_limit
from frappe.utils import cstr, dict_with_keys, strip_html
+from frappe.utils.caching import redis_cache
from frappe.website.utils import get_boot_data, get_comment_list, get_sidebar_items
from frappe.website.website_generator import WebsiteGenerator
@@ -19,9 +20,6 @@ from frappe.website.website_generator import WebsiteGenerator
class WebForm(WebsiteGenerator):
website = frappe._dict(no_cache=1)
- def onload(self):
- super().onload()
-
def validate(self):
super().validate()
@@ -639,3 +637,8 @@ def get_link_options(web_form_name, doctype, allow_read_on_all_link_options=Fals
raise frappe.PermissionError(
_("You don't have permission to access the {0} DocType.").format(doctype)
)
+
+
+@redis_cache(ttl=60 * 60)
+def get_published_web_forms() -> dict[str, str]:
+ return frappe.get_all("Web Form", ["name", "route", "modified"], {"published": 1})
diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py
index 2f89252626..02e419001c 100644
--- a/frappe/website/doctype/web_page/web_page.py
+++ b/frappe/website/doctype/web_page/web_page.py
@@ -31,10 +31,6 @@ class WebPage(WebsiteGenerator):
if not self.dynamic_route:
self.route = quoted(self.route)
- def clear_cache(self):
- super().clear_cache()
- get_dynamic_web_pages.clear_cache()
-
def get_context(self, context):
context.main_section = get_html_content_based_on_type(self, "main_section", self.content_type)
context.source_content_type = self.content_type
diff --git a/frappe/website/page_renderers/document_page.py b/frappe/website/page_renderers/document_page.py
index 6c760fa0b0..54ee58ddb9 100644
--- a/frappe/website/page_renderers/document_page.py
+++ b/frappe/website/page_renderers/document_page.py
@@ -100,10 +100,3 @@ def _find_matching_document_webview(route: str) -> tuple[str, str] | None:
except Exception as e:
if not frappe.db.is_missing_column(e):
raise e
-
-
-def clear_routing_cache():
- from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages
-
- _find_matching_document_webview.clear_cache()
- get_dynamic_web_pages.clear_cache()
diff --git a/frappe/website/router.py b/frappe/website/router.py
index 4493d437b8..98be1138e4 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -32,9 +32,10 @@ def get_page_info_from_web_page_with_dynamic_routes(path):
def get_page_info_from_web_form(path):
"""Query published web forms and evaluate if the route matches"""
+ from frappe.website.doctype.web_form.web_form import get_published_web_forms
+
rules, page_info = [], {}
- web_forms = frappe.get_all("Web Form", ["name", "route", "modified"], {"published": 1})
- for d in web_forms:
+ for d in get_published_web_forms():
rules.append(Rule(f"/{d.route}", endpoint=d.name))
rules.append(Rule(f"/{d.route}/list", endpoint=d.name))
rules.append(Rule(f"/{d.route}/new", endpoint=d.name))
@@ -314,3 +315,13 @@ def get_doctypes_with_web_view():
def get_start_folders():
return frappe.local.flags.web_pages_folders or ("www", "templates/pages")
+
+
+def clear_routing_cache():
+ from frappe.website.doctype.web_form.web_form import get_published_web_forms
+ from frappe.website.doctype.web_page.web_page import get_dynamic_web_pages
+ from frappe.website.page_renderers.document_page import _find_matching_document_webview
+
+ _find_matching_document_webview.clear_cache()
+ get_dynamic_web_pages.clear_cache()
+ get_published_web_forms.clear_cache()
diff --git a/frappe/website/utils.py b/frappe/website/utils.py
index 66b4198256..ff8c69639e 100644
--- a/frappe/website/utils.py
+++ b/frappe/website/utils.py
@@ -360,7 +360,7 @@ def get_html_content_based_on_type(doc, fieldname, content_type):
def clear_cache(path=None):
"""Clear website caches
:param path: (optional) for the given path"""
- from frappe.website.page_renderers.document_page import clear_routing_cache
+ from frappe.website.router import clear_routing_cache
for key in ("website_generator_routes", "website_pages", "website_full_index", "sitemap_routes"):
frappe.cache().delete_value(key)
From a7413fe4a7d7ef19a6d78dbf9d21eb464c7dbfa2 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sun, 4 Jun 2023 13:02:16 +0530
Subject: [PATCH 176/203] chore: separate out func call key for @redis_cache
---
frappe/utils/caching.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/utils/caching.py b/frappe/utils/caching.py
index 007582f25f..370227ea72 100644
--- a/frappe/utils/caching.py
+++ b/frappe/utils/caching.py
@@ -150,7 +150,7 @@ def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Calla
@wraps(func)
def redis_cache_wrapper(*args, **kwargs):
- func_call_key = func_key + str(__generate_request_cache_key(args, kwargs))
+ func_call_key = func_key + "::" + str(__generate_request_cache_key(args, kwargs))
if frappe.cache().exists(func_call_key):
return frappe.cache().get_value(func_call_key, user=user)
else:
From 54fabab0105c79e3084629d34d5d40d6908226d6 Mon Sep 17 00:00:00 2001
From: gn306029
Date: Mon, 5 Jun 2023 15:04:17 +0800
Subject: [PATCH 177/203] fix: currency formatter got incorrect format when use
precision 0 (#21239)
* fix: currency formatter got incorrect format when use precision 0
* Revert "fix: currency formatter got incorrect format when use precision 0"
This reverts commit 1919cf4763b16e0cca2c2596223443d901e00e27.
* fix: allow 0 as default precision
---------
Co-authored-by: Ankush Menat
---
frappe/public/js/frappe/form/formatters.js | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 9739eed8bb..637fd7063d 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -103,8 +103,9 @@ frappe.form.formatters = {
},
Currency: function (value, docfield, options, doc) {
var currency = frappe.meta.get_field_currency(docfield, doc);
- var precision =
- docfield.precision || cint(frappe.boot.sysdefaults.currency_precision) || 2;
+ var precision = cint(
+ docfield.precision ?? frappe.boot.sysdefaults.currency_precision ?? 2
+ );
// If you change anything below, it's going to hurt a company in UAE, a bit.
if (precision > 2) {
From 64613fec4835eca8a7afb9af5719971b826bf9e3 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 5 Jun 2023 15:11:46 +0530
Subject: [PATCH 178/203] fix: changed section with card url field to small
text
---
.../section_with_cards.json | 20 +++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/frappe/website/web_template/section_with_cards/section_with_cards.json b/frappe/website/web_template/section_with_cards/section_with_cards.json
index c891119f97..5501147d89 100644
--- a/frappe/website/web_template/section_with_cards/section_with_cards.json
+++ b/frappe/website/web_template/section_with_cards/section_with_cards.json
@@ -49,7 +49,7 @@
},
{
"fieldname": "card_1_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -79,7 +79,7 @@
},
{
"fieldname": "card_2_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -109,7 +109,7 @@
},
{
"fieldname": "card_3_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -139,7 +139,7 @@
},
{
"fieldname": "card_4_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -169,7 +169,7 @@
},
{
"fieldname": "card_5_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -199,7 +199,7 @@
},
{
"fieldname": "card_6_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -229,7 +229,7 @@
},
{
"fieldname": "card_7_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -259,7 +259,7 @@
},
{
"fieldname": "card_8_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -289,13 +289,13 @@
},
{
"fieldname": "card_9_url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
}
],
"idx": 0,
- "modified": "2021-05-03 13:26:34.470232",
+ "modified": "2023-06-05 13:26:34.470232",
"modified_by": "Administrator",
"module": "Website",
"name": "Section with Cards",
From 2358bd8fc1c9bf8ba2d89da56b259e7122974907 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 5 Jun 2023 15:17:00 +0530
Subject: [PATCH 179/203] fix: changed for section with features and
testimonial web temlates
---
.../section_with_features/section_with_features.json | 4 ++--
.../section_with_testimonials/section_with_testimonials.json | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/website/web_template/section_with_features/section_with_features.json b/frappe/website/web_template/section_with_features/section_with_features.json
index a5734aa293..2683e92aae 100644
--- a/frappe/website/web_template/section_with_features/section_with_features.json
+++ b/frappe/website/web_template/section_with_features/section_with_features.json
@@ -43,7 +43,7 @@
},
{
"fieldname": "url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
},
@@ -55,7 +55,7 @@
}
],
"idx": 2,
- "modified": "2020-10-26 17:43:08.219285",
+ "modified": "2023-06-05 13:26:34.470232",
"modified_by": "Administrator",
"module": "Website",
"name": "Section with Features",
diff --git a/frappe/website/web_template/section_with_testimonials/section_with_testimonials.json b/frappe/website/web_template/section_with_testimonials/section_with_testimonials.json
index c1ba071be2..dd1d3bd0bd 100644
--- a/frappe/website/web_template/section_with_testimonials/section_with_testimonials.json
+++ b/frappe/website/web_template/section_with_testimonials/section_with_testimonials.json
@@ -56,13 +56,13 @@
},
{
"fieldname": "url",
- "fieldtype": "Data",
+ "fieldtype": "Small Text",
"label": "URL",
"reqd": 0
}
],
"idx": 0,
- "modified": "2022-03-21 15:39:39.044104",
+ "modified": "2023-06-05 13:26:34.470232",
"modified_by": "Administrator",
"module": "Website",
"name": "Section with Testimonials",
From ba9e2aab175bd185bddf54d7ac8859a228c46140 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Mon, 5 Jun 2023 22:11:42 +0530
Subject: [PATCH 180/203] fix: avoid loading workspace content if already
loaded
---
frappe/public/js/frappe/views/workspace/workspace.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/views/workspace/workspace.js b/frappe/public/js/frappe/views/workspace/workspace.js
index b5fb0e2e54..894844497b 100644
--- a/frappe/public/js/frappe/views/workspace/workspace.js
+++ b/frappe/public/js/frappe/views/workspace/workspace.js
@@ -22,6 +22,7 @@ frappe.views.Workspace = class Workspace {
this.page = wrapper.page;
this.blocks = frappe.workspace_block.blocks;
this.is_read_only = true;
+ this.is_page_loaded = false;
this.pages = {};
this.sorted_public_items = [];
this.sorted_private_items = [];
@@ -248,10 +249,14 @@ frappe.views.Workspace = class Workspace {
this.update_selected_sidebar(page, true); //add selected on new page
if (!frappe.router.current_route[0]) {
+ this.is_page_loaded = true;
frappe.set_route(frappe.router.slug(page.public ? page.name : "private/" + page.name));
}
- this.show_page(page);
+ if (!this.is_page_loaded) {
+ this.show_page(page);
+ this.is_page_loaded = false;
+ }
}
update_selected_sidebar(page, add) {
From 90fd74859200d6151ca7bbbbc9378aa23efc8044 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Mon, 5 Jun 2023 22:27:01 +0530
Subject: [PATCH 181/203] fix(UX): Notify if newly created user has no roles
(#21251)
- System user with no role is useless
- By default adding a new system user doesn't give them ANY role so
they can't really access desk even if they have system user role.
---
frappe/core/doctype/user/user.json | 3 ++-
frappe/core/doctype/user/user.py | 16 ++++++++++++++++
2 files changed, 18 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index 654f20936e..0396776183 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -212,6 +212,7 @@
"read_only": 1
},
{
+ "allow_in_quick_entry": 1,
"fieldname": "role_profile_name",
"fieldtype": "Link",
"label": "Role Profile",
@@ -761,7 +762,7 @@
"link_fieldname": "user"
}
],
- "modified": "2023-05-24 15:20:06.434506",
+ "modified": "2023-06-05 17:26:04.127555",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 94ea8b16a0..81d9715c32 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -75,6 +75,7 @@ class User(Document):
self.validate_email_type(self.email)
self.validate_email_type(self.name)
self.add_system_manager_role()
+ self.check_roles_added()
self.set_system_user()
self.set_full_name()
self.check_enable_disable()
@@ -673,6 +674,21 @@ class User(Document):
if not self.time_zone:
self.time_zone = get_system_timezone()
+ def check_roles_added(self):
+ if self.user_type != "System User" or self.roles or not self.is_new():
+ return
+
+ frappe.msgprint(
+ _("Newly created user {0} has no roles enabled.").format(frappe.bold(self.name)),
+ title=_("No Roles Specified"),
+ indicator="orange",
+ primary_action={
+ "label": _("Add Roles"),
+ "client_action": "frappe.set_route",
+ "args": ["Form", self.doctype, self.name],
+ },
+ )
+
@frappe.whitelist()
def get_timezones():
From 4f459f7146ee2dec1c3ba113554543fa251004b1 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Tue, 6 Jun 2023 08:07:01 +0530
Subject: [PATCH 182/203] fix(unrelated): make create doctype using form
builder default button
---
frappe/core/doctype/doctype/doctype_list.js | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/frappe/core/doctype/doctype/doctype_list.js b/frappe/core/doctype/doctype/doctype_list.js
index c66edf1e21..f4811fa01d 100644
--- a/frappe/core/doctype/doctype/doctype_list.js
+++ b/frappe/core/doctype/doctype/doctype_list.js
@@ -6,16 +6,16 @@ frappe.listview_settings["DocType"] = {
setup_select_primary_button: function (me) {
let actions = [
- {
- label: __("Add DocType"),
- description: __("Create a new DocType"),
- action: () => frappe.new_doc("DocType"),
- },
{
label: __("Add DocType (Form Builder)"),
description: __("Use the form builder to create a new DocType"),
action: () => frappe.set_route("form-builder", "new-doctype"),
},
+ {
+ label: __("Add DocType"),
+ description: __("Create a new DocType"),
+ action: () => frappe.new_doc("DocType"),
+ },
];
frappe.utils.add_select_group_button(
From 34ee6a16776c060ea7add27bd3b0a5dc2b8c7e1f Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 6 Jun 2023 16:11:02 +0530
Subject: [PATCH 183/203] fix: declare the function (#21261)
[skip ci]
---
frappe/desk/doctype/form_tour/form_tour.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 8a65cc1619..390f519367 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -95,7 +95,7 @@ frappe.ui.form.on("Form Tour", {
},
});
-add_custom_button = (frm) => {
+let add_custom_button = (frm) => {
if (frm.doc.ui_tour) {
frm.add_custom_button(__("Reset"), function () {
frappe.confirm(
From 094b8589f750b660d169d136ce0747240471d3dd Mon Sep 17 00:00:00 2001
From: Ritwik Puri
Date: Wed, 7 Jun 2023 00:01:52 +0530
Subject: [PATCH 184/203] chore: add skipped in list view for patch log
(#21264)
---
frappe/core/doctype/patch_log/patch_log.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/patch_log/patch_log.json b/frappe/core/doctype/patch_log/patch_log.json
index 53e85b99d3..6be3ce089e 100644
--- a/frappe/core/doctype/patch_log/patch_log.json
+++ b/frappe/core/doctype/patch_log/patch_log.json
@@ -22,6 +22,7 @@
"default": "0",
"fieldname": "skipped",
"fieldtype": "Check",
+ "in_list_view": 1,
"label": "Skipped",
"read_only": 1
},
@@ -36,7 +37,7 @@
"icon": "fa fa-cog",
"idx": 1,
"links": [],
- "modified": "2023-05-10 19:27:10.883330",
+ "modified": "2023-06-07 00:00:01.369265",
"modified_by": "Administrator",
"module": "Core",
"name": "Patch Log",
From 1607eb9ef3324d9f603e0aa534ad3c502484971d Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Wed, 7 Jun 2023 09:57:43 +0530
Subject: [PATCH 185/203] fix: added breadcrumbs on workflow builder page
---
frappe/public/js/workflow_builder/store.js | 11 +++++++++++
1 file changed, 11 insertions(+)
diff --git a/frappe/public/js/workflow_builder/store.js b/frappe/public/js/workflow_builder/store.js
index 66e04adcb7..491e349ace 100644
--- a/frappe/public/js/workflow_builder/store.js
+++ b/frappe/public/js/workflow_builder/store.js
@@ -64,6 +64,7 @@ export const useStore = defineStore("workflow-builder-store", () => {
}
setup_undo_redo();
+ setup_breadcrumbs();
}
function reset_changes() {
@@ -105,6 +106,16 @@ export const useStore = defineStore("workflow-builder-store", () => {
workflow.value.elements.forEach((el) => (el.selected = false));
}
+ function setup_breadcrumbs() {
+ let breadcrumbs = `
+ ${__("Workflow")}
+ ${__(workflow_name.value)}
+ ${__("Workflow Builder")}
+ `;
+ frappe.breadcrumbs.clear();
+ frappe.breadcrumbs.$breadcrumbs.append(breadcrumbs);
+ }
+
function get_state_df(data) {
let doc_status_map = {
Draft: 0,
From 984963d75e70bdbf9d0abaed6f3974a4a6776e28 Mon Sep 17 00:00:00 2001
From: Daizy Modi
Date: Wed, 7 Jun 2023 13:56:50 +0530
Subject: [PATCH 186/203] chore: remove unused `get_docname` (#21270)
---
.../website/doctype/discussion_topic/discussion_topic.py | 7 -------
1 file changed, 7 deletions(-)
diff --git a/frappe/website/doctype/discussion_topic/discussion_topic.py b/frappe/website/doctype/discussion_topic/discussion_topic.py
index 7eb661cd02..ddc4933548 100644
--- a/frappe/website/doctype/discussion_topic/discussion_topic.py
+++ b/frappe/website/doctype/discussion_topic/discussion_topic.py
@@ -39,10 +39,3 @@ def save_message(reply, topic):
frappe.get_doc({"doctype": "Discussion Reply", "reply": reply, "topic": topic}).save(
ignore_permissions=True
)
-
-
-@frappe.whitelist(allow_guest=True)
-def get_docname(route):
- if not route:
- route = frappe.db.get_single_value("Website Settings", "home_page")
- return frappe.db.get_value("Web Page", {"route": route}, ["name"])
From 2cb2934f96e2847cf1901c2e195026c85ac4630e Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 7 Jun 2023 18:05:06 +0530
Subject: [PATCH 187/203] perf: query queued_recipients only once (#21277)
O(N) -> O(1) check for queued recipients
Caused by https://github.com/frappe/frappe/pull/18044
Co-authored-by: Suraj Shetty
---
frappe/email/doctype/newsletter/newsletter.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 4a2f69a44c..63049fe83d 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -143,7 +143,9 @@ class Newsletter(WebsiteGenerator):
"""Get list of pending recipients of the newsletter. These
recipients may not have receive the newsletter in the previous iteration.
"""
- return [x for x in self.newsletter_recipients if x not in self.get_queued_recipients()]
+
+ queued_recipients = set(self.get_queued_recipients())
+ return [x for x in self.newsletter_recipients if x not in queued_recipients]
def queue_all(self):
"""Queue Newsletter to all the recipients generated from the `Email Group` table"""
From 40b3cd82bc41fd0a99fa83a6c5f5c622e60d2ff6 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 7 Jun 2023 22:47:45 +0530
Subject: [PATCH 188/203] feat!: `frappe.cache()` -> `frappe.cache` (#21279)
This is more intuitive and consistent with other things like `frappe.db`.
PS: This is quite likely to break some weird usage which I can't guess right now. Normal usage inside request/job cycles will continue to work as it used to.
---
frappe/__init__.py | 14 ++++++++------
frappe/tests/test_caching.py | 13 +++++++------
frappe/utils/redis_wrapper.py | 4 ++++
3 files changed, 19 insertions(+), 12 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index ff2a663d50..7dbf7e81d0 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -152,6 +152,7 @@ def set_user_lang(user: str, user_language: str | None = None) -> None:
# local-globals
db = local("db")
+cache = local("_redis_cache")
qb = local("qb")
conf = local("conf")
form = form_dict = local("form_dict")
@@ -241,6 +242,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.dev_server = _dev_server
local.qb = get_query_builder(local.conf.db_type or "mariadb")
local.qb.get_query = get_query
+ local._redis_cache = _get_redis_cache()
setup_module_map()
if not _qb_patched.get(local.conf.db_type):
@@ -348,17 +350,17 @@ def destroy():
release_local(local)
-redis_server = None
+_redis_cache_conn = None
-def cache() -> "RedisWrapper":
+def _get_redis_cache() -> "RedisWrapper":
"""Returns redis connection."""
- global redis_server
- if not redis_server:
+ global _redis_cache_conn
+ if not _redis_cache_conn:
from frappe.utils.redis_wrapper import RedisWrapper
- redis_server = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311")
- return redis_server
+ _redis_cache_conn = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311")
+ return _redis_cache_conn
def get_traceback(with_context: bool = False) -> str:
diff --git a/frappe/tests/test_caching.py b/frappe/tests/test_caching.py
index c71f116649..f3f9d52f25 100644
--- a/frappe/tests/test_caching.py
+++ b/frappe/tests/test_caching.py
@@ -215,13 +215,14 @@ class TestDocumentCache(FrappeAPITestCase):
class TestRedisWrapper(FrappeAPITestCase):
def test_delete_keys(self):
- c = frappe.cache()
-
prefix = "test_del_"
for i in range(5):
- c.set_value(f"{prefix}{i}", 1)
+ frappe.cache.set_value(f"{prefix}{i}", 1)
- self.assertEqual(len(c.get_keys(prefix)), 5)
- c.delete_keys(prefix)
- self.assertEqual(len(c.get_keys(prefix)), 0)
+ self.assertEqual(len(frappe.cache.get_keys(prefix)), 5)
+ frappe.cache.delete_keys(prefix)
+ self.assertEqual(len(frappe.cache.get_keys(prefix)), 0)
+
+ def test_backward_compat_cache(self):
+ self.assertEqual(frappe.cache, frappe.cache())
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index 70280e4bf7..c23763f2b6 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -34,6 +34,10 @@ class RedisWrapper(redis.Redis):
except redis.exceptions.ConnectionError:
return False
+ def __call__(self):
+ """WARNING: Added for backward compatibility to support frappe.cache().method(...)"""
+ return self
+
def make_key(self, key, user=None, shared=False):
if shared:
return key
From b6669bb56ee81b1fdacaafca6d9d7688dc0f4259 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Thu, 8 Jun 2023 10:21:45 +0530
Subject: [PATCH 189/203] perf: remove localproxy for `frappe.cache` (#21281)
* perf: remove localproxy for `frappe.cache`
* chore: fix type hints
---
frappe/__init__.py | 18 ++++++++----------
1 file changed, 8 insertions(+), 10 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7dbf7e81d0..84ac41ff66 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -48,6 +48,7 @@ __title__ = "Frappe Framework"
controllers = {}
local = Local()
+cache = None
STANDARD_USERS = ("Guest", "Administrator")
_dev_server = int(sbool(os.environ.get("DEV_SERVER", False)))
@@ -152,7 +153,6 @@ def set_user_lang(user: str, user_language: str | None = None) -> None:
# local-globals
db = local("db")
-cache = local("_redis_cache")
qb = local("qb")
conf = local("conf")
form = form_dict = local("form_dict")
@@ -179,6 +179,7 @@ if TYPE_CHECKING:
db: MariaDBDatabase | PostgresDatabase
qb: MariaDB | Postgres
+ cache: RedisWrapper
# end: static analysis hack
@@ -242,7 +243,7 @@ def init(site: str, sites_path: str = ".", new_site: bool = False, force=False)
local.dev_server = _dev_server
local.qb = get_query_builder(local.conf.db_type or "mariadb")
local.qb.get_query = get_query
- local._redis_cache = _get_redis_cache()
+ setup_redis_cache_connection()
setup_module_map()
if not _qb_patched.get(local.conf.db_type):
@@ -350,17 +351,14 @@ def destroy():
release_local(local)
-_redis_cache_conn = None
+def setup_redis_cache_connection():
+ """Defines `frappe.cache` as `RedisWrapper` instance"""
+ global cache
-
-def _get_redis_cache() -> "RedisWrapper":
- """Returns redis connection."""
- global _redis_cache_conn
- if not _redis_cache_conn:
+ if not cache:
from frappe.utils.redis_wrapper import RedisWrapper
- _redis_cache_conn = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311")
- return _redis_cache_conn
+ cache = RedisWrapper.from_url(conf.get("redis_cache") or "redis://localhost:11311")
def get_traceback(with_context: bool = False) -> str:
From fa6dc03cc87ad74e11609e7373078366fdcb3e1b Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 8 Jun 2023 11:47:17 +0530
Subject: [PATCH 190/203] refactor: frappe.cache() usage to frappe.cache
(#21282)
---
frappe/__init__.py | 34 +++++++--------
frappe/auth.py | 16 +++----
.../test_milestone_tracker.py | 2 +-
frappe/boot.py | 6 +--
frappe/cache_manager.py | 42 ++++++++-----------
frappe/core/doctype/data_import/importer.py | 4 +-
frappe/core/doctype/doctype/doctype.py | 2 +-
.../domain_settings/domain_settings.py | 4 +-
frappe/core/doctype/report/report.py | 2 +-
frappe/core/doctype/role/role.py | 2 +-
.../doctype/server_script/server_script.py | 6 +--
.../server_script/server_script_utils.py | 4 +-
.../server_script/test_server_script.py | 4 +-
.../system_settings/system_settings.py | 4 +-
.../core/doctype/translation/translation.py | 4 +-
frappe/core/doctype/user/test_user.py | 4 +-
frappe/core/doctype/user/user.py | 22 +++++-----
frappe/core/doctype/user_group/user_group.py | 4 +-
.../user_permission/test_user_permission.py | 2 +-
.../user_permission/user_permission.py | 8 ++--
frappe/core/doctype/user_type/user_type.py | 4 +-
frappe/database/database.py | 12 +++---
frappe/database/mariadb/database.py | 4 +-
frappe/database/schema.py | 2 +-
frappe/defaults.py | 4 +-
frappe/deferred_insert.py | 8 ++--
frappe/desk/desktop.py | 12 +++---
.../dashboard_chart/dashboard_chart.py | 2 +-
.../desk/doctype/desktop_icon/desktop_icon.py | 10 ++---
frappe/desk/doctype/form_tour/form_tour.py | 8 ++--
.../global_search_settings.py | 4 +-
.../desk/doctype/kanban_board/kanban_board.py | 2 +-
frappe/desk/form/linked_with.py | 4 +-
frappe/desk/form/meta.py | 4 +-
frappe/desk/notifications.py | 16 ++++---
frappe/desk/page/setup_wizard/setup_wizard.py | 4 +-
frappe/desk/query_report.py | 4 +-
frappe/desk/search.py | 4 +-
frappe/email/__init__.py | 6 +--
.../doctype/email_account/email_account.py | 14 +++----
.../doctype/notification/notification.py | 6 +--
.../google_calendar/google_calendar.py | 4 +-
.../integrations/doctype/webhook/__init__.py | 4 +-
.../doctype/webhook/test_webhook.py | 2 +-
.../integrations/doctype/webhook/webhook.py | 2 +-
frappe/model/document.py | 4 +-
frappe/model/meta.py | 6 +--
frappe/model/utils/link_count.py | 8 ++--
frappe/model/utils/user_settings.py | 8 ++--
frappe/model/workflow.py | 8 ++--
frappe/modules/utils.py | 2 +-
frappe/monitor.py | 10 ++---
frappe/permissions.py | 2 +-
frappe/rate_limiter.py | 12 +++---
frappe/recorder.py | 22 +++++-----
frappe/sessions.py | 24 +++++------
.../energy_point_log/energy_point_log.py | 5 +--
.../energy_point_log/test_energy_point_log.py | 4 +-
frappe/tests/test_boot.py | 2 +-
frappe/tests/test_hooks.py | 4 +-
frappe/tests/test_monitor.py | 10 ++---
frappe/tests/test_rate_limiter.py | 18 ++++----
frappe/tests/test_twofactor.py | 2 +-
frappe/tests/test_webform.py | 2 +-
frappe/tests/test_website.py | 4 +-
frappe/translate.py | 28 ++++++-------
frappe/twofactor.py | 22 +++++-----
frappe/utils/__init__.py | 2 +-
frappe/utils/caching.py | 8 ++--
frappe/utils/change_log.py | 12 +++---
frappe/utils/dashboard.py | 2 +-
frappe/utils/data.py | 2 +-
frappe/utils/global_search.py | 8 ++--
frappe/utils/oauth.py | 2 +-
frappe/utils/password.py | 6 +--
frappe/utils/pdf.py | 4 +-
frappe/utils/redis_wrapper.py | 2 +-
frappe/utils/user.py | 4 +-
.../doctype/help_article/help_article.py | 6 +--
.../website/page_renderers/not_found_page.py | 2 +-
frappe/website/path_resolver.py | 8 ++--
frappe/website/router.py | 4 +-
frappe/website/utils.py | 29 +++++++------
.../doctype/workflow/test_workflow.py | 4 +-
frappe/workflow/doctype/workflow/workflow.py | 2 +-
frappe/www/login.py | 8 ++--
frappe/www/message.py | 2 +-
frappe/www/qrcode.py | 4 +-
frappe/www/sitemap.py | 2 +-
89 files changed, 313 insertions(+), 339 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 84ac41ff66..5e87785e2d 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -380,7 +380,7 @@ def errprint(msg: str) -> None:
def print_sql(enable: bool = True) -> None:
- return cache().set_value("flag_print_sql", enable)
+ return cache.set_value("flag_print_sql", enable)
def log(msg: str) -> None:
@@ -1016,7 +1016,7 @@ def is_table(doctype: str) -> bool:
def get_tables():
return db.get_values("DocType", filters={"istable": 1}, order_by=None, pluck=True)
- tables = cache().get_value("is_table", get_tables)
+ tables = cache.get_value("is_table", get_tables)
return doctype in tables
@@ -1043,7 +1043,7 @@ def generate_hash(txt: str | None = None, length: int = 56) -> str:
def reset_metadata_version():
"""Reset `metadata_version` (Client (Javascript) build ID) hash."""
v = generate_hash()
- cache().set_value("metadata_version", v)
+ cache.set_value("metadata_version", v)
return v
@@ -1079,7 +1079,7 @@ def set_value(doctype, docname, fieldname, value=None):
def get_cached_doc(*args, **kwargs) -> "Document":
- if (key := can_cache_doc(args)) and (doc := cache().get_value(key)):
+ if (key := can_cache_doc(args)) and (doc := cache.get_value(key)):
return doc
# Not found in cache, fetch from DB
@@ -1095,7 +1095,7 @@ def get_cached_doc(*args, **kwargs) -> "Document":
def _set_document_in_cache(key: str, doc: "Document") -> None:
- cache().set_value(key, doc)
+ cache.set_value(key, doc)
def can_cache_doc(args) -> str | None:
@@ -1122,9 +1122,9 @@ def get_document_cache_key(doctype: str, name: str):
def clear_document_cache(doctype: str, name: str | None = None) -> None:
def clear_in_redis():
if name is not None:
- cache().delete_value(get_document_cache_key(doctype, name))
+ cache.delete_value(get_document_cache_key(doctype, name))
else:
- cache().delete_keys(get_document_cache_key(doctype, ""))
+ cache.delete_keys(get_document_cache_key(doctype, ""))
clear_in_redis()
if hasattr(db, "after_commit"):
@@ -1214,7 +1214,7 @@ def get_doc(*args, **kwargs):
doc = frappe.model.document.get_doc(*args, **kwargs)
# Replace cache if stale one exists
- if (key := can_cache_doc(args)) and cache().exists(key):
+ if (key := can_cache_doc(args)) and cache.exists(key):
_set_document_in_cache(key, doc)
return doc
@@ -1448,13 +1448,13 @@ def get_installed_apps(sort=False, frappe_last=False, *, _ensure_on_bench=False)
if sort:
if not local.all_apps:
- local.all_apps = cache().get_value("all_apps", get_all_apps)
+ local.all_apps = cache.get_value("all_apps", get_all_apps)
deprecation_warning("`sort` argument is deprecated and will be removed in v15.")
installed = [app for app in local.all_apps if app in installed]
if _ensure_on_bench:
- all_apps = cache().get_value("all_apps", get_all_apps)
+ all_apps = cache.get_value("all_apps", get_all_apps)
installed = [app for app in installed if app in all_apps]
if frappe_last:
@@ -1525,7 +1525,7 @@ def get_hooks(
if conf.developer_mode:
hooks = _dict(_load_app_hooks())
else:
- hooks = _dict(cache().get_value("app_hooks", _load_app_hooks))
+ hooks = _dict(cache.get_value("app_hooks", _load_app_hooks))
if hook:
return hooks.get(hook, ([] if default == "_KEEP_DEFAULT_LIST" else default))
@@ -1555,11 +1555,9 @@ def append_hook(target, key, value):
def setup_module_map():
"""Rebuild map of all modules (internal)."""
- _cache = cache()
-
if conf.db_name:
- local.app_modules = _cache.get_value("app_modules")
- local.module_app = _cache.get_value("module_app")
+ local.app_modules = cache.get_value("app_modules")
+ local.module_app = cache.get_value("module_app")
if not (local.app_modules and local.module_app):
local.module_app, local.app_modules = {}, {}
@@ -1571,8 +1569,8 @@ def setup_module_map():
local.app_modules[app].append(module)
if conf.db_name:
- _cache.set_value("app_modules", local.app_modules)
- _cache.set_value("module_app", local.module_app)
+ cache.set_value("app_modules", local.app_modules)
+ cache.set_value("module_app", local.module_app)
def get_file_items(path, raise_not_found=False, ignore_empty_lines=True):
@@ -1861,7 +1859,7 @@ def redirect_to_message(title, html, http_status_code=None, context=None, indica
if indicator_color:
message["context"].update({"indicator_color": indicator_color})
- cache().set_value(f"message_id:{message_id}", message, expires_in_sec=60)
+ cache.set_value(f"message_id:{message_id}", message, expires_in_sec=60)
location = f"/message?id={message_id}"
if not getattr(local, "is_ajax", False):
diff --git a/frappe/auth.py b/frappe/auth.py
index f1cdac52bd..29c3e41694 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -188,10 +188,10 @@ class LoginManager:
frappe.response["full_name"] = self.full_name
# redirect information
- redirect_to = frappe.cache().hget("redirect_after_login", self.user)
+ redirect_to = frappe.cache.hget("redirect_after_login", self.user)
if redirect_to:
frappe.local.response["redirect_to"] = redirect_to
- frappe.cache().hdel("redirect_after_login", self.user)
+ frappe.cache.hdel("redirect_after_login", self.user)
frappe.local.cookie_manager.set_cookie("full_name", self.full_name)
frappe.local.cookie_manager.set_cookie("user_id", self.user)
@@ -482,15 +482,15 @@ class LoginAttemptTracker:
@property
def login_failed_count(self):
- return frappe.cache().hget("login_failed_count", self.user_name)
+ return frappe.cache.hget("login_failed_count", self.user_name)
@login_failed_count.setter
def login_failed_count(self, count):
- frappe.cache().hset("login_failed_count", self.user_name, count)
+ frappe.cache.hset("login_failed_count", self.user_name, count)
@login_failed_count.deleter
def login_failed_count(self):
- frappe.cache().hdel("login_failed_count", self.user_name)
+ frappe.cache.hdel("login_failed_count", self.user_name)
@property
def login_failed_time(self):
@@ -498,15 +498,15 @@ class LoginAttemptTracker:
For every user we track only First failed login attempt time within lock interval of time.
"""
- return frappe.cache().hget("login_failed_time", self.user_name)
+ return frappe.cache.hget("login_failed_time", self.user_name)
@login_failed_time.setter
def login_failed_time(self, timestamp):
- frappe.cache().hset("login_failed_time", self.user_name, timestamp)
+ frappe.cache.hset("login_failed_time", self.user_name, timestamp)
@login_failed_time.deleter
def login_failed_time(self):
- frappe.cache().hdel("login_failed_time", self.user_name)
+ frappe.cache.hdel("login_failed_time", self.user_name)
def add_failure_attempt(self):
"""Log user failure attempts into the system.
diff --git a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
index 3242145bc4..4316edd1ca 100644
--- a/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
+++ b/frappe/automation/doctype/milestone_tracker/test_milestone_tracker.py
@@ -9,7 +9,7 @@ class TestMilestoneTracker(FrappeTestCase):
def test_milestone(self):
frappe.db.delete("Milestone Tracker")
- frappe.cache().delete_key("milestone_tracker_map")
+ frappe.cache.delete_key("milestone_tracker_map")
milestone_tracker = frappe.get_doc(
dict(doctype="Milestone Tracker", document_type="ToDo", track_field="status")
diff --git a/frappe/boot.py b/frappe/boot.py
index 37d89365c4..8881d25bd6 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -149,10 +149,8 @@ def get_allowed_report_names(cache=False) -> set[str]:
def get_user_pages_or_reports(parent, cache=False):
- _cache = frappe.cache()
-
if cache:
- has_role = _cache.get_value("has_role:" + parent, user=frappe.session.user)
+ has_role = frappe.cache.get_value("has_role:" + parent, user=frappe.session.user)
if has_role:
return has_role
@@ -254,7 +252,7 @@ def get_user_pages_or_reports(parent, cache=False):
has_role.pop(r, None)
# Expire every six hours
- _cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
+ frappe.cache.set_value("has_role:" + parent, has_role, frappe.session.user, 21600)
return has_role
diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py
index 9c1754148a..6ee88d9d37 100644
--- a/frappe/cache_manager.py
+++ b/frappe/cache_manager.py
@@ -79,28 +79,25 @@ doctype_cache_keys = (
def clear_user_cache(user=None):
- cache = frappe.cache()
-
# this will automatically reload the global cache
# so it is important to clear this first
clear_notifications(user)
if user:
for name in user_cache_keys:
- cache.hdel(name, user)
- cache.delete_keys("user:" + user)
+ frappe.cache.hdel(name, user)
+ frappe.cache.delete_keys("user:" + user)
clear_defaults_cache(user)
else:
for name in user_cache_keys:
- cache.delete_key(name)
+ frappe.cache.delete_key(name)
clear_defaults_cache()
clear_global_cache()
def clear_domain_cache(user=None):
- cache = frappe.cache()
domain_cache_keys = ("domain_restricted_doctypes", "domain_restricted_pages")
- cache.delete_value(domain_cache_keys)
+ frappe.cache.delete_value(domain_cache_keys)
def clear_global_cache():
@@ -108,17 +105,17 @@ def clear_global_cache():
clear_doctype_cache()
clear_website_cache()
- frappe.cache().delete_value(global_cache_keys)
- frappe.cache().delete_value(bench_cache_keys)
+ frappe.cache.delete_value(global_cache_keys)
+ frappe.cache.delete_value(bench_cache_keys)
frappe.setup_module_map()
def clear_defaults_cache(user=None):
if user:
for p in [user] + common_default_keys:
- frappe.cache().hdel("defaults", p)
+ frappe.cache.hdel("defaults", p)
elif frappe.flags.in_install != "frappe":
- frappe.cache().delete_key("defaults")
+ frappe.cache.delete_key("defaults")
def clear_doctype_cache(doctype=None):
@@ -131,15 +128,13 @@ def clear_doctype_cache(doctype=None):
def _clear_doctype_cache_form_redis(doctype: str | None = None):
- cache = frappe.cache()
-
for key in ("is_table", "doctype_modules"):
- cache.delete_value(key)
+ frappe.cache.delete_value(key)
def clear_single(dt):
frappe.clear_document_cache(dt)
for name in doctype_cache_keys:
- cache.hdel(name, dt)
+ frappe.cache.hdel(name, dt)
if doctype:
clear_single(doctype)
@@ -163,8 +158,8 @@ def _clear_doctype_cache_form_redis(doctype: str | None = None):
else:
# clear all
for name in doctype_cache_keys:
- cache.delete_value(name)
- cache.delete_keys("document_cache::")
+ frappe.cache.delete_value(name)
+ frappe.cache.delete_keys("document_cache::")
def clear_controller_cache(doctype=None):
@@ -177,7 +172,7 @@ def clear_controller_cache(doctype=None):
def get_doctype_map(doctype, name, filters=None, order_by=None):
- return frappe.cache().hget(
+ return frappe.cache.hget(
get_doctype_map_key(doctype),
name,
lambda: frappe.get_all(doctype, filters=filters, order_by=order_by, ignore_ddl=True),
@@ -185,7 +180,7 @@ def get_doctype_map(doctype, name, filters=None, order_by=None):
def clear_doctype_map(doctype, name):
- frappe.cache().hdel(frappe.scrub(doctype) + "_map", name)
+ frappe.cache.hdel(frappe.scrub(doctype) + "_map", name)
def build_table_count_cache():
@@ -198,7 +193,6 @@ def build_table_count_cache():
):
return
- _cache = frappe.cache()
table_name = frappe.qb.Field("table_name").as_("name")
table_rows = frappe.qb.Field("table_rows").as_("count")
information_schema = frappe.qb.Schema("information_schema")
@@ -207,7 +201,7 @@ def build_table_count_cache():
as_dict=True
)
counts = {d.get("name").replace("tab", "", 1): d.get("count", None) for d in data}
- _cache.set_value("information_schema:counts", counts)
+ frappe.cache.set_value("information_schema:counts", counts)
return counts
@@ -221,11 +215,10 @@ def build_domain_restriced_doctype_cache(*args, **kwargs):
or frappe.flags.in_setup_wizard
):
return
- _cache = frappe.cache()
active_domains = frappe.get_active_domains()
doctypes = frappe.get_all("DocType", filters={"restrict_to_domain": ("IN", active_domains)})
doctypes = [doc.name for doc in doctypes]
- _cache.set_value("domain_restricted_doctypes", doctypes)
+ frappe.cache.set_value("domain_restricted_doctypes", doctypes)
return doctypes
@@ -239,10 +232,9 @@ def build_domain_restriced_page_cache(*args, **kwargs):
or frappe.flags.in_setup_wizard
):
return
- _cache = frappe.cache()
active_domains = frappe.get_active_domains()
pages = frappe.get_all("Page", filters={"restrict_to_domain": ("IN", active_domains)})
pages = [page.name for page in pages]
- _cache.set_value("domain_restricted_pages", pages)
+ frappe.cache.set_value("domain_restricted_pages", pages)
return pages
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index 20a8e7db9b..0b983d0be9 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -62,7 +62,7 @@ class Importer:
def before_import(self):
# set user lang for translations
- frappe.cache().hdel("lang", frappe.session.user)
+ frappe.cache.hdel("lang", frappe.session.user)
frappe.set_user_lang(frappe.session.user)
# set flags
@@ -1207,7 +1207,7 @@ def get_df_for_column_header(doctype, header):
def build_fields_dict_for_doctype():
return build_fields_dict_for_column_matching(doctype)
- df_by_labels_and_fieldname = frappe.cache().hget(
+ df_by_labels_and_fieldname = frappe.cache.hget(
"data_import_column_header_map", doctype, generator=build_fields_dict_for_doctype
)
return df_by_labels_and_fieldname.get(header)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 12545adb4e..6abc34a035 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -1710,7 +1710,7 @@ def check_fieldname_conflicts(docfield):
def clear_linked_doctype_cache():
- frappe.cache().delete_value("linked_doctypes_without_ignore_user_permissions_enabled")
+ frappe.cache.delete_value("linked_doctypes_without_ignore_user_permissions_enabled")
def check_email_append_to(doc):
diff --git a/frappe/core/doctype/domain_settings/domain_settings.py b/frappe/core/doctype/domain_settings/domain_settings.py
index 85b26f53dd..d963a14830 100644
--- a/frappe/core/doctype/domain_settings/domain_settings.py
+++ b/frappe/core/doctype/domain_settings/domain_settings.py
@@ -73,7 +73,7 @@ def get_active_domains():
active_domains.append("")
return active_domains
- return frappe.cache().get_value("active_domains", _get_active_domains)
+ return frappe.cache.get_value("active_domains", _get_active_domains)
def get_active_modules():
@@ -87,4 +87,4 @@ def get_active_modules():
active_modules.append(m.name)
return active_modules
- return frappe.cache().get_value("active_modules", _get_active_modules)
+ return frappe.cache.get_value("active_modules", _get_active_modules)
diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py
index 9b2a2ccc18..8cdbc24074 100644
--- a/frappe/core/doctype/report/report.py
+++ b/frappe/core/doctype/report/report.py
@@ -137,7 +137,7 @@ class Report(Document):
if execution_time > threshold and not self.prepared_report:
self.db_set("prepared_report", 1)
- frappe.cache().hset("report_execution_time", self.name, execution_time)
+ frappe.cache.hset("report_execution_time", self.name, execution_time)
return res
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index 31b82501cb..8e5ec269ea 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -24,7 +24,7 @@ class Role(Document):
frappe.throw(frappe._("Standard roles cannot be renamed"))
def after_insert(self):
- frappe.cache().hdel("roles", "Administrator")
+ frappe.cache.hdel("roles", "Administrator")
def validate(self):
if self.disabled:
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 07808d619b..758bd46a76 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -19,7 +19,7 @@ class ServerScript(Document):
self.check_if_compilable_in_restricted_context()
def on_update(self):
- frappe.cache().delete_value("server_script_map")
+ frappe.cache.delete_value("server_script_map")
self.sync_scheduler_events()
def on_trash(self):
@@ -168,11 +168,11 @@ class ServerScript(Document):
out.append([key, score])
return out
- items = frappe.cache().get_value("server_script_autocompletion_items")
+ items = frappe.cache.get_value("server_script_autocompletion_items")
if not items:
items = get_keys(get_safe_globals())
items = [{"value": d[0], "score": d[1]} for d in items]
- frappe.cache().set_value("server_script_autocompletion_items", items)
+ frappe.cache.set_value("server_script_autocompletion_items", items)
return items
diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py
index b807b43d10..6ba65e7353 100644
--- a/frappe/core/doctype/server_script/server_script_utils.py
+++ b/frappe/core/doctype/server_script/server_script_utils.py
@@ -55,7 +55,7 @@ def get_server_script_map():
if frappe.flags.in_patch and not frappe.db.table_exists("Server Script"):
return {}
- script_map = frappe.cache().get_value("server_script_map")
+ script_map = frappe.cache.get_value("server_script_map")
if script_map is None:
script_map = {"permission_query": {}}
enabled_server_scripts = frappe.get_all(
@@ -73,6 +73,6 @@ def get_server_script_map():
else:
script_map.setdefault("_api", {})[script.api_method] = script.name
- frappe.cache().set_value("server_script_map", script_map)
+ frappe.cache.set_value("server_script_map", script_map)
return script_map
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 4371806b32..af1352f02b 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -104,10 +104,10 @@ class TestServerScript(FrappeTestCase):
def tearDownClass(cls):
frappe.db.commit()
frappe.db.truncate("Server Script")
- frappe.cache().delete_value("server_script_map")
+ frappe.cache.delete_value("server_script_map")
def setUp(self):
- frappe.cache().delete_value("server_script_map")
+ frappe.cache.delete_value("server_script_map")
def test_doctype_event(self):
todo = frappe.get_doc(dict(doctype="ToDo", description="hello")).insert()
diff --git a/frappe/core/doctype/system_settings/system_settings.py b/frappe/core/doctype/system_settings/system_settings.py
index c4f35f3cc0..2fec4e87af 100644
--- a/frappe/core/doctype/system_settings/system_settings.py
+++ b/frappe/core/doctype/system_settings/system_settings.py
@@ -64,8 +64,8 @@ class SystemSettings(Document):
def on_update(self):
self.set_defaults()
- frappe.cache().delete_value("system_settings")
- frappe.cache().delete_value("time_zone")
+ frappe.cache.delete_value("system_settings")
+ frappe.cache.delete_value("time_zone")
if frappe.flags.update_last_reset_password_date:
update_last_reset_password_date()
diff --git a/frappe/core/doctype/translation/translation.py b/frappe/core/doctype/translation/translation.py
index 6afad00fad..a285806589 100644
--- a/frappe/core/doctype/translation/translation.py
+++ b/frappe/core/doctype/translation/translation.py
@@ -89,5 +89,5 @@ def create_translations(translation_map, language):
def clear_user_translation_cache(lang):
- frappe.cache().hdel(USER_TRANSLATION_KEY, lang)
- frappe.cache().hdel(MERGED_TRANSLATION_KEY, lang)
+ frappe.cache.hdel(USER_TRANSLATION_KEY, lang)
+ frappe.cache.hdel(MERGED_TRANSLATION_KEY, lang)
diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py
index d39d2062eb..b4d69d23d5 100644
--- a/frappe/core/doctype/user/test_user.py
+++ b/frappe/core/doctype/user/test_user.py
@@ -283,7 +283,7 @@ class TestUser(FrappeTestCase):
# Clear rate limit tracker to start fresh
key = f"rl:{data['cmd']}:{data['user']}"
- frappe.cache().delete(key)
+ frappe.cache.delete(key)
c = FrappeClient(url)
res1 = c.session.post(url, data=data, verify=c.verify, headers=c.headers)
@@ -330,7 +330,7 @@ class TestUser(FrappeTestCase):
sign_up(random_user, random_user_name, "/welcome"),
(1, "Please check your email for verification"),
)
- self.assertEqual(frappe.cache().hget("redirect_after_login", random_user), "/welcome")
+ self.assertEqual(frappe.cache.hget("redirect_after_login", random_user), "/welcome")
# re-register
self.assertTupleEqual(
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 81d9715c32..4c5cea3130 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -60,8 +60,8 @@ class User(Document):
def after_insert(self):
create_notification_settings(self.name)
- frappe.cache().delete_key("users_for_mentions")
- frappe.cache().delete_key("enabled_users")
+ frappe.cache.delete_key("users_for_mentions")
+ frappe.cache.delete_key("enabled_users")
def validate(self):
# clear new password
@@ -143,10 +143,10 @@ class User(Document):
frappe.defaults.set_default("time_zone", self.time_zone, self.name)
if self.has_value_changed("enabled"):
- frappe.cache().delete_key("users_for_mentions")
- frappe.cache().delete_key("enabled_users")
+ frappe.cache.delete_key("users_for_mentions")
+ frappe.cache.delete_key("enabled_users")
elif self.has_value_changed("allow_in_mentions") or self.has_value_changed("user_type"):
- frappe.cache().delete_key("users_for_mentions")
+ frappe.cache.delete_key("users_for_mentions")
def has_website_permission(self, ptype, user, verbose=False):
"""Returns true if current user is the session user"""
@@ -462,9 +462,9 @@ class User(Document):
frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True)
if self.get("allow_in_mentions"):
- frappe.cache().delete_key("users_for_mentions")
+ frappe.cache.delete_key("users_for_mentions")
- frappe.cache().delete_key("enabled_users")
+ frappe.cache.delete_key("enabled_users")
# delete user permissions
frappe.db.delete("User Permission", {"user": self.name})
@@ -760,10 +760,10 @@ def update_password(
user_doc, redirect_url = reset_user_data(user)
# get redirect url from cache
- redirect_to = frappe.cache().hget("redirect_after_login", user)
+ redirect_to = frappe.cache.hget("redirect_after_login", user)
if redirect_to:
redirect_url = redirect_to
- frappe.cache().hdel("redirect_after_login", user)
+ frappe.cache.hdel("redirect_after_login", user)
frappe.local.login_manager.login_as(user)
@@ -921,7 +921,7 @@ def sign_up(email: str, full_name: str, redirect_to: str) -> tuple[int, str]:
user.add_roles(default_role)
if redirect_to:
- frappe.cache().hset("redirect_after_login", user.name, redirect_to)
+ frappe.cache.hset("redirect_after_login", user.name, redirect_to)
if user.flags.email_sent:
return 1, _("Please check your email for verification")
@@ -1234,4 +1234,4 @@ def get_enabled_users():
enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
return enabled_users
- return frappe.cache().get_value("enabled_users", _get_enabled_users)
+ return frappe.cache.get_value("enabled_users", _get_enabled_users)
diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py
index 812f230f7a..7acdec3aaa 100644
--- a/frappe/core/doctype/user_group/user_group.py
+++ b/frappe/core/doctype/user_group/user_group.py
@@ -9,7 +9,7 @@ from frappe.model.document import Document
class UserGroup(Document):
def after_insert(self):
- frappe.cache().delete_key("user_groups")
+ frappe.cache.delete_key("user_groups")
def on_trash(self):
- frappe.cache().delete_key("user_groups")
+ frappe.cache.delete_key("user_groups")
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 8742d2e040..a38ec4d379 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -178,7 +178,7 @@ class TestUserPermission(FrappeTestCase):
frappe.db.set_value(
"User Permission", {"allow": "Person", "for_value": parent_record.name}, "hide_descendants", 1
)
- frappe.cache().delete_value("user_permissions")
+ frappe.cache.delete_value("user_permissions")
# check if adding perm on a group record with hide_descendants enabled,
# hides child records
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 63c1f40512..57214b82e2 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -17,11 +17,11 @@ class UserPermission(Document):
self.validate_default_permission()
def on_update(self):
- frappe.cache().hdel("user_permissions", self.user)
+ frappe.cache.hdel("user_permissions", self.user)
frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def on_trash(self):
- frappe.cache().hdel("user_permissions", self.user)
+ frappe.cache.hdel("user_permissions", self.user)
frappe.publish_realtime("update_user_permissions", user=self.user, after_commit=True)
def validate_user_permission(self):
@@ -74,7 +74,7 @@ def get_user_permissions(user=None):
if not user or user in ("Administrator", "Guest"):
return {}
- cached_user_permissions = frappe.cache().hget("user_permissions", user)
+ cached_user_permissions = frappe.cache.hget("user_permissions", user)
if cached_user_permissions is not None:
return cached_user_permissions
@@ -110,7 +110,7 @@ def get_user_permissions(user=None):
add_doc_to_perm(perm, doc, False)
out = frappe._dict(out)
- frappe.cache().hset("user_permissions", user, out)
+ frappe.cache.hset("user_permissions", user, out)
except frappe.db.SQLError as e:
if frappe.db.is_table_missing(e):
# called from patch
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index 39d9133412..9660963c19 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -18,7 +18,7 @@ class UserType(Document):
super().clear_cache()
if not self.is_standard:
- frappe.cache().delete_value("non_standard_user_types")
+ frappe.cache.delete_value("non_standard_user_types")
def on_update(self):
if self.is_standard:
@@ -290,7 +290,7 @@ def apply_permissions_for_non_standard_user_type(doc, method=None):
if not frappe.db.table_exists("User Type") or frappe.flags.in_migrate:
return
- user_types = frappe.cache().get_value(
+ user_types = frappe.cache.get_value(
"non_standard_user_types",
get_non_standard_user_types,
)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 2bac5a1ffc..e4c735c595 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -302,7 +302,7 @@ class Database:
"""Takes the query and logs it to various interfaces according to the settings."""
_query = None
- if frappe.conf.allow_tests and frappe.cache().get_value("flag_print_sql"):
+ if frappe.conf.allow_tests and frappe.cache.get_value("flag_print_sql"):
_query = _query or str(mogrified_query)
print(_query)
@@ -419,7 +419,7 @@ class Database:
@staticmethod
def clear_db_table_cache(query):
if query and is_query_type(query, ("drop", "create")):
- frappe.cache().delete_key("db_tables")
+ frappe.cache.delete_key("db_tables")
def get_description(self):
"""Returns result metadata."""
@@ -1067,7 +1067,7 @@ class Database:
def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
- cache_count = frappe.cache().get_value(f"doctype:count:{dt}")
+ cache_count = frappe.cache.get_value(f"doctype:count:{dt}")
if cache_count is not None:
return cache_count
count = frappe.qb.get_query(
@@ -1078,7 +1078,7 @@ class Database:
validate_filters=True,
).run(debug=debug)[0][0]
if not filters and cache:
- frappe.cache().set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
+ frappe.cache.set_value(f"doctype:count:{dt}", count, expires_in_sec=86400)
return count
@staticmethod
@@ -1109,7 +1109,7 @@ class Database:
def get_db_table_columns(self, table) -> list[str]:
"""Returns list of column names from given table."""
- columns = frappe.cache().hget("table_columns", table)
+ columns = frappe.cache.hget("table_columns", table)
if columns is None:
information_schema = frappe.qb.Schema("information_schema")
@@ -1121,7 +1121,7 @@ class Database:
)
if columns:
- frappe.cache().hset("table_columns", table, columns)
+ frappe.cache.hset("table_columns", table, columns)
return columns
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index f14fce2710..6a89966ee5 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -435,7 +435,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
to_query = not cached
if cached:
- tables = frappe.cache().get_value("db_tables")
+ tables = frappe.cache.get_value("db_tables")
to_query = not tables
if to_query:
@@ -447,7 +447,7 @@ class MariaDBDatabase(MariaDBConnectionUtil, MariaDBExceptionUtil, Database):
.where(information_schema.tables.table_schema != "information_schema")
.run(pluck=True)
)
- frappe.cache().set_value("db_tables", tables)
+ frappe.cache.set_value("db_tables", tables)
return tables
diff --git a/frappe/database/schema.py b/frappe/database/schema.py
index e65d7b980b..ed7d1d16fc 100644
--- a/frappe/database/schema.py
+++ b/frappe/database/schema.py
@@ -40,7 +40,7 @@ class DBTable:
if self.is_new():
self.create()
else:
- frappe.cache().hdel("table_columns", self.table_name)
+ frappe.cache.hdel("table_columns", self.table_name)
self.alter()
def create(self):
diff --git a/frappe/defaults.py b/frappe/defaults.py
index edbf784200..0b86e99efa 100644
--- a/frappe/defaults.py
+++ b/frappe/defaults.py
@@ -230,7 +230,7 @@ def clear_default(key=None, value=None, parent=None, name=None, parenttype=None)
def get_defaults_for(parent="__default"):
"""get all defaults"""
- defaults = frappe.cache().hget("defaults", parent)
+ defaults = frappe.cache.hget("defaults", parent)
if defaults is None:
# sort descending because first default must get precedence
@@ -256,7 +256,7 @@ def get_defaults_for(parent="__default"):
elif d.defvalue is not None:
defaults[d.defkey] = d.defvalue
- frappe.cache().hset("defaults", parent, defaults)
+ frappe.cache.hset("defaults", parent, defaults)
return defaults
diff --git a/frappe/deferred_insert.py b/frappe/deferred_insert.py
index 328d8dd555..5c7e7a7f0d 100644
--- a/frappe/deferred_insert.py
+++ b/frappe/deferred_insert.py
@@ -19,20 +19,20 @@ def deferred_insert(doctype: str, records: list[Union[dict, "Document"]] | str):
_records = records
try:
- frappe.cache().rpush(f"{queue_prefix}{doctype}", _records)
+ frappe.cache.rpush(f"{queue_prefix}{doctype}", _records)
except redis.exceptions.ConnectionError:
for record in records:
insert_record(record, doctype)
def save_to_db():
- queue_keys = frappe.cache().get_keys(queue_prefix)
+ queue_keys = frappe.cache.get_keys(queue_prefix)
for key in queue_keys:
record_count = 0
queue_key = get_key_name(key)
doctype = get_doctype_name(key)
- while frappe.cache().llen(queue_key) > 0 and record_count <= 500:
- records = frappe.cache().lpop(queue_key)
+ while frappe.cache.llen(queue_key) > 0 and record_count <= 500:
+ records = frappe.cache.lpop(queue_key)
records = json.loads(records.decode("utf-8"))
if isinstance(records, dict):
record_count += 1
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 46cda8fe5d..cf9f223d2a 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -62,10 +62,10 @@ class Workspace:
self.table_counts = get_table_with_counts()
self.restricted_doctypes = (
- frappe.cache().get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
+ frappe.cache.get_value("domain_restricted_doctypes") or build_domain_restriced_doctype_cache()
)
self.restricted_pages = (
- frappe.cache().get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
+ frappe.cache.get_value("domain_restricted_pages") or build_domain_restriced_page_cache()
)
def is_permitted(self):
@@ -88,16 +88,14 @@ class Workspace:
return True
def get_cached(self, cache_key, fallback_fn):
- _cache = frappe.cache()
-
- value = _cache.get_value(cache_key, user=frappe.session.user)
+ value = frappe.cache.get_value(cache_key, user=frappe.session.user)
if value:
return value
value = fallback_fn()
# Expire every six hour
- _cache.set_value(cache_key, value, frappe.session.user, 21600)
+ frappe.cache.set_value(cache_key, value, frappe.session.user, 21600)
return value
def get_can_read_items(self):
@@ -469,7 +467,7 @@ def get_workspace_sidebar_items():
def get_table_with_counts():
- counts = frappe.cache().get_value("information_schema:counts")
+ counts = frappe.cache.get_value("information_schema:counts")
if not counts:
counts = build_table_count_cache()
diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
index 5cbeb06e33..16f4efea9d 100644
--- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
+++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py
@@ -340,7 +340,7 @@ def get_charts_for_user(doctype, txt, searchfield, start, page_len, filters):
class DashboardChart(Document):
def on_update(self):
- frappe.cache().delete_key(f"chart-data:{self.name}")
+ frappe.cache.delete_key(f"chart-data:{self.name}")
if frappe.conf.developer_mode and self.is_standard:
export_to_files(record_list=[["Dashboard Chart", self.name]], record_module=self.module)
diff --git a/frappe/desk/doctype/desktop_icon/desktop_icon.py b/frappe/desk/doctype/desktop_icon/desktop_icon.py
index 63fa12b8fb..0d6e5bb815 100644
--- a/frappe/desk/doctype/desktop_icon/desktop_icon.py
+++ b/frappe/desk/doctype/desktop_icon/desktop_icon.py
@@ -28,7 +28,7 @@ def get_desktop_icons(user=None):
if not user:
user = frappe.session.user
- user_icons = frappe.cache().hget("desktop_icons", user)
+ user_icons = frappe.cache.hget("desktop_icons", user)
if not user_icons:
fields = [
@@ -120,7 +120,7 @@ def get_desktop_icons(user=None):
if d.label:
d.label = _(d.label)
- frappe.cache().hset("desktop_icons", user, user_icons)
+ frappe.cache.hset("desktop_icons", user, user_icons)
return user_icons
@@ -313,8 +313,8 @@ def get_all_icons():
def clear_desktop_icons_cache(user=None):
- frappe.cache().hdel("desktop_icons", user or frappe.session.user)
- frappe.cache().hdel("bootinfo", user or frappe.session.user)
+ frappe.cache.hdel("desktop_icons", user or frappe.session.user)
+ frappe.cache.hdel("bootinfo", user or frappe.session.user)
def get_user_copy(module_name, user=None):
@@ -445,7 +445,7 @@ def get_module_icons(user=None):
if not user:
icons = frappe.get_all("Desktop Icon", fields="*", filters={"standard": 1}, order_by="idx")
else:
- frappe.cache().hdel("desktop_icons", user)
+ frappe.cache.hdel("desktop_icons", user)
icons = get_user_icons(user)
for icon in icons:
diff --git a/frappe/desk/doctype/form_tour/form_tour.py b/frappe/desk/doctype/form_tour/form_tour.py
index 6838f15d8f..bdfdcf7c79 100644
--- a/frappe/desk/doctype/form_tour/form_tour.py
+++ b/frappe/desk/doctype/form_tour/form_tour.py
@@ -34,13 +34,13 @@ class FormTour(Document):
step.fieldtype = field_df.fieldtype
def on_update(self):
- frappe.cache().delete_key("bootinfo")
+ frappe.cache.delete_key("bootinfo")
if frappe.conf.developer_mode and self.is_standard:
export_to_files([["Form Tour", self.name]], self.module)
def on_trash(self):
- frappe.cache().delete_key("bootinfo")
+ frappe.cache.delete_key("bootinfo")
@frappe.whitelist()
@@ -51,7 +51,7 @@ def reset_tour(tour_name):
frappe.db.set_value(
"User", user, "onboarding_status", frappe.as_json(onboarding_status), update_modified=False
)
- frappe.cache().hdel("bootinfo", user)
+ frappe.cache.hdel("bootinfo", user)
frappe.msgprint(_("Successfully reset onboarding status for all users."), alert=True)
@@ -72,7 +72,7 @@ def update_user_status(value, step):
"User", frappe.session.user, "onboarding_status", value, update_modified=False
)
- frappe.cache().hdel("bootinfo", frappe.session.user)
+ frappe.cache.hdel("bootinfo", frappe.session.user)
def get_onboarding_ui_tours():
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index 4e2b1e85f9..f0bf985550 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -28,7 +28,7 @@ class GlobalSearchSettings(Document):
frappe.throw(_("Document Type {0} has been repeated.").format(repeated_dts))
# reset cache
- frappe.cache().hdel("global_search", "search_priorities")
+ frappe.cache.hdel("global_search", "search_priorities")
def get_doctypes_for_global_search():
@@ -36,7 +36,7 @@ def get_doctypes_for_global_search():
doctypes = frappe.get_all("Global Search DocType", fields=["document_type"], order_by="idx ASC")
return [d.document_type for d in doctypes] or []
- return frappe.cache().hget("global_search", "search_priorities", get_from_db)
+ return frappe.cache.hget("global_search", "search_priorities", get_from_db)
@frappe.whitelist()
diff --git a/frappe/desk/doctype/kanban_board/kanban_board.py b/frappe/desk/doctype/kanban_board/kanban_board.py
index e3257e25be..508407f76a 100644
--- a/frappe/desk/doctype/kanban_board/kanban_board.py
+++ b/frappe/desk/doctype/kanban_board/kanban_board.py
@@ -14,7 +14,7 @@ class KanbanBoard(Document):
def on_change(self):
frappe.clear_cache(doctype=self.reference_doctype)
- frappe.cache().delete_keys("_user_settings")
+ frappe.cache.delete_keys("_user_settings")
def before_insert(self):
for column in self.columns:
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index 9bc7b138dd..1ec604c34d 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -531,13 +531,13 @@ def get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
{"Address": {"fieldname": "customer"}..}
"""
if without_ignore_user_permissions_enabled:
- return frappe.cache().hget(
+ return frappe.cache.hget(
"linked_doctypes_without_ignore_user_permissions_enabled",
doctype,
lambda: _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled),
)
else:
- return frappe.cache().hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
+ return frappe.cache.hget("linked_doctypes", doctype, lambda: _get_linked_doctypes(doctype))
def _get_linked_doctypes(doctype, without_ignore_user_permissions_enabled=False):
diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py
index 62a9c89c81..580e8d5102 100644
--- a/frappe/desk/form/meta.py
+++ b/frappe/desk/form/meta.py
@@ -37,10 +37,10 @@ ASSET_KEYS = (
def get_meta(doctype, cached=True):
# don't cache for developer mode as js files, templates may be edited
if cached and not frappe.conf.developer_mode:
- meta = frappe.cache().hget("doctype_form_meta", doctype)
+ meta = frappe.cache.hget("doctype_form_meta", doctype)
if not meta:
meta = FormMeta(doctype)
- frappe.cache().hset("doctype_form_meta", doctype, meta)
+ frappe.cache.hset("doctype_form_meta", doctype, meta)
else:
meta = FormMeta(doctype)
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index 271f2b4074..6334b18d1c 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -34,13 +34,12 @@ def get_notifications():
return out
groups = list(config.get("for_doctype")) + list(config.get("for_module"))
- cache = frappe.cache()
notification_count = {}
notification_percent = {}
for name in groups:
- count = cache.hget("notification_count:" + name, frappe.session.user)
+ count = frappe.cache.hget("notification_count:" + name, frappe.session.user)
if count is not None:
notification_count[name] = count
@@ -83,7 +82,7 @@ def get_notifications_for_doctypes(config, notification_count):
else:
open_count_doctype[d] = result
- frappe.cache().hset("notification_count:" + d, frappe.session.user, result)
+ frappe.cache.hset("notification_count:" + d, frappe.session.user, result)
return open_count_doctype
@@ -139,7 +138,6 @@ def get_notifications_for_targets(config, notification_percent):
def clear_notifications(user=None):
if frappe.flags.in_install:
return
- cache = frappe.cache()
config = get_notification_config()
if not config:
@@ -151,17 +149,17 @@ def clear_notifications(user=None):
for name in groups:
if user:
- cache.hdel("notification_count:" + name, user)
+ frappe.cache.hdel("notification_count:" + name, user)
else:
- cache.delete_key("notification_count:" + name)
+ frappe.cache.delete_key("notification_count:" + name)
def clear_notification_config(user):
- frappe.cache().hdel("notification_config", user)
+ frappe.cache.hdel("notification_config", user)
def delete_notification_count_for(doctype):
- frappe.cache().delete_key("notification_count:" + doctype)
+ frappe.cache.delete_key("notification_count:" + doctype)
def clear_doctype_notifications(doc, method=None, *args, **kwargs):
@@ -230,7 +228,7 @@ def get_notification_config():
config[key].update(nc.get(key, {}))
return config
- return frappe.cache().hget("notification_config", user, _get)
+ return frappe.cache.hget("notification_config", user, _get)
def get_filters_for(doctype):
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index cb869fb5fc..a50588bdca 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -325,8 +325,8 @@ def load_country():
@frappe.whitelist()
def load_user_details():
return {
- "full_name": frappe.cache().hget("full_name", "signup"),
- "email": frappe.cache().hget("email", "signup"),
+ "full_name": frappe.cache.hget("full_name", "signup"),
+ "email": frappe.cache.hget("email", "signup"),
}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 69cdecb6dd..6f4bc716aa 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -119,7 +119,7 @@ def generate_report_result(
"report_summary": report_summary,
"skip_total_row": skip_total_row or 0,
"status": None,
- "execution_time": frappe.cache().hget("report_execution_time", report.name) or 0,
+ "execution_time": frappe.cache.hget("report_execution_time", report.name) or 0,
}
@@ -170,7 +170,7 @@ def get_script(report_name):
return {
"script": render_include(script),
"html_format": html_format,
- "execution_time": frappe.cache().hget("report_execution_time", report_name) or 0,
+ "execution_time": frappe.cache.hget("report_execution_time", report_name) or 0,
}
diff --git a/frappe/desk/search.py b/frappe/desk/search.py
index 67695e4e73..d347cc188c 100644
--- a/frappe/desk/search.py
+++ b/frappe/desk/search.py
@@ -311,8 +311,8 @@ def validate_and_sanitize_search_inputs(fn):
@frappe.whitelist()
def get_names_for_mentions(search_term):
- users_for_mentions = frappe.cache().get_value("users_for_mentions", get_users_for_mentions)
- user_groups = frappe.cache().get_value("user_groups", get_user_groups)
+ users_for_mentions = frappe.cache.get_value("users_for_mentions", get_users_for_mentions)
+ user_groups = frappe.cache.get_value("user_groups", get_user_groups)
filtered_mentions = []
for mention_data in users_for_mentions + user_groups:
diff --git a/frappe/email/__init__.py b/frappe/email/__init__.py
index 486db2a784..5c4d6f4c72 100644
--- a/frappe/email/__init__.py
+++ b/frappe/email/__init__.py
@@ -96,7 +96,7 @@ def get_communication_doctype(doctype, txt, searchfield, start, page_len, filter
def get_cached_contacts(txt):
- contacts = frappe.cache().hget("contacts", frappe.session.user) or []
+ contacts = frappe.cache.hget("contacts", frappe.session.user) or []
if not contacts:
return
@@ -113,9 +113,9 @@ def get_cached_contacts(txt):
def update_contact_cache(contacts):
- cached_contacts = frappe.cache().hget("contacts", frappe.session.user) or []
+ cached_contacts = frappe.cache.hget("contacts", frappe.session.user) or []
uncached_contacts = [d for d in contacts if d not in cached_contacts]
cached_contacts.extend(uncached_contacts)
- frappe.cache().hset("contacts", frappe.session.user, cached_contacts)
+ frappe.cache.hset("contacts", frappe.session.user, cached_contacts)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index faf28afdb3..0e9bbcaf3f 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -176,7 +176,7 @@ class EmailAccount(Document):
def get_incoming_server(self, in_receive=False, email_sync_rule="UNSEEN"):
"""Returns logged in POP3/IMAP connection object."""
- if frappe.cache().get_value("workers:no-internet") == True:
+ if frappe.cache.get_value("workers:no-internet") == True:
return None
oauth_token = self.get_oauth_token()
@@ -253,7 +253,7 @@ class EmailAccount(Document):
if self.no_failed > 2:
self.handle_incoming_connect_error(description=description)
else:
- frappe.cache().set_value("workers:no-internet", True)
+ frappe.cache.set_value("workers:no-internet", True)
return None
else:
raise
@@ -436,13 +436,13 @@ class EmailAccount(Document):
else:
self.set_failed_attempts_count(self.get_failed_attempts_count() + 1)
else:
- frappe.cache().set_value("workers:no-internet", True)
+ frappe.cache.set_value("workers:no-internet", True)
def set_failed_attempts_count(self, value):
- frappe.cache().set(f"{self.name}:email-account-failed-attempts", value)
+ frappe.cache.set(f"{self.name}:email-account-failed-attempts", value)
def get_failed_attempts_count(self):
- return cint(frappe.cache().get(f"{self.name}:email-account-failed-attempts"))
+ return cint(frappe.cache.get(f"{self.name}:email-account-failed-attempts"))
def receive(self):
"""Called by scheduler to receive emails from this EMail account using POP3/IMAP."""
@@ -766,9 +766,9 @@ def pull(now=False):
"""Will be called via scheduler, pull emails from all enabled Email accounts."""
from frappe.integrations.doctype.connected_app.connected_app import has_token
- if frappe.cache().get_value("workers:no-internet") == True:
+ if frappe.cache.get_value("workers:no-internet") == True:
if test_internet():
- frappe.cache().set_value("workers:no-internet", False)
+ frappe.cache.set_value("workers:no-internet", False)
return
doctype = frappe.qb.DocType("Email Account")
diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py
index aee68aa4e5..eb0868a91e 100644
--- a/frappe/email/doctype/notification/notification.py
+++ b/frappe/email/doctype/notification/notification.py
@@ -42,10 +42,10 @@ class Notification(Document):
self.validate_forbidden_types()
self.validate_condition()
self.validate_standard()
- frappe.cache().hdel("notifications", self.document_type)
+ frappe.cache.hdel("notifications", self.document_type)
def on_update(self):
- frappe.cache().hdel("notifications", self.document_type)
+ frappe.cache.hdel("notifications", self.document_type)
path = export_module_json(self, self.is_standard, self.module)
if path:
# js
@@ -378,7 +378,7 @@ def get_context(context):
self.message = frappe.utils.md_to_html(self.message)
def on_trash(self):
- frappe.cache().hdel("notifications", self.document_type)
+ frappe.cache.hdel("notifications", self.document_type)
@frappe.whitelist()
diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py
index d1cdd0d9e7..136e39e3a3 100644
--- a/frappe/integrations/doctype/google_calendar/google_calendar.py
+++ b/frappe/integrations/doctype/google_calendar/google_calendar.py
@@ -119,7 +119,7 @@ def authorize_access(g_calendar, reauthorize=None):
)
if not google_calendar.authorization_code or reauthorize:
- frappe.cache().hset("google_calendar", "google_calendar", google_calendar.name)
+ frappe.cache.hset("google_calendar", "google_calendar", google_calendar.name)
return get_authentication_url(client_id=google_settings.client_id, redirect_uri=redirect_uri)
else:
try:
@@ -163,7 +163,7 @@ def google_callback(code=None):
"""
Authorization code is sent to callback as per the API configuration
"""
- google_calendar = frappe.cache().hget("google_calendar", "google_calendar")
+ google_calendar = frappe.cache.hget("google_calendar", "google_calendar")
frappe.db.set_value("Google Calendar", google_calendar, "authorization_code", code)
frappe.db.commit()
diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py
index b9c96190ca..dcad1c8b5c 100644
--- a/frappe/integrations/doctype/webhook/__init__.py
+++ b/frappe/integrations/doctype/webhook/__init__.py
@@ -20,7 +20,7 @@ def run_webhooks(doc, method):
# TODO: remove this hazardous unnecessary cache in flags
if frappe.flags.webhooks is None:
# load webhooks from cache
- webhooks = frappe.cache().get_value("webhooks")
+ webhooks = frappe.cache.get_value("webhooks")
if webhooks is None:
# query webhooks
webhooks_list = frappe.get_all(
@@ -33,7 +33,7 @@ def run_webhooks(doc, method):
webhooks = {}
for w in webhooks_list:
webhooks.setdefault(w.webhook_doctype, []).append(w)
- frappe.cache().set_value("webhooks", webhooks)
+ frappe.cache.set_value("webhooks", webhooks)
frappe.flags.webhooks = webhooks
diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py
index 2edf2fcf5c..1701c418f7 100644
--- a/frappe/integrations/doctype/webhook/test_webhook.py
+++ b/frappe/integrations/doctype/webhook/test_webhook.py
@@ -102,7 +102,7 @@ class TestWebhook(FrappeTestCase):
def test_webhook_trigger_with_enabled_webhooks(self):
"""Test webhook trigger for enabled webhooks"""
- frappe.cache().delete_value("webhooks")
+ frappe.cache.delete_value("webhooks")
frappe.flags.webhooks = None
# Insert the user to db
diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py
index 1b56a1b129..a4d198a118 100644
--- a/frappe/integrations/doctype/webhook/webhook.py
+++ b/frappe/integrations/doctype/webhook/webhook.py
@@ -29,7 +29,7 @@ class Webhook(Document):
self.preview_document = None
def on_update(self):
- frappe.cache().delete_value("webhooks")
+ frappe.cache.delete_value("webhooks")
def validate_docevent(self):
if self.webhook_doctype:
diff --git a/frappe/model/document.py b/frappe/model/document.py
index f944b28a49..6ef81314f1 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -959,9 +959,7 @@ class Document(BaseDocument):
filters={"enabled": 1, "document_type": self.doctype},
)
- self.flags.notifications = frappe.cache().hget(
- "notifications", self.doctype, _get_notifications
- )
+ self.flags.notifications = frappe.cache.hget("notifications", self.doctype, _get_notifications)
if not self.flags.notifications:
return
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index 32c1326170..e55f72d1ba 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -59,11 +59,11 @@ def get_meta(doctype, cached=True) -> "Meta":
if not cached:
return Meta(doctype)
- if meta := frappe.cache().hget("doctype_meta", doctype):
+ if meta := frappe.cache.hget("doctype_meta", doctype):
return meta
meta = Meta(doctype)
- frappe.cache().hset("doctype_meta", doctype, meta)
+ frappe.cache.hset("doctype_meta", doctype, meta)
return meta
@@ -814,7 +814,7 @@ def trim_tables(doctype=None, dry_run=False, quiet=False):
def trim_table(doctype, dry_run=True):
- frappe.cache().hdel("table_columns", f"tab{doctype}")
+ frappe.cache.hdel("table_columns", f"tab{doctype}")
ignore_fields = default_fields + optional_fields + child_table_fields
columns = frappe.db.get_table_columns(doctype)
fields = frappe.get_meta(doctype, cached=False).get_fieldnames_with_value()
diff --git a/frappe/model/utils/link_count.py b/frappe/model/utils/link_count.py
index 49ed0d5a6c..65b5092d46 100644
--- a/frappe/model/utils/link_count.py
+++ b/frappe/model/utils/link_count.py
@@ -23,7 +23,7 @@ def flush_local_link_count():
if not new_links:
return
- link_count = frappe.cache().get_value("_link_count") or {}
+ link_count = frappe.cache.get_value("_link_count") or {}
for key, value in new_links.items():
if key in link_count:
@@ -31,13 +31,13 @@ def flush_local_link_count():
else:
link_count[key] = value
- frappe.cache().set_value("_link_count", link_count)
+ frappe.cache.set_value("_link_count", link_count)
new_links.clear()
def update_link_count():
"""increment link count in the `idx` column for the given document"""
- link_count = frappe.cache().get_value("_link_count")
+ link_count = frappe.cache.get_value("_link_count")
if link_count:
for (doctype, name), count in link_count.items():
@@ -50,4 +50,4 @@ def update_link_count():
if not frappe.db.is_table_missing(e): # table not found, single
raise e
# reset the count
- frappe.cache().delete_value("_link_count")
+ frappe.cache.delete_value("_link_count")
diff --git a/frappe/model/utils/user_settings.py b/frappe/model/utils/user_settings.py
index c12c7e27ba..02bc67b929 100644
--- a/frappe/model/utils/user_settings.py
+++ b/frappe/model/utils/user_settings.py
@@ -11,7 +11,7 @@ filter_dict = {"doctype": 0, "docfield": 1, "operator": 2, "value": 3}
def get_user_settings(doctype, for_update=False):
- user_settings = frappe.cache().hget("_user_settings", f"{doctype}::{frappe.session.user}")
+ user_settings = frappe.cache.hget("_user_settings", f"{doctype}::{frappe.session.user}")
if user_settings is None:
user_settings = frappe.db.sql(
@@ -41,12 +41,12 @@ def update_user_settings(doctype, user_settings, for_update=False):
current.update(user_settings)
- frappe.cache().hset("_user_settings", f"{doctype}::{frappe.session.user}", json.dumps(current))
+ frappe.cache.hset("_user_settings", f"{doctype}::{frappe.session.user}", json.dumps(current))
def sync_user_settings():
"""Sync from cache to database (called asynchronously via the browser)"""
- for key, data in frappe.cache().hgetall("_user_settings").items():
+ for key, data in frappe.cache.hgetall("_user_settings").items():
key = safe_decode(key)
doctype, user = key.split("::") # WTF?
frappe.db.multisql(
@@ -99,4 +99,4 @@ def update_user_settings_data(
)
# clear that user settings from the redis cache
- frappe.cache().hset("_user_settings", f"{user_setting.doctype}::{user_setting.user}", None)
+ frappe.cache.hset("_user_settings", f"{user_setting.doctype}::{user_setting.user}", None)
diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py
index d61d2b3a2b..1a52077331 100644
--- a/frappe/model/workflow.py
+++ b/frappe/model/workflow.py
@@ -26,12 +26,12 @@ class WorkflowPermissionError(frappe.ValidationError):
def get_workflow_name(doctype):
- workflow_name = frappe.cache().hget("workflow", doctype)
+ workflow_name = frappe.cache.hget("workflow", doctype)
if workflow_name is None:
workflow_name = frappe.db.get_value(
"Workflow", {"document_type": doctype, "is_active": 1}, "name"
)
- frappe.cache().hset("workflow", doctype, workflow_name or "")
+ frappe.cache.hset("workflow", doctype, workflow_name or "")
return workflow_name
@@ -228,10 +228,10 @@ def send_email_alert(workflow_name):
def get_workflow_field_value(workflow_name, field):
- value = frappe.cache().hget("workflow_" + workflow_name, field)
+ value = frappe.cache.hget("workflow_" + workflow_name, field)
if value is None:
value = frappe.db.get_value("Workflow", workflow_name, field)
- frappe.cache().hset("workflow_" + workflow_name, field, value)
+ frappe.cache.hset("workflow_" + workflow_name, field, value)
return value
diff --git a/frappe/modules/utils.py b/frappe/modules/utils.py
index 57d3e8f7ad..40e3b32690 100644
--- a/frappe/modules/utils.py
+++ b/frappe/modules/utils.py
@@ -214,7 +214,7 @@ def export_doc(doctype, name, module=None):
def get_doctype_module(doctype: str) -> str:
"""Returns **Module Def** name of given doctype."""
- doctype_module_map = frappe.cache().get_value(
+ doctype_module_map = frappe.cache.get_value(
"doctype_modules",
generator=lambda: dict(frappe.qb.from_("DocType").select("name", "module").run()),
)
diff --git a/frappe/monitor.py b/frappe/monitor.py
index b93ba1d3bb..da2deb859e 100644
--- a/frappe/monitor.py
+++ b/frappe/monitor.py
@@ -106,22 +106,22 @@ class Monitor:
traceback.print_exc()
def store(self):
- if frappe.cache().llen(MONITOR_REDIS_KEY) > MONITOR_MAX_ENTRIES:
- frappe.cache().ltrim(MONITOR_REDIS_KEY, 1, -1)
+ if frappe.cache.llen(MONITOR_REDIS_KEY) > MONITOR_MAX_ENTRIES:
+ frappe.cache.ltrim(MONITOR_REDIS_KEY, 1, -1)
serialized = json.dumps(self.data, sort_keys=True, default=str, separators=(",", ":"))
- frappe.cache().rpush(MONITOR_REDIS_KEY, serialized)
+ frappe.cache.rpush(MONITOR_REDIS_KEY, serialized)
def flush():
try:
# Fetch all the logs without removing from cache
- logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
+ logs = frappe.cache.lrange(MONITOR_REDIS_KEY, 0, -1)
if logs:
logs = list(map(frappe.safe_decode, logs))
with open(log_file(), "a", os.O_NONBLOCK) as f:
f.write("\n".join(logs))
f.write("\n")
# Remove fetched entries from cache
- frappe.cache().ltrim(MONITOR_REDIS_KEY, len(logs) - 1, -1)
+ frappe.cache.ltrim(MONITOR_REDIS_KEY, len(logs) - 1, -1)
except Exception:
traceback.print_exc()
diff --git a/frappe/permissions.py b/frappe/permissions.py
index 431132a0ae..67ed972c32 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -433,7 +433,7 @@ def get_roles(user=None, with_standard=True):
)
return roles + ["All", "Guest"]
- roles = frappe.cache().hget("roles", user, get)
+ roles = frappe.cache.hget("roles", user, get)
# filter standard if required
if not with_standard:
diff --git a/frappe/rate_limiter.py b/frappe/rate_limiter.py
index 0717124ba9..0448d7ea92 100644
--- a/frappe/rate_limiter.py
+++ b/frappe/rate_limiter.py
@@ -38,8 +38,8 @@ class RateLimiter:
timestamp = int(frappe.utils.now_datetime().timestamp())
self.window_number, self.spent = divmod(timestamp, self.window)
- self.key = frappe.cache().make_key(f"rate-limit-counter-{self.window_number}")
- self.counter = cint(frappe.cache().get(self.key))
+ self.key = frappe.cache.make_key(f"rate-limit-counter-{self.window_number}")
+ self.counter = cint(frappe.cache.get(self.key))
self.remaining = max(self.limit - self.counter, 0)
self.reset = self.window - self.spent
@@ -59,7 +59,7 @@ class RateLimiter:
self.end = datetime.utcnow()
self.duration = int((self.end - self.start).total_seconds() * 1000000)
- pipeline = frappe.cache().pipeline()
+ pipeline = frappe.cache.pipeline()
pipeline.incrby(self.key, self.duration)
pipeline.expire(self.key, self.window)
pipeline.execute()
@@ -137,11 +137,11 @@ def rate_limit(
cache_key = f"rl:{frappe.form_dict.cmd}:{identity}"
- value = frappe.cache().get(cache_key) or 0
+ value = frappe.cache.get(cache_key) or 0
if not value:
- frappe.cache().setex(cache_key, seconds, 0)
+ frappe.cache.setex(cache_key, seconds, 0)
- value = frappe.cache().incrby(cache_key, 1)
+ value = frappe.cache.incrby(cache_key, 1)
if value > _limit:
frappe.throw(
_("You hit the rate limit because of too many requests. Please try after sometime.")
diff --git a/frappe/recorder.py b/frappe/recorder.py
index 537d1ee996..8229b862af 100644
--- a/frappe/recorder.py
+++ b/frappe/recorder.py
@@ -65,7 +65,7 @@ def get_current_stack_frames():
def record(force=False):
if __debug__:
- if frappe.cache().get_value(RECORDER_INTERCEPT_FLAG) or force:
+ if frappe.cache.get_value(RECORDER_INTERCEPT_FLAG) or force:
frappe.local._recorder = Recorder()
@@ -109,7 +109,7 @@ class Recorder:
"duration": float(f"{(datetime.datetime.now() - self.time).total_seconds() * 1000:0.3f}"),
"method": self.method,
}
- frappe.cache().hset(RECORDER_REQUEST_SPARSE_HASH, self.uuid, request_data)
+ frappe.cache.hset(RECORDER_REQUEST_SPARSE_HASH, self.uuid, request_data)
frappe.publish_realtime(
event="recorder-dump-event",
message=json.dumps(request_data, default=str),
@@ -121,7 +121,7 @@ class Recorder:
request_data["calls"] = self.calls
request_data["headers"] = self.headers
request_data["form_dict"] = self.form_dict
- frappe.cache().hset(RECORDER_REQUEST_HASH, self.uuid, request_data)
+ frappe.cache.hset(RECORDER_REQUEST_HASH, self.uuid, request_data)
def mark_duplicates(self):
counts = Counter([call["query"] for call in self.calls])
@@ -162,21 +162,21 @@ def administrator_only(function):
@do_not_record
@administrator_only
def status(*args, **kwargs):
- return bool(frappe.cache().get_value(RECORDER_INTERCEPT_FLAG))
+ return bool(frappe.cache.get_value(RECORDER_INTERCEPT_FLAG))
@frappe.whitelist()
@do_not_record
@administrator_only
def start(*args, **kwargs):
- frappe.cache().set_value(RECORDER_INTERCEPT_FLAG, 1)
+ frappe.cache.set_value(RECORDER_INTERCEPT_FLAG, 1)
@frappe.whitelist()
@do_not_record
@administrator_only
def stop(*args, **kwargs):
- frappe.cache().delete_value(RECORDER_INTERCEPT_FLAG)
+ frappe.cache.delete_value(RECORDER_INTERCEPT_FLAG)
@frappe.whitelist()
@@ -184,9 +184,9 @@ def stop(*args, **kwargs):
@administrator_only
def get(uuid=None, *args, **kwargs):
if uuid:
- result = frappe.cache().hget(RECORDER_REQUEST_HASH, uuid)
+ result = frappe.cache.hget(RECORDER_REQUEST_HASH, uuid)
else:
- result = list(frappe.cache().hgetall(RECORDER_REQUEST_SPARSE_HASH).values())
+ result = list(frappe.cache.hgetall(RECORDER_REQUEST_SPARSE_HASH).values())
return result
@@ -194,15 +194,15 @@ def get(uuid=None, *args, **kwargs):
@do_not_record
@administrator_only
def export_data(*args, **kwargs):
- return list(frappe.cache().hgetall(RECORDER_REQUEST_HASH).values())
+ return list(frappe.cache.hgetall(RECORDER_REQUEST_HASH).values())
@frappe.whitelist()
@do_not_record
@administrator_only
def delete(*args, **kwargs):
- frappe.cache().delete_value(RECORDER_REQUEST_SPARSE_HASH)
- frappe.cache().delete_value(RECORDER_REQUEST_HASH)
+ frappe.cache.delete_value(RECORDER_REQUEST_SPARSE_HASH)
+ frappe.cache.delete_value(RECORDER_REQUEST_HASH)
def record_queries(func: Callable):
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 9c739f3a96..64a1a6b663 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -85,8 +85,8 @@ def delete_session(sid=None, user=None, reason="Session Expired"):
# we should just ignore it till database is back up again.
return
- frappe.cache().hdel("session", sid)
- frappe.cache().hdel("last_db_session_update", sid)
+ frappe.cache.hdel("session", sid)
+ frappe.cache.hdel("last_db_session_update", sid)
if sid and not user:
table = DocType("Sessions")
user_details = (
@@ -139,17 +139,17 @@ def get():
bootinfo = None
if not getattr(frappe.conf, "disable_session_cache", None):
# check if cache exists
- bootinfo = frappe.cache().hget("bootinfo", frappe.session.user)
+ bootinfo = frappe.cache.hget("bootinfo", frappe.session.user)
if bootinfo:
bootinfo["from_cache"] = 1
- bootinfo["user"]["recent"] = json.dumps(frappe.cache().hget("user_recent", frappe.session.user))
+ bootinfo["user"]["recent"] = json.dumps(frappe.cache.hget("user_recent", frappe.session.user))
if not bootinfo:
# if not create it
bootinfo = get_bootinfo()
- frappe.cache().hset("bootinfo", frappe.session.user, bootinfo)
+ frappe.cache.hset("bootinfo", frappe.session.user, bootinfo)
try:
- frappe.cache().ping()
+ frappe.cache.ping()
except redis.exceptions.ConnectionError:
message = _("Redis cache server not running. Please contact Administrator / Tech support")
if "messages" in bootinfo:
@@ -161,7 +161,7 @@ def get():
if frappe.local.request:
bootinfo["change_log"] = get_change_log()
- bootinfo["metadata_version"] = frappe.cache().get_value("metadata_version")
+ bootinfo["metadata_version"] = frappe.cache.get_value("metadata_version")
if not bootinfo["metadata_version"]:
bootinfo["metadata_version"] = frappe.reset_metadata_version()
@@ -276,7 +276,7 @@ class Session:
)
# also add to memcache
- frappe.cache().hset("session", self.data.sid, self.data)
+ frappe.cache.hset("session", self.data.sid, self.data)
def resume(self):
"""non-login request: load a session"""
@@ -320,7 +320,7 @@ class Session:
return data
def get_session_data_from_cache(self):
- data = frappe.cache().hget("session", self.sid)
+ data = frappe.cache.hget("session", self.sid)
if data:
data = frappe._dict(data)
session_data = data.get("data", {})
@@ -377,7 +377,7 @@ class Session:
self.data["data"]["lang"] = str(frappe.lang)
# update session in db
- last_updated = frappe.cache().hget("last_db_session_update", self.sid)
+ last_updated = frappe.cache.hget("last_db_session_update", self.sid)
time_diff = frappe.utils.time_diff_in_seconds(now, last_updated) if last_updated else None
# database persistence is secondary, don't update it too often
@@ -397,11 +397,11 @@ class Session:
)
frappe.db.commit()
- frappe.cache().hset("last_db_session_update", self.sid, now)
+ frappe.cache.hset("last_db_session_update", self.sid, now)
updated_in_db = True
- frappe.cache().hset("session", self.sid, self.data)
+ frappe.cache.hset("session", self.sid, self.data)
return updated_in_db
diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py
index 658d333c44..a9b013b0e1 100644
--- a/frappe/social/doctype/energy_point_log/energy_point_log.py
+++ b/frappe/social/doctype/energy_point_log/energy_point_log.py
@@ -38,7 +38,7 @@ class EnergyPointLog(Document):
"energy_point_alert", message=alert_dict, user=self.user, after_commit=True
)
- frappe.cache().hdel("energy_points", self.user)
+ frappe.cache.hdel("energy_points", self.user)
if self.type != "Review" and frappe.get_cached_value(
"Notification Settings", self.user, "energy_points_system_notifications"
@@ -222,9 +222,6 @@ def add_review_points(user, points):
@frappe.whitelist()
def get_energy_points(user):
- # points = frappe.cache().hget('energy_points', user,
- # lambda: get_user_energy_and_review_points(user))
- # TODO: cache properly
points = get_user_energy_and_review_points(user)
return frappe._dict(points.get(user, {}))
diff --git a/frappe/social/doctype/energy_point_log/test_energy_point_log.py b/frappe/social/doctype/energy_point_log/test_energy_point_log.py
index c97e2a44e4..2b88d33500 100644
--- a/frappe/social/doctype/energy_point_log/test_energy_point_log.py
+++ b/frappe/social/doctype/energy_point_log/test_energy_point_log.py
@@ -26,13 +26,13 @@ class TestEnergyPointLog(FrappeTestCase):
settings.save()
def setUp(self):
- frappe.cache().delete_value("energy_point_rule_map")
+ frappe.cache.delete_value("energy_point_rule_map")
def tearDown(self):
frappe.set_user("Administrator")
frappe.db.delete("Energy Point Log")
frappe.db.delete("Energy Point Rule")
- frappe.cache().delete_value("energy_point_rule_map")
+ frappe.cache.delete_value("energy_point_rule_map")
def test_user_energy_point(self):
frappe.set_user("test@example.com")
diff --git a/frappe/tests/test_boot.py b/frappe/tests/test_boot.py
index 232c379e08..8ad2a94aeb 100644
--- a/frappe/tests/test_boot.py
+++ b/frappe/tests/test_boot.py
@@ -65,7 +65,7 @@ class TestBootData(FrappeTestCase):
).insert(ignore_permissions=True)
get_user_pages_or_reports("Report")
- allowed_reports = frappe.cache().get_value("has_role:Report", user=frappe.session.user)
+ allowed_reports = frappe.cache.get_value("has_role:Report", user=frappe.session.user)
# Test user must not see admin user's report
self.assertNotIn("Test Admin Report", allowed_reports)
diff --git a/frappe/tests/test_hooks.py b/frappe/tests/test_hooks.py
index 3ecc2c2b89..41a734e7ad 100644
--- a/frappe/tests/test_hooks.py
+++ b/frappe/tests/test_hooks.py
@@ -26,7 +26,7 @@ class TestHooks(FrappeTestCase):
hooks.override_doctype_class = {"ToDo": ["frappe.tests.test_hooks.CustomToDo"]}
# Clear cache
- frappe.cache().delete_value("app_hooks")
+ frappe.cache.delete_value("app_hooks")
clear_controller_cache("ToDo")
todo = frappe.get_doc(doctype="ToDo", description="asdf")
@@ -45,7 +45,7 @@ class TestHooks(FrappeTestCase):
hooks.has_permission["Address"] = address_has_permission_hook
# Clear cache
- frappe.cache().delete_value("app_hooks")
+ frappe.cache.delete_value("app_hooks")
# Init User and Address
username = "test@example.com"
diff --git a/frappe/tests/test_monitor.py b/frappe/tests/test_monitor.py
index e59ebcde31..74c8c07b9f 100644
--- a/frappe/tests/test_monitor.py
+++ b/frappe/tests/test_monitor.py
@@ -12,7 +12,7 @@ from frappe.utils.response import build_response
class TestMonitor(FrappeTestCase):
def setUp(self):
frappe.conf.monitor = 1
- frappe.cache().delete_value(MONITOR_REDIS_KEY)
+ frappe.cache.delete_value(MONITOR_REDIS_KEY)
def test_enable_monitor(self):
set_request(method="GET", path="/api/method/frappe.ping")
@@ -21,7 +21,7 @@ class TestMonitor(FrappeTestCase):
frappe.monitor.start()
frappe.monitor.stop(response)
- logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
+ logs = frappe.cache.lrange(MONITOR_REDIS_KEY, 0, -1)
self.assertEqual(len(logs), 1)
log = frappe.parse_json(logs[0].decode())
@@ -39,7 +39,7 @@ class TestMonitor(FrappeTestCase):
frappe.monitor.start()
frappe.monitor.stop(response=None)
- logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
+ logs = frappe.cache.lrange(MONITOR_REDIS_KEY, 0, -1)
self.assertEqual(len(logs), 1)
log = frappe.parse_json(logs[0].decode())
@@ -52,7 +52,7 @@ class TestMonitor(FrappeTestCase):
frappe.local.site, "frappe.ping", None, None, {}, is_async=False
)
- logs = frappe.cache().lrange(MONITOR_REDIS_KEY, 0, -1)
+ logs = frappe.cache.lrange(MONITOR_REDIS_KEY, 0, -1)
self.assertEqual(len(logs), 1)
log = frappe.parse_json(logs[0].decode())
self.assertEqual(log.transaction_type, "job")
@@ -79,4 +79,4 @@ class TestMonitor(FrappeTestCase):
def tearDown(self):
frappe.conf.monitor = 0
- frappe.cache().delete_value(MONITOR_REDIS_KEY)
+ frappe.cache.delete_value(MONITOR_REDIS_KEY)
diff --git a/frappe/tests/test_rate_limiter.py b/frappe/tests/test_rate_limiter.py
index c8485d6c69..292a688484 100644
--- a/frappe/tests/test_rate_limiter.py
+++ b/frappe/tests/test_rate_limiter.py
@@ -20,7 +20,7 @@ class TestRateLimiter(FrappeTestCase):
self.assertTrue(hasattr(frappe.local, "rate_limiter"))
self.assertIsInstance(frappe.local.rate_limiter, RateLimiter)
- frappe.cache().delete(frappe.local.rate_limiter.key)
+ frappe.cache.delete(frappe.local.rate_limiter.key)
delattr(frappe.local, "rate_limiter")
def test_apply_without_limit(self):
@@ -53,8 +53,8 @@ class TestRateLimiter(FrappeTestCase):
self.assertEqual(int(headers["X-RateLimit-Limit"]), 10000)
self.assertEqual(int(headers["X-RateLimit-Remaining"]), 0)
- frappe.cache().delete(limiter.key)
- frappe.cache().delete(frappe.local.rate_limiter.key)
+ frappe.cache.delete(limiter.key)
+ frappe.cache.delete(frappe.local.rate_limiter.key)
delattr(frappe.local, "rate_limiter")
def test_respond_under_limit(self):
@@ -64,7 +64,7 @@ class TestRateLimiter(FrappeTestCase):
response = frappe.rate_limiter.respond()
self.assertEqual(response, None)
- frappe.cache().delete(frappe.local.rate_limiter.key)
+ frappe.cache.delete(frappe.local.rate_limiter.key)
delattr(frappe.local, "rate_limiter")
def test_headers_under_limit(self):
@@ -79,7 +79,7 @@ class TestRateLimiter(FrappeTestCase):
self.assertEqual(int(headers["X-RateLimit-Limit"]), 10000)
self.assertEqual(int(headers["X-RateLimit-Remaining"]), 10000)
- frappe.cache().delete(frappe.local.rate_limiter.key)
+ frappe.cache.delete(frappe.local.rate_limiter.key)
delattr(frappe.local, "rate_limiter")
def test_reject_over_limit(self):
@@ -90,7 +90,7 @@ class TestRateLimiter(FrappeTestCase):
limiter = RateLimiter(0.01, 86400)
self.assertRaises(frappe.TooManyRequestsError, limiter.apply)
- frappe.cache().delete(limiter.key)
+ frappe.cache.delete(limiter.key)
def test_do_not_reject_under_limit(self):
limiter = RateLimiter(0.01, 86400)
@@ -100,13 +100,13 @@ class TestRateLimiter(FrappeTestCase):
limiter = RateLimiter(0.02, 86400)
self.assertEqual(limiter.apply(), None)
- frappe.cache().delete(limiter.key)
+ frappe.cache.delete(limiter.key)
def test_update_method(self):
limiter = RateLimiter(0.01, 86400)
time.sleep(0.01)
limiter.update()
- self.assertEqual(limiter.duration, cint(frappe.cache().get(limiter.key)))
+ self.assertEqual(limiter.duration, cint(frappe.cache.get(limiter.key)))
- frappe.cache().delete(limiter.key)
+ frappe.cache.delete(limiter.key)
diff --git a/frappe/tests/test_twofactor.py b/frappe/tests/test_twofactor.py
index 053b755b65..96c0a80ead 100644
--- a/frappe/tests/test_twofactor.py
+++ b/frappe/tests/test_twofactor.py
@@ -61,7 +61,7 @@ class TestTwoFactor(FrappeTestCase):
self.assertTrue(verification_obj)
self.assertTrue(tmp_id)
for k in ["_usr", "_pwd", "_otp_secret"]:
- self.assertTrue(frappe.cache().get(f"{tmp_id}{k}"), f"{k} not available")
+ self.assertTrue(frappe.cache.get(f"{tmp_id}{k}"), f"{k} not available")
def test_two_factor_is_enabled(self):
"""
diff --git a/frappe/tests/test_webform.py b/frappe/tests/test_webform.py
index cde963a915..d8b9254a09 100644
--- a/frappe/tests/test_webform.py
+++ b/frappe/tests/test_webform.py
@@ -80,4 +80,4 @@ def set_webform_hook(key, value):
delattr(hooks, hook)
setattr(hooks, key, value)
- frappe.cache().delete_key("app_hooks")
+ frappe.cache.delete_key("app_hooks")
diff --git a/frappe/tests/test_website.py b/frappe/tests/test_website.py
index 01f6e4f7cc..841f7f1e71 100644
--- a/frappe/tests/test_website.py
+++ b/frappe/tests/test_website.py
@@ -46,7 +46,7 @@ class TestWebsite(FrappeTestCase):
frappe.db.set_value("Portal Settings", None, "default_portal_home", "test-portal-home")
frappe.set_user("test-user-for-home-page@example.com")
- frappe.cache().hdel("home_page", frappe.session.user)
+ frappe.cache.hdel("home_page", frappe.session.user)
self.assertEqual(get_home_page(), "test-portal-home")
frappe.db.set_value("Portal Settings", None, "default_portal_home", "")
@@ -210,7 +210,7 @@ class TestWebsite(FrappeTestCase):
self.assertEqual(response.headers.get("Location"), "/courses/data")
delattr(frappe.hooks, "website_redirects")
- frappe.cache().delete_key("app_hooks")
+ frappe.cache.delete_key("app_hooks")
def test_custom_page_renderer(self):
from frappe import get_hooks
diff --git a/frappe/translate.py b/frappe/translate.py
index 041e983432..f35a4b7ec3 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -125,7 +125,7 @@ def get_parent_language(lang: str) -> str:
def get_user_lang(user: str = None) -> str:
"""Set frappe.local.lang from user preferences on session beginning or resumption"""
user = user or frappe.session.user
- lang = frappe.cache().hget("lang", user)
+ lang = frappe.cache.hget("lang", user)
if not lang:
# User.language => Session Defaults => frappe.local.lang => 'en'
@@ -136,7 +136,7 @@ def get_user_lang(user: str = None) -> str:
or "en"
)
- frappe.cache().hset("lang", user, lang)
+ frappe.cache.hset("lang", user, lang)
return lang
@@ -168,9 +168,8 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
:param name: name of the document for which assets are to be returned.
"""
fortype = fortype.lower()
- cache = frappe.cache()
asset_key = fortype + ":" + (name or "-")
- translation_assets = cache.hget("translation_assets", frappe.local.lang) or {}
+ translation_assets = frappe.cache.hget("translation_assets", frappe.local.lang) or {}
if asset_key not in translation_assets:
messages = []
@@ -210,7 +209,7 @@ def get_dict(fortype: str, name: str | None = None) -> dict[str, str]:
# remove untranslated
message_dict = {k: v for k, v in message_dict.items() if k != v}
translation_assets[asset_key] = message_dict
- cache.hset("translation_assets", frappe.local.lang, translation_assets)
+ frappe.cache.hset("translation_assets", frappe.local.lang, translation_assets)
translation_map: dict = translation_assets[asset_key]
@@ -292,7 +291,7 @@ def get_all_translations(lang: str) -> dict[str, str]:
return all_translations
try:
- return frappe.cache().hget(MERGED_TRANSLATION_KEY, lang, generator=_merge_translations)
+ return frappe.cache.hget(MERGED_TRANSLATION_KEY, lang, generator=_merge_translations)
except Exception:
# People mistakenly call translation function on global variables
# where locals are not initalized, translations dont make much sense there
@@ -361,19 +360,18 @@ def get_user_translations(lang):
user_translations[key] = value
return user_translations
- return frappe.cache().hget(USER_TRANSLATION_KEY, lang, generator=_read_from_db)
+ return frappe.cache.hget(USER_TRANSLATION_KEY, lang, generator=_read_from_db)
def clear_cache():
"""Clear all translation assets from :meth:`frappe.cache`"""
- cache = frappe.cache()
- cache.delete_key("langinfo")
+ frappe.cache.delete_key("langinfo")
# clear translations saved in boot cache
- cache.delete_key("bootinfo")
- cache.delete_key("translation_assets")
- cache.delete_key(USER_TRANSLATION_KEY)
- cache.delete_key(MERGED_TRANSLATION_KEY)
+ frappe.cache.delete_key("bootinfo")
+ frappe.cache.delete_key("translation_assets")
+ frappe.cache.delete_key(USER_TRANSLATION_KEY)
+ frappe.cache.delete_key(MERGED_TRANSLATION_KEY)
def get_messages_for_app(app, deduplicate=True):
@@ -1273,9 +1271,9 @@ def get_all_languages(with_language_name: bool = False) -> list:
frappe.connect()
if with_language_name:
- return frappe.cache().get_value("languages_with_name", get_all_language_with_name)
+ return frappe.cache.get_value("languages_with_name", get_all_language_with_name)
else:
- return frappe.cache().get_value("languages", get_language_codes)
+ return frappe.cache.get_value("languages", get_language_codes)
@frappe.whitelist(allow_guest=True)
diff --git a/frappe/twofactor.py b/frappe/twofactor.py
index c4292b0533..65f94cae90 100644
--- a/frappe/twofactor.py
+++ b/frappe/twofactor.py
@@ -74,8 +74,8 @@ def get_cached_user_pass():
user = pwd = None
tmp_id = frappe.form_dict.get("tmp_id")
if tmp_id:
- user = frappe.safe_decode(frappe.cache().get(tmp_id + "_usr"))
- pwd = frappe.safe_decode(frappe.cache().get(tmp_id + "_pwd"))
+ user = frappe.safe_decode(frappe.cache.get(tmp_id + "_usr"))
+ pwd = frappe.safe_decode(frappe.cache.get(tmp_id + "_pwd"))
return (user, pwd)
@@ -101,13 +101,13 @@ def cache_2fa_data(user, token, otp_secret, tmp_id):
# set increased expiry time for SMS and Email
if verification_method in ["SMS", "Email"]:
expiry_time = frappe.flags.token_expiry or 300
- frappe.cache().set(tmp_id + "_token", token)
- frappe.cache().expire(tmp_id + "_token", expiry_time)
+ frappe.cache.set(tmp_id + "_token", token)
+ frappe.cache.expire(tmp_id + "_token", expiry_time)
else:
expiry_time = frappe.flags.otp_expiry or 180
for k, v in {"_usr": user, "_pwd": pwd, "_otp_secret": otp_secret}.items():
- frappe.cache().set(f"{tmp_id}{k}", v)
- frappe.cache().expire(f"{tmp_id}{k}", expiry_time)
+ frappe.cache.set(f"{tmp_id}{k}", v)
+ frappe.cache.expire(f"{tmp_id}{k}", expiry_time)
def two_factor_is_enabled_for_(user):
@@ -160,8 +160,8 @@ def confirm_otp_token(login_manager, otp=None, tmp_id=None):
return True
if not tmp_id:
tmp_id = frappe.form_dict.get("tmp_id")
- hotp_token = frappe.cache().get(tmp_id + "_token")
- otp_secret = frappe.cache().get(tmp_id + "_otp_secret")
+ hotp_token = frappe.cache.get(tmp_id + "_token")
+ otp_secret = frappe.cache.get(tmp_id + "_otp_secret")
if not otp_secret:
raise ExpiredLoginException(_("Login session expired, refresh page to retry"))
@@ -170,7 +170,7 @@ def confirm_otp_token(login_manager, otp=None, tmp_id=None):
hotp = pyotp.HOTP(otp_secret)
if hotp_token:
if hotp.verify(otp, int(hotp_token)):
- frappe.cache().delete(tmp_id + "_token")
+ frappe.cache.delete(tmp_id + "_token")
tracker.add_success_attempt()
return True
else:
@@ -308,8 +308,8 @@ def get_link_for_qrcode(user, totp_uri):
key_user = f"{key}_user"
key_uri = f"{key}_uri"
lifespan = int(frappe.db.get_single_value("System Settings", "lifespan_qrcode_image")) or 240
- frappe.cache().set_value(key_uri, totp_uri, expires_in_sec=lifespan)
- frappe.cache().set_value(key_user, user, expires_in_sec=lifespan)
+ frappe.cache.set_value(key_uri, totp_uri, expires_in_sec=lifespan)
+ frappe.cache.set_value(key_user, user, expires_in_sec=lifespan)
return get_url(f"/qrcode?k={key}")
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index b7dc565555..d6b8186a2f 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -970,7 +970,7 @@ def get_assets_json():
if not hasattr(frappe.local, "assets_json"):
if not frappe.conf.developer_mode:
- frappe.local.assets_json = frappe.cache().get_value(
+ frappe.local.assets_json = frappe.cache.get_value(
"assets_json",
_get_assets,
shared=True,
diff --git a/frappe/utils/caching.py b/frappe/utils/caching.py
index 370227ea72..fbfbddbd88 100644
--- a/frappe/utils/caching.py
+++ b/frappe/utils/caching.py
@@ -143,7 +143,7 @@ def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Calla
func_key = f"{func.__module__}.{func.__qualname__}"
def clear_cache():
- frappe.cache().delete_keys(func_key)
+ frappe.cache.delete_keys(func_key)
func.clear_cache = clear_cache
func.ttl = ttl if not callable(ttl) else 3600
@@ -151,12 +151,12 @@ def redis_cache(ttl: int | None = 3600, user: str | bool | None = None) -> Calla
@wraps(func)
def redis_cache_wrapper(*args, **kwargs):
func_call_key = func_key + "::" + str(__generate_request_cache_key(args, kwargs))
- if frappe.cache().exists(func_call_key):
- return frappe.cache().get_value(func_call_key, user=user)
+ if frappe.cache.exists(func_call_key):
+ return frappe.cache.get_value(func_call_key, user=user)
else:
val = func(*args, **kwargs)
ttl = getattr(func, "ttl", 3600)
- frappe.cache().set_value(func_call_key, val, expires_in_sec=ttl, user=user)
+ frappe.cache.set_value(func_call_key, val, expires_in_sec=ttl, user=user)
return val
return redis_cache_wrapper
diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py
index a4b56686c2..586024f931 100644
--- a/frappe/utils/change_log.py
+++ b/frappe/utils/change_log.py
@@ -267,19 +267,17 @@ def check_release_on_github(app: str):
def add_message_to_redis(update_json):
# "update-message" will store the update message string
# "update-user-set" will be a set of users
- cache = frappe.cache()
- cache.set_value("update-info", json.dumps(update_json))
+ frappe.cache.set_value("update-info", json.dumps(update_json))
user_list = [x.name for x in frappe.get_all("User", filters={"enabled": True})]
system_managers = [user for user in user_list if "System Manager" in frappe.get_roles(user)]
- cache.sadd("update-user-set", *system_managers)
+ frappe.cache.sadd("update-user-set", *system_managers)
@frappe.whitelist()
def show_update_popup():
- cache = frappe.cache()
user = frappe.session.user
- update_info = cache.get_value("update-info")
+ update_info = frappe.cache.get_value("update-info")
if not update_info:
return
@@ -287,7 +285,7 @@ def show_update_popup():
# Check if user is int the set of users to send update message to
update_message = ""
- if cache.sismember("update-user-set", user):
+ if frappe.cache.sismember("update-user-set", user):
for update_type in updates:
release_links = ""
for app in updates[update_type]:
@@ -308,4 +306,4 @@ def show_update_popup():
if update_message:
frappe.msgprint(update_message, title=_("New updates are available"), indicator="green")
- cache.srem("update-user-set", user)
+ frappe.cache.srem("update-user-set", user)
diff --git a/frappe/utils/dashboard.py b/frappe/utils/dashboard.py
index 980107ce2b..9066f3172c 100644
--- a/frappe/utils/dashboard.py
+++ b/frappe/utils/dashboard.py
@@ -25,7 +25,7 @@ def cache_source(function):
if int(kwargs.get("refresh") or 0):
results = generate_and_cache_results(kwargs, function, cache_key, chart)
else:
- cached_results = frappe.cache().get_value(cache_key)
+ cached_results = frappe.cache.get_value(cache_key)
if cached_results:
results = frappe.parse_json(frappe.safe_decode(cached_results))
else:
diff --git a/frappe/utils/data.py b/frappe/utils/data.py
index 3fe854bbf6..deb5ea486f 100644
--- a/frappe/utils/data.py
+++ b/frappe/utils/data.py
@@ -326,7 +326,7 @@ def get_system_timezone():
if frappe.local.flags.in_test:
return _get_system_timezone()
- return frappe.cache().get_value("time_zone", _get_system_timezone)
+ return frappe.cache.get_value("time_zone", _get_system_timezone)
def convert_utc_to_timezone(utc_timestamp, time_zone):
diff --git a/frappe/utils/global_search.py b/frappe/utils/global_search.py
index 5e5c1da141..af01692b94 100644
--- a/frappe/utils/global_search.py
+++ b/frappe/utils/global_search.py
@@ -61,7 +61,7 @@ def get_doctypes_with_global_search(with_child_tables=True):
return doctypes
- return frappe.cache().get_value("doctypes_with_global_search", _get)
+ return frappe.cache.get_value("doctypes_with_global_search", _get)
def rebuild_for_doctype(doctype):
@@ -371,17 +371,17 @@ def sync_global_search():
:param flags:
:return:
"""
- while frappe.cache().llen("global_search_queue") > 0:
+ while frappe.cache.llen("global_search_queue") > 0:
# rpop to follow FIFO
# Last one should override all previous contents of same document
- value = json.loads(frappe.cache().rpop("global_search_queue").decode("utf-8"))
+ value = json.loads(frappe.cache.rpop("global_search_queue").decode("utf-8"))
sync_value(value)
def sync_value_in_queue(value):
try:
# append to search queue if connected
- frappe.cache().lpush("global_search_queue", json.dumps(value))
+ frappe.cache.lpush("global_search_queue", json.dumps(value))
except redis.exceptions.ConnectionError:
# not connected, sync directly
sync_value(value)
diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py
index d07011afd1..6a2b87d29f 100644
--- a/frappe/utils/oauth.py
+++ b/frappe/utils/oauth.py
@@ -210,7 +210,7 @@ def login_oauth_user(
if frappe.utils.cint(generate_login_token):
login_token = frappe.generate_hash(length=32)
- frappe.cache().set_value(
+ frappe.cache.set_value(
f"login_token:{login_token}", frappe.local.session.sid, expires_in_sec=120
)
diff --git a/frappe/utils/password.py b/frappe/utils/password.py
index fa2e03bde5..2bd477216d 100644
--- a/frappe/utils/password.py
+++ b/frappe/utils/password.py
@@ -128,9 +128,9 @@ def check_password(user, pwd, doctype="User", fieldname="password", delete_track
def delete_login_failed_cache(user):
- frappe.cache().hdel("last_login_tried", user)
- frappe.cache().hdel("login_failed_count", user)
- frappe.cache().hdel("locked_account_time", user)
+ frappe.cache.hdel("last_login_tried", user)
+ frappe.cache.hdel("login_failed_count", user)
+ frappe.cache.hdel("locked_account_time", user)
def update_password(user, pwd, doctype="User", fieldname="password", logout_all_sessions=False):
diff --git a/frappe/utils/pdf.py b/frappe/utils/pdf.py
index a0ba4a6de1..9b7c9a6ce4 100644
--- a/frappe/utils/pdf.py
+++ b/frappe/utils/pdf.py
@@ -273,13 +273,13 @@ def toggle_visible_pdf(soup):
def get_wkhtmltopdf_version():
- wkhtmltopdf_version = frappe.cache().hget("wkhtmltopdf_version", None)
+ wkhtmltopdf_version = frappe.cache.hget("wkhtmltopdf_version", None)
if not wkhtmltopdf_version:
try:
res = subprocess.check_output(["wkhtmltopdf", "--version"])
wkhtmltopdf_version = res.decode("utf-8").split(" ")[1]
- frappe.cache().hset("wkhtmltopdf_version", None, wkhtmltopdf_version)
+ frappe.cache.hset("wkhtmltopdf_version", None, wkhtmltopdf_version)
except Exception:
pass
diff --git a/frappe/utils/redis_wrapper.py b/frappe/utils/redis_wrapper.py
index c23763f2b6..45be0c63e8 100644
--- a/frappe/utils/redis_wrapper.py
+++ b/frappe/utils/redis_wrapper.py
@@ -251,7 +251,7 @@ class RedisWrapper(redis.Redis):
def hdel_keys(self, name_starts_with, key):
"""Delete hash names with wildcard `*` and key"""
- for name in frappe.cache().get_keys(name_starts_with):
+ for name in self.get_keys(name_starts_with):
name = name.split("|", 1)[1]
self.hdel(name, key)
diff --git a/frappe/utils/user.py b/frappe/utils/user.py
index f37e52f7be..35dec3aa60 100644
--- a/frappe/utils/user.py
+++ b/frappe/utils/user.py
@@ -59,7 +59,7 @@ class UserPermissions:
return user
if not frappe.flags.in_install_db and not frappe.flags.in_test:
- user_doc = frappe.cache().hget("user_doc", self.name, get_user_doc)
+ user_doc = frappe.cache.hget("user_doc", self.name, get_user_doc)
if user_doc:
self.doc = frappe.get_doc(user_doc)
@@ -186,7 +186,7 @@ class UserPermissions:
filters={"property": "allow_import", "value": "1"},
)
- frappe.cache().hset("can_import", frappe.session.user, self.can_import)
+ frappe.cache.hset("can_import", frappe.session.user, self.can_import)
def get_defaults(self):
import frappe.defaults
diff --git a/frappe/website/doctype/help_article/help_article.py b/frappe/website/doctype/help_article/help_article.py
index e70de07703..108f3cb615 100644
--- a/frappe/website/doctype/help_article/help_article.py
+++ b/frappe/website/doctype/help_article/help_article.py
@@ -93,7 +93,7 @@ def get_sidebar_items():
as_dict=True,
)
- return frappe.cache().get_value("knowledge_base:category_sidebar", _get)
+ return frappe.cache.get_value("knowledge_base:category_sidebar", _get)
def clear_cache():
@@ -105,8 +105,8 @@ def clear_cache():
def clear_website_cache(path=None):
- frappe.cache().delete_value("knowledge_base:category_sidebar")
- frappe.cache().delete_value("knowledge_base:faq")
+ frappe.cache.delete_value("knowledge_base:category_sidebar")
+ frappe.cache.delete_value("knowledge_base:faq")
@frappe.whitelist(allow_guest=True)
diff --git a/frappe/website/page_renderers/not_found_page.py b/frappe/website/page_renderers/not_found_page.py
index 98aeb19057..704dca77d1 100644
--- a/frappe/website/page_renderers/not_found_page.py
+++ b/frappe/website/page_renderers/not_found_page.py
@@ -21,7 +21,7 @@ class NotFoundPage(TemplatePage):
def render(self):
if self.can_cache_404():
- frappe.cache().hset("website_404", self.request_url, True)
+ frappe.cache.hset("website_404", self.request_url, True)
return super().render()
def can_cache_404(self):
diff --git a/frappe/website/path_resolver.py b/frappe/website/path_resolver.py
index c7874b1671..37bfb3ee56 100644
--- a/frappe/website/path_resolver.py
+++ b/frappe/website/path_resolver.py
@@ -29,7 +29,7 @@ class PathResolver:
request = frappe.local.request or request
# check if the request url is in 404 list
- if request.url and can_cache() and frappe.cache().hget("website_404", request.url):
+ if request.url and can_cache() and frappe.cache.hget("website_404", request.url):
return self.path, NotFoundPage(self.path)
try:
@@ -110,7 +110,7 @@ def resolve_redirect(path, query_string=None):
if not redirects:
return
- redirect_to = frappe.cache().hget("website_redirects", path)
+ redirect_to = frappe.cache.hget("website_redirects", path)
if redirect_to:
frappe.flags.redirect_location = redirect_to
@@ -130,7 +130,7 @@ def resolve_redirect(path, query_string=None):
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)
+ frappe.cache.hset("website_redirects", path_to_match, redirect_to)
raise frappe.Redirect
@@ -177,4 +177,4 @@ def get_website_rules():
# dont cache in development
return _get()
- return frappe.cache().get_value("website_route_rules", _get)
+ return frappe.cache.get_value("website_route_rules", _get)
diff --git a/frappe/website/router.py b/frappe/website/router.py
index 98be1138e4..14648f15e9 100644
--- a/frappe/website/router.py
+++ b/frappe/website/router.py
@@ -100,7 +100,7 @@ def get_pages(app=None):
return pages
- return frappe.cache().get_value("website_pages", lambda: _build(app))
+ return frappe.cache.get_value("website_pages", lambda: _build(app))
def get_pages_from_path(start, app, app_path):
@@ -310,7 +310,7 @@ def get_doctypes_with_web_view():
]
return doctypes
- return frappe.cache().get_value("doctypes_with_web_view", _get)
+ return frappe.cache.get_value("doctypes_with_web_view", _get)
def get_start_folders():
diff --git a/frappe/website/utils.py b/frappe/website/utils.py
index ff8c69639e..922abbb751 100644
--- a/frappe/website/utils.py
+++ b/frappe/website/utils.py
@@ -23,15 +23,14 @@ CLEANUP_PATTERN_3 = re.compile(r"(-)\1+")
def delete_page_cache(path):
- cache = frappe.cache()
- cache.delete_value("full_index")
+ frappe.cache.delete_value("full_index")
groups = ("website_page", "page_context")
if path:
for name in groups:
- cache.hdel(name, path)
+ frappe.cache.hdel(name, path)
else:
for name in groups:
- cache.delete_key(name)
+ frappe.cache.delete_key(name)
def find_first_image(html):
@@ -127,7 +126,7 @@ def get_home_page():
# dont return cached homepage in development
return _get_home_page()
- return frappe.cache().hget("home_page", frappe.session.user, _get_home_page)
+ return frappe.cache.hget("home_page", frappe.session.user, _get_home_page)
def get_home_page_via_hooks():
@@ -296,7 +295,7 @@ def get_full_index(route=None, app=None):
return children_map
- children_map = frappe.cache().get_value("website_full_index", _build)
+ children_map = frappe.cache.get_value("website_full_index", _build)
frappe.local.flags.children_map = children_map
@@ -363,13 +362,13 @@ def clear_cache(path=None):
from frappe.website.router import clear_routing_cache
for key in ("website_generator_routes", "website_pages", "website_full_index", "sitemap_routes"):
- frappe.cache().delete_value(key)
+ frappe.cache.delete_value(key)
clear_routing_cache()
- frappe.cache().delete_value("website_404")
+ frappe.cache.delete_value("website_404")
if path:
- frappe.cache().hdel("website_redirects", path)
+ frappe.cache.hdel("website_redirects", path)
delete_page_cache(path)
else:
clear_sitemap()
@@ -383,7 +382,7 @@ def clear_cache(path=None):
"page_context",
"website_page",
):
- frappe.cache().delete_value(key)
+ frappe.cache.delete_value(key)
for method in frappe.get_hooks("website_clear_cache"):
frappe.get_attr(method)(path)
@@ -439,7 +438,7 @@ def get_sidebar_items(parent_sidebar, basepath=None):
def get_portal_sidebar_items():
- sidebar_items = frappe.cache().hget("portal_menu_items", frappe.session.user)
+ sidebar_items = frappe.cache.hget("portal_menu_items", frappe.session.user)
if sidebar_items is None:
sidebar_items = []
roles = frappe.get_roles()
@@ -462,7 +461,7 @@ def get_portal_sidebar_items():
i["enabled"] = 1
add_items(sidebar_items, items_via_hooks)
- frappe.cache().hset("portal_menu_items", frappe.session.user, sidebar_items)
+ frappe.cache.hset("portal_menu_items", frappe.session.user, sidebar_items)
return sidebar_items
@@ -507,7 +506,7 @@ def cache_html(func):
def cache_html_decorator(*args, **kwargs):
if can_cache():
html = None
- page_cache = frappe.cache().hget("website_page", args[0].path)
+ page_cache = frappe.cache.hget("website_page", args[0].path)
if page_cache and frappe.local.lang in page_cache:
html = page_cache[frappe.local.lang]
if html:
@@ -516,9 +515,9 @@ def cache_html(func):
html = func(*args, **kwargs)
context = args[0].context
if can_cache(context.no_cache):
- page_cache = frappe.cache().hget("website_page", args[0].path) or {}
+ page_cache = frappe.cache.hget("website_page", args[0].path) or {}
page_cache[frappe.local.lang] = html
- frappe.cache().hset("website_page", args[0].path, page_cache)
+ frappe.cache.hset("website_page", args[0].path, page_cache)
return html
diff --git a/frappe/workflow/doctype/workflow/test_workflow.py b/frappe/workflow/doctype/workflow/test_workflow.py
index b7740242c3..7da7f60109 100644
--- a/frappe/workflow/doctype/workflow/test_workflow.py
+++ b/frappe/workflow/doctype/workflow/test_workflow.py
@@ -33,7 +33,7 @@ class TestWorkflow(FrappeTestCase):
"postgres": 'ALTER TABLE "tabWorkflow Action" ADD COLUMN "user" varchar(140)',
}
)
- frappe.cache().delete_value("table_columns")
+ frappe.cache.delete_value("table_columns")
def tearDown(self):
frappe.delete_doc("Workflow", "Test ToDo")
@@ -49,7 +49,7 @@ class TestWorkflow(FrappeTestCase):
"postgres": 'ALTER TABLE "tabWorkflow Action" DROP COLUMN "user"',
}
)
- frappe.cache().delete_value("table_columns")
+ frappe.cache.delete_value("table_columns")
def test_default_condition(self):
"""test default condition is set"""
diff --git a/frappe/workflow/doctype/workflow/workflow.py b/frappe/workflow/doctype/workflow/workflow.py
index 018b567ee9..56c17261b7 100644
--- a/frappe/workflow/doctype/workflow/workflow.py
+++ b/frappe/workflow/doctype/workflow/workflow.py
@@ -17,7 +17,7 @@ class Workflow(Document):
def on_update(self):
self.update_doc_status()
frappe.clear_cache(doctype=self.document_type)
- frappe.cache().delete_key("workflow_" + self.name) # clear cache created in model/workflow.py
+ frappe.cache.delete_key("workflow_" + self.name) # clear cache created in model/workflow.py
def create_custom_field_for_workflow_state(self):
frappe.clear_cache(doctype=self.document_type)
diff --git a/frappe/www/login.py b/frappe/www/login.py
index 3cd9edf7ce..0aa1eba0cd 100644
--- a/frappe/www/login.py
+++ b/frappe/www/login.py
@@ -104,7 +104,7 @@ def get_context(context):
@frappe.whitelist(allow_guest=True)
def login_via_token(login_token: str):
- sid = frappe.cache().get_value(f"login_token:{login_token}", expires=True)
+ sid = frappe.cache.get_value(f"login_token:{login_token}", expires=True)
if not sid:
frappe.respond_as_web_page(_("Invalid Request"), _("Invalid Login Token"), http_status_code=417)
return
@@ -150,7 +150,7 @@ def _generate_temporary_login_link(email: str, expiry: int):
_("User with email address {0} does not exist").format(email), frappe.DoesNotExistError
)
key = frappe.generate_hash()
- frappe.cache().set_value(f"one_time_login_key:{key}", email, expires_in_sec=expiry * 60)
+ frappe.cache.set_value(f"one_time_login_key:{key}", email, expires_in_sec=expiry * 60)
return get_url(f"/api/method/frappe.www.login.login_via_key?key={key}")
@@ -159,10 +159,10 @@ def _generate_temporary_login_link(email: str, expiry: int):
@rate_limit(limit=5, seconds=60 * 60)
def login_via_key(key: str):
cache_key = f"one_time_login_key:{key}"
- email = frappe.cache().get_value(cache_key)
+ email = frappe.cache.get_value(cache_key)
if email:
- frappe.cache().delete_value(cache_key)
+ frappe.cache.delete_value(cache_key)
frappe.local.login_manager.login_as(email)
diff --git a/frappe/www/message.py b/frappe/www/message.py
index d8e359f07c..dbd6ac3863 100644
--- a/frappe/www/message.py
+++ b/frappe/www/message.py
@@ -20,7 +20,7 @@ def get_context(context):
elif frappe.local.form_dict.id:
message_id = frappe.local.form_dict.id
key = f"message_id:{message_id}"
- message = frappe.cache().get_value(key, expires=True)
+ message = frappe.cache.get_value(key, expires=True)
if message:
message_context.update(message.get("context", {}))
if message.get("http_status_code"):
diff --git a/frappe/www/qrcode.py b/frappe/www/qrcode.py
index e76dc65540..e6d683694f 100644
--- a/frappe/www/qrcode.py
+++ b/frappe/www/qrcode.py
@@ -29,8 +29,8 @@ def get_query_key():
def get_user_svg_from_cache():
"""Get User and SVG code from cache."""
key = get_query_key()
- totp_uri = frappe.cache().get_value(f"{key}_uri")
- user = frappe.cache().get_value(f"{key}_user")
+ totp_uri = frappe.cache.get_value(f"{key}_uri")
+ user = frappe.cache.get_value(f"{key}_user")
if not totp_uri or not user:
frappe.throw(_("Page has expired!"), frappe.PermissionError)
if not frappe.db.exists("User", user):
diff --git a/frappe/www/sitemap.py b/frappe/www/sitemap.py
index 57e9d27049..47d809d384 100644
--- a/frappe/www/sitemap.py
+++ b/frappe/www/sitemap.py
@@ -69,4 +69,4 @@ def get_public_pages_from_doctypes():
return routes
- return frappe.cache().get_value("sitemap_routes", get_sitemap_routes)
+ return frappe.cache.get_value("sitemap_routes", get_sitemap_routes)
From 39374f3906a96b6153403cc3527329d6cc005bb0 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 8 Jun 2023 11:48:06 +0530
Subject: [PATCH 191/203] chore: ignore redis.cache commit
[skip ci]
---
.git-blame-ignore-revs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 03efd1d30d..e87590b976 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -37,3 +37,6 @@ c0c5b2ebdddbe8898ce2d5e5365f4931ff73b6bf
# minor formatting fix in `user.py`
f223bc02490902dfcc32892058f13f343d51fbaf
+
+# frappe.cache() -> frappe.cache
+fa6dc03cc87ad74e11609e7373078366fdcb3e1b
From 3446ca91556a201ef71d69fb52b1b68dbd41457e Mon Sep 17 00:00:00 2001
From: Ritwik Puri
Date: Thu, 8 Jun 2023 13:14:37 +0530
Subject: [PATCH 192/203] fix: consider default value for content in
update_workspace2 patch (#21258)
---
frappe/desk/doctype/workspace/workspace.json | 2 +-
frappe/patches.txt | 2 +-
frappe/patches/v14_0/update_workspace2.py | 28 +++++++-------------
3 files changed, 12 insertions(+), 20 deletions(-)
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index 2759acd228..0769b2a81b 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -211,7 +211,7 @@
],
"in_create": 1,
"links": [],
- "modified": "2023-05-17 14:52:38.110224",
+ "modified": "2023-06-08 14:52:38.110224",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
diff --git a/frappe/patches.txt b/frappe/patches.txt
index d3d5e3ee15..ba9c5180ba 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -183,7 +183,6 @@ frappe.patches.v13_0.reset_corrupt_defaults
frappe.patches.v13_0.remove_share_for_std_users
execute:frappe.reload_doc('custom', 'doctype', 'custom_field')
frappe.email.doctype.email_queue.patches.drop_search_index_on_message_id
-frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema
frappe.patches.v14_0.remove_post_and_post_comment
@@ -201,6 +200,7 @@ execute:frappe.reload_doc("desk", "doctype", "Form Tour")
[post_model_sync]
execute:frappe.get_doc('Role', 'Guest').save() # remove desk access
frappe.core.doctype.role.patches.v13_set_default_desk_properties
+frappe.patches.v14_0.update_workspace2 # 06.06.2023
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_github_endpoints #08-11-2021
diff --git a/frappe/patches/v14_0/update_workspace2.py b/frappe/patches/v14_0/update_workspace2.py
index a6c9db503f..1a322c1f84 100644
--- a/frappe/patches/v14_0/update_workspace2.py
+++ b/frappe/patches/v14_0/update_workspace2.py
@@ -5,28 +5,15 @@ from frappe import _
def execute():
- frappe.reload_doc("desk", "doctype", "workspace", force=True)
-
- child_tables = frappe.get_all(
- "DocField",
- pluck="options",
- filters={"fieldtype": ["in", frappe.model.table_fields], "parent": "Workspace"},
- )
-
- for child_table in child_tables:
- if child_table != "Has Role":
- frappe.reload_doc("desk", "doctype", child_table, force=True)
-
- for seq, workspace in enumerate(frappe.get_all("Workspace", order_by="name asc")):
+ for seq, workspace in enumerate(frappe.get_all("Workspace")):
doc = frappe.get_doc("Workspace", workspace.name)
content = create_content(doc)
update_workspace(doc, seq, content)
- frappe.db.commit()
def create_content(doc):
content = []
- if doc.onboarding:
+ if doc.get("onboarding"):
content.append({"type": "onboarding", "data": {"onboarding_name": doc.onboarding, "col": 12}})
if doc.charts:
invalid_links = []
@@ -44,7 +31,7 @@ def create_content(doc):
content.append(
{
"type": "header",
- "data": {"text": doc.shortcuts_label or _("Your Shortcuts"), "level": 4, "col": 12},
+ "data": {"text": doc.get("shortcuts_label") or _("Your Shortcuts"), "level": 4, "col": 12},
}
)
for s in doc.shortcuts:
@@ -60,7 +47,7 @@ def create_content(doc):
content.append(
{
"type": "header",
- "data": {"text": doc.cards_label or _("Reports & Masters"), "level": 4, "col": 12},
+ "data": {"text": doc.get("cards_label") or _("Reports & Masters"), "level": 4, "col": 12},
}
)
for l in doc.links:
@@ -74,7 +61,12 @@ def create_content(doc):
def update_workspace(doc, seq, content):
- if not doc.title and not doc.content and not doc.is_standard and not doc.public:
+ if (
+ not doc.title
+ and (not doc.content or doc.content == "[]")
+ and not doc.get("is_standard")
+ and not doc.public
+ ):
doc.sequence_id = seq + 1
doc.content = json.dumps(content)
doc.public = 0 if doc.for_user else 1
From a21412e12a7589ed9a1d3d831e31a699319cd18f Mon Sep 17 00:00:00 2001
From: Vishal Kumar
Date: Thu, 8 Jun 2023 13:15:43 +0530
Subject: [PATCH 193/203] feat: allow no_smtp_authentication to be set from
site config (#21274)
---
frappe/email/doctype/email_account/email_account.py | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 0e9bbcaf3f..80b1224343 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -384,6 +384,10 @@ class EmailAccount(Document):
"name": {"conf_names": ("email_sender_name",), "default": "Frappe"},
"auth_method": {"conf_names": ("auth_method"), "default": "Basic"},
"from_site_config": {"default": True},
+ "no_smtp_authentication": {
+ "conf_names": ("disable_mail_smtp_authentication",),
+ "default": 0,
+ },
}
account_details = {}
From 2b225563097fb4c392191c69c34baa293ca478b7 Mon Sep 17 00:00:00 2001
From: phot0n
Date: Mon, 5 Jun 2023 15:25:17 +0530
Subject: [PATCH 194/203] chore: remove deprecated send method from smtp
---
frappe/email/smtp.py | 30 ------------------------------
1 file changed, 30 deletions(-)
diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py
index 3b22bc4ce4..7b15440ccf 100644
--- a/frappe/email/smtp.py
+++ b/frappe/email/smtp.py
@@ -13,36 +13,6 @@ class InvalidEmailCredentials(frappe.ValidationError):
pass
-def send(email, append_to=None, retry=1):
- """Deprecated: Send the message or add it to Outbox Email"""
-
- def _send(retry):
- from frappe.email.doctype.email_account.email_account import EmailAccount
-
- try:
- email_account = EmailAccount.find_outgoing(match_by_doctype=append_to)
- smtpserver = email_account.get_smtp_server()
-
- # validate is called in as_string
- email_body = email.as_string()
-
- smtpserver.sess.sendmail(email.sender, email.recipients + (email.cc or []), email_body)
- except smtplib.SMTPSenderRefused:
- frappe.throw(_("Invalid login or password"), title="Email Failed")
- raise
- except smtplib.SMTPRecipientsRefused:
- frappe.msgprint(_("Invalid recipient address"), title="Email Failed")
- raise
- except (smtplib.SMTPServerDisconnected, smtplib.SMTPAuthenticationError):
- if not retry:
- raise
- else:
- retry = retry - 1
- _send(retry)
-
- _send(retry)
-
-
class SMTPServer:
def __init__(
self,
From 742a6082ac9e490512c4422a9cbdc15464ae9d6f Mon Sep 17 00:00:00 2001
From: phot0n
Date: Mon, 5 Jun 2023 15:26:51 +0530
Subject: [PATCH 195/203] fix: remove unnecessary statuses from email queue and
only append emails to sent if imap is enabled
---
.../doctype/email_account/email_account.json | 6 +++---
.../doctype/email_account/email_account.py | 21 +++++++------------
.../doctype/email_domain/email_domain.json | 3 ++-
.../email/doctype/email_queue/email_queue.js | 12 ++++-------
.../doctype/email_queue/email_queue.json | 4 ++--
.../email/doctype/email_queue/email_queue.py | 21 +++++++------------
6 files changed, 26 insertions(+), 41 deletions(-)
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index 85241b8194..d61165b787 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -508,7 +508,7 @@
},
{
"default": "0",
- "depends_on": "eval:!doc.domain && doc.enable_outgoing",
+ "depends_on": "eval:!doc.domain && doc.enable_outgoing && doc.enable_incoming && doc.use_imap",
"fieldname": "append_emails_to_sent_folder",
"fieldtype": "Check",
"hide_days": 1,
@@ -616,7 +616,7 @@
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2022-12-28 14:56:18.754804",
+ "modified": "2023-06-05 15:03:08.538819",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
@@ -639,4 +639,4 @@
"sort_order": "DESC",
"states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 80b1224343..159e6d9583 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -652,21 +652,16 @@ class EmailAccount(Document):
frappe.throw(_("Automatic Linking can be activated only for one Email Account."))
def append_email_to_sent_folder(self, message):
- email_server = None
- try:
- email_server = self.get_incoming_server(in_receive=True)
- except Exception:
- self.log_error("Email Connection Error")
-
- if not email_server:
+ if not (self.enable_incoming and self.use_imap):
+ # don't try appending if enable incoming and imap is not set
return
- if email_server.imap:
- try:
- message = safe_encode(message)
- email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
- except Exception:
- self.log_error("Unable to add to Sent folder")
+ try:
+ email_server = self.get_incoming_server(in_receive=True)
+ message = safe_encode(message)
+ email_server.imap.append("Sent", "\\Seen", imaplib.Time2Internaldate(time.time()), message)
+ except Exception:
+ self.log_error("Unable to add to Sent folder")
def get_oauth_token(self):
if self.auth_method == "OAuth":
diff --git a/frappe/email/doctype/email_domain/email_domain.json b/frappe/email/doctype/email_domain/email_domain.json
index c162060436..5cb4c19940 100644
--- a/frappe/email/doctype/email_domain/email_domain.json
+++ b/frappe/email/doctype/email_domain/email_domain.json
@@ -107,6 +107,7 @@
},
{
"default": "0",
+ "depends_on": "eval:doc.use_imap",
"fieldname": "append_emails_to_sent_folder",
"fieldtype": "Check",
"label": "Append Emails to Sent Folder"
@@ -133,7 +134,7 @@
"link_fieldname": "domain"
}
],
- "modified": "2022-08-19 12:55:06.434541",
+ "modified": "2023-06-05 12:55:06.434541",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Domain",
diff --git a/frappe/email/doctype/email_queue/email_queue.js b/frappe/email/doctype/email_queue/email_queue.js
index 2ac4b6f7fe..7d05053d4e 100644
--- a/frappe/email/doctype/email_queue/email_queue.js
+++ b/frappe/email/doctype/email_queue/email_queue.js
@@ -3,7 +3,7 @@
frappe.ui.form.on("Email Queue", {
refresh: function (frm) {
- if (["Not Sent", "Partially Sent"].indexOf(frm.doc.status) != -1) {
+ if (["Not Sent", "Partially Sent"].includes(frm.doc.status)) {
let button = frm.add_custom_button("Send Now", function () {
frappe.call({
method: "frappe.email.doctype.email_queue.email_queue.send_now",
@@ -16,9 +16,7 @@ frappe.ui.form.on("Email Queue", {
},
});
});
- }
-
- if (["Error", "Partially Errored"].indexOf(frm.doc.status) != -1) {
+ } else if (frm.doc.status == "Error") {
let button = frm.add_custom_button("Retry Sending", function () {
frm.call({
method: "retry_sending",
@@ -26,10 +24,8 @@ frappe.ui.form.on("Email Queue", {
name: frm.doc.name,
},
btn: button,
- callback: function (r) {
- if (!r.exc) {
- frm.set_value("status", "Not Sent");
- }
+ callback: function () {
+ frm.reload_doc();
},
});
});
diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json
index ac8d656678..ce8c3b5e63 100644
--- a/frappe/email/doctype/email_queue/email_queue.json
+++ b/frappe/email/doctype/email_queue/email_queue.json
@@ -58,7 +58,7 @@
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
- "options": "\nNot Sent\nSending\nSent\nError\nExpired"
+ "options": "Not Sent\nSending\nSent\nPartially Sent\nError\nExpired"
},
{
"fieldname": "error",
@@ -152,7 +152,7 @@
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2023-03-16 12:15:17.850292",
+ "modified": "2023-06-05 12:15:17.850292",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index d254c87a0a..d977f2097c 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -147,7 +147,7 @@ class EmailQueue(Document):
frappe.flags.sent_mail = message
return
- if ctx.email_account_doc.append_emails_to_sent_folder and ctx.sent_to:
+ if ctx.email_account_doc.append_emails_to_sent_folder:
ctx.email_account_doc.append_email_to_sent_folder(message)
@staticmethod
@@ -224,25 +224,21 @@ class SendMailContext:
self.log_exception(exc_type, exc_val, exc_tb)
if exc_type in exceptions:
- email_status = "Partially Sent" if self.sent_to else "Not Sent"
- self.queue_doc.update_status(status=email_status, commit=True)
+ update_fields = {"status": "Partially Sent" if self.sent_to else "Not Sent"}
elif exc_type:
if self.queue_doc.retry < get_email_retry_limit():
update_fields = {"status": "Not Sent", "retry": self.queue_doc.retry + 1}
else:
- update_fields = {"status": (self.sent_to and "Partially Errored") or "Error"}
- self.queue_doc.update_status(**update_fields, commit=True)
+ update_fields = {"status": "Error"}
else:
- email_status = self.is_mail_sent_to_all() and "Sent"
- email_status = email_status or (self.sent_to and "Partially Sent") or "Not Sent"
-
update_fields = {
- "status": email_status,
+ "status": "Sent",
"email_account": self.email_account_doc.name
if self.email_account_doc.is_exists_in_db()
else None,
}
- self.queue_doc.update_status(**update_fields, commit=True)
+
+ self.queue_doc.update_status(**update_fields, commit=True)
def log_exception(self, exc_type, exc_val, exc_tb):
if exc_type:
@@ -262,9 +258,6 @@ class SendMailContext:
recipient.update_db(status="Sent", commit=True)
self.sent_to.append(recipient.recipient)
- def is_mail_sent_to_all(self):
- return sorted(self.sent_to) == sorted(rec.recipient for rec in self.queue_doc.recipients)
-
def get_message_object(self, message):
return Parser(policy=SMTPUTF8).parsestr(message)
@@ -379,7 +372,7 @@ def retry_sending(name):
doc = frappe.get_doc("Email Queue", name)
doc.check_permission()
- if doc and (doc.status == "Error" or doc.status == "Partially Errored"):
+ if doc and doc.status == "Error":
doc.status = "Not Sent"
for d in doc.recipients:
if d.status != "Sent":
From df7afa93b8c2e4609d78f8d64751fb336c34165f Mon Sep 17 00:00:00 2001
From: phot0n
Date: Wed, 7 Jun 2023 02:22:50 +0530
Subject: [PATCH 196/203] chore: log traceback directly to the queue doc
---
.../doctype/email_queue/email_queue.json | 3 +-
.../email/doctype/email_queue/email_queue.py | 32 ++++++++-----------
2 files changed, 15 insertions(+), 20 deletions(-)
diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json
index ce8c3b5e63..b1df78c564 100644
--- a/frappe/email/doctype/email_queue/email_queue.json
+++ b/frappe/email/doctype/email_queue/email_queue.json
@@ -61,6 +61,7 @@
"options": "Not Sent\nSending\nSent\nPartially Sent\nError\nExpired"
},
{
+ "depends_on": "eval:doc.error",
"fieldname": "error",
"fieldtype": "Code",
"label": "Error"
@@ -152,7 +153,7 @@
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2023-06-05 12:15:17.850292",
+ "modified": "2023-06-07 02:21:28.769620",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index d977f2097c..b465c32eea 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -140,7 +140,9 @@ class EmailQueue(Document):
method(self, self.sender, recipient.recipient, message)
else:
if not frappe.flags.in_test:
- ctx.smtp_session.sendmail(from_addr=self.sender, to_addrs=recipient.recipient, msg=message)
+ ctx.smtp_server.session.sendmail(
+ from_addr=self.sender, to_addrs=recipient.recipient, msg=message
+ )
ctx.add_to_sent_list(recipient)
if frappe.flags.in_test:
@@ -217,19 +219,24 @@ class SendMailContext:
smtplib.SMTPHeloError,
JobTimeoutException,
]
+ trace = "".join(traceback.format_tb(exc_tb)) if exc_tb else None
if not self.retain_smtp_session:
self.smtp_server.quit()
- self.log_exception(exc_type, exc_val, exc_tb)
-
if exc_type in exceptions:
- update_fields = {"status": "Partially Sent" if self.sent_to else "Not Sent"}
+ update_fields = {"status": "Partially Sent" if self.sent_to else "Not Sent", "error": trace}
elif exc_type:
+ update_fields = {"error": trace}
if self.queue_doc.retry < get_email_retry_limit():
- update_fields = {"status": "Not Sent", "retry": self.queue_doc.retry + 1}
+ update_fields.update(
+ {
+ "status": "Partially Sent" if self.sent_to else "Not Sent",
+ "retry": self.queue_doc.retry + 1,
+ }
+ )
else:
- update_fields = {"status": "Error"}
+ update_fields.update({"status": "Error"})
else:
update_fields = {
"status": "Sent",
@@ -240,19 +247,6 @@ class SendMailContext:
self.queue_doc.update_status(**update_fields, commit=True)
- def log_exception(self, exc_type, exc_val, exc_tb):
- if exc_type:
- traceback_string = "".join(traceback.format_tb(exc_tb))
- traceback_string += f"\n Queue Name: {self.queue_doc.name}"
-
- self.queue_doc.log_error("Email sending failed", traceback_string)
-
- @property
- def smtp_session(self):
- if frappe.flags.in_test:
- return
- return self.smtp_server.session
-
def add_to_sent_list(self, recipient):
# Update recipient status
recipient.update_db(status="Sent", commit=True)
From aac74726ff56e9d730239ff203e0199bc81e692f Mon Sep 17 00:00:00 2001
From: phot0n
Date: Wed, 7 Jun 2023 02:35:06 +0530
Subject: [PATCH 197/203] chore: add recipient to sent list regardless of email
sending method
---
frappe/email/doctype/email_queue/email_queue.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index b465c32eea..ab63c3c0d6 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -143,7 +143,8 @@ class EmailQueue(Document):
ctx.smtp_server.session.sendmail(
from_addr=self.sender, to_addrs=recipient.recipient, msg=message
)
- ctx.add_to_sent_list(recipient)
+
+ ctx.add_to_sent_list(recipient)
if frappe.flags.in_test:
frappe.flags.sent_mail = message
@@ -240,9 +241,6 @@ class SendMailContext:
else:
update_fields = {
"status": "Sent",
- "email_account": self.email_account_doc.name
- if self.email_account_doc.is_exists_in_db()
- else None,
}
self.queue_doc.update_status(**update_fields, commit=True)
From 4ceafe14e31febeb1265fd767c2ace57e024d0fe Mon Sep 17 00:00:00 2001
From: phot0n
Date: Thu, 8 Jun 2023 15:53:44 +0530
Subject: [PATCH 198/203] chore: remove unused is_background_task and
add_to_sent_list -> update_recipient_status_to_sent
* set status to be hidden in queue doc
* don't maintain a list of sent to recipeint, a boolean is enough to set the status to partially sent
---
.../doctype/email_queue/email_queue.json | 3 +-
.../email/doctype/email_queue/email_queue.py | 37 +++++++++----------
frappe/email/queue.py | 1 -
3 files changed, 20 insertions(+), 21 deletions(-)
diff --git a/frappe/email/doctype/email_queue/email_queue.json b/frappe/email/doctype/email_queue/email_queue.json
index b1df78c564..15934ee8e7 100644
--- a/frappe/email/doctype/email_queue/email_queue.json
+++ b/frappe/email/doctype/email_queue/email_queue.json
@@ -55,6 +55,7 @@
"default": "Not Sent",
"fieldname": "status",
"fieldtype": "Select",
+ "hidden": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
@@ -153,7 +154,7 @@
"idx": 1,
"in_create": 1,
"links": [],
- "modified": "2023-06-07 02:21:28.769620",
+ "modified": "2023-06-08 15:31:52.789186",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Queue",
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index ab63c3c0d6..06345f709e 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -123,20 +123,19 @@ class EmailQueue(Document):
return True
- def send(self, is_background_task: bool = False, smtp_server_instance: SMTPServer = None):
+ def send(self, smtp_server_instance: SMTPServer = None):
"""Send emails to recipients."""
if not self.can_send_now():
return
- with SendMailContext(self, is_background_task, smtp_server_instance) as ctx:
+ with SendMailContext(self, smtp_server_instance) as ctx:
message = None
for recipient in self.recipients:
- if not recipient.is_mail_to_be_sent():
+ if recipient.is_mail_sent():
continue
message = ctx.build_message(recipient.recipient)
- method = get_hook_method("override_email_send")
- if method:
+ if method := get_hook_method("override_email_send"):
method(self, self.sender, recipient.recipient, message)
else:
if not frappe.flags.in_test:
@@ -144,7 +143,7 @@ class EmailQueue(Document):
from_addr=self.sender, to_addrs=recipient.recipient, msg=message
)
- ctx.add_to_sent_list(recipient)
+ ctx.update_recipient_status_to_sent(recipient)
if frappe.flags.in_test:
frappe.flags.sent_mail = message
@@ -180,24 +179,22 @@ class EmailQueue(Document):
@task(queue="short")
-def send_mail(email_queue_name, is_background_task=False, smtp_server_instance: SMTPServer = None):
+def send_mail(email_queue_name, smtp_server_instance: SMTPServer = None):
"""This is equivalent to EmailQueue.send.
This provides a way to make sending mail as a background job.
"""
record = EmailQueue.find(email_queue_name)
- record.send(is_background_task=is_background_task, smtp_server_instance=smtp_server_instance)
+ record.send(smtp_server_instance=smtp_server_instance)
class SendMailContext:
def __init__(
self,
queue_doc: Document,
- is_background_task: bool = False,
smtp_server_instance: SMTPServer = None,
):
self.queue_doc: EmailQueue = queue_doc
- self.is_background_task = is_background_task
self.email_account_doc = queue_doc.get_email_account()
self.smtp_server = smtp_server_instance or self.email_account_doc.get_smtp_server()
@@ -206,7 +203,9 @@ class SendMailContext:
# Note: smtp session will have to be manually closed
self.retain_smtp_session = bool(smtp_server_instance)
- self.sent_to = [rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()]
+ self.sent_to_atleast_one_recipient = any(
+ rec.recipient for rec in self.queue_doc.recipients if rec.is_mail_sent()
+ )
def __enter__(self):
self.queue_doc.update_status(status="Sending", commit=True)
@@ -226,29 +225,29 @@ class SendMailContext:
self.smtp_server.quit()
if exc_type in exceptions:
- update_fields = {"status": "Partially Sent" if self.sent_to else "Not Sent", "error": trace}
+ update_fields = {
+ "status": "Partially Sent" if self.sent_to_atleast_one_recipient else "Not Sent",
+ "error": trace,
+ }
elif exc_type:
update_fields = {"error": trace}
if self.queue_doc.retry < get_email_retry_limit():
update_fields.update(
{
- "status": "Partially Sent" if self.sent_to else "Not Sent",
+ "status": "Partially Sent" if self.sent_to_atleast_one_recipient else "Not Sent",
"retry": self.queue_doc.retry + 1,
}
)
else:
update_fields.update({"status": "Error"})
else:
- update_fields = {
- "status": "Sent",
- }
+ update_fields = {"status": "Sent"}
self.queue_doc.update_status(**update_fields, commit=True)
- def add_to_sent_list(self, recipient):
- # Update recipient status
+ def update_recipient_status_to_sent(self, recipient):
+ self.sent_to_atleast_one_recipient = True
recipient.update_db(status="Sent", commit=True)
- self.sent_to.append(recipient.recipient)
def get_message_object(self, message):
return Parser(policy=SMTPUTF8).parsestr(message)
diff --git a/frappe/email/queue.py b/frappe/email/queue.py
index 7d4b92baf1..75bb46f00c 100755
--- a/frappe/email/queue.py
+++ b/frappe/email/queue.py
@@ -154,7 +154,6 @@ def flush(from_test=False):
frappe.enqueue(
method=send_mail,
email_queue_name=row.name,
- is_background_task=not from_test,
now=from_test,
job_name=job_name,
queue="short",
From 129fdc803a6d2d0c4121b598010a6028f312ab01 Mon Sep 17 00:00:00 2001
From: phot0n
Date: Thu, 8 Jun 2023 16:20:10 +0530
Subject: [PATCH 199/203] chore: fix linter
---
frappe/email/doctype/email_account/email_account.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 159e6d9583..3f6051ffc8 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -386,7 +386,7 @@ class EmailAccount(Document):
"from_site_config": {"default": True},
"no_smtp_authentication": {
"conf_names": ("disable_mail_smtp_authentication",),
- "default": 0,
+ "default": 0,
},
}
From 19d211f1d23a92f74b2a1bcacfdb97954c2b51d7 Mon Sep 17 00:00:00 2001
From: Devin Slauenwhite
Date: Thu, 8 Jun 2023 07:44:44 -0400
Subject: [PATCH 200/203] feat: rearranging standard fields in customize form
(#19822)
* feat: rearranging standing fields
* fix: fixed creation of property setter
* refactor: renamed setup_sortable
* fix: loading field_order property
* refactor: removed redundant db call
* fix: field_order not found
* test: Added tests for field order in customize form
* refactor: better naming
* refactor: simplified logic
* feat: Updating field order on custom field creation
* feat: Added support for custom fiels
* refactor: moving to meta
* refactor: changed property type to json
* fix: new standard field insert order.
* fix: don't modify insert_after of system generated custom fields.
# This is because system generated fields are to be treated as standard fields. If the user restores the form to default, this value will be used to reset the original position.
# The new position of form fields are stored in the field_order Property Setter.
* fix: treat system generated fields as standard fields when sorting.
* revert: check for is_system_generated
* Revert "fix: new standard field insert order."
This reverts commit 6cdbe42f28d5944165dd100a9bb9172463951fda.
* fix: prioritize field_order over insert_after.
# Use insert_after as fallback in event the field doesn't exist in field_order
* fix(test): delete existing custom field
* fix: order of standard fields without field_order property.
* Revert "Revert "fix: new standard field insert order.""
This reverts commit c830f1ba2fe1e602b09c11a897869d9992097c1e.
* test: field order of newly migrated standard fields.
* fix(test): clear test_standard_field from previous test run.
* fix: sort with insert_after for system generated fields.
* fix(test): reset standard field creation before re-run and after successful test.
* fix: insert_after position should be + 1
* chore: remove debug statement
* test: system generated customized fields
* chore: remove print
* chore: lint all
* fix: show quick link to Table MultiSelect DocTypes
* refactor: change backend implementation of `CustomizeForm` and `Meta`
* test: simplify tests
* fix: rename `idx` to `index` for clarity
* perf: define `existing_fields` conditionally
---------
Co-authored-by: Aradhya
Co-authored-by: Aradhya Tripathi <67282231+Aradhya-Tripathi@users.noreply.github.com>
Co-authored-by: Sagar Vora
---
.../doctype/customize_form/customize_form.js | 31 +++-----
.../doctype/customize_form/customize_form.py | 28 ++++++++
.../customize_form/test_customize_form.py | 12 ++++
frappe/model/meta.py | 71 +++++++++++++++----
4 files changed, 104 insertions(+), 38 deletions(-)
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 8549c239e5..aefa0e9a7e 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -49,14 +49,6 @@ frappe.ui.form.on("Customize Form", {
grid_row.row.addClass("highlight");
}
});
-
- $(frm.wrapper).on("grid-make-sortable", function (e, frm) {
- frm.trigger("setup_sortable");
- });
-
- $(frm.wrapper).on("grid-move-row", function (e, frm) {
- frm.trigger("setup_sortable");
- });
},
doc_type: function (frm) {
@@ -71,7 +63,7 @@ frappe.ui.form.on("Customize Form", {
frm.set_value("doc_type", "");
} else {
frm.refresh();
- frm.trigger("setup_sortable");
+ frm.trigger("add_customize_child_table_button");
frm.trigger("setup_default_views");
}
}
@@ -87,23 +79,16 @@ frappe.ui.form.on("Customize Form", {
frm.trigger("setup_default_views");
},
- setup_sortable: function (frm) {
+ add_customize_child_table_button: function (frm) {
frm.doc.fields.forEach(function (f) {
- if (!f.is_custom_field || f.is_system_generated) {
- f._sortable = false;
- }
+ if (!in_list(["Table", "Table MultiSelect"], f.fieldtype)) return;
- if (f.fieldtype == "Table") {
- frm.add_custom_button(
- f.options,
- function () {
- frm.set_value("doc_type", f.options);
- },
- __("Customize Child Table")
- );
- }
+ frm.add_custom_button(
+ f.options,
+ () => frm.set_value("doc_type", f.options),
+ __("Customize Child Table")
+ );
});
- frm.fields_dict.fields.grid.refresh();
},
refresh: function (frm) {
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 9aa61869d3..24a9559f97 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -214,11 +214,39 @@ class CustomizeForm(Document):
# action and links
self.set_property_setters_for_actions_and_links(meta)
+ def set_property_setter_for_field_order(self, meta):
+ new_order = [df.fieldname for df in self.fields]
+ existing_order = getattr(meta, "field_order", None)
+ default_order = [
+ fieldname for fieldname, df in meta._fields.items() if not getattr(df, "is_custom_field", False)
+ ]
+
+ if new_order == default_order:
+ if existing_order:
+ delete_property_setter(self.doc_type, "field_order")
+
+ return
+
+ if existing_order and new_order == json.loads(existing_order):
+ return
+
+ frappe.make_property_setter(
+ {
+ "doctype": self.doc_type,
+ "doctype_or_field": "DocType",
+ "property": "field_order",
+ "value": json.dumps(new_order),
+ },
+ is_system_generated=False,
+ )
+
def set_property_setters_for_doctype(self, meta):
for prop, prop_type in doctype_properties.items():
if self.get(prop) != meta.get(prop):
self.make_property_setter(prop, self.get(prop), prop_type)
+ self.set_property_setter_for_field_order(meta)
+
def set_property_setters_for_docfield(self, meta, df, meta_df):
for prop, prop_type in docfield_properties.items():
if prop != "idx" and (df.get(prop) or "") != (meta_df[0].get(prop) or ""):
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 8d98dc4149..149ef85e28 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -425,3 +425,15 @@ class TestCustomizeForm(FrappeTestCase):
self.assertEqual(
frappe.db.get_value("Property Setter", property_setter_filters, "value"), "Test Description"
)
+
+ def test_custom_field_order(self):
+ # shuffle fields
+ customize_form = self.get_customize_form(doctype="ToDo")
+ customize_form.fields.insert(0, customize_form.fields.pop())
+ customize_form.save_customization()
+
+ field_order_property = json.loads(
+ frappe.db.get_value("Property Setter", {"doc_type": "ToDo", "property": "field_order"}, "value")
+ )
+
+ self.assertEqual(field_order_property, [df.fieldname for df in frappe.get_meta("ToDo").fields])
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index e55f72d1ba..d97146c480 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -134,13 +134,10 @@ class Meta(Document):
self.init_field_caches()
return
- has_custom_fields = self.add_custom_fields()
+ self.add_custom_fields()
self.apply_property_setters()
self.init_field_caches()
-
- if has_custom_fields:
- self.sort_fields()
-
+ self.sort_fields()
self.get_valid_columns()
self.set_custom_permissions()
self.add_custom_links_and_actions()
@@ -361,7 +358,6 @@ class Meta(Document):
return
self.extend("fields", custom_fields)
- return True
def apply_property_setters(self):
"""
@@ -372,11 +368,11 @@ class Meta(Document):
if not frappe.db.table_exists("Property Setter"):
return
- property_setters = frappe.db.sql(
- """select * from `tabProperty Setter` where
- doc_type=%s""",
- (self.name,),
- as_dict=1,
+ property_setters = frappe.db.get_values(
+ "Property Setter",
+ filters={"doc_type": self.name},
+ fieldname="*",
+ as_dict=True,
)
if not property_setters:
@@ -452,14 +448,56 @@ class Meta(Document):
self._table_fields = self.get("fields", {"fieldtype": ["in", table_fields]})
def sort_fields(self):
- """Sort custom fields on the basis of insert_after"""
+ """
+ Sort fields on the basis of following rules (priority descending):
+ - `field_order` property setter
+ - `insert_after` computed based on default order for standard fields
+ - `insert_after` property for custom fields
+ """
- field_order = []
+ if field_order := getattr(self, "field_order", []):
+ field_order = [fieldname for fieldname in json.loads(field_order) if fieldname in self._fields]
+
+ # all fields match, best case scenario
+ if len(field_order) == len(self.fields):
+ self._update_fields_based_on_order(field_order)
+ return
+
+ # if the first few standard fields are not in the field order, prepare to prepend them
+ if self.fields[0].fieldname not in field_order:
+ fields_to_prepend = []
+ standard_field_found = False
+
+ for fieldname, field in self._fields.items():
+ if getattr(field, "is_custom_field", False):
+ # all custom fields from here on
+ break
+
+ if fieldname in field_order:
+ standard_field_found = True
+ break
+
+ fields_to_prepend.append(fieldname)
+
+ if standard_field_found:
+ field_order = fields_to_prepend + field_order
+ else:
+ # worst case scenario, invalidate field_order
+ field_order = fields_to_prepend
+
+ existing_fields = set(field_order) if field_order else False
insert_after_map = {}
- for field in self.fields:
+ for index, field in enumerate(self.fields):
+ if existing_fields and field.fieldname in existing_fields:
+ continue
+
if not getattr(field, "is_custom_field", False):
- field_order.append(field.fieldname)
+ if existing_fields:
+ # compute insert_after from previous field
+ insert_after_map.setdefault(self.fields[index - 1].fieldname, []).append(field.fieldname)
+ else:
+ field_order.append(field.fieldname)
elif insert_after := getattr(field, "insert_after", None):
insert_after_map.setdefault(insert_after, []).append(field.fieldname)
@@ -471,6 +509,9 @@ class Meta(Document):
if insert_after_map:
_update_field_order_based_on_insert_after(field_order, insert_after_map)
+ self._update_fields_based_on_order(field_order)
+
+ def _update_fields_based_on_order(self, field_order):
sorted_fields = []
for idx, fieldname in enumerate(field_order, 1):
From 924ecc71f64e63f9e1d5044ef5016fafb730d4c2 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 8 Jun 2023 17:52:21 +0530
Subject: [PATCH 201/203] chore: vendor cgitb (#21288)
To Avoid:
```
/home/ankush/benches/develop/apps/frappe/frappe/utils/error.py:4: DeprecationWarning: 'cgitb' is deprecated and slated for removal in Python 3.13
import cgitb
```
---
frappe/utils/error.py | 58 ++++++++++++++++++++++++++++++++++++++++---
1 file changed, 55 insertions(+), 3 deletions(-)
diff --git a/frappe/utils/error.py b/frappe/utils/error.py
index 2c450750e1..47c5b055a8 100644
--- a/frappe/utils/error.py
+++ b/frappe/utils/error.py
@@ -1,7 +1,6 @@
# Copyright (c) 2015, Maxwell Morais and contributors
# License: MIT. See LICENSE
-import cgitb
import datetime
import functools
import inspect
@@ -103,7 +102,7 @@ def get_snapshot(exception, context=10):
finally:
lnum[0] += 1
- vars = cgitb.scanvars(reader, frame, locals)
+ vars = _scanvars(reader, frame, locals)
# if it is a view, replace with generated code
# if file.endswith('html'):
@@ -123,7 +122,7 @@ def get_snapshot(exception, context=10):
for name, where, value in vars:
if name in f["dump"]:
continue
- if value is not cgitb.__UNDEF__:
+ if value is not __UNDEF__:
if where == "global":
name = f"global {name:s}"
elif where != "local":
@@ -257,3 +256,56 @@ def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None):
return wrapper_raise_error_on_no_output
return decorator_raise_error_on_no_output
+
+
+# Vendored from cgitb standard library reused under PSF License:
+# https://github.com/python/cpython/blob/main/LICENSE
+
+
+import keyword
+import tokenize
+
+__UNDEF__ = [] # a special sentinel object
+
+
+def _scanvars(reader, frame, locals):
+ """Scan one logical line of Python and look up values of variables used."""
+ vars, lasttoken, parent, prefix, value = [], None, None, "", __UNDEF__
+ for ttype, token, start, end, line in tokenize.generate_tokens(reader):
+ if ttype == tokenize.NEWLINE:
+ break
+ if ttype == tokenize.NAME and token not in keyword.kwlist:
+ if lasttoken == ".":
+ if parent is not __UNDEF__:
+ value = getattr(parent, token, __UNDEF__)
+ vars.append((prefix + token, prefix, value))
+ else:
+ where, value = _lookup(token, frame, locals)
+ vars.append((token, where, value))
+ elif token == ".":
+ prefix += lasttoken + "."
+ parent = value
+ else:
+ parent, prefix = None, ""
+ lasttoken = token
+ return vars
+
+
+def _lookup(name, frame, locals):
+ """Find the value for a given name in the given environment."""
+ if name in locals:
+ return "local", locals[name]
+ if name in frame.f_globals:
+ return "global", frame.f_globals[name]
+ if "__builtins__" in frame.f_globals:
+ builtins = frame.f_globals["__builtins__"]
+ if type(builtins) is type({}): # noqa
+ if name in builtins:
+ return "builtin", builtins[name]
+ else:
+ if hasattr(builtins, name):
+ return "builtin", getattr(builtins, name)
+ return None, __UNDEF__
+
+
+# end: vendored code
From e98519211e7a2de57dca8e72148daf9155ff57ea Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 8 Jun 2023 18:07:12 +0530
Subject: [PATCH 202/203] feat: let people reset layout back to original
(#21290)
---
.../doctype/customize_form/customize_form.js | 30 +++++++++++++++++++
.../doctype/customize_form/customize_form.py | 20 ++++++++++++-
2 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index aefa0e9a7e..3937079365 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -126,6 +126,14 @@ frappe.ui.form.on("Customize Form", {
__("Actions")
);
+ frm.add_custom_button(
+ __("Reset Layout"),
+ () => {
+ frm.trigger("reset_layout");
+ },
+ __("Actions")
+ );
+
frm.add_custom_button(
__("Set Permissions"),
function () {
@@ -164,6 +172,28 @@ frappe.ui.form.on("Customize Form", {
}
},
+ reset_layout(frm) {
+ frappe.confirm(
+ __("Layout will be reset to standard layout, are you sure you want to do this?"),
+ null,
+ () => {
+ return frm.call({
+ doc: frm.doc,
+ method: "reset_to_defaults",
+ callback: function (r) {
+ if (!r.exc) {
+ frappe.show_alert({
+ message: __("Layout Reset"),
+ indicator: "green",
+ });
+ frappe.customize_form.clear_locals_and_refresh(frm);
+ }
+ },
+ });
+ }
+ );
+ },
+
setup_export(frm) {
if (frappe.boot.developer_mode) {
frm.add_custom_button(
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 24a9559f97..f403079cd8 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -35,7 +35,7 @@ class CustomizeForm(Document):
if not self.doc_type:
return
- meta = frappe.get_meta(self.doc_type)
+ meta = frappe.get_meta(self.doc_type, cached=False)
self.validate_doctype(meta)
@@ -568,6 +568,24 @@ class CustomizeForm(Document):
reset_customization(self.doc_type)
self.fetch_to_customize()
+ @frappe.whitelist()
+ def reset_layout(self):
+ if not self.doc_type:
+ return
+
+ property_setter = frappe.db.get_value(
+ "Property Setter",
+ filters={
+ "doc_type": self.doc_type,
+ "property": "field_order",
+ "is_system_generated": False,
+ },
+ )
+ if property_setter:
+ frappe.delete_doc("Property Setter", property_setter)
+
+ self.fetch_to_customize()
+
@classmethod
def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool:
"""allow type change, if both old_type and new_type are in same field group.
From e1764d5a4b2b84420130e60b2e6d7bdbed2d29f6 Mon Sep 17 00:00:00 2001
From: Sagar Vora
Date: Thu, 8 Jun 2023 19:43:29 +0530
Subject: [PATCH 203/203] feat: log `pid` and `user` in request logs (#21267)
---
frappe/app.py | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frappe/app.py b/frappe/app.py
index 55855efaf9..ddde313ace 100644
--- a/frappe/app.py
+++ b/frappe/app.py
@@ -157,6 +157,8 @@ def log_request(request, response):
{
"site": get_site_name(request.host),
"remote_addr": getattr(request, "remote_addr", "NOTFOUND"),
+ "pid": os.getpid(),
+ "user": getattr(frappe.local.session, "user", "NOTFOUND"),
"base_url": getattr(request, "base_url", "NOTFOUND"),
"full_path": getattr(request, "full_path", "NOTFOUND"),
"method": getattr(request, "method", "NOTFOUND"),