From 1952920add38964450ae12121e3f82f628780be8 Mon Sep 17 00:00:00 2001
From: hrwx
Date: Tue, 15 Jun 2021 22:53:24 +0530
Subject: [PATCH 001/246] feat: Convert datetime field values to system
timezone
Co-authored-by: Sahil Khan
---
frappe/boot.py | 12 +++-
frappe/core/doctype/user/user.js | 7 ++-
frappe/core/doctype/user/user.py | 8 +++
.../js/frappe/form/controls/datetime.js | 44 ++++++++++++-
frappe/public/js/frappe/utils/datetime.js | 61 +++++++++++--------
5 files changed, 101 insertions(+), 31 deletions(-)
diff --git a/frappe/boot.py b/frappe/boot.py
index 0589e32ac8..feace1a66d 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -17,6 +17,7 @@ from frappe.social.doctype.energy_point_log.energy_point_log import get_energy_p
from frappe.model.base_document import get_controller
from frappe.social.doctype.post.post import frequently_visited_links
from frappe.core.doctype.navbar_settings.navbar_settings import get_navbar_settings, get_app_logo
+from frappe.utils import get_time_zone
def get_bootinfo():
"""build and return boot info"""
@@ -58,6 +59,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()
+ set_time_zone(bootinfo)
# ipinfo
if frappe.session.data.get('ipinfo'):
@@ -220,8 +222,8 @@ def load_translations(bootinfo):
bootinfo["__messages"] = messages
def get_user_info():
- user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image',
- 'gender', 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type'],
+ user_info = frappe.db.get_all('User', fields=['`name`', 'full_name as fullname', 'user_image as image', 'gender',
+ 'email', 'username', 'bio', 'location', 'interest', 'banner_image', 'allowed_in_mentions', 'user_type', 'time_zone'],
filters=dict(enabled=1))
user_info_map = {d.name: d for d in user_info}
@@ -324,3 +326,9 @@ def get_desk_settings():
def get_notification_settings():
return frappe.get_cached_doc('Notification Settings', frappe.session.user)
+
+def set_time_zone(bootinfo):
+ bootinfo.time_zone = {
+ "system_time_zone": get_time_zone(),
+ "user_time_zone": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
+ }
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 8c5b89c5fc..7c9e00d6bc 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -77,7 +77,12 @@ frappe.ui.form.on('User', {
}
},
refresh: function(frm) {
- var doc = frm.doc;
+ let doc = frm.doc;
+
+ if (frm.is_new()) {
+ frm.set_value("time_zone", frappe.sys_defaults.time_zone);
+ }
+
if (in_list(['System User', 'Website User'], frm.doc.user_type)
&& !frm.is_new() && !frm.roles_editor && frm.can_edit_roles) {
frm.reload_doc();
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 13063b8fd2..93963c6015 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -80,6 +80,7 @@ class User(Document):
self.validate_roles()
self.validate_allowed_modules()
self.validate_user_image()
+ self.set_time_zone()
if self.language == "Loading...":
self.language = None
@@ -594,6 +595,13 @@ class User(Document):
return user
+ def set_time_zone(self):
+ from frappe.utils import get_time_zone
+
+ if not self.time_zone:
+ frappe.msgprint(_("User Time Zone was not set, defaulting to System Time Zone."), title=_("User Time Zone"))
+ self.time_zone = get_time_zone()
+
@frappe.whitelist()
def get_timezones():
import pytz
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index 341a933066..c634aac293 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -1,4 +1,16 @@
frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.ControlDate {
+ set_formatted_input(value) {
+ if (this.timepicker_only) return;
+ if (!this.datepicker) return;
+ if (!value) {
+ this.datepicker.clear();
+ return;
+ } else if (value === "Today") {
+ value = this.get_now_date();
+ }
+
+ this.$input && this.$input.val(this.format_for_input(value));
+ }
set_date_options() {
super.set_date_options();
this.today_text = __("Now");
@@ -14,10 +26,36 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
get_now_date() {
return frappe.datetime.now_datetime(true);
}
+ parse(value) {
+ if (value) {
+ value = frappe.datetime.user_to_str(value, false);
+
+ if (!frappe.datetime.is_timezone_same()) {
+ value = frappe.datetime.convert_to_system_tz(value, true);
+ }
+
+ return value;
+ }
+ }
+ format_for_input(value) {
+ if (!value) return "";
+
+ let m = frappe.datetime.is_timezone_same();
+ if (!frappe.datetime.is_timezone_same()) {
+ m = frappe.datetime.convert_to_user_tz(value, true)
+ value = frappe.datetime.convert_to_user_tz(value, true);
+ }
+
+ return frappe.datetime.str_to_user(value, false);
+ }
set_description() {
- const { description } = this.df;
- const { time_zone } = frappe.sys_defaults;
- if (!this.df.hide_timezone && !frappe.datetime.is_timezone_same()) {
+ const description = this.df.description;
+ const time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user_time_zone : frappe.sys_defaults.time_zone;
+
+ if (!this.df.hide_timezone) {
+ // Always show the timezone when rendering the Datetime field since the datetime value will
+ // always be in system_time_zone rather then local time.
+
if (!description) {
this.df.description = time_zone;
} else if (!description.includes(time_zone)) {
diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js
index 0c6fea2986..3d00762c94 100644
--- a/frappe/public/js/frappe/utils/datetime.js
+++ b/frappe/public/js/frappe/utils/datetime.js
@@ -13,33 +13,44 @@ frappe.provide("frappe.datetime");
$.extend(frappe.datetime, {
convert_to_user_tz: function(date, format) {
// format defaults to true
- if(frappe.sys_defaults.time_zone) {
- var date_obj = moment.tz(date, frappe.sys_defaults.time_zone).local();
+ // Converts the datetime string to system time zone first since the database only stores datetime in
+ // system time zone and then convert the string to user time zone(from User doctype).
+ let date_obj = null;
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
+ date_obj = moment.tz(date, frappe.boot.time_zone.system_time_zone)
+ .clone()
+ .tz(frappe.boot.time_zone.user_time_zone);
} else {
- var date_obj = moment(date);
+ date_obj = moment(date);
}
- return (format===false) ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
+ return format===false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
},
convert_to_system_tz: function(date, format) {
// format defaults to true
-
- if(frappe.sys_defaults.time_zone) {
- var date_obj = moment(date).tz(frappe.sys_defaults.time_zone);
+ // Converts the datetime string to user time zone (from User doctype) first since this fn is called in datetime which accepts datetime
+ // in user time zone then convert the string to user time zone.
+ // This is done so that only one timezone is present in database and we do not end up storing local timezone since it changes
+ // as per the location of user.
+ let date_obj = null;
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
+ date_obj = moment.tz(date, frappe.boot.time_zone.user_time_zone)
+ .clone()
+ .tz(frappe.boot.time_zone.system_time_zone);
} else {
- var date_obj = moment(date);
+ date_obj = moment(date);
}
- return (format===false) ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
+ return format===false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
},
is_timezone_same: function() {
- if(frappe.sys_defaults.time_zone) {
- return moment().tz(frappe.sys_defaults.time_zone).utcOffset() === moment().utcOffset();
- } else {
- return true;
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
+ return moment().tz(frappe.boot.time_zone.system_time_zone).utcOffset() === moment().tz(frappe.boot.time_zone.user_time_zone).utcOffset();
}
+
+ return true;
},
str_to_obj: function(d) {
@@ -186,18 +197,18 @@ $.extend(frappe.datetime, {
},
_date: function(format, as_obj = false) {
- const time_zone = frappe.sys_defaults && frappe.sys_defaults.time_zone;
- let date;
- if (time_zone) {
- date = moment.tz(time_zone);
- } else {
- date = moment();
- }
- if (as_obj) {
- return frappe.datetime.moment_to_date_obj(date);
- } else {
- return date.format(format);
- }
+ /**
+ * Whenever we are getting now_date/datetime, always make sure dates are fetched using usertime zone.
+ * This is to make sure that time is as per user time zone set in User doctype, If a user had to change the timezone,
+ * we will end up having multiple timezone by not honouring timezone in User doctype.
+ * This will make sure that at any point we know which timezone the user if following and not have random timezone
+ * when the timezone of the local machine changes.
+ */
+ let time_zone = frappe.boot.time_zone.user_time_zone || frappe.boot.time_zone.system_time_zone;
+ if (!time_zone) time_zone = frappe.sys_defaults.time_zone;
+
+ let date = moment.tz(time_zone);
+ return as_obj ? frappe.datetime.moment_to_date_obj(date) : date.format(format);
},
moment_to_date_obj: function(moment) {
From 02640b791f24a9d089e8a65852bcf2ca0f12bf9e Mon Sep 17 00:00:00 2001
From: hrwx
Date: Tue, 15 Jun 2021 23:31:30 +0530
Subject: [PATCH 002/246] fix: check if frappe.sys_defaults.time_zone exists
---
.../core/doctype/system_settings/system_settings.js | 6 ++++++
frappe/core/doctype/user/user.js | 6 ++++++
frappe/core/doctype/user/user.py | 1 -
frappe/public/js/frappe/form/controls/datetime.js | 11 ++++++-----
frappe/public/js/frappe/form/controls/time.js | 2 +-
frappe/public/js/frappe/form/formatters.js | 8 ++------
frappe/public/js/frappe/utils/datetime.js | 7 +++----
7 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index c0c9074cbc..aefe3786bd 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -32,5 +32,11 @@ frappe.ui.form.on("System Settings", {
frm.set_value('prepared_report_expiry_period', 7);
}
}
+ },
+ after_save: function(frm) {
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone !== frm.doc.time_zone) {
+ // Clear cache after saving to refresh the values of time_zone
+ frappe.ui.toolbar.clear_cache();
+ }
}
});
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 7c9e00d6bc..819684cdfe 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -271,6 +271,12 @@ frappe.ui.form.on('User', {
}
}
});
+ },
+ after_save: function(frm) {
+ if (frappe.boot.time_zone && frappe.boot.time_zone.user_time_zone !== frm.doc.time_zone) {
+ // Clear cache after saving to refresh the values of time_zone
+ frappe.ui.toolbar.clear_cache();
+ }
}
});
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 93963c6015..cf2b045c6d 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -599,7 +599,6 @@ class User(Document):
from frappe.utils import get_time_zone
if not self.time_zone:
- frappe.msgprint(_("User Time Zone was not set, defaulting to System Time Zone."), title=_("User Time Zone"))
self.time_zone = get_time_zone()
@frappe.whitelist()
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index c634aac293..a8b19c4ece 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -30,7 +30,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
if (value) {
value = frappe.datetime.user_to_str(value, false);
- if (!frappe.datetime.is_timezone_same()) {
+ if (!frappe.datetime.is_system_time_zone()) {
value = frappe.datetime.convert_to_system_tz(value, true);
}
@@ -40,9 +40,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
format_for_input(value) {
if (!value) return "";
- let m = frappe.datetime.is_timezone_same();
- if (!frappe.datetime.is_timezone_same()) {
- m = frappe.datetime.convert_to_user_tz(value, true)
+ if (!frappe.datetime.is_system_time_zone()) {
value = frappe.datetime.convert_to_user_tz(value, true);
}
@@ -50,7 +48,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
}
set_description() {
const description = this.df.description;
- const time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user_time_zone : frappe.sys_defaults.time_zone;
+ const time_zone = this.get_user_time_zone();
if (!this.df.hide_timezone) {
// Always show the timezone when rendering the Datetime field since the datetime value will
@@ -64,6 +62,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
}
super.set_description();
}
+ get_user_time_zone() {
+ return frappe.boot.time_zone ? frappe.boot.time_zone.user_time_zone : frappe.sys_defaults.time_zone;
+ }
set_datepicker() {
super.set_datepicker();
if (this.datepicker.opts.timeFormat.indexOf('s') == -1) {
diff --git a/frappe/public/js/frappe/form/controls/time.js b/frappe/public/js/frappe/form/controls/time.js
index a7b6645681..f7fcc4c618 100644
--- a/frappe/public/js/frappe/form/controls/time.js
+++ b/frappe/public/js/frappe/form/controls/time.js
@@ -71,7 +71,7 @@ frappe.ui.form.ControlTime = class ControlTime extends frappe.ui.form.ControlDat
set_description() {
const { description } = this.df;
const { time_zone } = frappe.sys_defaults;
- if (!frappe.datetime.is_timezone_same()) {
+ if (!frappe.datetime.is_system_time_zone()) {
if (!description) {
this.df.description = time_zone;
} else if (!description.includes(time_zone)) {
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index b9a838688d..caecd65336 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -167,12 +167,8 @@ frappe.form.formatters = {
},
Datetime: function(value) {
if(value) {
- var m = moment(frappe.datetime.convert_to_user_tz(value));
- if(frappe.boot.sysdefaults.time_zone) {
- m = m.tz(frappe.boot.sysdefaults.time_zone);
- }
- return m.format(frappe.boot.sysdefaults.date_format.toUpperCase()
- + ' ' + frappe.boot.sysdefaults.time_format);
+ return moment(frappe.datetime.convert_to_user_tz(value))
+ .format(frappe.boot.sysdefaults.date_format.toUpperCase() + ' ' + frappe.boot.sysdefaults.time_format);
} else {
return "";
}
diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js
index 3d00762c94..ae7308cf7b 100644
--- a/frappe/public/js/frappe/utils/datetime.js
+++ b/frappe/public/js/frappe/utils/datetime.js
@@ -45,7 +45,7 @@ $.extend(frappe.datetime, {
return format===false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
},
- is_timezone_same: function() {
+ is_system_time_zone: function() {
if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
return moment().tz(frappe.boot.time_zone.system_time_zone).utcOffset() === moment().tz(frappe.boot.time_zone.user_time_zone).utcOffset();
}
@@ -204,10 +204,9 @@ $.extend(frappe.datetime, {
* This will make sure that at any point we know which timezone the user if following and not have random timezone
* when the timezone of the local machine changes.
*/
- let time_zone = frappe.boot.time_zone.user_time_zone || frappe.boot.time_zone.system_time_zone;
- if (!time_zone) time_zone = frappe.sys_defaults.time_zone;
-
+ let time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user_time_zone || frappe.boot.time_zone.system_time_zone : frappe.sys_defaults.time_zone;
let date = moment.tz(time_zone);
+
return as_obj ? frappe.datetime.moment_to_date_obj(date) : date.format(format);
},
From 8a8fbad0f3232c113043b9a86b0a6571a77f88d1 Mon Sep 17 00:00:00 2001
From: leela
Date: Wed, 25 Aug 2021 09:16:11 +0530
Subject: [PATCH 003/246] refactor: getting submitted linked reference docs
---
frappe/desk/form/linked_with.py | 344 +++++++++++++++++++++++++------
frappe/tests/test_linked_with.py | 113 ++++++++++
2 files changed, 399 insertions(+), 58 deletions(-)
create mode 100644 frappe/tests/test_linked_with.py
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index ae48b7fc6b..0b5fe58f93 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -2,6 +2,9 @@
# MIT License. See license.txt
import json
from collections import defaultdict
+from os import link
+import itertools
+from typing import List
import frappe
import frappe.desk.form.load
@@ -12,69 +15,296 @@ from frappe.modules import load_doctype_module
@frappe.whitelist()
-def get_submitted_linked_docs(doctype, name, docs=None, visited=None):
+def get_submitted_linked_docs(doctype: str, name: str) -> List[tuple]:
+ """ Get all the nested submitted documents those are present in referencing tables (dependent tables).
+
+ :param doctype: Document type
+ :param name: Name of the document
+
+ Usecase:
+ * User should be able to cancel the linked documents along with the one user trying to cancel.
+
+ Case1: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to sd2-n2 and sd2-n2 is linked to sd3-n3,
+ Getting submittable linked docs of `sd1-n1`should give both sd2-n2 and sd3-n3.
+ Case2: If document sd1-n1 (document name n1 from sumittable doctype sd1) is linked to d2-n2 and d2-n2 is linked to sd3-n3,
+ Getting submittable linked docs of `sd1-n1`should give None. (because d2-n2 is not a submittable doctype)
+ Case3: If document sd1-n1 (document name n1 from submittable doctype sd1) is linked to d2-n2 & sd2-n2. d2-n2 is linked to sd3-n3.
+ Getting submittable linked docs of `sd1-n1`should give sd2-n2.
+
+ Logic:
+ -----
+ 1. We can find linked documents only if we know how the doctypes are related.
+ 2. As we need only submittable documents, we can limit doctype relations search to submittable doctypes by
+ finding the relationships(Foreign key references) across submittable doctypes.
+ 3. Searching for links is going to be a tree like structure where at every level,
+ you will be finding documents using parent document and parent document links.
"""
- Get all nested submitted linked doctype linkinfo
+ tree = SubmittableDocumentTree(doctype, name)
+ visited_documents = tree.get_all_children()
+ docs = []
- Arguments:
- doctype (str) - The doctype for which get all linked doctypes
- name (str) - The docname for which get all linked doctypes
+ for dt, names in visited_documents.items():
+ docs.extend([{'doctype': dt, 'name': name, 'docstatus': 1} for name in names])
- Keyword Arguments:
- docs (list of dict) - (Optional) Get list of dictionary for linked doctype.
-
- Returns:
- dict - Return list of documents and link count
- """
-
- if not docs:
- docs = []
-
- if not visited:
- visited = {}
-
- if doctype not in visited:
- visited[doctype] = []
-
- if name in visited[doctype]:
- return
-
- linkinfo = get_linked_doctypes(doctype)
- linked_docs = get_linked_docs(doctype, name, linkinfo)
-
- link_count = 0
- visited[doctype].append(name)
-
- for link_doctype, link_names in linked_docs.items():
-
- for link in link_names:
- if link['name'] == name:
- continue
-
- docinfo = link.update({"doctype": link_doctype})
- validated_doc = validate_linked_doc(docinfo)
-
- if not validated_doc:
- continue
-
- link_count += 1
-
- links = get_submitted_linked_docs(link_doctype, link.name, docs, visited)
- if links:
- docs.append({
- "doctype": link_doctype,
- "name": link.name,
- "docstatus": link.docstatus,
- "link_count": links.get("count")
- })
-
- # sort linked documents by ascending number of links
- docs.sort(key=lambda doc: doc.get("link_count"))
return {
"docs": docs,
- "count": link_count
+ "count": len(docs)
}
+class SubmittableDocumentTree:
+ def __init__(self, doctype: str, name: str):
+ """Construct a tree for the submitable linked documents.
+
+ * Node has properties like doctype and docnames. Represented as Node(doctype, docnames).
+ * Nodes are linked by doctype relationships like table, link and dynamic links.
+ * Node is referenced(linked) by many other documents and those are the child nodes.
+
+ NOTE: child document is a property of child node (not same as Frappe child docs of a table field).
+ """
+ self.root_doctype = doctype
+ self.root_docname = name
+
+ # Documents those are yet to be visited for linked documents.
+ self.to_be_visited_documents = {doctype: [name]}
+ self.visited_documents = defaultdict(list)
+
+ self._submittable_doctypes = None # All submittable doctypes in the system
+ self._references_across_doctypes = None # doctype wise links/references
+
+ def get_all_children(self):
+ """Get all nodes of a tree except the root node (all the nested submitted
+ documents those are present in referencing tables (dependent tables).
+ """
+ while self.to_be_visited_documents:
+ next_level_children = defaultdict(list)
+ for parent_dt in list(self.to_be_visited_documents):
+ parent_docs = self.to_be_visited_documents.get(parent_dt)
+ if not parent_docs:
+ del self.to_be_visited_documents[parent_dt]
+ continue
+
+ child_docs = self.get_next_level_children(parent_dt, parent_docs)
+ self.visited_documents[parent_dt].extend(parent_docs)
+ for linked_dt, linked_names in child_docs.items():
+ not_visited_child_docs = set(linked_names) - set(self.visited_documents.get(linked_dt, []))
+ next_level_children[linked_dt].extend(not_visited_child_docs)
+
+ self.to_be_visited_documents = next_level_children
+
+ # Remove root node from visited documents
+ if self.root_docname in self.visited_documents.get(self.root_doctype, []):
+ self.visited_documents[self.root_doctype].remove(self.root_docname)
+
+ return self.visited_documents
+
+ def get_next_level_children(self, parent_dt, parent_names):
+ """Get immediate children of a Node(parent_dt, parent_names)
+ """
+ referencing_fields = self.get_doctype_references(parent_dt)
+
+ child_docs = defaultdict(list)
+ for field in referencing_fields:
+ links = get_referencing_documents(parent_dt, parent_names.copy(), field, get_parent_if_child_table_doc=True,
+ parent_filters=[('docstatus', '=', 1)], allowed_parents=self.get_link_sources()) or {}
+ for dt, names in links.items():
+ child_docs[dt].extend(names)
+ return child_docs
+
+ def get_doctype_references(self, doctype):
+ """Get references for a given document.
+ """
+ if self._references_across_doctypes is None:
+ get_links_to = self.get_document_sources()
+ limit_link_doctypes = self.get_link_sources()
+ self._references_across_doctypes = get_references_across_doctypes(
+ get_links_to, limit_link_doctypes)
+ return self._references_across_doctypes.get(doctype, [])
+
+ def get_document_sources(self):
+ """Returns list of doctypes from where we access submittable documents.
+ """
+ return list(set(self.get_link_sources() + [self.root_doctype]))
+
+ def get_link_sources(self):
+ """limit doctype links to these doctypes.
+ """
+ return list(set(self.get_submittable_doctypes()) - set(get_exempted_doctypes() or []))
+
+ def get_submittable_doctypes(self) -> List[str]:
+ """Returns list of submittable doctypes.
+ """
+ if not self._submittable_doctypes:
+ self._submittable_doctypes = frappe.db.get_list('DocType', {'is_submittable': 1}, pluck='name')
+ return self._submittable_doctypes
+
+
+def get_child_tables_of_doctypes(doctypes: List[str]=None):
+ """Returns child tables by doctype.
+ """
+ filters=[['fieldtype','=', 'Table']]
+ filters_for_docfield = filters
+ filters_for_customfield = filters
+
+ if doctypes:
+ filters_for_docfield = filters + [['parent', 'in', tuple(doctypes)]]
+ filters_for_customfield = filters + [['dt', 'in', tuple(doctypes)]]
+
+ links = frappe.get_all("DocField",
+ fields=["parent", "fieldname", "options as child_table"],
+ filters=filters_for_docfield,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as parent", "fieldname", "options as child_table"],
+ filters=filters_for_customfield,
+ as_list=1)
+
+ child_tables_by_doctype = defaultdict(list)
+ for doctype, fieldname, child_table in links:
+ child_tables_by_doctype[doctype].append(
+ {'doctype': doctype, 'fieldname': fieldname, 'child_table': child_table})
+ return child_tables_by_doctype
+
+
+def get_references_across_doctypes(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None) -> List:
+ """Find doctype wise foreign key references.
+
+ :param to_doctypes: Get links of these doctypes.
+ :param limit_link_doctypes: limit links to these doctypes.
+
+ * Include child table, link and dynamic link references.
+ """
+ if limit_link_doctypes:
+ child_tables_by_doctype = get_child_tables_of_doctypes(limit_link_doctypes)
+ all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
+ limit_link_doctypes = limit_link_doctypes + all_child_tables
+ else:
+ child_tables_by_doctype = get_child_tables_of_doctypes()
+ all_child_tables = [each['child_table'] for each in itertools.chain(*child_tables_by_doctype.values())]
+
+ references_by_link_fields = get_references_across_doctypes_by_link_field(to_doctypes, limit_link_doctypes)
+ references_by_dlink_fields = get_references_across_doctypes_by_dynamic_link_field(to_doctypes, limit_link_doctypes)
+
+ references = references_by_link_fields.copy()
+ for k, v in references_by_dlink_fields.items():
+ references.setdefault(k, []).extend(v)
+
+ for doctype, links in references.items():
+ for link in links:
+ link['is_child'] = (link['doctype'] in all_child_tables)
+ return references
+
+
+def get_references_across_doctypes_by_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
+ """Find doctype wise foreign key references based on link fields.
+
+ :param to_doctypes: Get links to these doctypes.
+ :param limit_link_doctypes: limit links to these doctypes.
+ """
+ filters=[['fieldtype','=', 'Link']]
+
+ if to_doctypes:
+ filters += [['options', 'in', tuple(to_doctypes)]]
+
+ filters_for_docfield = filters[:]
+ filters_for_customfield = filters[:]
+
+ if limit_link_doctypes:
+ filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
+ filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
+
+ links = frappe.get_all("DocField",
+ fields=["parent", "fieldname", "options as linked_to"],
+ filters=filters_for_docfield,
+ as_list=1)
+
+ links+= frappe.get_all("Custom Field",
+ fields=["dt as parent", "fieldname", "options as linked_to"],
+ filters=filters_for_customfield,
+ as_list=1)
+
+ links_by_doctype = defaultdict(list)
+ for doctype, fieldname, linked_to in links:
+ links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname})
+ return links_by_doctype
+
+
+def get_references_across_doctypes_by_dynamic_link_field(to_doctypes: List[str]=None, limit_link_doctypes: List[str]=None):
+ """Find doctype wise foreign key references based on dynamic link fields.
+
+ :param to_doctypes: Get links to these doctypes.
+ :param limit_link_doctypes: limit links to these doctypes.
+ """
+
+ filters=[['fieldtype','=', 'Dynamic Link']]
+
+ filters_for_docfield = filters[:]
+ filters_for_customfield = filters[:]
+
+ if limit_link_doctypes:
+ filters_for_docfield += [['parent', 'in', tuple(limit_link_doctypes)]]
+ filters_for_customfield += [['dt', 'in', tuple(limit_link_doctypes)]]
+
+ # find dynamic links of parents
+ links = frappe.get_all("DocField",
+ fields=["parent as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters_for_docfield,
+ as_list=1)
+
+ links += frappe.get_all("Custom Field",
+ fields=["dt as doctype", "fieldname", "options as doctype_fieldname"],
+ filters=filters_for_customfield,
+ as_list=1)
+
+ links_by_doctype = defaultdict(list)
+ for doctype, fieldname, doctype_fieldname in links:
+ try:
+ filters = [[doctype_fieldname, 'in', to_doctypes]] if to_doctypes else []
+ for linked_to in frappe.db.get_all(doctype, pluck=doctype_fieldname, filters = filters, distinct=1):
+ if linked_to:
+ links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname, 'doctype_fieldname': doctype_fieldname})
+ except frappe.db.ProgrammingError as e:
+ # TODO: FIXME
+ continue
+ return links_by_doctype
+
+def get_referencing_documents(reference_doctype: str, reference_names: List[str],
+ link_info: dict, get_parent_if_child_table_doc: bool=True,
+ parent_filters: List[list]=None, child_filters=None, allowed_parents=None):
+ """Get linked documents based on link_info.
+
+ :param reference_doctype: reference doctype to find links
+ :param reference_names: reference document names to find links for
+ :param link_info: linking details to get the linked documents
+ Ex: {'doctype': 'Purchase Invoice Advance', 'fieldname': 'reference_name',
+ 'doctype_fieldname': 'reference_type', 'is_child': True}
+ :param get_parent_if_child_table_doc: Get parent record incase linked document is a child table record.
+ :param parent_filters: filters to apply on if not a child table.
+ :param child_filters: apply filters if it is a child table.
+ :param allowed_parents: list of parents allowed in case of get_parent_if_child_table_doc
+ is enabled.
+ """
+ from_table = link_info['doctype']
+ filters = [[link_info['fieldname'], 'in', tuple(reference_names)]]
+ if link_info.get('doctype_fieldname'):
+ filters.append([link_info['doctype_fieldname'], '=', reference_doctype])
+
+ if not link_info.get('is_child'):
+ filters.extend(parent_filters or [])
+ return {from_table: frappe.db.get_all(from_table, filters, pluck='name')}
+
+
+ filters.extend(child_filters or [])
+ res = frappe.db.get_all(from_table, filters = filters, fields = ['name', 'parenttype', 'parent'])
+ documents = defaultdict(list)
+
+ for parent, rows in itertools.groupby(res, key = lambda row: row['parenttype']):
+ if allowed_parents and parent not in allowed_parents:
+ continue
+ filters = (parent_filters or []) + [['name', 'in', tuple([row.parent for row in rows])]]
+ documents[parent].extend(frappe.db.get_all(parent, filters=filters, pluck='name') or [])
+ return documents
+
@frappe.whitelist()
def cancel_all_linked_docs(docs, ignore_doctypes_on_cancel_all=[]):
@@ -107,7 +337,6 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
Returns:
bool: True if linked document passes all validations, else False
"""
-
#ignore doctype to cancel
if docinfo.get("doctype") in ignore_doctypes_on_cancel_all:
return False
@@ -130,7 +359,6 @@ def validate_linked_doc(docinfo, ignore_doctypes_on_cancel_all=[]):
def get_exempted_doctypes():
""" Get list of doctypes exempted from being auto-cancelled """
-
auto_cancel_exempt_doctypes = []
for doctypes in frappe.get_hooks('auto_cancel_exempted_doctypes'):
auto_cancel_exempt_doctypes.append(doctypes)
diff --git a/frappe/tests/test_linked_with.py b/frappe/tests/test_linked_with.py
new file mode 100644
index 0000000000..64da8e51e0
--- /dev/null
+++ b/frappe/tests/test_linked_with.py
@@ -0,0 +1,113 @@
+import frappe, unittest
+from frappe.core.doctype.doctype.test_doctype import new_doctype
+from frappe.desk.form import linked_with
+
+
+class TestLinkedWith(unittest.TestCase):
+ def setUp(self):
+ parent_doc = new_doctype("Parent Doc")
+ parent_doc.is_submittable = 1
+ parent_doc.insert()
+
+ child_doc1 = new_doctype("Child Doc1",
+ fields=[
+ {
+ "label": "Parent Doc",
+ "fieldname": "parent_doc",
+ "fieldtype": "Link",
+ "options": "Parent Doc"
+ },
+ {
+ "label": "Reference field",
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "options": "reference_doctype"
+ },
+ {
+ "label": "Reference Doctype",
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "options": "DocType"
+ }
+
+ ], unique=0)
+ child_doc1.is_submittable = 1
+ child_doc1.insert()
+
+ child_doc2 = new_doctype("Child Doc2",
+ fields=[
+ {
+ "label": "Parent Doc",
+ "fieldname": "parent_doc",
+ "fieldtype": "Link",
+ "options": "Parent Doc"
+ },
+ {
+ "label": "Child Doc1",
+ "fieldname": "child_doc1",
+ "fieldtype": "Link",
+ "options": "Child Doc1"
+ }
+
+ ], unique=0)
+ child_doc2.is_submittable = 1
+ child_doc2.insert()
+
+ def tearDown(self):
+ for doctype in ['Parent Doc', 'Child Doc1', 'Child Doc2']:
+ frappe.delete_doc("DocType", doctype)
+
+ def test_get_doctype_references_by_link_field(self):
+ references = linked_with.get_references_across_doctypes_by_link_field(to_doctypes = ['Parent Doc'])
+ self.assertEqual(len(references['Parent Doc']), 3)
+ self.assertIn({'doctype': 'Child Doc1', 'fieldname': 'parent_doc'}, references['Parent Doc'])
+ self.assertIn({'doctype': 'Child Doc2', 'fieldname': 'parent_doc'}, references['Parent Doc'])
+
+ references = linked_with.get_references_across_doctypes_by_link_field(to_doctypes = ['Child Doc1'])
+ self.assertEqual(len(references['Child Doc1']), 2)
+ self.assertIn({'doctype': 'Child Doc2', 'fieldname': 'child_doc1'}, references['Child Doc1'])
+
+ references = linked_with.get_references_across_doctypes_by_link_field(
+ to_doctypes = ['Child Doc1', 'Parent Doc'], limit_link_doctypes=['Child Doc1'])
+ self.assertEqual(len(references['Child Doc1']), 1)
+ self.assertEqual(len(references['Parent Doc']), 1)
+ self.assertIn({'doctype': 'Child Doc1', 'fieldname': 'parent_doc'}, references['Parent Doc'])
+
+ def test_get_doctype_references_by_dlink_field(self):
+ references = linked_with.get_references_across_doctypes_by_dynamic_link_field(
+ to_doctypes = ['Parent Doc'], limit_link_doctypes = ['Parent Doc', 'Child Doc1', 'Child Doc2'])
+ self.assertFalse(references)
+
+ parent_record = frappe.get_doc({'doctype': 'Parent Doc'}).insert()
+
+ child_record = frappe.get_doc({
+ 'doctype': 'Child Doc1',
+ 'reference_doctype': 'Parent Doc',
+ 'reference_name': parent_record.name
+ }).insert()
+
+ references = linked_with.get_references_across_doctypes_by_dynamic_link_field(
+ to_doctypes = ['Parent Doc'], limit_link_doctypes = ['Parent Doc', 'Child Doc1', 'Child Doc2'])
+
+ self.assertEqual(len(references['Parent Doc']), 1)
+ self.assertEqual(references['Parent Doc'][0]['doctype'], 'Child Doc1')
+ self.assertEqual(references['Parent Doc'][0]['doctype_fieldname'], 'reference_doctype')
+
+ child_record.delete()
+ parent_record.delete()
+
+ def test_get_submitted_linked_docs(self):
+ parent_record = frappe.get_doc({'doctype': 'Parent Doc'}).insert()
+
+ child_record = frappe.get_doc({
+ 'doctype': 'Child Doc1',
+ 'reference_doctype': 'Parent Doc',
+ 'reference_name': parent_record.name,
+ 'docstatus': 1
+ }).insert()
+
+ linked_docs = linked_with.get_submitted_linked_docs(parent_record.doctype, parent_record.name)["docs"]
+ self.assertIn(child_record.name,linked_docs[0]['name'])
+ child_record.cancel()
+ child_record.delete()
+ parent_record.delete()
From f608fbeddd8ae9ee21b38a4c4a5615d8c7bc99fc Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Tue, 16 Mar 2021 16:05:28 +0100
Subject: [PATCH 004/246] feat: sync mutliple IMAP folders in `Email Account`
When working with IMAP accounts, frappe should allow the user
to choose multiple folders to look for new mails. This helps
users to separate their frappe-related email from other
conversations. Use cases range from sieve filters in the
mail server that stuff incoming mail in various mail folders
to people manually sorting their e-mail. In both cases, we
can have different import policies for different folders, and
we can avoid importing unrelated email.
Created a new child table `IMAP Folder` with following fields:
- Folder Name (user-modifiable)
- Append To (user-modifiable)
- UIDVALIDITY (hidden)
- UIDNEXT (hidden)
Doctype `Email Account` and `receive.py` code adjusted so that
emails with the changes are processed correctly and Frappe
only logs in to the imap server once per sync.
Created a patch that copies the data from the old fields into
the new child table with `INBOX` as default `folder_name`. This
keeps existing setups working without manual changes.
The original fields
- uidvalidity
- uidnext
- append_to
are still available for the pop3 setups. In IMAP, these fields
are hidden user and not used.
Added a test case in `Email Account` that validates data to make
sure a IMAP folder is provided if the use_imap is true.
Also added some code formatting changes in email_account.js to get rid
of sider checks failures that block this change
---
.../doctype/communication/communication.json | 8 ++
.../doctype/communication/communication.py | 1 +
.../communication/test_communication.py | 1 +
.../doctype/email_account/email_account.js | 30 +++++---
.../doctype/email_account/email_account.json | 17 ++++-
.../doctype/email_account/email_account.py | 73 ++++++++++++-------
.../email_account/test_email_account.py | 16 ++++
.../doctype/email_account/test_records.json | 3 +-
frappe/email/doctype/imap_folder/__init__.py | 0
.../doctype/imap_folder/imap_folder.json | 53 ++++++++++++++
.../email/doctype/imap_folder/imap_folder.py | 8 ++
frappe/email/receive.py | 54 ++++++++------
frappe/email/test_smtp.py | 8 +-
frappe/patches.txt | 1 +
frappe/patches/v14_0/copy_mail_data.py | 33 +++++++++
15 files changed, 241 insertions(+), 65 deletions(-)
create mode 100644 frappe/email/doctype/imap_folder/__init__.py
create mode 100644 frappe/email/doctype/imap_folder/imap_folder.json
create mode 100644 frappe/email/doctype/imap_folder/imap_folder.py
create mode 100644 frappe/patches/v14_0/copy_mail_data.py
diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json
index 849df66a5f..9e154146b3 100644
--- a/frappe/core/doctype/communication/communication.json
+++ b/frappe/core/doctype/communication/communication.json
@@ -51,6 +51,7 @@
"email_inbox",
"message_id",
"uid",
+ "imap_folder",
"email_status",
"has_attachment",
"feedback_section",
@@ -382,6 +383,13 @@
"label": "Timeline Links",
"options": "Communication Link",
"permlevel": 2
+ },
+ {
+ "fieldname": "imap_folder",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "IMAP Folder",
+ "read_only": 1
}
],
"icon": "fa fa-comment",
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index 66bb3909da..5714d122eb 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -42,6 +42,7 @@ class Communication(Document, CommunicationEmailMixin):
"action": "Read",
"communication": self.name,
"uid": self.uid,
+ "imap_folder": self.imap_folder,
"email_account": self.email_account
}).insert(ignore_permissions=True)
frappe.db.commit()
diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py
index b0c8e1fcee..f26e70771b 100644
--- a/frappe/core/doctype/communication/test_communication.py
+++ b/frappe/core/doctype/communication/test_communication.py
@@ -291,6 +291,7 @@ def create_email_account():
"unreplied_for_mins": 20,
"send_notification_to": "test_comm@example.com",
"pop3_server": "pop.test.example.com",
+ "imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
"no_remaining":"0",
"enable_automatic_linking": 1
}).insert(ignore_permissions=True)
diff --git a/frappe/email/doctype/email_account/email_account.js b/frappe/email/doctype/email_account/email_account.js
index 277bf43eb6..54f0d2372d 100644
--- a/frappe/email/doctype/email_account/email_account.js
+++ b/frappe/email/doctype/email_account/email_account.js
@@ -109,6 +109,15 @@ frappe.ui.form.on("Email Account", {
onload: function(frm) {
frm.set_df_property("append_to", "only_select", true);
frm.set_query("append_to", "frappe.email.doctype.email_account.email_account.get_append_to");
+ frm.set_query("append_to", "imap_folder", function() {
+ return {
+ query: "frappe.email.doctype.email_account.email_account.get_append_to"
+ };
+ });
+ if (frm.doc.__islocal) {
+ frm.add_child("imap_folder", {"folder_name": "INBOX"});
+ frm.refresh_field("imap_folder");
+ }
},
refresh: function(frm) {
@@ -117,7 +126,7 @@ frappe.ui.form.on("Email Account", {
frm.events.notify_if_unreplied(frm);
frm.events.show_gmail_message_for_less_secure_apps(frm);
- if(frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
+ if (frappe.route_flags.delete_user_from_locals && frappe.route_flags.linked_user) {
delete frappe.route_flags.delete_user_from_locals;
delete locals['User'][frappe.route_flags.linked_user];
}
@@ -125,7 +134,7 @@ frappe.ui.form.on("Email Account", {
show_gmail_message_for_less_secure_apps: function(frm) {
frm.dashboard.clear_headline();
- if(frm.doc.service==="GMail") {
+ if (frm.doc.service==="GMail") {
frm.dashboard.set_headline_alert('Gmail will only work if you allow access for less secure \
apps in Gmail settings. Read this for details ');
@@ -137,8 +146,8 @@ frappe.ui.form.on("Email Account", {
frm.events.update_domain(frm);
},
- update_domain: function(frm){
- if (!frm.doc.email_id && !frm.doc.service){
+ update_domain: function(frm) {
+ if (!frm.doc.email_id && !frm.doc.service) {
return;
}
@@ -148,7 +157,7 @@ frappe.ui.form.on("Email Account", {
args: {
"email_id": frm.doc.email_id
},
- callback: function (r) {
+ callback: function(r) {
if (r.message) {
frm.events.set_domain_fields(frm, r.message);
}
@@ -157,7 +166,7 @@ frappe.ui.form.on("Email Account", {
},
set_domain_fields: function(frm, args) {
- if(!args){
+ if (!args) {
args = frappe.route_flags.set_domain_values? frappe.route_options: {};
}
@@ -172,10 +181,8 @@ frappe.ui.form.on("Email Account", {
email_sync_option: function(frm) {
// confirm if the ALL sync option is selected
- if(frm.doc.email_sync_option == "ALL"){
- var msg = __("You are selecting Sync Option as ALL, It will resync all \
- read as well as unread message from server. This may also cause the duplication\
- of Communication (emails).");
+ if (frm.doc.email_sync_option == "ALL") {
+ var msg = __("You are selecting Sync Option as ALL, It will resync all read as well as unread message from server. This may also cause the duplication of Communication (emails).");
frappe.confirm(msg, null, function() {
frm.set_value("email_sync_option", "UNSEEN");
});
@@ -184,8 +191,7 @@ frappe.ui.form.on("Email Account", {
warn_autoreply_on_incoming: function(frm) {
if (frm.doc.enable_incoming && frm.doc.enable_auto_reply && frm.doc.__islocal) {
- var msg = __("Enabling auto reply on an incoming email account will send automated replies \
- to all the synchronized emails. Do you wish to continue?");
+ var msg = __("Enabling auto reply on an incoming email account will send automated replies to all the synchronized emails. Do you wish to continue?");
frappe.confirm(msg, null, function() {
frm.set_value("enable_auto_reply", 0);
frappe.show_alert({message: __("Disabled Auto Reply"), indicator: "blue"});
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index e20f38c74a..bf9a79529b 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -31,6 +31,8 @@
"attachment_limit",
"email_sync_option",
"initial_sync_count",
+ "section_break_25",
+ "imap_folder",
"section_break_12",
"append_emails_to_sent_folder",
"append_to",
@@ -204,7 +206,7 @@
"label": "Attachment Limit (MB)"
},
{
- "depends_on": "enable_incoming",
+ "depends_on": "eval: doc.enable_incoming && !doc.use_imap",
"description": "Append as communication against this DocType (must have fields, \"Status\", \"Subject\")",
"fieldname": "append_to",
"fieldtype": "Link",
@@ -562,6 +564,18 @@
"fieldname": "account_section",
"fieldtype": "Section Break",
"label": "Account"
+ },
+ {
+ "depends_on": "eval: doc.use_imap && doc.enable_incoming",
+ "fieldname": "imap_folder",
+ "fieldtype": "Table",
+ "label": "IMAP Folder",
+ "options": "IMAP Folder"
+ },
+ {
+ "fieldname": "section_break_25",
+ "fieldtype": "Section Break",
+ "label": "IMAP Details"
}
],
"icon": "fa fa-inbox",
@@ -571,6 +585,7 @@
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index d90c56d90d..d5b683c9e3 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -67,6 +67,10 @@ class EmailAccount(Document):
else:
self.login_id = None
+ # validate the imap settings
+ if self.enable_incoming and self.use_imap and len(self.imap_folder) <= 0:
+ frappe.throw(_("You need to set one IMAP folder for {0}").format(frappe.bold(self.email_id)))
+
duplicate_email_account = frappe.get_all("Email Account", filters={
"email_id": self.email_id,
"name": ("!=", self.name)
@@ -100,10 +104,11 @@ class EmailAccount(Document):
for e in self.get_unreplied_notification_emails():
validate_email_address(e, True)
- if self.enable_incoming and self.append_to:
- valid_doctypes = [d[0] for d in get_append_to()]
- if self.append_to not in valid_doctypes:
- frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
+ for folder in self.imap_folder:
+ if self.enable_incoming and folder.append_to:
+ valid_doctypes = [d[0] for d in get_append_to()]
+ if folder.append_to not in valid_doctypes:
+ frappe.throw(_("Append To can be one of {0}").format(comma_or(valid_doctypes)))
def validate_smtp_conn(self):
if not self.smtp_server:
@@ -177,13 +182,13 @@ class EmailAccount(Document):
return None
args = frappe._dict({
+ "email_account_name": self.email_account_name,
"email_account": self.name,
"host": self.email_server,
"use_ssl": self.use_ssl,
"username": getattr(self, "login_id", None) or self.email_id,
"use_imap": self.use_imap,
"email_sync_rule": email_sync_rule,
- "uid_validity": self.uidvalidity,
"incoming_port": get_port(self),
"initial_sync_count": self.initial_sync_count or 100
})
@@ -457,6 +462,14 @@ class EmailAccount(Document):
"""retrive and return inbound mails.
"""
+ mails = []
+
+ def process_mail(messages):
+ for index, message in enumerate(messages.get("latest_messages", [])):
+ uid = messages['uid_list'][index] if messages.get('uid_list') else None
+ seen_status = 1 if messages.get('seen_status', {}).get(uid) == 'SEEN' else 0
+ mails.append(InboundMail(message, self, uid, seen_status))
+
if frappe.local.flags.in_test:
return [InboundMail(msg, self) for msg in test_mails or []]
@@ -466,17 +479,23 @@ class EmailAccount(Document):
email_sync_rule = self.build_email_sync_rule()
try:
email_server = self.get_incoming_server(in_receive=True, email_sync_rule=email_sync_rule)
- messages = email_server.get_messages() or {}
+ if self.use_imap:
+ # process all given imap folder
+ for folder in self.imap_folder:
+ email_server.select_imap_folder(folder.folder_name)
+ email_server.settings['uid_validity'] = folder.uidvalidity
+ messages = email_server.get_messages(folder=folder.folder_name) or {}
+ process_mail(messages)
+ else:
+ # process the pop3 account
+ messages = email_server.get_messages() or {}
+ process_mail(messages)
+ # close connection to mailserver
+ email_server.logout()
except Exception:
frappe.log_error(title=_("Error while connecting to email account {0}").format(self.name))
return []
- mails = []
- for index, message in enumerate(messages.get("latest_messages", [])):
- uid = messages['uid_list'][index] if messages.get('uid_list') else None
- seen_status = 1 if messages.get('seen_status', {}).get(uid)=='SEEN' else 0
- mails.append(InboundMail(message, self, uid, seen_status))
-
return mails
def handle_bad_emails(self, uid, raw, reason):
@@ -547,23 +566,22 @@ class EmailAccount(Document):
else:
return self.email_sync_option or "UNSEEN"
- def mark_emails_as_read_unread(self):
+ def mark_emails_as_read_unread(self, email_server=None, folder_name="INBOX"):
""" mark Email Flag Queue of self.email_account mails as read"""
-
if not self.use_imap:
return
- flags = frappe.db.sql("""select name, communication, uid, action from
- `tabEmail Flag Queue` where is_completed=0 and email_account={email_account}
- """.format(email_account=frappe.db.escape(self.name)), as_dict=True)
+ flags = frappe.db.sql("""select name, communication, uid, action, imap_folder from
+ `tabEmail Flag Queue` where is_completed=0 and email_account={email_account} and imap_folder={folder_name}
+ """.format(email_account=frappe.db.escape(self.name),folder_name=frappe.db.escape(folder_name)), as_dict=True)
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
if flags and uid_list:
- email_server = self.get_incoming_server()
+ if not email_server:
+ email_server = self.get_incoming_server()
if not email_server:
return
-
- email_server.update_flag(uid_list=uid_list)
+ email_server.update_flag(folder_name, uid_list=uid_list)
# mark communication as read
docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
@@ -651,15 +669,19 @@ def test_internet(host="8.8.8.8", port=53, timeout=3):
def notify_unreplied():
"""Sends email notifications if there are unreplied Communications
and `notify_if_unreplied` is set as true."""
-
for email_account in frappe.get_all("Email Account", "name", filters={"enable_incoming": 1, "notify_if_unreplied": 1}):
email_account = frappe.get_doc("Email Account", email_account.name)
- if email_account.append_to:
+ if email_account.use_imap:
+ append_to = [folder.get("append_to") for folder in email_account.imap_folder]
+ else:
+ append_to = email_account.append_to
+
+ if append_to:
# get open communications younger than x mins, for given doctype
for comm in frappe.get_all("Communication", "name", filters=[
{"sent_or_received": "Received"},
- {"reference_doctype": email_account.append_to},
+ {"reference_doctype": ("in", append_to)},
{"unread_notification_sent": 0},
{"email_account":email_account.name},
{"creation": ("<", datetime.now() - timedelta(seconds = (email_account.unreplied_for_mins or 30) * 60))},
@@ -702,9 +724,6 @@ def pull_from_email_account(email_account):
email_account = frappe.get_doc("Email Account", email_account)
email_account.receive()
- # mark Email Flag Queue mail as read
- email_account.mark_emails_as_read_unread()
-
def get_max_email_uid(email_account):
# get maximum uid of emails
max_uid = 1
@@ -800,4 +819,4 @@ def set_email_password(email_account, user, password):
frappe.db.rollback()
return False
- return True
\ No newline at end of file
+ return True
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index 21dc4b84c4..51f722a7af 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -229,6 +229,22 @@ class TestEmailAccount(unittest.TestCase):
email_account.handle_bad_emails(uid=-1, raw=mail_content, reason="Testing")
self.assertTrue(frappe.db.get_value("Unhandled Email", {'message_id': message_id}))
+ def test_imap_folder(self):
+ # assert tests if imap_folder >= 1 and imap is checked
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+
+ self.assertTrue(email_account.use_imap)
+ self.assertTrue(email_account.enable_incoming)
+ self.assertTrue(len(email_account.imap_folder) > 0)
+
+ def test_imap_folder_missing(self):
+ # Test the Exception in validate() that verifies the imap_folder list
+ email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
+ email_account.imap_folder = []
+
+ with self.assertRaises(Exception):
+ email_account.validate()
+
class TestInboundMail(unittest.TestCase):
@classmethod
def setUpClass(cls):
diff --git a/frappe/email/doctype/email_account/test_records.json b/frappe/email/doctype/email_account/test_records.json
index 15ca2a886e..450895d7a6 100644
--- a/frappe/email/doctype/email_account/test_records.json
+++ b/frappe/email/doctype/email_account/test_records.json
@@ -4,7 +4,6 @@
"is_global": 1,
"doctype": "Email Account",
"domain":"example.com",
- "append_to": "ToDo",
"email_account_name": "_Test Email Account 1",
"enable_outgoing": 1,
"smtp_server": "test.example.com",
@@ -20,6 +19,8 @@
"send_notification_to": "test_unreplied@example.com",
"pop3_server": "pop.test.example.com",
"no_remaining":"0",
+ "append_to": "ToDo",
+ "imap_folder": [{"folder_name": "INBOX", "append_to": "ToDo"}],
"track_email_status": 1
},
{
diff --git a/frappe/email/doctype/imap_folder/__init__.py b/frappe/email/doctype/imap_folder/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/email/doctype/imap_folder/imap_folder.json b/frappe/email/doctype/imap_folder/imap_folder.json
new file mode 100644
index 0000000000..bab50dea39
--- /dev/null
+++ b/frappe/email/doctype/imap_folder/imap_folder.json
@@ -0,0 +1,53 @@
+{
+ "actions": [],
+ "creation": "2021-09-21 11:38:13.521979",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "folder_name",
+ "append_to",
+ "uidvalidity",
+ "uidnext"
+ ],
+ "fields": [
+ {
+ "fieldname": "folder_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Folder Name",
+ "reqd": 1
+ },
+ {
+ "fieldname": "append_to",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Append To",
+ "options": "DocType"
+ },
+ {
+ "fieldname": "uidvalidity",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "UIDVALIDITY"
+ },
+ {
+ "fieldname": "uidnext",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "UIDNEXT"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-21 11:53:00.811236",
+ "modified_by": "Administrator",
+ "module": "Email",
+ "name": "IMAP Folder",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
diff --git a/frappe/email/doctype/imap_folder/imap_folder.py b/frappe/email/doctype/imap_folder/imap_folder.py
new file mode 100644
index 0000000000..b0bb36b677
--- /dev/null
+++ b/frappe/email/doctype/imap_folder/imap_folder.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class IMAPFolder(Document):
+ pass
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index a755ec5e74..d2b4376dfd 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -111,7 +111,17 @@ class EmailServer:
frappe.msgprint(_('Invalid User Name or Support Password. Please rectify and try again.'))
raise
- def get_messages(self):
+ def select_imap_folder(self, folder):
+ self.imap.select(folder)
+
+ def logout(self):
+ if cint(self.settings.use_imap):
+ self.imap.logout()
+ else:
+ self.pop.quit()
+ return
+
+ def get_messages(self, folder="INBOX"):
"""Returns new email messages in a list."""
if not (self.check_mails() or self.connect()):
return []
@@ -126,7 +136,8 @@ class EmailServer:
self.latest_messages = []
self.seen_status = {}
self.uid_reindexed = False
- uid_list = email_list = self.get_new_mails()
+
+ uid_list = email_list = self.get_new_mails(folder)
if not email_list:
return
@@ -160,13 +171,6 @@ class EmailServer:
else:
raise
- finally:
- # no matter the exception, pop should quit if connected
- if cint(self.settings.use_imap):
- self.imap.logout()
- else:
- self.pop.quit()
-
out = { "latest_messages": self.latest_messages }
if self.settings.use_imap:
out.update({
@@ -177,15 +181,15 @@ class EmailServer:
return out
- def get_new_mails(self):
+ def get_new_mails(self, folder):
"""Return list of new mails"""
if cint(self.settings.use_imap):
email_list = []
- self.check_imap_uidvalidity()
+ self.check_imap_uidvalidity(folder)
readonly = False if self.settings.email_sync_rule == "UNSEEN" else True
- self.imap.select("Inbox", readonly=readonly)
+ self.imap.select(folder, readonly=readonly)
response, message = self.imap.uid('search', None, self.settings.email_sync_rule)
if message[0]:
email_list = message[0].split()
@@ -194,11 +198,11 @@ class EmailServer:
return email_list
- def check_imap_uidvalidity(self):
+ def check_imap_uidvalidity(self, folder):
# compare the UIDVALIDITY of email account and imap server
uid_validity = self.settings.uid_validity
- response, message = self.imap.status("Inbox", "(UIDVALIDITY UIDNEXT)")
+ response, message = self.imap.status(folder, "(UIDVALIDITY UIDNEXT)")
current_uid_validity = self.parse_imap_response("UIDVALIDITY", message[0]) or 0
uidnext = int(self.parse_imap_response("UIDNEXT", message[0]) or "1")
@@ -210,10 +214,18 @@ class EmailServer:
"""update `tabCommunication` set uid=-1 where communication_medium='Email'
and email_account=%s""", (self.settings.email_account,)
)
- frappe.db.sql(
- """update `tabEmail Account` set uidvalidity=%s, uidnext=%s where
- name=%s""", (current_uid_validity, uidnext, self.settings.email_account)
- )
+ if self.settings.use_imap:
+ # new update for the IMAP Folder DoctType
+ frappe.db.sql(
+ """update `tabIMAP Folder` set uidvalidity=%s, uidnext=%s where
+ parent=%s and folder_name=%s""",
+ (current_uid_validity, uidnext, self.settings.email_account_name, folder)
+ )
+ else:
+ frappe.db.sql(
+ """update `tabEmail Account` set uidvalidity=%s, uidnext=%s where
+ name=%s""", (current_uid_validity, uidnext, self.settings.email_account)
+ )
# uid validity not found pulling emails for first time
if not uid_validity:
@@ -232,6 +244,7 @@ class EmailServer:
def parse_imap_response(self, cmd, response):
pattern = r"(?<={cmd} )[0-9]*".format(cmd=cmd)
match = re.search(pattern, response.decode('utf-8'), re.U | re.I)
+
if match:
return match.group(0)
else:
@@ -340,16 +353,15 @@ class EmailServer:
return error_msg
- def update_flag(self, uid_list={}):
+ def update_flag(self, folder, uid_list={}):
""" set all uids mails the flag as seen """
-
if not uid_list:
return
if not self.connect():
return
- self.imap.select("Inbox")
+ self.imap.select(folder)
for uid, operation in uid_list.items():
if not uid: continue
diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py
index 58e4fdd8a6..127bdd44ce 100644
--- a/frappe/email/test_smtp.py
+++ b/frappe/email/test_smtp.py
@@ -16,11 +16,12 @@ class TestSMTP(unittest.TestCase):
make_server(port, 0, 1)
def test_get_email_account(self):
- existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing", "append_to"])
+ existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing","append_to", "use_imap"])
unset_details = {
"enable_outgoing": 0,
"default_outgoing": 0,
- "append_to": None
+ "append_to": None,
+ "use_imap": 0
}
for email_account in existing_email_accounts:
frappe.db.set_value('Email Account', email_account['name'], unset_details)
@@ -60,7 +61,8 @@ def create_email_account(email_id, password, enable_outgoing, default_outgoing=0
"enable_incoming": 1,
"append_to":append_to,
"is_dummy_password": 1,
- "smtp_server": "localhost"
+ "smtp_server": "localhost",
+ "use_imap": 0
}
email_account = frappe.new_doc('Email Account')
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 41ca1a1724..37fa5379d6 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -183,3 +183,4 @@ frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.rename_cancelled_documents
frappe.patches.v14_0.update_workspace2 # 25.08.2021
+frappe.patches.v14_0.copy_mail_data #08.03.21
diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py
new file mode 100644
index 0000000000..d3a4baca88
--- /dev/null
+++ b/frappe/patches/v14_0/copy_mail_data.py
@@ -0,0 +1,33 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ # patch for all Email Account with the flag use_imap
+ for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}):
+ # get all data from Email Account
+ doc = frappe.get_doc("Email Account", email_account.name)
+
+ imap_list = [folder.folder_name for folder in doc.imap_folder]
+ # and append the old data to the child table
+ if doc.uidvalidity or doc.uidnext and "INBOX" not in imap_list:
+ doc.append("imap_folder", {
+ "folder_name": "INBOX",
+ "append_to": doc.append_to,
+ "uid_validity": doc.uidvalidity,
+ "uidnext": doc.uidnext,
+ })
+
+ doc.save()
+
+ frappe.db.sql(
+ """
+ update
+ `tabEmail Flag Queue`
+ set
+ imap_folder = "INBOX"
+ where
+ email_account = '%s'
+ and imap_folder is NULL
+ """ % (doc.name)
+ )
From 8ea6158690e405a46f683c240f62052ed3701fb1 Mon Sep 17 00:00:00 2001
From: Aradhya-Tripathi
Date: Thu, 14 Oct 2021 16:13:49 +0530
Subject: [PATCH 005/246] refactor: removed aggregation functions at db level
---
frappe/database/database.py | 12 ------------
1 file changed, 12 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index e98cc22f41..88c3241303 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -766,18 +766,6 @@ class Database(object):
except Exception:
return None
- def min(self, dt, fieldname, filters=None, **kwargs):
- return self.query.build_conditions(dt, filters=filters).select(Min(Column(fieldname))).run(**kwargs)[0][0] or 0
-
- def max(self, dt, fieldname, filters=None, **kwargs):
- return self.query.build_conditions(dt, filters=filters).select(Max(Column(fieldname))).run(**kwargs)[0][0] or 0
-
- def avg(self, dt, fieldname, filters=None, **kwargs):
- return self.query.build_conditions(dt, filters=filters).select(Avg(Column(fieldname))).run(**kwargs)[0][0] or 0
-
- def sum(self, dt, fieldname, filters=None, **kwargs):
- return self.query.build_conditions(dt, filters=filters).select(Sum(Column(fieldname))).run(**kwargs)[0][0] or 0
-
def count(self, dt, filters=None, debug=False, cache=False):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
From 18e2ab7e084447749dd189103a42ab8337ad497e Mon Sep 17 00:00:00 2001
From: Aradhya-Tripathi
Date: Thu, 14 Oct 2021 16:54:44 +0530
Subject: [PATCH 006/246] refactor: moved aggregation functions from safe_exec
---
frappe/utils/safe_exec.py | 4 ----
1 file changed, 4 deletions(-)
diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py
index 8f6b33a838..a439ef0a8b 100644
--- a/frappe/utils/safe_exec.py
+++ b/frappe/utils/safe_exec.py
@@ -163,10 +163,6 @@ def get_safe_globals():
get_default=frappe.db.get_default,
exists=frappe.db.exists,
count=frappe.db.count,
- min=frappe.db.min,
- max=frappe.db.max,
- avg=frappe.db.avg,
- sum=frappe.db.sum,
escape=frappe.db.escape,
sql=read_sql,
commit=frappe.db.commit,
From 44216958f9276b9c6ba11db834523e58767533de Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Thu, 28 Oct 2021 11:08:10 +0200
Subject: [PATCH 007/246] setting `use_imap` flag
---
frappe/email/doctype/email_account/test_email_account.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/email/doctype/email_account/test_email_account.py b/frappe/email/doctype/email_account/test_email_account.py
index 51f722a7af..6d26f9f070 100644
--- a/frappe/email/doctype/email_account/test_email_account.py
+++ b/frappe/email/doctype/email_account/test_email_account.py
@@ -25,6 +25,7 @@ class TestEmailAccount(unittest.TestCase):
email_account = frappe.get_doc("Email Account", "_Test Email Account 1")
email_account.db_set("enable_incoming", 1)
email_account.db_set("enable_auto_reply", 1)
+ email_account.db_set("use_imap", 1)
@classmethod
def tearDownClass(cls):
From 151768d1d9c8b1b6dbe06d2c5d4c450f0630c7e0 Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Wed, 3 Nov 2021 09:36:00 +0100
Subject: [PATCH 008/246] added method param again after merge
---
frappe/email/receive.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index bba31b122f..e7989660b8 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -353,7 +353,7 @@ class EmailServer:
return error_msg
- def update_flag(self, uid_list=None):
+ def update_flag(self, folder, uid_list=None):
""" set all uids mails the flag as seen """
if not uid_list:
return
From 1f70c27e9f3ec11ce04bd857d7cae6f2ff2356bc Mon Sep 17 00:00:00 2001
From: hrwx
Date: Mon, 15 Nov 2021 14:33:28 +0000
Subject: [PATCH 009/246] chore: rename timezone keys
---
frappe/boot.py | 4 ++--
.../system_settings/system_settings.js | 6 ++---
frappe/core/doctype/user/user.js | 6 ++---
frappe/core/doctype/user/user.py | 14 +++++-------
.../js/frappe/form/controls/datetime.js | 2 +-
frappe/public/js/frappe/utils/datetime.js | 22 +++++++++++--------
6 files changed, 26 insertions(+), 28 deletions(-)
diff --git a/frappe/boot.py b/frappe/boot.py
index fd7564d75a..e671d8b37d 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -329,6 +329,6 @@ def get_notification_settings():
def set_time_zone(bootinfo):
bootinfo.time_zone = {
- "system_time_zone": get_time_zone(),
- "user_time_zone": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
+ "system": get_time_zone(),
+ "user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None) or get_time_zone()
}
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index aefe3786bd..0164a1a683 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -34,9 +34,7 @@ frappe.ui.form.on("System Settings", {
}
},
after_save: function(frm) {
- if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone !== frm.doc.time_zone) {
- // Clear cache after saving to refresh the values of time_zone
- frappe.ui.toolbar.clear_cache();
- }
+ // Clear cache after saving to refresh the values of boot.
+ frappe.ui.toolbar.clear_cache();
}
});
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 48dc2d1672..681080b2b3 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -274,10 +274,8 @@ frappe.ui.form.on('User', {
});
},
after_save: function(frm) {
- if (frappe.boot.time_zone && frappe.boot.time_zone.user_time_zone !== frm.doc.time_zone) {
- // Clear cache after saving to refresh the values of time_zone
- frappe.ui.toolbar.clear_cache();
- }
+ // Clear cache after saving to refresh the values of boot.
+ frappe.ui.toolbar.clear_cache();
}
});
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index fd19f4d82e..76bdbbbeb8 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -7,7 +7,7 @@ import frappe.defaults
import frappe.permissions
from frappe.model.document import Document
from frappe.utils import (cint, flt, has_gravatar, escape_html, format_datetime,
- now_datetime, get_formatted_email, today)
+ now_datetime, get_formatted_email, today, get_time_zone)
from frappe import throw, msgprint, _
from frappe.utils.password import update_password as _update_password, check_password, get_password_reset_limit
from frappe.desk.notifications import clear_notifications
@@ -231,11 +231,11 @@ class User(Document):
def validate_share(self, docshare):
pass
# if docshare.user == self.name:
- # if self.user_type=="System User":
- # if docshare.share != 1:
- # frappe.throw(_("Sorry! User should have complete access to their own record."))
- # else:
- # frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
+ # if self.user_type=="System User":
+ # if docshare.share != 1:
+ # frappe.throw(_("Sorry! User should have complete access to their own record."))
+ # else:
+ # frappe.throw(_("Sorry! Sharing with Website User is prohibited."))
def send_password_notification(self, new_password):
try:
@@ -592,8 +592,6 @@ class User(Document):
return user
def set_time_zone(self):
- from frappe.utils import get_time_zone
-
if not self.time_zone:
self.time_zone = get_time_zone()
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index b69f40e9c4..3142f1bf0f 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -63,7 +63,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
super.set_description();
}
get_user_time_zone() {
- return frappe.boot.time_zone ? frappe.boot.time_zone.user_time_zone : frappe.sys_defaults.time_zone;
+ return frappe.boot.time_zone ? frappe.boot.time_zone.user : frappe.sys_defaults.time_zone;
}
set_datepicker() {
super.set_datepicker();
diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js
index 71fdbbb897..c85cbd42e7 100644
--- a/frappe/public/js/frappe/utils/datetime.js
+++ b/frappe/public/js/frappe/utils/datetime.js
@@ -16,10 +16,10 @@ $.extend(frappe.datetime, {
// Converts the datetime string to system time zone first since the database only stores datetime in
// system time zone and then convert the string to user time zone(from User doctype).
let date_obj = null;
- if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
- date_obj = moment.tz(date, frappe.boot.time_zone.system_time_zone)
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) {
+ date_obj = moment.tz(date, frappe.boot.time_zone.system)
.clone()
- .tz(frappe.boot.time_zone.user_time_zone);
+ .tz(frappe.boot.time_zone.user);
} else {
date_obj = moment(date);
}
@@ -34,10 +34,10 @@ $.extend(frappe.datetime, {
// This is done so that only one timezone is present in database and we do not end up storing local timezone since it changes
// as per the location of user.
let date_obj = null;
- if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
- date_obj = moment.tz(date, frappe.boot.time_zone.user_time_zone)
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) {
+ date_obj = moment.tz(date, frappe.boot.time_zone.user)
.clone()
- .tz(frappe.boot.time_zone.system_time_zone);
+ .tz(frappe.boot.time_zone.system);
} else {
date_obj = moment(date);
}
@@ -46,13 +46,17 @@ $.extend(frappe.datetime, {
},
is_system_time_zone: function() {
- if (frappe.boot.time_zone && frappe.boot.time_zone.system_time_zone && frappe.boot.time_zone.user_time_zone) {
- return moment().tz(frappe.boot.time_zone.system_time_zone).utcOffset() === moment().tz(frappe.boot.time_zone.user_time_zone).utcOffset();
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) {
+ return moment().tz(frappe.boot.time_zone.system).utcOffset() === moment().tz(frappe.boot.time_zone.user).utcOffset();
}
return true;
},
+ is_timezone_same: function() {
+ return frappe.datetime.is_system_time_zone();
+ },
+
str_to_obj: function(d) {
return moment(d, frappe.defaultDatetimeFormat)._d;
},
@@ -204,7 +208,7 @@ $.extend(frappe.datetime, {
* This will make sure that at any point we know which timezone the user if following and not have random timezone
* when the timezone of the local machine changes.
*/
- let time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user_time_zone || frappe.boot.time_zone.system_time_zone : frappe.sys_defaults.time_zone;
+ let time_zone = frappe.boot.time_zone ? frappe.boot.time_zone.user || frappe.boot.time_zone.system : frappe.sys_defaults.time_zone;
let date = moment.tz(time_zone);
return as_obj ? frappe.datetime.moment_to_date_obj(date) : date.format(format);
From 576efed7f5a9cd29d01e746bcd506bde4292a811 Mon Sep 17 00:00:00 2001
From: hrwx
Date: Mon, 15 Nov 2021 14:35:56 +0000
Subject: [PATCH 010/246] chore: move timezone clear cache to on_update
---
frappe/core/doctype/system_settings/system_settings.js | 8 +++++---
frappe/core/doctype/user/user.js | 8 +++++---
2 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.js b/frappe/core/doctype/system_settings/system_settings.js
index 0164a1a683..4eeab0274b 100644
--- a/frappe/core/doctype/system_settings/system_settings.js
+++ b/frappe/core/doctype/system_settings/system_settings.js
@@ -33,8 +33,10 @@ frappe.ui.form.on("System Settings", {
}
}
},
- after_save: function(frm) {
- // Clear cache after saving to refresh the values of boot.
- frappe.ui.toolbar.clear_cache();
+ on_update: function(frm) {
+ if (frappe.boot.time_zone && frappe.boot.time_zone.system !== frm.doc.time_zone) {
+ // Clear cache after saving to refresh the values of boot.
+ frappe.ui.toolbar.clear_cache();
+ }
}
});
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 681080b2b3..79c2665a05 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -273,9 +273,11 @@ frappe.ui.form.on('User', {
}
});
},
- after_save: function(frm) {
- // Clear cache after saving to refresh the values of boot.
- frappe.ui.toolbar.clear_cache();
+ on_update: function(frm) {
+ if (frappe.boot.time_zone && frappe.boot.time_zone.user !== frm.doc.time_zone) {
+ // Clear cache after saving to refresh the values of boot.
+ frappe.ui.toolbar.clear_cache();
+ }
}
});
From 008954aed875063959e0e8dd9c235b59da14d80f Mon Sep 17 00:00:00 2001
From: Aradhya-Tripathi
Date: Tue, 16 Nov 2021 15:12:18 +0530
Subject: [PATCH 011/246] feat: added Pseudocolumn to qb utils
---
frappe/query_builder/utils.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
index 386ddda751..b7d22fc354 100644
--- a/frappe/query_builder/utils.py
+++ b/frappe/query_builder/utils.py
@@ -8,6 +8,7 @@ from pypika.queries import Column
import frappe
from .builder import MariaDB, Postgres
+from pypika.terms import PseudoColumn
class db_type_is(Enum):
From 6a8515af85374ccbdc9a877aa391ac86ce515170 Mon Sep 17 00:00:00 2001
From: Aradhya-Tripathi
Date: Tue, 16 Nov 2021 15:12:39 +0530
Subject: [PATCH 012/246] feat: Added pluck to get_values
---
frappe/database/database.py | 22 ++++++++++++----------
1 file changed, 12 insertions(+), 10 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index a7dd9b6b66..09d19f21de 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -335,7 +335,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, cache=False, for_update=False, run=True):
+ debug=False, order_by=None, cache=False, for_update=False, run=True, **kwargs):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@@ -362,7 +362,7 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update, run=run)
+ order_by, cache=cache, for_update=for_update, run=run, **kwargs)
if not run:
return ret
@@ -370,7 +370,7 @@ class Database(object):
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, update=None, cache=False, for_update=False, run=True):
+ debug=False, order_by=None, update=None, cache=False, for_update=False, run=True, **kwargs):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -396,7 +396,7 @@ class Database(object):
if isinstance(filters, list):
order_by = order_by or "modified_desc"
- out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run)
+ out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run, **kwargs)
else:
fields = fieldname
@@ -408,9 +408,11 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
- order_by = order_by or "modified"
+ if not kwargs.get("no_order"):
+ order_by = order_by or "modified"
+ kwargs.pop("no_order", None)
out = self._get_values_from_table(
- fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run
+ fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run, **kwargs
)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -540,7 +542,7 @@ class Database(object):
return self.get_single_value(*args, **kwargs)
def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None,
- update=None, for_update=False, run=True):
+ update=None, for_update=False, run=True, **kwargs):
field_objects = []
if not isinstance(fields, Criterion):
@@ -563,17 +565,17 @@ class Database(object):
if fields=="*":
query = criterion.select(fields)
as_dict = True
- r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run)
+ r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, **kwargs)
return r
- def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True):
+ def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True, **kwargs):
names = list(filter(None, names))
if names:
return self.get_all(doctype,
fields=['name', field],
filters=[['name', 'in', names]],
- debug=debug, as_list=1, run=run)
+ debug=debug, as_list=1, run=run, **kwargs),
else:
return {}
From 09e7af70abd0cc5d7421fc1e9621e8b5d933fa21 Mon Sep 17 00:00:00 2001
From: Aradhya-Tripathi
Date: Tue, 16 Nov 2021 15:12:58 +0530
Subject: [PATCH 013/246] refactor: converted raw queries
---
.../doctype/custom_field/custom_field.py | 5 +--
frappe/sessions.py | 43 ++++++++++++++-----
2 files changed, 34 insertions(+), 14 deletions(-)
diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py
index 8c22d3c45c..8f7b21dd24 100644
--- a/frappe/custom/doctype/custom_field/custom_field.py
+++ b/frappe/custom/doctype/custom_field/custom_field.py
@@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.model.docfield import supports_translation
from frappe.model import core_doctypes_list
+from frappe.query_builder.functions import IfNull
class CustomField(Document):
def autoname(self):
@@ -115,9 +116,7 @@ def get_fields_label(doctype=None):
def create_custom_field_if_values_exist(doctype, df):
df = frappe._dict(df)
if df.fieldname in frappe.db.get_table_columns(doctype) and \
- frappe.db.sql("""select count(*) from `tab{doctype}`
- where ifnull({fieldname},'')!=''""".format(doctype=doctype, fieldname=df.fieldname))[0][0]:
-
+ frappe.db.count(dt=doctype, filters=IfNull(df.fieldname, "") != ""):
create_custom_field(doctype, df)
def create_custom_field(doctype, df, ignore_validate=False):
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 9a0f19df80..9b96435093 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -17,6 +17,8 @@ import redis
from urllib.parse import unquote
from frappe.cache_manager import clear_user_cache
from frappe.query_builder import Order, DocType
+from frappe.query_builder.utils import PseudoColumn
+from frappe.query_builder.functions import Now
@frappe.whitelist()
@@ -97,12 +99,23 @@ def clear_all_sessions(reason=None):
def get_expired_sessions():
'''Returns list of expired sessions'''
+ sessions = DocType("Sessions")
+
expired = []
for device in ("desktop", "mobile"):
- expired += frappe.db.sql_list("""SELECT `sid`
- FROM `tabSessions`
- WHERE (NOW() - `lastupdate`) > %s
- AND device = %s""", (get_expiry_period_for_query(device), device))
+ expired.extend(
+ frappe.db.get_values(
+ sessions,
+ filters=(
+ PseudoColumn(f"({Now() - sessions.lastupdate})")
+ > get_expiry_period_for_query(device)
+ )
+ & (sessions.device == device),
+ fieldname="sid",
+ no_order=True,
+ pluck=True,
+ )
+ )
return expired
@@ -305,14 +318,22 @@ class Session:
return data and data.data
def get_session_data_from_db(self):
- self.device = frappe.db.sql('SELECT `device` FROM `tabSessions` WHERE `sid`=%s', self.sid)
- self.device = self.device and self.device[0][0] or 'desktop'
+ sessions = DocType("Sessions")
- rec = frappe.db.sql("""
- SELECT `user`, `sessiondata`
- FROM `tabSessions` WHERE `sid`=%s AND
- (NOW() - lastupdate) < %s
- """, (self.sid, get_expiry_period_for_query(self.device)))
+ self.device = frappe.db.get_values(
+ sessions, filters=sessions.sid == self.sid, fieldname="device", no_order=True,
+ )
+ self.device = self.device and self.device[0][0] or 'desktop'
+ rec = frappe.db.get_values(
+ sessions,
+ filters=(sessions.sid == self.sid)
+ & (
+ PseudoColumn(f"({Now() - sessions.lastupdate})")
+ < get_expiry_period_for_query(self.device)
+ ),
+ fieldname=["user", "sessiondata"],
+ no_order=True,
+ )
if rec:
data = frappe._dict(frappe.safe_eval(rec and rec[0][1] or '{}'))
From b081496912e3821e6f8581686908572451220308 Mon Sep 17 00:00:00 2001
From: Aradhya-Tripathi
Date: Tue, 16 Nov 2021 22:44:51 +0530
Subject: [PATCH 014/246] fix: added additional conditions when filters are
None
---
frappe/database/database.py | 4 ++--
frappe/database/query.py | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 09d19f21de..18f5b4dda9 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -85,7 +85,7 @@ class Database(object):
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None,
- explain=False, run=True, pluck=False):
+ explain=False, run=True, pluck=False, **kwargs):
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
@@ -553,7 +553,7 @@ class Database(object):
field_objects.append(field)
criterion = self.query.build_conditions(
- table=doctype, filters=filters, orderby=order_by, for_update=for_update
+ table=doctype, filters=filters, orderby=order_by, for_update=for_update, **kwargs,
)
if isinstance(fields, (list, tuple)):
query = criterion.select(*field_objects)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 3545efb412..7341a7eb78 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -224,6 +224,7 @@ class Query:
"""
conditions = self.get_condition(table, **kwargs)
if not filters:
+ conditions = self.add_conditions(conditions, **kwargs)
return conditions
for key in filters:
From a3c992bce4e0165a92953b5db8e32c641525cf66 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 16 Nov 2021 23:41:23 +0530
Subject: [PATCH 015/246] fix: fixed order in get_values tests
---
frappe/tests/test_db.py | 15 +++++++++++----
1 file changed, 11 insertions(+), 4 deletions(-)
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 9077655dc9..19ff73fc95 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -24,8 +24,8 @@ class TestDB(unittest.TestCase):
self.assertNotEqual(frappe.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest")
self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
- self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"]), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
- self.assertEqual(frappe.db.get_value("User", {}, "Min(name)"), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
+ self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], no_order=True), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
+ self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", no_order=True), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower())
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
@@ -34,8 +34,15 @@ class TestDB(unittest.TestCase):
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name >= 't' ORDER BY MODIFIED DESC""")[0][0],
frappe.db.get_value("User", {"name": [">=", "t"]}))
- self.assertIn("concat_ws", frappe.db.get_value("User", filters={"name": "Administrator"}, fieldname=Concat_ws(" ", "LastName"), run=False).lower())
-
+ self.assertIn(
+ "concat_ws",
+ frappe.db.get_value(
+ "User",
+ filters={"name": "Administrator"},
+ fieldname=Concat_ws(" ", "LastName"),
+ run=False,
+ ).lower(),
+ )
def test_set_value(self):
todo1 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 1')).insert()
todo2 = frappe.get_doc(dict(doctype='ToDo', description = 'test_set_value 2')).insert()
From fcd480b2a82b036bb021bee391ce5e703caaff12 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Thu, 18 Nov 2021 09:56:41 +0530
Subject: [PATCH 016/246] refactor: Converted queries in translate
---
frappe/translate.py | 27 ++++++++++++++++++---------
1 file changed, 18 insertions(+), 9 deletions(-)
diff --git a/frappe/translate.py b/frappe/translate.py
index 03720f115d..2905af6490 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -457,17 +457,26 @@ def get_messages_from_workflow(doctype=None, app_name=None):
workflows.extend(frappe.get_all('Workflow', filters=fixture.get('filters')))
messages = []
+ document_state = DocType("Workflow Document State")
for w in workflows:
- states = frappe.db.sql(
- 'select distinct state from `tabWorkflow Document State` where parent=%s',
- (w['name'],), as_dict=True)
-
+ states = frappe.db.get_values(
+ document_state,
+ filters=document_state.parent == w["name"],
+ fieldname="state",
+ distinct=True,
+ as_dict=True,
+ no_order=True,
+ )
messages.extend([('Workflow: ' + w['name'], state['state']) for state in states if is_translatable(state['state'])])
-
- states = frappe.db.sql(
- 'select distinct message from `tabWorkflow Document State` where parent=%s and message is not null',
- (w['name'],), as_dict=True)
-
+ states = frappe.db.get_values(
+ document_state,
+ filters=(document_state.parent == w["name"])
+ & (document_state.message.isnotnull()),
+ fieldname="message",
+ distinct=True,
+ no_order=True,
+ as_dict=True,
+ )
messages.extend([("Workflow: " + w['name'], state['message'])
for state in states if is_translatable(state['message'])])
From ae59fd7c58495cccb2eeb0fef37923b3a6015f13 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Fri, 19 Nov 2021 22:58:32 +0530
Subject: [PATCH 017/246] feat: added no_order to execute
---
frappe/database/database.py | 10 +++++++---
frappe/model/db_query.py | 6 +++++-
2 files changed, 12 insertions(+), 4 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 6703ae0ff3..6de8c9e72a 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -395,7 +395,6 @@ class Database(object):
return self.value_cache[(doctype, filters, fieldname)]
if isinstance(filters, list):
- order_by = order_by or "modified_desc"
out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run, **kwargs)
else:
@@ -571,10 +570,15 @@ class Database(object):
def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True, **kwargs):
names = list(filter(None, names))
if names:
- return self.get_all(doctype,
+ return self.get_all(
+ doctype,
fields=field,
filters=names,
- debug=debug, as_list=1, run=run)
+ debug=debug,
+ as_list=1,
+ run=run,
+ **kwargs,
+ )
else:
return {}
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 6181832363..a6c502d129 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -35,7 +35,7 @@ class DatabaseQuery(object):
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
- run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List:
+ run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None, no_order=False) -> List:
if not ignore_permissions and \
not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \
not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype):
@@ -90,6 +90,7 @@ class DatabaseQuery(object):
self.run = run
self.strict = strict
self.ignore_ddl = ignore_ddl
+ self.no_order = no_order
# for contextual user permission check
# to determine which user permission is applicable on link field of specific doctype
@@ -128,6 +129,9 @@ class DatabaseQuery(object):
args.fields = 'distinct ' + args.fields
args.order_by = '' # TODO: recheck for alternative
+ if self.no_order:
+ args.order_by = ""
+
query = """select %(fields)s
from %(tables)s
%(conditions)s
From ed7b3f54a7453d2be9b8f25b8738649f377afcf5 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Sat, 20 Nov 2021 13:21:01 +0530
Subject: [PATCH 018/246] refactor: converted more queries
---
frappe/translate.py | 11 ++++++++---
frappe/twofactor.py | 2 +-
2 files changed, 9 insertions(+), 4 deletions(-)
diff --git a/frappe/translate.py b/frappe/translate.py
index 2905af6490..23072d064c 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -480,9 +480,14 @@ def get_messages_from_workflow(doctype=None, app_name=None):
messages.extend([("Workflow: " + w['name'], state['message'])
for state in states if is_translatable(state['message'])])
- actions = frappe.db.sql(
- 'select distinct action from `tabWorkflow Transition` where parent=%s',
- (w['name'],), as_dict=True)
+ actions = frappe.db.get_values(
+ "Workflow Transition",
+ filters={"parent": w["name"]},
+ fieldname="action",
+ as_dict=True,
+ distinct=True,
+ no_order=True,
+ )
messages.extend([("Workflow: " + w['name'], action['action']) \
for action in actions if is_translatable(action['action'])])
diff --git a/frappe/twofactor.py b/frappe/twofactor.py
index 6ae53ed717..bd49d588b0 100644
--- a/frappe/twofactor.py
+++ b/frappe/twofactor.py
@@ -417,4 +417,4 @@ def reset_otp_secret(user):
enqueue(method=frappe.sendmail, queue='short', timeout=300, event=None, is_async=True, job_name=None, now=False, **email_args)
return frappe.msgprint(_("OTP Secret has been reset. Re-registration will be required on next login."))
else:
- return frappe.throw(_("OTP secret can only be reset by the Administrator."))
+ return frappe.throw(_("OTP secret can only be reset by the Administrator."))
\ No newline at end of file
From 1bdff9f3bdd7c579ba5d4a089b5f7362cf0247d1 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Sat, 20 Nov 2021 21:33:03 +0530
Subject: [PATCH 019/246] refactor: converted queries in init
---
frappe/__init__.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 4218aa113b..7db70ed39b 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -790,7 +790,9 @@ def has_website_permission(doc=None, ptype='read', user=None, verbose=False, doc
def is_table(doctype):
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
def get_tables():
- return db.sql_list("select name from tabDocType where istable=1")
+ return db.get_values(
+ "DocType", filters={"istable": 1}, no_order=True, pluck=True
+ )
tables = cache().get_value("is_table", get_tables)
return doctype in tables
From ae68ad53a7778dc6df97d887ef2c5bacaea66d5e Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Sat, 20 Nov 2021 23:51:06 +0530
Subject: [PATCH 020/246] feat: Added permissions to database API
---
frappe/database/database.py | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 6de8c9e72a..6d6ed467b6 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -116,6 +116,10 @@ class Database(object):
if not run:
return query
+ if not kwargs.get("ignore_permissions", True):
+ tables = self.get_tables_from_query(query)
+ self.check_permissions(doctype=tables, **kwargs)
+
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -260,6 +264,24 @@ class Database(object):
self.commit()
self.sql(query, debug=debug)
+ @staticmethod
+ def check_permissions(doctype, **kwargs):
+ kwargs.pop("ignore_permissions")
+ if isinstance(doctype, str):
+ doctype = [doctype]
+
+ for dt in doctype:
+ dt = re.sub("tab", "", dt)
+ if not frappe.has_permission(
+ dt, "select", **kwargs
+ ) and not frappe.has_permission(dt, "read", **kwargs):
+ frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(dt))
+ raise frappe.PermissionError(dt)
+
+ @staticmethod
+ def get_tables_from_query(query: str):
+ return [table for table in re.findall(r"\w+", query) if table.startswith("tab")]
+
def check_transaction_status(self, query):
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
executed in one transaction. This is to ensure that writes are always flushed otherwise this
From aede12d8b7af65b85269273da1deff1dd3400539 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Tue, 23 Nov 2021 10:05:22 +0530
Subject: [PATCH 021/246] feat: letter head tour
---
.../form_tour/letter_head/letter_head.json | 53 +++++++++++++++++++
frappe/public/js/frappe/form/form_tour.js | 40 ++++++++++++--
2 files changed, 89 insertions(+), 4 deletions(-)
create mode 100644 frappe/printing/form_tour/letter_head/letter_head.json
diff --git a/frappe/printing/form_tour/letter_head/letter_head.json b/frappe/printing/form_tour/letter_head/letter_head.json
new file mode 100644
index 0000000000..66730b4e38
--- /dev/null
+++ b/frappe/printing/form_tour/letter_head/letter_head.json
@@ -0,0 +1,53 @@
+{
+ "creation": "2021-11-22 15:26:53.878805",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-22 15:26:53.878805",
+ "modified_by": "Administrator",
+ "module": "Printing",
+ "name": "Letter Head",
+ "owner": "Administrator",
+ "reference_doctype": "Letter Head",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Let's name your first Letter Head with your company's name",
+ "field": "",
+ "fieldname": "letter_head_name",
+ "fieldtype": "Data",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Letter Head Name",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Letter Head Name"
+ },
+ {
+ "description": "Select the image containing only header part of your letter Head.",
+ "field": "",
+ "fieldname": "image",
+ "fieldtype": "Attach Image",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Image",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Image"
+ },
+ {
+ "description": "You can mark the Letter Head as default",
+ "field": "",
+ "fieldname": "is_default",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Letter Head",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Default Letter Head"
+ }
+ ],
+ "title": "Letter Head"
+}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 0694aa634a..9bae776d66 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -42,7 +42,7 @@ frappe.ui.form.FormTour = class FormTour {
this.tour = { steps: frappe.tour[this.frm.doctype] };
}
}
-
+
if (on_finish) this.on_finish = on_finish;
this.init_driver();
@@ -65,9 +65,10 @@ frappe.ui.form.FormTour = class FormTour {
const driver_step = this.get_step(step, on_next);
this.driver_steps.push(driver_step);
-
+
if (step.fieldtype == 'Table') this.handle_table_step(step);
if (step.is_table_field) this.handle_child_table_step(step);
+ //if (step.fieldtype == 'Attach Image') this.handle_attach_image_steps(step);
});
if (this.tour.save_on_complete) {
@@ -139,7 +140,7 @@ frappe.ui.form.FormTour = class FormTour {
const is_next_field_in_curr_table = next_step.parent_field == curr_step.field;
if (!is_next_field_in_curr_table) return;
-
+
const rows = this.frm.doc[curr_step.fieldname];
const table_has_rows = rows && rows.length > 0;
if (table_has_rows) {
@@ -242,6 +243,7 @@ frappe.ui.form.FormTour = class FormTour {
}
add_step_to_save() {
+ console.log("save")
const page_id = `[id="page-${this.frm.doctype}"]`;
const $save_btn = `${page_id} .standard-actions .primary-action`;
const save_step = {
@@ -262,4 +264,34 @@ frappe.ui.form.FormTour = class FormTour {
this.driver_steps.push(save_step);
frappe.ui.form.on(this.frm.doctype, 'after_save', () => this.on_finish && this.on_finish());
}
-};
\ No newline at end of file
+
+ handle_attach_image_steps() {
+ $('.btn-attach').one('click', () => {
+ frappe.utils.sleep(300)
+ setTimeout(() => {
+ const modal_element = $(".file-uploader").closest(".modal-content");
+ modal_element.css("z-index", "1000004 !important");
+ const attach_dialog_step = {
+ element: modal_element[0],
+ allowClose: false,
+ overlayClickNext: false,
+ popover: {
+ title: __("Select an Image"),
+ description: "",
+ position: "left",
+ doneBtnText: __("Next")
+ }
+ };
+
+ this.driver_steps.splice(this.driver.currentStep + 1, 0, attach_dialog_step);
+ this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM
+ frappe.utils.sleep(300).then(() => this.driver.start(this.driver.currentStep + 1));
+ console.log('click', this.driver_steps)
+ }, 1000);
+
+ modal_element.on('hidden.bs.modal', () => {
+ this.driver.moveNext();
+ })
+ })
+ }
+};
From 30278a393936ae4397c9b758ba6b5aab8d6b65ef Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 13:29:59 +0530
Subject: [PATCH 022/246] refactor: removed no_order to support order_by None
as valid input
---
frappe/database/database.py | 7 +++----
frappe/model/db_query.py | 16 +++++++---------
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 6d6ed467b6..6978c31182 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -392,7 +392,7 @@ class Database(object):
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, update=None, cache=False, for_update=False, run=True, **kwargs):
+ debug=False, order_by="default_ordering", update=None, cache=False, for_update=False, run=True, **kwargs):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -429,9 +429,8 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
- if not kwargs.get("no_order"):
- order_by = order_by or "modified"
- kwargs.pop("no_order", None)
+ if order_by:
+ order_by = "modified" if order_by == "default_ordering" else order_by
out = self._get_values_from_table(
fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run, **kwargs
)
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index a6c502d129..c94819a8c3 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -29,13 +29,13 @@ class DatabaseQuery(object):
self.reference_doctype = None
def execute(self, fields=None, filters=None, or_filters=None,
- docstatus=None, group_by=None, order_by=None, limit_start=False,
+ docstatus=None, group_by=None, order_by="default_ordering", limit_start=False,
limit_page_length=None, as_list=False, with_childnames=False, debug=False,
ignore_permissions=False, user=None, with_comment_count=False,
join='left join', distinct=False, start=None, page_length=None, limit=None,
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False,
update=None, add_total_row=None, user_settings=None, reference_doctype=None,
- run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None, no_order=False) -> List:
+ run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List:
if not ignore_permissions and \
not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \
not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype):
@@ -90,7 +90,6 @@ class DatabaseQuery(object):
self.run = run
self.strict = strict
self.ignore_ddl = ignore_ddl
- self.no_order = no_order
# for contextual user permission check
# to determine which user permission is applicable on link field of specific doctype
@@ -129,9 +128,6 @@ class DatabaseQuery(object):
args.fields = 'distinct ' + args.fields
args.order_by = '' # TODO: recheck for alternative
- if self.no_order:
- args.order_by = ""
-
query = """select %(fields)s
from %(tables)s
%(conditions)s
@@ -707,7 +703,7 @@ class DatabaseQuery(object):
def set_order_by(self, args):
meta = frappe.get_meta(self.doctype)
- if self.order_by:
+ if self.order_by and self.order_by != "default_ordering":
args.order_by = self.order_by
else:
args.order_by = ""
@@ -733,11 +729,13 @@ class DatabaseQuery(object):
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
- args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
+ if self.order_by:
+ args.order_by = f"`tab{self.doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
# draft docs always on top
if hasattr(meta, 'is_submittable') and meta.is_submittable:
- args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}"
+ if self.order_by:
+ args.order_by = f"`tab{self.doctype}`.docstatus asc, {args.order_by}"
def validate_order_by_and_group_by(self, parameters):
"""Check order by, group by so that atleast one column is selected and does not have subquery"""
From 9ccf467acb917d32d45e193e13b838262937c6de Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 13:30:42 +0530
Subject: [PATCH 023/246] fix: fixed no order arg in converted queries
---
frappe/__init__.py | 2 +-
frappe/sessions.py | 6 +++---
frappe/tests/test_db.py | 4 ++--
frappe/translate.py | 6 +++---
4 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 7db70ed39b..f3e36c3b3b 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -791,7 +791,7 @@ def is_table(doctype):
"""Returns True if `istable` property (indicating child Table) is set for given DocType."""
def get_tables():
return db.get_values(
- "DocType", filters={"istable": 1}, no_order=True, pluck=True
+ "DocType", filters={"istable": 1}, order_by=None, pluck=True
)
tables = cache().get_value("is_table", get_tables)
diff --git a/frappe/sessions.py b/frappe/sessions.py
index cbe20cffb3..9034d94f72 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -112,7 +112,7 @@ def get_expired_sessions():
)
& (sessions.device == device),
fieldname="sid",
- no_order=True,
+ order_by=None,
pluck=True,
)
)
@@ -323,7 +323,7 @@ class Session:
sessions = DocType("Sessions")
self.device = frappe.db.get_values(
- sessions, filters=sessions.sid == self.sid, fieldname="device", no_order=True,
+ sessions, filters=sessions.sid == self.sid, fieldname="device", order_by=None,
)
self.device = self.device and self.device[0][0] or 'desktop'
rec = frappe.db.get_values(
@@ -334,7 +334,7 @@ class Session:
< get_expiry_period_for_query(self.device)
),
fieldname=["user", "sessiondata"],
- no_order=True,
+ order_by=None,
)
if rec:
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index b39bedc4e5..472528b174 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -24,8 +24,8 @@ class TestDB(unittest.TestCase):
self.assertNotEqual(frappe.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest")
self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
- self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], no_order=True), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
- self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", no_order=True), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
+ self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], order_by=True), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
+ self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", order_by=True), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower())
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
diff --git a/frappe/translate.py b/frappe/translate.py
index 23072d064c..c5ef24bc2a 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -465,7 +465,7 @@ def get_messages_from_workflow(doctype=None, app_name=None):
fieldname="state",
distinct=True,
as_dict=True,
- no_order=True,
+ order_by=None,
)
messages.extend([('Workflow: ' + w['name'], state['state']) for state in states if is_translatable(state['state'])])
states = frappe.db.get_values(
@@ -474,7 +474,7 @@ def get_messages_from_workflow(doctype=None, app_name=None):
& (document_state.message.isnotnull()),
fieldname="message",
distinct=True,
- no_order=True,
+ order_by=None,
as_dict=True,
)
messages.extend([("Workflow: " + w['name'], state['message'])
@@ -486,7 +486,7 @@ def get_messages_from_workflow(doctype=None, app_name=None):
fieldname="action",
as_dict=True,
distinct=True,
- no_order=True,
+ order_by=None,
)
messages.extend([("Workflow: " + w['name'], action['action']) \
From c4f76c056890f07dd9ed526ef9d0859fe96742a3 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 14:21:20 +0530
Subject: [PATCH 024/246] fix: fixed order_by arg in get_value
---
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 6978c31182..35d054e5ca 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -357,7 +357,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by=None, cache=False, for_update=False, run=True, **kwargs):
+ debug=False, order_by="default_ordering", cache=False, for_update=False, run=True, **kwargs):
"""Returns a document property or list of properties.
:param doctype: DocType name.
From 50db6d5dd4774830ccffbfe24d9465fc66092d52 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 14:47:03 +0530
Subject: [PATCH 025/246] refactor: refactored query in sessions
---
frappe/sessions.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 9034d94f72..ce7950c24e 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -324,8 +324,7 @@ class Session:
self.device = frappe.db.get_values(
sessions, filters=sessions.sid == self.sid, fieldname="device", order_by=None,
- )
- self.device = self.device and self.device[0][0] or 'desktop'
+ ) or 'desktop'
rec = frappe.db.get_values(
sessions,
filters=(sessions.sid == self.sid)
From 71a21f5d7d260a74183478fc712ab7111ed178f1 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Tue, 23 Nov 2021 15:21:46 +0530
Subject: [PATCH 026/246] feat: customizations onboarding
---
frappe/core/form_tour/doctype/doctype.json | 53 +++++++++++++
.../form_tour/custom_field/custom_field.json | 77 +++++++++++++++++++
.../customization/customization.json | 41 ++++++++++
.../custom_doctype/custom_doctype.json | 21 +++++
.../custom_field/custom_field.json | 21 +++++
.../naming_series/naming_series.json | 20 +++++
.../print_format/print_format.json | 21 +++++
.../role_permissions/role_permissions.json | 20 +++++
.../onboarding_step/workflows/workflows.json | 20 +++++
9 files changed, 294 insertions(+)
create mode 100644 frappe/core/form_tour/doctype/doctype.json
create mode 100644 frappe/custom/form_tour/custom_field/custom_field.json
create mode 100644 frappe/custom/module_onboarding/customization/customization.json
create mode 100644 frappe/custom/onboarding_step/custom_doctype/custom_doctype.json
create mode 100644 frappe/custom/onboarding_step/custom_field/custom_field.json
create mode 100644 frappe/custom/onboarding_step/naming_series/naming_series.json
create mode 100644 frappe/custom/onboarding_step/print_format/print_format.json
create mode 100644 frappe/custom/onboarding_step/role_permissions/role_permissions.json
create mode 100644 frappe/custom/onboarding_step/workflows/workflows.json
diff --git a/frappe/core/form_tour/doctype/doctype.json b/frappe/core/form_tour/doctype/doctype.json
new file mode 100644
index 0000000000..866d3ea508
--- /dev/null
+++ b/frappe/core/form_tour/doctype/doctype.json
@@ -0,0 +1,53 @@
+{
+ "creation": "2021-11-23 12:38:52.807353",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-23 15:13:02.794031",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Doctype",
+ "owner": "Administrator",
+ "reference_doctype": "DocType",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a Module to which this Doctype would belong",
+ "field": "",
+ "fieldname": "module",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Module",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Module"
+ },
+ {
+ "description": "Check this to make the Docytpe as Custom",
+ "field": "",
+ "fieldname": "custom",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Custom?",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Custom "
+ },
+ {
+ "description": "Add fields to this Custom Doctype",
+ "field": "",
+ "fieldname": "fields",
+ "fieldtype": "Table",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Fields",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Fields"
+ }
+ ],
+ "title": "Doctype"
+}
\ No newline at end of file
diff --git a/frappe/custom/form_tour/custom_field/custom_field.json b/frappe/custom/form_tour/custom_field/custom_field.json
new file mode 100644
index 0000000000..58656a3386
--- /dev/null
+++ b/frappe/custom/form_tour/custom_field/custom_field.json
@@ -0,0 +1,77 @@
+{
+ "creation": "2021-11-23 12:22:32.922700",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-11-23 12:28:09.923397",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "reference_doctype": "Custom Field",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a Document for which you want the Custom Field",
+ "field": "",
+ "fieldname": "dt",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Document",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Document"
+ },
+ {
+ "description": "Enter a Label for this field",
+ "field": "",
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Label",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Label"
+ },
+ {
+ "description": "Select an appropriate Field Type that suits your requirements",
+ "field": "",
+ "fieldname": "fieldtype",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Field Type",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Field Type"
+ },
+ {
+ "description": "Select the label after which you want to insert new field.",
+ "field": "",
+ "fieldname": "insert_after",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Insert After",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Insert After"
+ },
+ {
+ "description": "Check this to make it a mandatory field",
+ "field": "",
+ "fieldname": "reqd",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Is Mandatory Field",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Is Mandatory Field"
+ }
+ ],
+ "title": "Custom Field"
+}
\ No newline at end of file
diff --git a/frappe/custom/module_onboarding/customization/customization.json b/frappe/custom/module_onboarding/customization/customization.json
new file mode 100644
index 0000000000..db2654443d
--- /dev/null
+++ b/frappe/custom/module_onboarding/customization/customization.json
@@ -0,0 +1,41 @@
+{
+ "allow_roles": [
+ {
+ "role": "All"
+ }
+ ],
+ "creation": "2021-11-23 12:21:11.384229",
+ "docstatus": 0,
+ "doctype": "Module Onboarding",
+ "documentation_url": "https://docs.erpnext.com/docs/v13/user/manual/en/customize-erpnext",
+ "idx": 0,
+ "is_complete": 0,
+ "modified": "2021-11-23 15:04:15.826436",
+ "modified_by": "Administrator",
+ "module": "Custom",
+ "name": "Customization",
+ "owner": "Administrator",
+ "steps": [
+ {
+ "step": "Custom Field"
+ },
+ {
+ "step": "Custom Doctype"
+ },
+ {
+ "step": "Naming Series"
+ },
+ {
+ "step": "Workflows"
+ },
+ {
+ "step": "Role Permissions"
+ },
+ {
+ "step": "Print Format"
+ }
+ ],
+ "subtitle": "Custom Field, Custom Doctype, Naming Series, Role Permission, Workflow, Print Formats, Reports",
+ "success_message": "Customization onboarding is all done!",
+ "title": "Customization"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/custom_doctype/custom_doctype.json b/frappe/custom/onboarding_step/custom_doctype/custom_doctype.json
new file mode 100644
index 0000000000..1f8601abee
--- /dev/null
+++ b/frappe/custom/onboarding_step/custom_doctype/custom_doctype.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Learn more about creating new DocTypes",
+ "creation": "2021-11-23 12:30:04.407568",
+ "description": "A DocType (Document Type) is used to insert forms in ERPNext. Forms such as Customer, Orders, and Invoices are Doctypes in the backend. You can also create new DocTypes to create new forms in ERPNext as per your business needs.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 12:30:04.407568",
+ "modified_by": "Administrator",
+ "name": "Custom Doctype",
+ "owner": "Administrator",
+ "reference_document": "DocType",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Custom Document Types",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/custom_field/custom_field.json b/frappe/custom/onboarding_step/custom_field/custom_field.json
new file mode 100644
index 0000000000..4044cf2456
--- /dev/null
+++ b/frappe/custom/onboarding_step/custom_field/custom_field.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Learn how to add Custom Fields",
+ "creation": "2021-11-23 12:21:09.479808",
+ "description": "Every form in ERPNext has a standard set of fields. If you need to capture some information, but there is no standard Field available for it, you can insert Custom Field for it.\n\nOnce custom fields are added, you can use them for reports and analytics charts as well.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 12:21:09.479808",
+ "modified_by": "Administrator",
+ "name": "Custom Field",
+ "owner": "Administrator",
+ "reference_document": "Custom Field",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Create Custom Fields",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/naming_series/naming_series.json b/frappe/custom/onboarding_step/naming_series/naming_series.json
new file mode 100644
index 0000000000..d24bf340c2
--- /dev/null
+++ b/frappe/custom/onboarding_step/naming_series/naming_series.json
@@ -0,0 +1,20 @@
+{
+ "action": "Watch Video",
+ "creation": "2021-11-23 13:57:45.091427",
+ "description": "Each document created in ERPNext can have a unique ID generated for it, using a prefix defined for it. Though each document has some prefix pre-configured, you can further customize it using tools like Naming Series Tool and Document Naming Rule.\n",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 13:57:45.091427",
+ "modified_by": "Administrator",
+ "name": "Naming Series",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Setup Naming Series",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/IGyISSfI1qU"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/print_format/print_format.json b/frappe/custom/onboarding_step/print_format/print_format.json
new file mode 100644
index 0000000000..681ef85b95
--- /dev/null
+++ b/frappe/custom/onboarding_step/print_format/print_format.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Learn about Standard and Custom Print Formats",
+ "creation": "2021-11-23 15:04:12.728513",
+ "description": "Print Formats allow you can define looks for documents when printed or converted to PDF. You can also create a custom Print Format using drag-and-drop tools.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 15:04:12.728513",
+ "modified_by": "Administrator",
+ "name": "Print Format",
+ "owner": "Administrator",
+ "reference_document": "Print Format",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Customize Print Formats",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/role_permissions/role_permissions.json b/frappe/custom/onboarding_step/role_permissions/role_permissions.json
new file mode 100644
index 0000000000..9d903b8bab
--- /dev/null
+++ b/frappe/custom/onboarding_step/role_permissions/role_permissions.json
@@ -0,0 +1,20 @@
+{
+ "action": "Watch Video",
+ "creation": "2021-11-23 14:00:27.208500",
+ "description": "In ERPNext, you can add your Employees as Users, and give them restricted access. Tools like Role Permission and User Permission allow you to define rules which give restricted access to the user to masters and transactions.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 14:00:27.208500",
+ "modified_by": "Administrator",
+ "name": "Role Permissions",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Setup Limited Access for a User",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/g3mk45o1zAg"
+}
\ No newline at end of file
diff --git a/frappe/custom/onboarding_step/workflows/workflows.json b/frappe/custom/onboarding_step/workflows/workflows.json
new file mode 100644
index 0000000000..060e9ae87d
--- /dev/null
+++ b/frappe/custom/onboarding_step/workflows/workflows.json
@@ -0,0 +1,20 @@
+{
+ "action": "Watch Video",
+ "creation": "2021-11-23 13:58:58.530044",
+ "description": "Workflows allow you to define custom rules for the approval process of a particular document in ERPNext. You can also set complex Workflow Rules and set approval conditions.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-11-23 13:58:58.530044",
+ "modified_by": "Administrator",
+ "name": "Workflows",
+ "owner": "Administrator",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Setup Approval Workflows",
+ "validate_action": 1,
+ "video_url": "https://youtu.be/yObJUg9FxFs"
+}
\ No newline at end of file
From 59f54aa60c1b1ed5365d4d54353c36d4216a160a Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 14:52:05 +0530
Subject: [PATCH 027/246] refactor: changed default_ordering to
keep_default_ordering
---
frappe/database/database.py | 6 +++---
frappe/sessions.py | 3 ++-
frappe/tests/test_db.py | 4 ++--
3 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 35d054e5ca..8f5a0d1a1f 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -357,7 +357,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by="default_ordering", cache=False, for_update=False, run=True, **kwargs):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, run=True, **kwargs):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@@ -392,7 +392,7 @@ class Database(object):
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by="default_ordering", update=None, cache=False, for_update=False, run=True, **kwargs):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, run=True, **kwargs):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -430,7 +430,7 @@ class Database(object):
if (filters is not None) and (filters!=doctype or doctype=="DocType"):
try:
if order_by:
- order_by = "modified" if order_by == "default_ordering" else order_by
+ order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
out = self._get_values_from_table(
fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run, **kwargs
)
diff --git a/frappe/sessions.py b/frappe/sessions.py
index ce7950c24e..6a192ee010 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -324,7 +324,8 @@ class Session:
self.device = frappe.db.get_values(
sessions, filters=sessions.sid == self.sid, fieldname="device", order_by=None,
- ) or 'desktop'
+ )
+ self.device = self.device and self.device[0][0] or "desktop"
rec = frappe.db.get_values(
sessions,
filters=(sessions.sid == self.sid)
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 472528b174..60c8db6ab6 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -24,8 +24,8 @@ class TestDB(unittest.TestCase):
self.assertNotEqual(frappe.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest")
self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
- self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], order_by=True), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
- self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", order_by=True), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
+ self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], order_by=None), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
+ self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", order_by=None), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower())
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
From d55d19ea5053f695ad29e8ed9db92df508ff161b Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 15:44:41 +0530
Subject: [PATCH 028/246] refactor: using throw instead of raise
---
frappe/database/database.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 8f5a0d1a1f..1cf434947e 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -275,8 +275,7 @@ class Database(object):
if not frappe.has_permission(
dt, "select", **kwargs
) and not frappe.has_permission(dt, "read", **kwargs):
- frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(dt))
- raise frappe.PermissionError(dt)
+ frappe.throw(_(f"Insufficient Permission for {frappe.bold(dt)}"))
@staticmethod
def get_tables_from_query(query: str):
From 08d94d991fd06c071c83edfde9cb107b385bd533 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 15:49:16 +0530
Subject: [PATCH 029/246] refactor: changed query in sessions
---
frappe/sessions.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/frappe/sessions.py b/frappe/sessions.py
index 6a192ee010..f0609cd74e 100644
--- a/frappe/sessions.py
+++ b/frappe/sessions.py
@@ -322,10 +322,9 @@ class Session:
def get_session_data_from_db(self):
sessions = DocType("Sessions")
- self.device = frappe.db.get_values(
+ self.device = frappe.db.get_value(
sessions, filters=sessions.sid == self.sid, fieldname="device", order_by=None,
- )
- self.device = self.device and self.device[0][0] or "desktop"
+ ) or "desktop"
rec = frappe.db.get_values(
sessions,
filters=(sessions.sid == self.sid)
From 7a0c06b46a09ebdd1721d9d5146bfe7f6d8bb6f1 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Tue, 23 Nov 2021 15:56:55 +0530
Subject: [PATCH 030/246] fix: fixed semgrep issues
---
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 1cf434947e..52ce173cee 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -275,7 +275,7 @@ class Database(object):
if not frappe.has_permission(
dt, "select", **kwargs
) and not frappe.has_permission(dt, "read", **kwargs):
- frappe.throw(_(f"Insufficient Permission for {frappe.bold(dt)}"))
+ frappe.throw(_("Insufficient Permission for {0}").format(frappe.bold(dt)))
@staticmethod
def get_tables_from_query(query: str):
From f947fb9cf24922746eab35a85cebcb0f2ce4a3e6 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Tue, 23 Nov 2021 17:36:08 +0530
Subject: [PATCH 031/246] fix: attach image modal for tour
---
frappe/public/js/frappe/form/form_tour.js | 20 +++++++++-----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 9bae776d66..7946495b45 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -68,7 +68,7 @@ frappe.ui.form.FormTour = class FormTour {
if (step.fieldtype == 'Table') this.handle_table_step(step);
if (step.is_table_field) this.handle_child_table_step(step);
- //if (step.fieldtype == 'Attach Image') this.handle_attach_image_steps(step);
+ if (step.fieldtype == 'Attach Image') this.handle_attach_image_steps(step);
});
if (this.tour.save_on_complete) {
@@ -243,7 +243,6 @@ frappe.ui.form.FormTour = class FormTour {
}
add_step_to_save() {
- console.log("save")
const page_id = `[id="page-${this.frm.doctype}"]`;
const $save_btn = `${page_id} .standard-actions .primary-action`;
const save_step = {
@@ -267,10 +266,8 @@ frappe.ui.form.FormTour = class FormTour {
handle_attach_image_steps() {
$('.btn-attach').one('click', () => {
- frappe.utils.sleep(300)
setTimeout(() => {
const modal_element = $(".file-uploader").closest(".modal-content");
- modal_element.css("z-index", "1000004 !important");
const attach_dialog_step = {
element: modal_element[0],
allowClose: false,
@@ -285,13 +282,14 @@ frappe.ui.form.FormTour = class FormTour {
this.driver_steps.splice(this.driver.currentStep + 1, 0, attach_dialog_step);
this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM
- frappe.utils.sleep(300).then(() => this.driver.start(this.driver.currentStep + 1));
- console.log('click', this.driver_steps)
- }, 1000);
-
- modal_element.on('hidden.bs.modal', () => {
this.driver.moveNext();
- })
- })
+ this.driver.overlay.refresh();
+
+ modal_element.closest('.modal').on('hidden.bs.modal', () => {
+ this.driver.moveNext();
+ });
+
+ }, 500);
+ });
}
};
From e0712171a6b09a2523a057baa2626c6f25b70eff Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Tue, 23 Nov 2021 15:54:00 +0100
Subject: [PATCH 032/246] fix: frappe-linter, all queries in email_account.py
and receive.py rewritten according to these specifications:
https://frappeframework.com/docs/user/en/api/query-builder
---
.../doctype/email_account/email_account.py | 44 ++++++++++++-------
frappe/email/receive.py | 32 ++++++++------
2 files changed, 47 insertions(+), 29 deletions(-)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index d5b683c9e3..59db7d04c7 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -549,7 +549,11 @@ class EmailAccount(Document):
def on_trash(self):
"""Clear communications where email account is linked"""
- frappe.db.sql("update `tabCommunication` set email_account='' where email_account=%s", self.name)
+ Communication = frappe.qb.from_("Communication")
+ frappe.qb.update(Communication) \
+ .set(Communication.email_account == "") \
+ .where(email_account == self.name).run()
+
remove_user_email_inbox(email_account=self.name)
def after_rename(self, old, new, merge=False):
@@ -571,9 +575,15 @@ class EmailAccount(Document):
if not self.use_imap:
return
- flags = frappe.db.sql("""select name, communication, uid, action, imap_folder from
- `tabEmail Flag Queue` where is_completed=0 and email_account={email_account} and imap_folder={folder_name}
- """.format(email_account=frappe.db.escape(self.name),folder_name=frappe.db.escape(folder_name)), as_dict=True)
+ EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
+ flags = (
+ frappe.qb.from_(EmailFlagQueue)
+ .select(EmailFlagQueue.name, EmailFlagQueue.communication, EmailFlagQueue.uid,
+ EmailFlagQueue.action, EmailFlagQueue.imap_folder)
+ .where(EmailFlagQueue.is_completed == 0)
+ .where(EmailFlagQueue.email_account == frappe.db.escape(self.name))
+ .where(EmailFlagQueue.folder_name == frappe.db.escape(folder_name))
+ ).run(as_dict=True)
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
if flags and uid_list:
@@ -594,16 +604,20 @@ class EmailAccount(Document):
self.set_communication_seen_status(docnames, seen=0)
docnames = ",".join([ "'%s'"%flag.get("name") for flag in flags ])
- frappe.db.sql(""" update `tabEmail Flag Queue` set is_completed=1
- where name in ({docnames})""".format(docnames=docnames))
+
+ EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
+ frappe.qb.update(EmailFlagQueue) \
+ .set(EmailFlagQueue.is_completed, 1) \
+ .where(EmailFlagQueue.name.isin(docnames)).run()
def set_communication_seen_status(self, docnames, seen=0):
""" mark Email Flag Queue of self.email_account mails as read"""
if not docnames:
return
-
- frappe.db.sql(""" update `tabCommunication` set seen={seen}
- where name in ({docnames})""".format(docnames=docnames, seen=seen))
+ Communication = frappe.qb.from_("Communication")
+ frappe.qb.update(Communication) \
+ .set(Communication.seen == seen) \
+ .where(Communication.name.isin(docnames)).run()
def check_automatic_linking_email_account(self):
if self.enable_automatic_linking:
@@ -780,12 +794,12 @@ def setup_user_email_inbox(email_account, awaiting_password, email_id, enable_ou
update_user_email_settings = True
if update_user_email_settings:
- frappe.db.sql("""UPDATE `tabUser Email` SET awaiting_password = %(awaiting_password)s,
- enable_outgoing = %(enable_outgoing)s WHERE email_account = %(email_account)s""", {
- "email_account": email_account,
- "enable_outgoing": enable_outgoing,
- "awaiting_password": awaiting_password or 0
- })
+ UserEmail = frappe.qb.from_("User Email")
+ frappe.qb.update(UserEmail) \
+ .set(UserEmail.awaiting_password == awaiting_password or 0) \
+ .set(UserEmail.enable_outgoing == enable_outgoing) \
+ .where(UserEmail.email_account == email_account).run()
+
else:
users = " and ".join([frappe.bold(user.get("name")) for user in user_names])
frappe.msgprint(_("Enabled email inbox for user {0}").format(users))
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index e7989660b8..4f4ed6d48e 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -210,22 +210,26 @@ class EmailServer:
if not uid_validity or uid_validity != current_uid_validity:
# uidvalidity changed & all email uids are reindexed by server
- frappe.db.sql(
- """update `tabCommunication` set uid=-1 where communication_medium='Email'
- and email_account=%s""", (self.settings.email_account,)
- )
+ Communication = frappe.qb.DocType("Communication")
+ frappe.qb.update(Communication) \
+ .set(Communication.uid, -1) \
+ .where(Communication.communication_medium == "Email") \
+ .where(Communication.email_account == self.settings.email_account).run()
+
if self.settings.use_imap:
- # new update for the IMAP Folder DoctType
- frappe.db.sql(
- """update `tabIMAP Folder` set uidvalidity=%s, uidnext=%s where
- parent=%s and folder_name=%s""",
- (current_uid_validity, uidnext, self.settings.email_account_name, folder)
- )
+ # new update for the IMAP Folder DocType
+ IMAPFolder = frappe.qb.DocType("IMAP Folder")
+ frappe.qb.update(IMAPFolder) \
+ .set(IMAPFolder.uidvalidity, current_uid_validity) \
+ .set(IMAPFolder.uidnext, uidnext) \
+ .where(IMAPFolder.parent == self.settings.email_account_name) \
+ .where(IMAPFolder.folder_name == folder).run()
else:
- frappe.db.sql(
- """update `tabEmail Account` set uidvalidity=%s, uidnext=%s where
- name=%s""", (current_uid_validity, uidnext, self.settings.email_account)
- )
+ EmailAccount = frappe.qb.DocType("Email Account")
+ frappe.qb.update(EmailAccount) \
+ .set(EmailAccount.uidvalidity, current_uid_validity) \
+ .set(EmailAccount.uidnext, uidnext) \
+ .where(EmailAccount.name == self.settings.email_account_name).run()
# uid validity not found pulling emails for first time
if not uid_validity:
From 88e17771851f03bfd57f477cf3e8ec6a15e5f936 Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Tue, 23 Nov 2021 16:05:56 +0100
Subject: [PATCH 033/246] fix: frappe-linter, query rewritten
---
frappe/patches/v14_0/copy_mail_data.py | 17 +++++------------
1 file changed, 5 insertions(+), 12 deletions(-)
diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py
index d3a4baca88..8780ab8630 100644
--- a/frappe/patches/v14_0/copy_mail_data.py
+++ b/frappe/patches/v14_0/copy_mail_data.py
@@ -19,15 +19,8 @@ def execute():
})
doc.save()
-
- frappe.db.sql(
- """
- update
- `tabEmail Flag Queue`
- set
- imap_folder = "INBOX"
- where
- email_account = '%s'
- and imap_folder is NULL
- """ % (doc.name)
- )
+ EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
+ frappe.qb.update(EmailFlagQueue) \
+ .set(EmailFlagQueue.imap_folder, "INBOX") \
+ .where(EmailFlagQueue.email_account == doc.name) \
+ .where(EmailFlagQueue.imap_folder.isnull()).run()
From dbfb959e0c9ef85e37456aa9ae0c72ef919cb1c9 Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Tue, 23 Nov 2021 16:13:23 +0100
Subject: [PATCH 034/246] fix: sider
---
frappe/email/doctype/email_account/email_account.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 59db7d04c7..3f163547f3 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -552,7 +552,7 @@ class EmailAccount(Document):
Communication = frappe.qb.from_("Communication")
frappe.qb.update(Communication) \
.set(Communication.email_account == "") \
- .where(email_account == self.name).run()
+ .where(Communication.email_account == self.name).run()
remove_user_email_inbox(email_account=self.name)
@@ -578,8 +578,7 @@ class EmailAccount(Document):
EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
flags = (
frappe.qb.from_(EmailFlagQueue)
- .select(EmailFlagQueue.name, EmailFlagQueue.communication, EmailFlagQueue.uid,
- EmailFlagQueue.action, EmailFlagQueue.imap_folder)
+ .select(EmailFlagQueue.name, EmailFlagQueue.communication, EmailFlagQueue.uid, EmailFlagQueue.action, EmailFlagQueue.imap_folder)
.where(EmailFlagQueue.is_completed == 0)
.where(EmailFlagQueue.email_account == frappe.db.escape(self.name))
.where(EmailFlagQueue.folder_name == frappe.db.escape(folder_name))
From 9f1f3e345a438bbc25a97ddf45aaa4901bbd69d8 Mon Sep 17 00:00:00 2001
From: mtraeber
Date: Tue, 23 Nov 2021 16:19:00 +0100
Subject: [PATCH 035/246] fix: sider
---
frappe/email/doctype/email_account/email_account.py | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 3f163547f3..18c9ca1737 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -575,13 +575,13 @@ class EmailAccount(Document):
if not self.use_imap:
return
- EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
+ EmailFlagQ = frappe.qb.DocType("Email Flag Queue")
flags = (
- frappe.qb.from_(EmailFlagQueue)
- .select(EmailFlagQueue.name, EmailFlagQueue.communication, EmailFlagQueue.uid, EmailFlagQueue.action, EmailFlagQueue.imap_folder)
- .where(EmailFlagQueue.is_completed == 0)
- .where(EmailFlagQueue.email_account == frappe.db.escape(self.name))
- .where(EmailFlagQueue.folder_name == frappe.db.escape(folder_name))
+ frappe.qb.from_(EmailFlagQ)
+ .select(EmailFlagQ.name, EmailFlagQ.communication, EmailFlagQ.uid, EmailFlagQ.action, EmailFlagQ.imap_folder)
+ .where(EmailFlagQ.is_completed == 0)
+ .where(EmailFlagQ.email_account == frappe.db.escape(self.name))
+ .where(EmailFlagQ.folder_name == frappe.db.escape(folder_name))
).run(as_dict=True)
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
From 8ad52df6a47d77500aebbfca641d3f30e704917a Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 24 Nov 2021 11:57:56 +0530
Subject: [PATCH 036/246] feat: first document and include name field check in
form tour
---
frappe/desk/doctype/form_tour/form_tour.json | 24 ++++++++++++++++++--
1 file changed, 22 insertions(+), 2 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index e4ea528fcc..494a17bafb 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -9,8 +9,11 @@
"title",
"reference_doctype",
"module",
+ "column_break_6",
"is_standard",
"save_on_complete",
+ "first_document",
+ "include_name_field",
"section_break_3",
"steps"
],
@@ -62,11 +65,28 @@
"label": "Module",
"options": "Module Def",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "first_document",
+ "fieldtype": "Check",
+ "label": "Show First Document Tour"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.first_document",
+ "fieldname": "include_name_field",
+ "fieldtype": "Check",
+ "label": "Include Name Field"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-06-06 20:32:54.068774",
+ "modified": "2021-11-24 10:12:23.365136",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
@@ -88,4 +108,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
From 303edc8c0499eadb8e171132bbe820d011666819 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Wed, 24 Nov 2021 15:53:03 +0530
Subject: [PATCH 037/246] refactor: added get_sql function to query class
(encapsulation++)
---
frappe/database/database.py | 41 +++++---------------
frappe/database/query.py | 75 +++++++++++++++++++++++++++++++++----
2 files changed, 78 insertions(+), 38 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 52ce173cee..4ac86b76f2 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -116,10 +116,6 @@ class Database(object):
if not run:
return query
- if not kwargs.get("ignore_permissions", True):
- tables = self.get_tables_from_query(query)
- self.check_permissions(doctype=tables, **kwargs)
-
if re.search(r'ifnull\(', query, flags=re.IGNORECASE):
# replaces ifnull in query with coalesce
query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE)
@@ -264,22 +260,6 @@ class Database(object):
self.commit()
self.sql(query, debug=debug)
- @staticmethod
- def check_permissions(doctype, **kwargs):
- kwargs.pop("ignore_permissions")
- if isinstance(doctype, str):
- doctype = [doctype]
-
- for dt in doctype:
- dt = re.sub("tab", "", dt)
- if not frappe.has_permission(
- dt, "select", **kwargs
- ) and not frappe.has_permission(dt, "read", **kwargs):
- frappe.throw(_("Insufficient Permission for {0}").format(frappe.bold(dt)))
-
- @staticmethod
- def get_tables_from_query(query: str):
- return [table for table in re.findall(r"\w+", query) if table.startswith("tab")]
def check_transaction_status(self, query):
"""Raises exception if more than 20,000 `INSERT`, `UPDATE` queries are
@@ -571,19 +551,18 @@ class Database(object):
else:
field_objects.append(field)
- criterion = self.query.build_conditions(
- table=doctype, filters=filters, orderby=order_by, for_update=for_update, **kwargs,
+ query = self.query.get_sql(
+ table=doctype,
+ filters=filters,
+ orderby=order_by,
+ for_update=for_update,
+ field_objects=field_objects,
+ fields=fields,
+ **kwargs,
)
- if isinstance(fields, (list, tuple)):
- query = criterion.select(*field_objects)
+ if fields=="*":
+ as_dict = True
- elif isinstance(fields, Criterion):
- query = criterion.select(fields)
-
- else:
- if fields=="*":
- query = criterion.select(fields)
- as_dict = True
r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, **kwargs)
return r
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 7341a7eb78..c962fc3675 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -1,4 +1,5 @@
import operator
+import re
from typing import Any, Dict, List, Tuple, Union
import frappe
@@ -246,7 +247,12 @@ class Query:
conditions = self.add_conditions(conditions, **kwargs)
return conditions
- def build_conditions(self, table: str, filters: Union[Dict[str, Union[str, int]], str, int] = None, **kwargs) -> frappe.qb:
+ def build_conditions(
+ self,
+ table: str,
+ filters: Union[Dict[str, Union[str, int]], str, int] = None,
+ **kwargs
+ ) -> frappe.qb:
"""Build conditions for sql query
Args:
@@ -256,13 +262,68 @@ class Query:
Returns:
frappe.qb: frappe.qb conditions object
"""
- if isinstance(filters, Criterion):
- return self.criterion_query(table, filters, **kwargs)
-
if isinstance(filters, int) or isinstance(filters, str):
filters = {"name": str(filters)}
- if isinstance(filters, (list, tuple)):
- return self.misc_query(table, filters, **kwargs)
+ if isinstance(filters, Criterion):
+ criterion = self.criterion_query(table, filters, **kwargs)
- return self.dict_query(filters=filters, table=table, **kwargs)
+ elif isinstance(filters, (list, tuple)):
+ criterion = self.misc_query(table, filters, **kwargs)
+
+ else:
+ criterion = self.dict_query(filters=filters, table=table, **kwargs)
+
+ return criterion
+
+ def get_sql(
+ self,
+ table: str,
+ fields: Union[List, Tuple],
+ filters: Union[Dict[str, Union[str, int]], str, int] = None,
+ **kwargs
+ ):
+ criterion = self.build_conditions(table, filters, **kwargs)
+ if isinstance(fields, (list, tuple)):
+ query = criterion.select(*kwargs.get("field_objects"))
+
+ elif isinstance(fields, Criterion):
+ query = criterion.select(fields)
+
+ else:
+ if fields=="*":
+ query = criterion.select(fields)
+
+ return query
+
+
+class Permission:
+ @classmethod
+ def check_permissions(cls, query, **kwargs):
+ if not isinstance(query, str):
+ query = query.get_sql()
+
+ doctype = cls.get_tables_from_query(query)
+ if isinstance(doctype, str):
+ doctype = [doctype]
+
+ for dt in doctype:
+ dt = re.sub("tab", "", dt)
+ if not frappe.has_permission(
+ dt,
+ "select",
+ user=kwargs.get("user"),
+ parent_doctype=kwargs.get("parent_doctype"),
+ ) and not frappe.has_permission(
+ dt,
+ "read",
+ user=kwargs.get("user"),
+ parent_doctype=kwargs.get("parent_doctype"),
+ ):
+ frappe.throw(
+ _("Insufficient Permission for {0}").format(frappe.bold(dt))
+ )
+
+ @staticmethod
+ def get_tables_from_query(query: str):
+ return [table for table in re.findall(r"\w+", query) if table.startswith("tab")]
From c9b05f7d95a51d39b65cc46f5603294de81f6ce5 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Wed, 24 Nov 2021 16:10:29 +0530
Subject: [PATCH 038/246] fix: fixed as_dict in get_values
---
frappe/database/database.py | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 4ac86b76f2..4b1f835218 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -560,7 +560,11 @@ class Database(object):
fields=fields,
**kwargs,
)
- if fields=="*":
+ if (
+ fields == "*"
+ and not isinstance(fields, (list, tuple))
+ and not isinstance(fields, Criterion)
+ ):
as_dict = True
r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, **kwargs)
From 7323689654714cd30fe552aa87579fc0946b612d Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 24 Nov 2021 16:13:04 +0530
Subject: [PATCH 039/246] fix: tour of existing document
---
frappe/core/form_tour/doctype/doctype.json | 4 ++-
frappe/desk/doctype/form_tour/form_tour.js | 16 +++++++++-
frappe/desk/doctype/form_tour/form_tour.json | 5 +--
frappe/public/js/frappe/form/form_tour.js | 2 --
.../js/frappe/widgets/onboarding_widget.js | 32 +++++++++++++++++--
5 files changed, 51 insertions(+), 8 deletions(-)
diff --git a/frappe/core/form_tour/doctype/doctype.json b/frappe/core/form_tour/doctype/doctype.json
index 866d3ea508..3b77241201 100644
--- a/frappe/core/form_tour/doctype/doctype.json
+++ b/frappe/core/form_tour/doctype/doctype.json
@@ -2,9 +2,11 @@
"creation": "2021-11-23 12:38:52.807353",
"docstatus": 0,
"doctype": "Form Tour",
+ "first_document": 0,
"idx": 0,
+ "include_name_field": 1,
"is_standard": 1,
- "modified": "2021-11-23 15:13:02.794031",
+ "modified": "2021-11-24 12:35:44.895630",
"modified_by": "Administrator",
"module": "Core",
"name": "Doctype",
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 8d70dcd3dc..2c6355749b 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -15,10 +15,13 @@ frappe.ui.form.on('Form Tour', {
frm.add_custom_button(__('Show Tour'), async () => {
const issingle = await check_if_single(frm.doc.reference_doctype);
+ const name = await get_first_document(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) {
+ route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
}
@@ -120,4 +123,15 @@ function get_child_field(child_table, child_name, fieldname) {
async function check_if_single(doctype) {
const { message } = await frappe.db.get_value('DocType', doctype, 'issingle');
return message.issingle || 0;
-}
\ No newline at end of file
+}
+
+async function get_first_document(doctype) {
+ let docname;
+
+ await frappe.db.get_list(doctype).then(res => {
+ if (Array.isArray(res) && res.length)
+ docname = res[0].name
+ });
+
+ return docname || 'new';
+}
diff --git a/frappe/desk/doctype/form_tour/form_tour.json b/frappe/desk/doctype/form_tour/form_tour.json
index 494a17bafb..6f3bd56a4e 100644
--- a/frappe/desk/doctype/form_tour/form_tour.json
+++ b/frappe/desk/doctype/form_tour/form_tour.json
@@ -86,10 +86,11 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-11-24 10:12:23.365136",
+ "modified": "2021-11-24 12:03:45.449311",
"modified_by": "Administrator",
"module": "Desk",
"name": "Form Tour",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -108,4 +109,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 9bae776d66..3aca58a159 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -243,7 +243,6 @@ frappe.ui.form.FormTour = class FormTour {
}
add_step_to_save() {
- console.log("save")
const page_id = `[id="page-${this.frm.doctype}"]`;
const $save_btn = `${page_id} .standard-actions .primary-action`;
const save_step = {
@@ -286,7 +285,6 @@ frappe.ui.form.FormTour = class FormTour {
this.driver_steps.splice(this.driver.currentStep + 1, 0, attach_dialog_step);
this.update_driver_steps(); // need to define again, since driver.js only considers steps which are inside DOM
frappe.utils.sleep(300).then(() => this.driver.start(this.driver.currentStep + 1));
- console.log('click', this.driver_steps)
}, 1000);
modal_element.on('hidden.bs.modal', () => {
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 7237de2fb6..9827e0911d 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -304,8 +304,9 @@ export default class OnboardingWidget extends Widget {
frappe.set_route("Form", step.reference_document);
}
- create_entry(step) {
+ async create_entry(step) {
let current_route = frappe.get_route();
+ let docname = await this.get_first_document(step.reference_document);
frappe.route_hooks = {};
frappe.route_hooks.after_load = (frm) => {
@@ -313,7 +314,19 @@ export default class OnboardingWidget extends Widget {
frappe.msgprint({
message: __("Awesome, now try making an entry yourself"),
title: __("Great"),
+ primary_action: {
+ action: () => {
+ frappe.set_route(current_route).then(() => {
+ this.mark_complete(step);
+ });
+ },
+ label: __("Continue"),
+ }
});
+
+ frappe.msg_dialog.custom_onhide = () => {
+ this.mark_complete(step);
+ };
};
frm.tour
.init({ on_finish })
@@ -351,7 +364,7 @@ export default class OnboardingWidget extends Widget {
frappe.route_hooks.after_save = callback;
}
- frappe.set_route('Form', step.reference_document, 'new');
+ frappe.set_route('Form', step.reference_document, docname);
}
show_quick_entry(step) {
@@ -552,4 +565,19 @@ export default class OnboardingWidget extends Widget {
}
});
}
+
+ async get_first_document(doctype) {
+ const { message } = await frappe.db.get_value('Form Tour', { 'reference_doctype': doctype }, ["first_document"])
+ let docname;
+
+ if (message.first_document) {
+ await frappe.db.get_list(doctype).then(res => {
+ if (Array.isArray(res) && res.length)
+ docname = res[0].name
+ });
+ }
+
+
+ return docname || 'new';
+ }
}
From 6a045f69f5d606fcf0c64aff9212b38dda1a25d8 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Wed, 24 Nov 2021 16:55:45 +0530
Subject: [PATCH 040/246] fix: fixed translate import
---
frappe/database/query.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index c962fc3675..69328cb206 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -3,7 +3,8 @@ import re
from typing import Any, Dict, List, Tuple, Union
import frappe
-from frappe.query_builder import Criterion, Order, Field
+from frappe import _
+from frappe.query_builder import Criterion, Field, Order
def like(key: str, value: str) -> frappe.qb:
From 64be2a343efded59830f6193933e487f586b7d1b Mon Sep 17 00:00:00 2001
From: Summayya
Date: Wed, 24 Nov 2021 18:49:28 +0530
Subject: [PATCH 041/246] fix: add condition to check allow_import
---
frappe/public/js/frappe/list/list_view.js | 4 ++--
frappe/public/js/frappe/model/model.js | 11 +++++++----
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 64530e15ef..0104fd44c1 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1484,8 +1484,8 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
get_menu_items() {
const doctype = this.doctype;
const items = [];
-
- if (frappe.model.can_import(doctype)) {
+
+ if (frappe.model.can_import(this.meta)) {
items.push({
label: __("Import"),
action: () =>
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 9e394a7433..37eba8d612 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -318,11 +318,14 @@ $.extend(frappe.model, {
},
can_import: function(doctype, frm) {
- // system manager can always import
- if(frappe.user_roles.includes("System Manager")) return true;
+ if(doctype.allow_import) {
+ // system manager can always import
+ if(frappe.user_roles.includes("System Manager")) return true;
- if(frm) return frm.perm[0].import===1;
- return frappe.boot.user.can_import.indexOf(doctype)!==-1;
+ if(frm) return frm.perm[0].import===1;
+ return frappe.boot.user.can_import.indexOf(doctype)!==-1;
+ }
+ else return false;
},
can_export: function(doctype, frm) {
From 7cc6da212219e59a1d9be45b68b9d64db1b665f0 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 24 Nov 2021 19:43:12 +0530
Subject: [PATCH 042/246] fix: sort result by creation
---
frappe/desk/doctype/form_tour/form_tour.js | 2 +-
frappe/public/js/frappe/widgets/onboarding_widget.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 2c6355749b..4aa1f7607f 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -128,7 +128,7 @@ async function check_if_single(doctype) {
async function get_first_document(doctype) {
let docname;
- await frappe.db.get_list(doctype).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/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 9827e0911d..1de44a89d5 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -571,7 +571,7 @@ export default class OnboardingWidget extends Widget {
let docname;
if (message.first_document) {
- await frappe.db.get_list(doctype).then(res => {
+ await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
if (Array.isArray(res) && res.length)
docname = res[0].name
});
From 5b9c4e57816a1d13eaf2282b4b86c2cfdda172c4 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 24 Nov 2021 20:11:29 +0530
Subject: [PATCH 043/246] fix: space and semicolon
---
frappe/desk/doctype/form_tour/form_tour.js | 4 ++--
frappe/public/js/frappe/widgets/onboarding_widget.js | 4 ++--
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/desk/doctype/form_tour/form_tour.js b/frappe/desk/doctype/form_tour/form_tour.js
index 4aa1f7607f..6a7c736fac 100644
--- a/frappe/desk/doctype/form_tour/form_tour.js
+++ b/frappe/desk/doctype/form_tour/form_tour.js
@@ -20,7 +20,7 @@ frappe.ui.form.on('Form Tour', {
if (issingle) {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype);
- } else if(frm.doc.first_document) {
+ } else if (frm.doc.first_document) {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, name);
} else {
route_changed = frappe.set_route('Form', frm.doc.reference_doctype, 'new');
@@ -130,7 +130,7 @@ async function get_first_document(doctype) {
await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
if (Array.isArray(res) && res.length)
- docname = res[0].name
+ docname = res[0].name;
});
return docname || 'new';
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 1de44a89d5..110d617f73 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -567,13 +567,13 @@ export default class OnboardingWidget extends Widget {
}
async get_first_document(doctype) {
- const { message } = await frappe.db.get_value('Form Tour', { 'reference_doctype': doctype }, ["first_document"])
+ const { message } = await frappe.db.get_value('Form Tour', { 'reference_doctype': doctype }, ["first_document"]);
let docname;
if (message.first_document) {
await frappe.db.get_list(doctype, { order_by: "creation" }).then(res => {
if (Array.isArray(res) && res.length)
- docname = res[0].name
+ docname = res[0].name;
});
}
From 088fdca74db53ffe48cc2357e85e2ee263bb5456 Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Mon, 22 Nov 2021 13:03:38 +0530
Subject: [PATCH 044/246] refactor: bench browse
* Manage colours through click
* Standardize the command's behaviour for consistency :')
* Use click instead of webbrowser module
* Simplify logic
---
frappe/commands/site.py | 50 ++++++++++++++++++++---------------------
1 file changed, 24 insertions(+), 26 deletions(-)
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 3c7f2f5525..fb8d7e0a6c 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -829,39 +829,37 @@ def publish_realtime(context, event, message, room, user, doctype, docname, afte
@pass_context
def browse(context, site, user=None):
'''Opens the site on web browser'''
- from frappe.auth import LoginManager
- from frappe.auth import CookieManager
- import webbrowser
+ from frappe.auth import CookieManager, LoginManager
- site = context.sites[0] if context.sites else site
+ site = get_site(context, raise_err=False) or site
if not site:
- click.echo('''Please provide site name\n\nUsage:\n\tbench browse [site-name]\nor\n\tbench --site [site-name] browse''')
- return
+ raise SiteNotSpecifiedError
- site = site.lower()
+ if site not in frappe.utils.get_sites():
+ click.echo(f"\nSite named {click.style(site, bold=True)} doesn't exist\n", err=True)
+ sys.exit(1)
- if site in frappe.utils.get_sites():
- frappe.init(site=site)
- frappe.connect()
+ frappe.init(site=site)
+ frappe.connect()
- sid = ''
- if user:
- if frappe.conf.developer_mode or user == "Administrator":
- frappe.utils.set_request(path="/")
- frappe.local.cookie_manager = CookieManager()
- frappe.local.login_manager = LoginManager()
- frappe.local.login_manager.login_as(user)
- sid = f'/app?sid={frappe.session.sid}'
- else:
- print("Please enable developer mode to login as a user")
+ sid = ''
+ if user:
+ if frappe.conf.developer_mode or user == "Administrator":
+ frappe.utils.set_request(path="/")
+ frappe.local.cookie_manager = CookieManager()
+ frappe.local.login_manager = LoginManager()
+ frappe.local.login_manager.login_as(user)
+ sid = f'/app?sid={frappe.session.sid}'
+ else:
+ click.echo("Please enable developer mode to login as a user")
- url = f'{frappe.utils.get_site_url(site)}{sid}'
- if user == "Administrator":
- print(f'Login URL: {url}')
- webbrowser.open(url, new=2)
- else:
- click.echo("\nSite named \033[1m{}\033[0m doesn't exist\n".format(site))
+ url = f'{frappe.utils.get_site_url(site)}{sid}'
+
+ if user == "Administrator":
+ click.echo(f'Login URL: {url}')
+
+ click.launch(url)
@click.command('start-recording')
From 5914f35bb761ec2d25c014eb349e7d96c727d490 Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Tue, 23 Nov 2021 11:49:19 +0530
Subject: [PATCH 045/246] fix: Change site archive path to archived/sites
---
frappe/commands/site.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index fb8d7e0a6c..6d3ed1af16 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -447,11 +447,10 @@ def disable_user(context, email):
@pass_context
def migrate(context, skip_failing=False, skip_search_index=False):
"Run patches, sync schema and rebuild files/translations"
- import re
from frappe.migrate import migrate
for site in context.sites:
- print('Migrating', site)
+ click.secho(f"Migrating {site}", fg="green")
frappe.init(site=site)
frappe.connect()
try:
@@ -697,8 +696,7 @@ def _drop_site(site, root_login='root', root_password=None, archived_sites_path=
drop_user_and_database(frappe.conf.db_name, root_login, root_password)
- if not archived_sites_path:
- archived_sites_path = os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived_sites')
+ archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites')
if not os.path.exists(archived_sites_path):
os.mkdir(archived_sites_path)
From 0657524888c2d148be4976d21b353f2f4e321cbe Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Thu, 25 Nov 2021 12:49:10 +0530
Subject: [PATCH 046/246] refactor: Retire color in favour of click.secho
* Add type hints for Command test suite
* Remove support utils - dead code
---
frappe/tests/test_commands.py | 41 +++++++----------------------------
1 file changed, 8 insertions(+), 33 deletions(-)
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index 94389cd7a3..14ed77eeeb 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -7,7 +7,7 @@ import os
import shlex
import shutil
import subprocess
-import sys
+from typing import List
import unittest
import glob
@@ -18,37 +18,11 @@ from frappe.installer import add_to_installed_apps, remove_app
from frappe.utils import add_to_date, get_bench_relative_path, now
from frappe.utils.backups import fetch_latest_backups
-
-# TODO: check frappe.cli.coloured_output to set coloured output!
-def supports_color():
- """
- Returns True if the running system's terminal supports color, and False
- otherwise.
- """
- plat = sys.platform
- supported_platform = plat != 'Pocket PC' and (plat != 'win32' or 'ANSICON' in os.environ)
- # isatty is not always implemented, #6223.
- is_a_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
- return supported_platform and is_a_tty
+# imports - third party imports
+import click
-class color(dict):
- nc = "\033[0m"
- blue = "\033[94m"
- green = "\033[92m"
- yellow = "\033[93m"
- red = "\033[91m"
- silver = "\033[90m"
-
- def __getattr__(self, key):
- if supports_color():
- ret = self.get(key)
- else:
- ret = ""
- return ret
-
-
-def clean(value):
+def clean(value) -> str:
"""Strips and converts bytes to str
Args:
@@ -64,7 +38,7 @@ def clean(value):
return value
-def missing_in_backup(doctypes, file):
+def missing_in_backup(doctypes: List, file: os.PathLike) -> List:
"""Returns list of missing doctypes in the backup.
Args:
@@ -86,7 +60,7 @@ def missing_in_backup(doctypes, file):
if predicate.format(doctype).lower() not in content]
-def exists_in_backup(doctypes, file):
+def exists_in_backup(doctypes: List, file: os.PathLike) -> bool:
"""Checks if the list of doctypes exist in the database.sql.gz file supplied
Args:
@@ -118,7 +92,8 @@ class BaseTestCommands(unittest.TestCase):
kwargs = site
self.command = " ".join(command.split()).format(**kwargs)
- print("{0}$ {1}{2}".format(color.silver, self.command, color.nc))
+ click.secho(self.command, fg="bright_black")
+
command = shlex.split(self.command)
self._proc = subprocess.run(command, input=cmd_input, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
self.stdout = clean(self._proc.stdout)
From f04b2157f199da71a53d0986dfba56e075a4fecd Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Thu, 25 Nov 2021 13:07:38 +0530
Subject: [PATCH 047/246] refactor: getting rid of kwargs
---
frappe/database/database.py | 16 ++++++++--------
1 file changed, 8 insertions(+), 8 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 4b1f835218..bfdaebde75 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -85,7 +85,7 @@ class Database(object):
def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0,
debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None,
- explain=False, run=True, pluck=False, **kwargs):
+ explain=False, run=True, pluck=False):
"""Execute a SQL query and fetch all rows.
:param query: SQL query.
@@ -336,7 +336,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, run=True, **kwargs):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, run=True):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@@ -363,7 +363,7 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update, run=run, **kwargs)
+ order_by, cache=cache, for_update=for_update, run=run)
if not run:
return ret
@@ -371,7 +371,7 @@ class Database(object):
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, run=True, **kwargs):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, run=True):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -396,7 +396,7 @@ class Database(object):
return self.value_cache[(doctype, filters, fieldname)]
if isinstance(filters, list):
- out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug, run=run, **kwargs)
+ out = self._get_value_for_many_names(doctype, filters, fieldname, order_by, debug=debug, run=run)
else:
fields = fieldname
@@ -411,7 +411,7 @@ class Database(object):
if order_by:
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
out = self._get_values_from_table(
- fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run, **kwargs
+ fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run
)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -570,17 +570,17 @@ class Database(object):
r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, **kwargs)
return r
- def _get_value_for_many_names(self, doctype, names, field, debug=False, run=True, **kwargs):
+ def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True):
names = list(filter(None, names))
if names:
return self.get_all(
doctype,
fields=field,
filters=names,
+ order_by=order_by,
debug=debug,
as_list=1,
run=run,
- **kwargs,
)
else:
return {}
From 435cf503facde7ba4b89866a896e06ac78d3e2e0 Mon Sep 17 00:00:00 2001
From: Summayya
Date: Thu, 25 Nov 2021 15:00:22 +0530
Subject: [PATCH 048/246] refactor: rremove else statement
---
frappe/public/js/frappe/model/model.js | 13 ++++++-------
1 file changed, 6 insertions(+), 7 deletions(-)
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index 37eba8d612..badd1859e3 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -318,14 +318,13 @@ $.extend(frappe.model, {
},
can_import: function(doctype, frm) {
- if(doctype.allow_import) {
- // system manager can always import
- if(frappe.user_roles.includes("System Manager")) return true;
+ if (!doctype.allow_import) return false;
- if(frm) return frm.perm[0].import===1;
- return frappe.boot.user.can_import.indexOf(doctype)!==-1;
- }
- else return false;
+ // system manager can always import
+ if (frappe.user_roles.includes("System Manager")) return true;
+
+ if (frm) return frm.perm[0].import===1;
+ return frappe.boot.user.can_import.indexOf(doctype.name)!==-1;
},
can_export: function(doctype, frm) {
From fc65c2cd3666a1d6fb146fdd8aa4a325a5d9cfb4 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Thu, 25 Nov 2021 16:20:06 +0530
Subject: [PATCH 049/246] fix: fixed pluck in execute
---
frappe/database/database.py | 69 +++++++++++++++++++++++++++++--------
1 file changed, 54 insertions(+), 15 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index bfdaebde75..411587aa7e 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -336,7 +336,7 @@ class Database(object):
return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache)
def get_value(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, run=True):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", cache=False, for_update=False, run=True, pluck=False):
"""Returns a document property or list of properties.
:param doctype: DocType name.
@@ -363,7 +363,7 @@ class Database(object):
"""
ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug,
- order_by, cache=cache, for_update=for_update, run=run)
+ order_by, cache=cache, for_update=for_update, run=run, pluck=pluck)
if not run:
return ret
@@ -371,7 +371,8 @@ class Database(object):
return ((len(ret[0]) > 1 or as_dict) and ret[0] or ret[0][0]) if ret else None
def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False,
- debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, run=True):
+ debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False,
+ run=True, pluck=False):
"""Returns multiple document properties.
:param doctype: DocType name.
@@ -396,7 +397,7 @@ class Database(object):
return self.value_cache[(doctype, filters, fieldname)]
if isinstance(filters, list):
- out = self._get_value_for_many_names(doctype, filters, fieldname, order_by, debug=debug, run=run)
+ out = self._get_value_for_many_names(doctype, filters, fieldname, order_by, debug=debug, run=run, pluck=pluck)
else:
fields = fieldname
@@ -411,7 +412,16 @@ class Database(object):
if order_by:
order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by
out = self._get_values_from_table(
- fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update, run=run
+ fields,
+ filters,
+ doctype,
+ as_dict,
+ debug,
+ order_by,
+ update,
+ for_update=for_update,
+ run=run,
+ pluck=pluck,
)
except Exception as e:
if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)):
@@ -424,14 +434,24 @@ class Database(object):
else:
raise
else:
- out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run)
+ out = self.get_values_from_single(fields, filters, doctype, as_dict, debug, update, run=run, pluck=pluck)
if cache and isinstance(filters, str):
self.value_cache[(doctype, filters, fieldname)] = out
return out
- def get_values_from_single(self, fields, filters, doctype, as_dict=False, debug=False, update=None, run=True):
+ def get_values_from_single(
+ self,
+ fields,
+ filters,
+ doctype,
+ as_dict=False,
+ debug=False,
+ update=None,
+ run=True,
+ pluck=False,
+ ):
"""Get values from `tabSingles` (Single DocTypes) (internal).
:param fields: List of fields,
@@ -457,10 +477,16 @@ class Database(object):
return [map(values.get, fields)]
else:
- r = self.sql("""select field, value
+ r = self.sql(
+ """select field, value
from `tabSingles` where field in (%s) and doctype=%s"""
- % (', '.join(['%s'] * len(fields)), '%s'),
- tuple(fields) + (doctype,), as_dict=False, debug=debug, run=run)
+ % (", ".join(["%s"] * len(fields)), "%s"),
+ tuple(fields) + (doctype,),
+ as_dict=False,
+ debug=debug,
+ run=run,
+ pluck=pluck,
+ )
if not run:
return r
if as_dict:
@@ -540,8 +566,19 @@ class Database(object):
"""Alias for get_single_value"""
return self.get_single_value(*args, **kwargs)
- def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None,
- update=None, for_update=False, run=True, **kwargs):
+ def _get_values_from_table(
+ self,
+ fields,
+ filters,
+ doctype,
+ as_dict,
+ debug,
+ order_by=None,
+ update=None,
+ for_update=False,
+ run=True,
+ pluck=False,
+ ):
field_objects = []
if not isinstance(fields, Criterion):
@@ -558,7 +595,6 @@ class Database(object):
for_update=for_update,
field_objects=field_objects,
fields=fields,
- **kwargs,
)
if (
fields == "*"
@@ -567,10 +603,12 @@ class Database(object):
):
as_dict = True
- r = self.sql(query, as_dict=as_dict, debug=debug, update=update, run=run, **kwargs)
+ r = self.sql(
+ query, as_dict=as_dict, debug=debug, update=update, run=run, pluck=pluck
+ )
return r
- def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True):
+ def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False):
names = list(filter(None, names))
if names:
return self.get_all(
@@ -578,6 +616,7 @@ class Database(object):
fields=field,
filters=names,
order_by=order_by,
+ pluck=pluck,
debug=debug,
as_list=1,
run=run,
From 0ea0f7dfa3fd7f4282e35e0a9fe03ce3c4016d21 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Thu, 25 Nov 2021 16:39:44 +0530
Subject: [PATCH 050/246] fix: fixed default ordering in execute
---
frappe/model/db_query.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index c94819a8c3..16c0d18d9f 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -29,7 +29,7 @@ class DatabaseQuery(object):
self.reference_doctype = None
def execute(self, fields=None, filters=None, or_filters=None,
- docstatus=None, group_by=None, order_by="default_ordering", limit_start=False,
+ docstatus=None, group_by=None, order_by="KEEP_DEFAULT_ORDERING", limit_start=False,
limit_page_length=None, as_list=False, with_childnames=False, debug=False,
ignore_permissions=False, user=None, with_comment_count=False,
join='left join', distinct=False, start=None, page_length=None, limit=None,
@@ -703,7 +703,7 @@ class DatabaseQuery(object):
def set_order_by(self, args):
meta = frappe.get_meta(self.doctype)
- if self.order_by and self.order_by != "default_ordering":
+ if self.order_by and self.order_by != "KEEP_DEFAULT_ORDERING":
args.order_by = self.order_by
else:
args.order_by = ""
From 83272fd1ecfc818a1d39a8c3b9d226fff67d893d Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Thu, 25 Nov 2021 16:47:58 +0530
Subject: [PATCH 051/246] style: Remove extra tabs
Co-authored-by: gavin
---
frappe/public/js/frappe/list/list_view.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 0104fd44c1..9218217162 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1484,7 +1484,6 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
get_menu_items() {
const doctype = this.doctype;
const items = [];
-
if (frappe.model.can_import(this.meta)) {
items.push({
label: __("Import"),
From c13e1838d7afec30fe1b018ccd9aff7c9bd0db70 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Thu, 25 Nov 2021 17:57:07 +0530
Subject: [PATCH 052/246] fix: module load issue, tour description and save
field overlay
---
frappe/core/form_tour/doctype/doctype.json | 11 ++++++-----
frappe/public/js/frappe/form/form_tour.js | 3 ++-
frappe/utils/install.py | 2 ++
3 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/frappe/core/form_tour/doctype/doctype.json b/frappe/core/form_tour/doctype/doctype.json
index e38b9640cc..391d3ecf40 100644
--- a/frappe/core/form_tour/doctype/doctype.json
+++ b/frappe/core/form_tour/doctype/doctype.json
@@ -6,7 +6,7 @@
"idx": 0,
"include_name_field": 1,
"is_standard": 1,
- "modified": "2021-11-24 17:25:18.317075",
+ "modified": "2021-11-25 17:03:01.646360",
"modified_by": "Administrator",
"module": "Core",
"name": "Doctype",
@@ -15,7 +15,7 @@
"save_on_complete": 1,
"steps": [
{
- "description": "Select a Module to which this Doctype would belong",
+ "description": "Select a Module to which this DocType would belong",
"field": "",
"fieldname": "module",
"fieldtype": "Link",
@@ -27,19 +27,20 @@
"title": "Module"
},
{
- "description": "Check this to make the Docytpe as Custom",
+ "description": "Check this to make the DocType as Custom",
"field": "",
"fieldname": "custom",
"fieldtype": "Check",
- "has_next_condition": 0,
+ "has_next_condition": 1,
"is_table_field": 0,
"label": "Custom?",
+ "next_step_condition": "eval: doc.custom",
"parent_field": "",
"position": "Left",
"title": "Custom "
},
{
- "description": "Add fields to this Custom Doctype",
+ "description": "A Field (or a docfield) defines a property of a DocType. You can define the column name, label, datatype and more for DocFields. For instance, a ToDo doctype has fields description, status and priority. These ultimately become columns in the database table tabToDo.",
"field": "",
"fieldname": "fields",
"fieldtype": "Table",
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 9b93d160ed..84c7afdef4 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -18,6 +18,7 @@ frappe.ui.form.FormTour = class FormTour {
// if last step is to save, then attach a listener to save button
if (step.options.is_save_step) {
$(step.options.element).one('click', () => this.driver.reset());
+ this.driver.overlay.refresh();
}
// focus on input
@@ -54,7 +55,7 @@ frappe.ui.form.FormTour = class FormTour {
include_name_field() {
const name_step = {
- "description": "Enter a name",
+ "description": `Enter a name for this ${this.frm.doctype}`,
"fieldname": "__newname",
"title": "Name",
"position": "right",
diff --git a/frappe/utils/install.py b/frappe/utils/install.py
index 1cc94a68d6..2bb58a6b4c 100644
--- a/frappe/utils/install.py
+++ b/frappe/utils/install.py
@@ -5,6 +5,8 @@ import getpass
from frappe.utils.password import update_password
def before_install():
+ frappe.reload_doc("desk", "doctype", "form_tour_step")
+ frappe.reload_doc("desk", "doctype", "form_tour")
frappe.reload_doc("core", "doctype", "docfield")
frappe.reload_doc("core", "doctype", "docperm")
frappe.reload_doc("core", "doctype", "doctype_action")
From 171e8b06f83ea79a7b76dd6edf5252c1684fc076 Mon Sep 17 00:00:00 2001
From: Manuel <57345036+mtraeber@users.noreply.github.com>
Date: Thu, 25 Nov 2021 13:30:45 +0100
Subject: [PATCH 053/246] Update
frappe/email/doctype/email_account/email_account.py
Co-authored-by: gavin
---
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 18c9ca1737..1e5d1dc0ba 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -549,7 +549,7 @@ class EmailAccount(Document):
def on_trash(self):
"""Clear communications where email account is linked"""
- Communication = frappe.qb.from_("Communication")
+ Communication = frappe.qb.DocType("Communication")
frappe.qb.update(Communication) \
.set(Communication.email_account == "") \
.where(Communication.email_account == self.name).run()
From 4d2f9157ae3342e816481fa55d77d676663568e3 Mon Sep 17 00:00:00 2001
From: Summayya
Date: Thu, 25 Nov 2021 20:25:04 +0530
Subject: [PATCH 054/246] refactor: add meta as separate parameter
---
frappe/public/js/frappe/list/list_view.js | 2 +-
frappe/public/js/frappe/model/model.js | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 0104fd44c1..582aaa2ec7 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1485,7 +1485,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
const doctype = this.doctype;
const items = [];
- if (frappe.model.can_import(this.meta)) {
+ if (frappe.model.can_import(doctype, null, this.meta)) {
items.push({
label: __("Import"),
action: () =>
diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js
index badd1859e3..041905408a 100644
--- a/frappe/public/js/frappe/model/model.js
+++ b/frappe/public/js/frappe/model/model.js
@@ -317,14 +317,14 @@ $.extend(frappe.model, {
return doc && doc.__last_sync_on && ((new Date() - doc.__last_sync_on)) < 5000;
},
- can_import: function(doctype, frm) {
- if (!doctype.allow_import) return false;
+ can_import: function(doctype, frm, meta=null) {
+ if (meta && !meta.allow_import) return false;
// system manager can always import
if (frappe.user_roles.includes("System Manager")) return true;
if (frm) return frm.perm[0].import===1;
- return frappe.boot.user.can_import.indexOf(doctype.name)!==-1;
+ return frappe.boot.user.can_import.indexOf(doctype)!==-1;
},
can_export: function(doctype, frm) {
From 48111964ad67de96ce30b961ddcd6357d73f74a0 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Thu, 25 Nov 2021 22:06:50 +0530
Subject: [PATCH 055/246] fix: Remove unnecessary style file for note
---
frappe/desk/doctype/note/note.css | 3 ---
1 file changed, 3 deletions(-)
delete mode 100644 frappe/desk/doctype/note/note.css
diff --git a/frappe/desk/doctype/note/note.css b/frappe/desk/doctype/note/note.css
deleted file mode 100644
index b5026d2e46..0000000000
--- a/frappe/desk/doctype/note/note.css
+++ /dev/null
@@ -1,3 +0,0 @@
-.like-disabled-input{
- background-color: #fff;
-}
\ No newline at end of file
From fdab567fc2340cafe6ab88c53d0596c29adec272 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Fri, 26 Nov 2021 08:47:54 +0530
Subject: [PATCH 056/246] style: Remove unnecessary tabs
---
frappe/public/js/frappe/list/list_view.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js
index 582aaa2ec7..3c9f1e39fb 100644
--- a/frappe/public/js/frappe/list/list_view.js
+++ b/frappe/public/js/frappe/list/list_view.js
@@ -1484,7 +1484,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList {
get_menu_items() {
const doctype = this.doctype;
const items = [];
-
+
if (frappe.model.can_import(doctype, null, this.meta)) {
items.push({
label: __("Import"),
From ba6d96855b909fe9660453b7daf3a420b398610e Mon Sep 17 00:00:00 2001
From: Sagar Sharma
Date: Wed, 10 Nov 2021 20:52:13 +0530
Subject: [PATCH 057/246] fix: Image(link) render as text in print format
(cherry picked from commit df3692b51e66771c9308a9c7c744c943c1f95752)
---
frappe/templates/print_formats/standard_macros.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/templates/print_formats/standard_macros.html b/frappe/templates/print_formats/standard_macros.html
index 9986e45999..580a41f959 100644
--- a/frappe/templates/print_formats/standard_macros.html
+++ b/frappe/templates/print_formats/standard_macros.html
@@ -153,7 +153,7 @@ data-fieldname="{{ df.fieldname }}" data-fieldtype="{{ df.fieldtype }}"
{% elif df.fieldtype=="Signature" %}
- {% elif df.fieldtype in ("Attach", "Attach Image") and frappe.utils.is_image(doc[df.fieldname]) %}
+ {% elif df.fieldtype in ("Attach", "Attach Image") %}
{% elif df.fieldtype=="HTML" %}
From d57fe8f7990cd370be3c3c121c5c7398aedb08b9 Mon Sep 17 00:00:00 2001
From: Manuel <57345036+mtraeber@users.noreply.github.com>
Date: Fri, 26 Nov 2021 08:08:18 +0100
Subject: [PATCH 058/246] Update
frappe/email/doctype/email_account/email_account.py
Co-authored-by: Abhishek Saxena <33656173+saxenabhishek@users.noreply.github.com>
---
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 1e5d1dc0ba..52623fb358 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -551,7 +551,7 @@ class EmailAccount(Document):
"""Clear communications where email account is linked"""
Communication = frappe.qb.DocType("Communication")
frappe.qb.update(Communication) \
- .set(Communication.email_account == "") \
+ .set(Communication.email_account, "") \
.where(Communication.email_account == self.name).run()
remove_user_email_inbox(email_account=self.name)
From 22434d065cf3a4a24f6e0659fa1669892019512c Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Fri, 26 Nov 2021 13:09:48 +0530
Subject: [PATCH 059/246] feat: Added aggregation functions to qb functions
refactor: changed args to aggregation funcs to match db level aggregation funcs
---
frappe/query_builder/functions.py | 39 +++++++++++++++++++++++++++++++
1 file changed, 39 insertions(+)
diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py
index 39c67178c2..ddb10831c5 100644
--- a/frappe/query_builder/functions.py
+++ b/frappe/query_builder/functions.py
@@ -2,6 +2,8 @@ from pypika.functions import *
from pypika.terms import Function
from frappe.query_builder.utils import ImportMapper, db_type_is
from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR
+from frappe.database.query import Query
+from .utils import Column
class Concat_ws(Function):
@@ -22,3 +24,40 @@ Match = ImportMapper(
db_type_is.POSTGRES: TO_TSVECTOR
}
)
+
+
+def max(dt, fieldname, filters=None, **kwargs):
+ return (
+ Query()
+ .build_conditions(dt, filters)
+ .select(Max(Column(fieldname)))
+ .run(**kwargs)[0][0]
+ or 0
+ )
+
+def min(dt, fieldname, filters=None, **kwargs):
+ return (
+ Query()
+ .build_conditions(dt, filters)
+ .select(Min(Column(fieldname)))
+ .run(**kwargs)[0][0]
+ or 0
+ )
+
+def avg(dt, fieldname, filters=None, **kwargs):
+ return (
+ Query()
+ .build_conditions(dt, filters)
+ .select(Avg(Column(fieldname)))
+ .run(**kwargs)[0][0]
+ or 0
+ )
+
+def sum(dt, fieldname, filters=None, **kwargs):
+ return (
+ Query()
+ .build_conditions(dt, filters)
+ .select(Sum(Column(fieldname)))
+ .run(**kwargs)[0][0]
+ or 0
+ )
\ No newline at end of file
From 971a581359c8514f0112626a5cd6b6fdce758ee2 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Fri, 26 Nov 2021 14:01:57 +0530
Subject: [PATCH 060/246] fix: order of reload for doctypes
---
frappe/utils/install.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/utils/install.py b/frappe/utils/install.py
index 2bb58a6b4c..c8c118ede8 100644
--- a/frappe/utils/install.py
+++ b/frappe/utils/install.py
@@ -5,12 +5,12 @@ import getpass
from frappe.utils.password import update_password
def before_install():
- frappe.reload_doc("desk", "doctype", "form_tour_step")
- frappe.reload_doc("desk", "doctype", "form_tour")
frappe.reload_doc("core", "doctype", "docfield")
frappe.reload_doc("core", "doctype", "docperm")
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")
frappe.reload_doc("core", "doctype", "doctype")
def after_install():
From 9a190c145ed389d01b9d1dbc945ab78b39bbf6a6 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Fri, 26 Nov 2021 14:19:23 +0530
Subject: [PATCH 061/246] fix: translatable strings
---
frappe/templates/styles/discussion_style.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/templates/styles/discussion_style.css b/frappe/templates/styles/discussion_style.css
index f1dab60589..975376c484 100644
--- a/frappe/templates/styles/discussion_style.css
+++ b/frappe/templates/styles/discussion_style.css
@@ -37,7 +37,7 @@
}
.no-discussions {
- width: 500px;
+ width: 80%;
margin: 0 auto;
text-align: center;
}
From e3bdf110061c4e36fd934e205303d559884247b3 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Fri, 26 Nov 2021 14:19:59 +0530
Subject: [PATCH 062/246] refactor: moved aggregation functions to Query
Builder
---
frappe/__init__.py | 7 ++++++-
frappe/query_builder/__init__.py | 2 +-
frappe/query_builder/utils.py | 11 +++++++++++
3 files changed, 18 insertions(+), 2 deletions(-)
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 895bdcaddc..a87f930be6 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -28,7 +28,11 @@ from .exceptions import *
from .utils.jinja import (get_jenv, get_template, render_template, get_email_from_template, get_jloader)
from .utils.lazy_loader import lazy_import
-from frappe.query_builder import get_query_builder, patch_query_execute
+from frappe.query_builder import (
+ get_query_builder,
+ patch_query_execute,
+ patch_query_aggregation,
+)
__version__ = '14.0.0-dev'
@@ -211,6 +215,7 @@ def init(site, sites_path=None, new_site=False):
setup_module_map()
patch_query_execute()
+ patch_query_aggregation()
local.initialised = True
diff --git a/frappe/query_builder/__init__.py b/frappe/query_builder/__init__.py
index 4a1fe8fb84..9c7432142f 100644
--- a/frappe/query_builder/__init__.py
+++ b/frappe/query_builder/__init__.py
@@ -1,2 +1,2 @@
from pypika import *
-from frappe.query_builder.utils import Column, DocType, get_query_builder, patch_query_execute
+from frappe.query_builder.utils import Column, DocType, get_query_builder, patch_query_execute, patch_query_aggregation
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
index 386ddda751..e2916a859c 100644
--- a/frappe/query_builder/utils.py
+++ b/frappe/query_builder/utils.py
@@ -66,3 +66,14 @@ def patch_query_execute():
raise BuilderIdentificationFailed
builder_class.run = execute_query
+
+
+def patch_query_aggregation():
+ """Patch aggregation functions to frappe.qb
+ """
+ from frappe.query_builder.functions import max, min, avg, sum
+
+ frappe.qb.max = max
+ frappe.qb.min = min
+ frappe.qb.avg = avg
+ frappe.qb.sum = sum
\ No newline at end of file
From b60452ca18dce578f1d097361bdf3abbb40cea13 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Fri, 26 Nov 2021 15:13:24 +0530
Subject: [PATCH 063/246] fix: removed unrelated code
---
frappe/templates/styles/discussion_style.css | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/templates/styles/discussion_style.css b/frappe/templates/styles/discussion_style.css
index 975376c484..f1dab60589 100644
--- a/frappe/templates/styles/discussion_style.css
+++ b/frappe/templates/styles/discussion_style.css
@@ -37,7 +37,7 @@
}
.no-discussions {
- width: 80%;
+ width: 500px;
margin: 0 auto;
text-align: center;
}
From b5c73648dc84cd4d8238080e80757e6a15472f29 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Fri, 26 Nov 2021 15:44:14 +0530
Subject: [PATCH 064/246] refactor: made DRY-er functions
---
frappe/query_builder/functions.py | 38 ++++++++++---------------------
frappe/query_builder/utils.py | 10 ++++----
2 files changed, 17 insertions(+), 31 deletions(-)
diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py
index ddb10831c5..c98df775b7 100644
--- a/frappe/query_builder/functions.py
+++ b/frappe/query_builder/functions.py
@@ -26,38 +26,24 @@ Match = ImportMapper(
)
-def max(dt, fieldname, filters=None, **kwargs):
+def _aggregate(function, dt, fieldname, filters, **kwargs):
return (
Query()
.build_conditions(dt, filters)
- .select(Max(Column(fieldname)))
+ .select(function(Column(fieldname)))
.run(**kwargs)[0][0]
or 0
)
-def min(dt, fieldname, filters=None, **kwargs):
- return (
- Query()
- .build_conditions(dt, filters)
- .select(Min(Column(fieldname)))
- .run(**kwargs)[0][0]
- or 0
- )
-def avg(dt, fieldname, filters=None, **kwargs):
- return (
- Query()
- .build_conditions(dt, filters)
- .select(Avg(Column(fieldname)))
- .run(**kwargs)[0][0]
- or 0
- )
+def _max(dt, fieldname, filters=None, **kwargs):
+ return _aggregate(Max, dt, fieldname, filters, **kwargs)
-def sum(dt, fieldname, filters=None, **kwargs):
- return (
- Query()
- .build_conditions(dt, filters)
- .select(Sum(Column(fieldname)))
- .run(**kwargs)[0][0]
- or 0
- )
\ No newline at end of file
+def _min(dt, fieldname, filters=None, **kwargs):
+ return _aggregate(Min, dt, fieldname, filters, **kwargs)
+
+def _avg(dt, fieldname, filters=None, **kwargs):
+ return _aggregate(Avg, dt, fieldname, filters, **kwargs)
+
+def _sum(dt, fieldname, filters=None, **kwargs):
+ return _aggregate(Sum, dt, fieldname, filters, **kwargs)
\ No newline at end of file
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
index e2916a859c..08768aa11e 100644
--- a/frappe/query_builder/utils.py
+++ b/frappe/query_builder/utils.py
@@ -71,9 +71,9 @@ def patch_query_execute():
def patch_query_aggregation():
"""Patch aggregation functions to frappe.qb
"""
- from frappe.query_builder.functions import max, min, avg, sum
+ from frappe.query_builder.functions import _max, _min, _avg, _sum
- frappe.qb.max = max
- frappe.qb.min = min
- frappe.qb.avg = avg
- frappe.qb.sum = sum
\ No newline at end of file
+ frappe.qb.max = _max
+ frappe.qb.min = _min
+ frappe.qb.avg = _avg
+ frappe.qb.sum = _sum
\ No newline at end of file
From 7e35dc4913ee61425bf6ba4c4d09037d9f893033 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Fri, 26 Nov 2021 16:30:34 +0530
Subject: [PATCH 065/246] fix: add frappe.as_json for safe_exec scripts
---
frappe/utils/safe_exec.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py
index cc9662b4eb..00b7822104 100644
--- a/frappe/utils/safe_exec.py
+++ b/frappe/utils/safe_exec.py
@@ -85,6 +85,7 @@ def get_safe_globals():
loads=json.loads,
dumps=json.dumps
),
+ as_json=frappe.as_json,
dict=dict,
log=frappe.log,
_dict=frappe._dict,
From 5a6d7ee191149291dc2b84391ca8fddeb9543388 Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Fri, 26 Nov 2021 17:29:44 +0530
Subject: [PATCH 066/246] refactor: get_mapping_module doesn't need to access
to instance
---
.../data_migration_plan.py | 33 ++++++++++---------
1 file changed, 17 insertions(+), 16 deletions(-)
diff --git a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
index 94ed77e2ec..d13912b431 100644
--- a/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
+++ b/frappe/data_migration/doctype/data_migration_plan/data_migration_plan.py
@@ -1,5 +1,4 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
@@ -8,6 +7,20 @@ from frappe.modules.export_file import export_to_files, create_init_py
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
from frappe.model.document import Document
+
+def get_mapping_module(module, mapping_name):
+ app_name = frappe.db.get_value("Module Def", module, "app_name")
+ mapping_name = frappe.scrub(mapping_name)
+ module = frappe.scrub(module)
+
+ try:
+ return frappe.get_module(
+ f"{app_name}.{module}.data_migration_mapping.{mapping_name}"
+ )
+ except ImportError:
+ return None
+
+
class DataMigrationPlan(Document):
def on_update(self):
# update custom fields in mappings
@@ -54,26 +67,14 @@ class DataMigrationPlan(Document):
frappe.flags.ignore_in_install = False
def pre_process_doc(self, mapping_name, doc):
- module = self.get_mapping_module(mapping_name)
+ module = get_mapping_module(self.module, mapping_name)
if module and hasattr(module, 'pre_process'):
return module.pre_process(doc)
return doc
def post_process_doc(self, mapping_name, local_doc=None, remote_doc=None):
- module = self.get_mapping_module(mapping_name)
+ module = get_mapping_module(self.module, mapping_name)
if module and hasattr(module, 'post_process'):
return module.post_process(local_doc=local_doc, remote_doc=remote_doc)
-
- def get_mapping_module(self, mapping_name):
- try:
- module_def = frappe.get_doc("Module Def", self.module)
- module = frappe.get_module('{app}.{module}.data_migration_mapping.{mapping_name}'.format(
- app= module_def.app_name,
- module=frappe.scrub(self.module),
- mapping_name=frappe.scrub(mapping_name)
- ))
- return module
- except ImportError:
- return None
From 171ff3ba4c8c4d3cb5f23dd1ed6bf8df2f845e6c Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Fri, 26 Nov 2021 19:16:40 +0530
Subject: [PATCH 067/246] fix: Make strings translatable
---
frappe/public/js/frappe/form/form_tour.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/public/js/frappe/form/form_tour.js b/frappe/public/js/frappe/form/form_tour.js
index 84c7afdef4..7fefb59ac6 100644
--- a/frappe/public/js/frappe/form/form_tour.js
+++ b/frappe/public/js/frappe/form/form_tour.js
@@ -55,9 +55,9 @@ frappe.ui.form.FormTour = class FormTour {
include_name_field() {
const name_step = {
- "description": `Enter a name for this ${this.frm.doctype}`,
+ "description": __("Enter a name for this {0}", [this.frm.doctype]),
"fieldname": "__newname",
- "title": "Name",
+ "title": __("Document Name"),
"position": "right",
"is_table_field": 0
};
From 6b9b28825987b8af04e070623631e7a3f61876c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Micha=C5=82=20W=C4=99glowski-Hodur?=
Date: Fri, 26 Nov 2021 14:50:37 +0100
Subject: [PATCH 068/246] fix: Correct the polish translations to correctly
show buttons in Frappe framework. (#15093)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Co-authored-by: Michał Węglowski-Hodur
---
frappe/translations/pl.csv | 100 ++++++++++++++++++-------------------
1 file changed, 50 insertions(+), 50 deletions(-)
diff --git a/frappe/translations/pl.csv b/frappe/translations/pl.csv
index 3fa85687f1..7e6b10385e 100644
--- a/frappe/translations/pl.csv
+++ b/frappe/translations/pl.csv
@@ -27,7 +27,7 @@ Assign To,Przypisano do,
Attachment,Załącznik,
Attachments,Załączniki,
Author,Autor,
-Auto Repeat,Auto Repeat,
+Auto Repeat,Powtarzanie automatyczne,
Base URL,Podstawowy adres URL,
Based On,BazujÄ…cy na,
Beginner,PoczÄ…tkujÄ…cy,
@@ -58,10 +58,10 @@ Custom?,Niestandardowy?,
Date Format,Format daty,
Datetime,Data-czas,
Day,Dzień,
-Default Letter Head,Domyślny nagłówek pisma,
+Default Letter Head,Nagłówek domyślny,
Defaults,Wartości domyślne,
Delivery Status,Status dostawy,
-Department,Departament,
+Department,Dział,
Details,Szczegóły,
Document Name,Nazwa dokumentu,
Document Status,Stan dokumentu,
@@ -69,21 +69,21 @@ Document Type,Typ Dokumentu,
Domain,Domena,
Domains,Domeny,
Draft,Wersja robocza,
-Edit,Edytować,
+Edit,Edycja,
Email Account,Konto e-mail,
Email Address,Adres e-mail,
Email ID,ID e-mail,
Email Sent,Wiadomość wysłana,
Email Template,Szablon e-maila,
-Enable,Włączyć,
+Enable,Włącz,
Enabled,Aktywny,
-End Date,Data zakonczenia,
-Error Code: {0},Kod błędu {0},
+End Date,Data zakończenia,
+Error Code: {0},Kod błędu: {0},
Error Log,Dziennik błędów,
Event,Wydarzenie,
-Expand All,Rozwiń wszystkie,
-Fail,Zawieść,
-Failed,Nieudane,
+Expand All,Rozwiń wszystko,
+Fail,Nie powiodło się.,
+Failed,Nie powiodło się.,
Fax,Faks,
Feedback,Informacja zwrotna,
Female,Kobieta,
@@ -114,25 +114,25 @@ Inactive,Nieaktywny,
Insert,Wstaw,
Interests,Zainteresowania,
Introduction,Wprowadzenie,
-Is Active,Jest aktywny,
-Is Completed,Jest zakończony,
-Is Default,Jest domyślny,
+Is Active,Aktywny,
+Is Completed,Zakończony,
+Is Default,Domyślny,
Kanban Board,Kanban Board,
-Label,etykieta,
-Language Name,Nazwa Język,
+Label,Etykieta,
+Language Name,Język,
Last Name,Nazwisko,
Leaderboard,Tabela liderów,
Letter Head,Nagłówek,
Level,Poziom,
Limit,Limit,
-Log,Log,
+Log,Dziennik,
Logs,Dzienniki,
Low,Niski,
-Maintenance Manager,Menager Konserwacji,
+Maintenance Manager,Menedżer Konserwacji,
Maintenance User,Użytkownik Konserwacji,
Male,Mężczyzna,
-Mandatory,ObowiÄ…zkowe,
-Mapping,Mapowanie,
+Mandatory,Wymagane,
+Mapping,Odwzorowanie,
Mapping Type,Typ odwzorowania,
Medium,Åšredni,
Meeting,Spotkanie,
@@ -144,8 +144,8 @@ Monday,Poniedziałek,
Monthly,Miesięcznie,
More,Więcej,
More Information,Więcej informacji,
-More...,Jeszcze...,
-Move,ruch,
+More...,Więcej...,
+Move,PrzenieÅ›,
My Account,Moje Konto,
New Address,Nowy adres,
New Contact,Nowy kontakt,
@@ -159,7 +159,7 @@ Not Permitted,Niedozwolone,
Not active,Nieaktywny,
Notes,Notatki,
Number,Numer,
-Online,online,
+Online,Online,
Operation,Operacja,
Options,Opcje,
Other,Inne,
@@ -180,7 +180,7 @@ Please set Email Address,Proszę ustawić adres e-mail,
Portal,Portal,
Portal Settings,Ustawienia,
Preview,PodglÄ…d,
-Primary,Podstawowy,
+Primary,Główny,
Print Format,Format Druku,
Print Settings,Ustawienia drukowania,
Print taxes with zero amount,Drukowanie podatków z zerową kwotą,
@@ -196,17 +196,17 @@ Range,Przedział,
Rating,Ocena,
Received,Otrzymano,
Recipients,Adresaci,
-Redirect URL,przekierowanie,
-Reference,Referencja,
-Reference Date,Data Odniesienia,
+Redirect URL,Adres przekierowania,
+Reference,Odnośnik,
+Reference Date,Data odnośnika,
Reference Document,Dokument referencyjny,
-Reference Document Type,Oznaczenie typu dokumentu,
-Reference Owner,Odniesienie Właściciel,
-Reference Type,Typ Odniesienia,
-Refresh Token,Odśwież Reklamowe,
+Reference Document Type,Typ dokumentu referencyjnego,
+Reference Owner,Właściciel odnośnika,
+Reference Type,Typ odnośnika,
+Refresh Token,Odśwież token,
Region,Region,
Rejected,Odrzucono,
-Reopen,Otworzyć na nowo,
+Reopen,Otwórz ponownie,
Replied,Odpowiedziane,
Report,Raport,
Report Builder,Kreator raportów,
@@ -217,37 +217,37 @@ Role,Rola,
Route,Trasa,
Sales Manager,Menadżer Sprzedaży,
Sales Master Manager,Główny Menadżer Sprzedaży,
-Sales User,Sprzedaż użytkownika,
+Sales User,Użytkownik Sprzedaży,
Salutation,Forma grzecznościowa,
-Sample,Próba,
+Sample,Próbka,
Saturday,Sobota,
-Saved,Zapisane,
+Saved,Zapisano,
Scan Barcode,Skanuj kod kreskowy,
Scheduled,Zaplanowane,
Search,Szukaj,
-Secret Key,Sekretny klucz,
+Secret Key,Klucz tajny,
Select,Wybierz,
Select DocType,Wybierz DocType,
Send Now,Wyślij teraz,
Sent,Wysłano,
-Series {0} already used in {1},Seria {0} już zostały użyte w {1},
+Series {0} already used in {1},Seria {0} już została użyte w {1},
Service,Usługa,
Set as Default,Ustaw jako domyślne,
Settings,Ustawienia,
Shipping,Wysyłka,
-Short Name,Skrócona nazwa,
+Short Name,Nazwa skrócona,
Slideshow,Pokaz slajdów,
-Some information is missing,Niektóre informacje brakuje,
+Some information is missing,Brakuje wymaganych informacji.,
Source,Źródło,
-Source Name,Źródło Nazwa,
+Source Name,Nazwa źródła,
Standard,Standard,
-Start Date,Data startu,
+Start Date,Data rozpoczęcia,
Start Import,Rozpocznij importowanie,
State,Stan,
Stopped,Zatrzymany,
Subject,Temat,
Submit,Zatwierdź,
-Successful,Udany,
+Successful,Zakończono pomyślnie.,
Summary,Podsumowanie,
Sunday,Niedziela,
System Manager,System Manager,
@@ -256,15 +256,15 @@ Task,Zadanie,
Tax Category,Kategoria podatku,
Test,Test,
Thank you,Dziękuję,
-The page you are looking for is missing. This could be because it is moved or there is a typo in the link.,"Strona, której szukasz nie brakuje. To może być dlatego, że porusza się lub jest literówka w linku.",
-Timespan,Okres czasu,
+The page you are looking for is missing. This could be because it is moved or there is a typo in the link.,"Strona, której szukasz nie istnieje. Być może została przeniesiona, bądź odnośnik jest nieprawidłowy.",
+Timespan,Okres,
To,Do,
To Date,Do daty,
Tools,Narzędzia,
Traceback,Traceback,
URL,URL,
Unsubscribed,Nie zarejestrowany,
-Use Sandbox,Korzystanie Sandbox,
+Use Sandbox,Użyj piaskownicy,
User,Użytkownik,
User ID,ID Użytkownika,
Users,Użytkownicy,
@@ -291,14 +291,14 @@ old_parent,old_parent,
'In List View' not allowed for type {0} in row {1},Pole 'W widoku listy' nie jest dozwolone dla typu {0} w lini {1},
'Recipients' not specified,"Odbiorcy" nie podano,
(Ctrl + G),(Ctrl + G),
-** Failed: {0} to {1}: {2},** Nie udało: {0} {1}: {2},
+** Failed: {0} to {1}: {2},** Nie udało się: {0} {1}: {2},
**Currency** Master,** Waluta ** Główna,
0 - Draft; 1 - Submitted; 2 - Cancelled,0 - Projekt; 1 - Wysłane; 2 - Anulowane,
-0 is highest,0 jest nawyższe,
-1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent,1 jednostka Walutowa = [?] części zdawkowych. Na przykład 1 zł = 100 groszy,
+0 is highest,0 jest najwyższą wartością,
+1 Currency = [?] Fraction\nFor e.g. 1 USD = 100 Cent,1 jednostka Walutowa = [?] części dziesiętnych. Na przykład 1 zł = 100 groszy,
1 comment,1 komentarz,
1 hour ago,1 godzinÄ™ temu,
-1 minute ago,1 minuta temu,
+1 minute ago,1 minutÄ™ temu,
1 month ago,1 miesiÄ…c temu,
1 year ago,1 rok temu,
; not allowed in condition,; Niedozwolony w stanie,
@@ -4095,7 +4095,7 @@ Browser,PrzeglÄ…darka,
Browser Version,Wersja przeglÄ…darki,
Web Template Field,Pole szablonu sieci Web,
Section,Sekcja,
-Hide,Ukryć,
+Hide,Ukryj,
Enable In App Website Tracking,Włącz śledzenie witryn w aplikacji,
Enable Google Indexing,Włącz indeksowanie Google,
"To use Google Indexing, enable Google Settings .","Aby korzystać z indeksowania Google, włącz Ustawienia Google .",
@@ -4215,7 +4215,7 @@ since yesterday,od wczoraj,
since last week,od zeszłego tygodnia,
since last month,od ostatniego miesiÄ…ca,
since last year,od zeszłego roku,
-Show,Pokazać,
+Show,Pokaż,
New Number Card,Nowa karta z numerem,
Your Shortcuts,Twoje skróty,
You haven't added any Dashboard Charts or Number Cards yet.,Nie dodałeś jeszcze żadnych wykresów ani kart z numerami.,
From 58ca570f67e47fc3704a5932a14a773e6c87b832 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Fri, 26 Nov 2021 20:19:47 +0530
Subject: [PATCH 069/246] fix: Selection color in dark mode
---
frappe/public/scss/desk/dark.scss | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss
index 35cdffc91a..f894704ca2 100644
--- a/frappe/public/scss/desk/dark.scss
+++ b/frappe/public/scss/desk/dark.scss
@@ -161,4 +161,9 @@
--right-arrow-svg: url("data: image/svg+xml;utf8, ");
--left-arrow-svg: url("data: image/svg+xml;utf8, ");
+
+ ::selection {
+ color: var(--text-color);
+ background: var(--gray-500);
+ }
}
From ae31a0d6fbca7e947de5f212a26bbdb7e0fcfcbe Mon Sep 17 00:00:00 2001
From: hrwx
Date: Sat, 27 Nov 2021 00:24:21 +0000
Subject: [PATCH 070/246] feat: add option to disable notification
---
.../system_settings/system_settings.json | 17 +++++++++++++++--
frappe/utils/change_log.py | 19 ++++++++++---------
2 files changed, 25 insertions(+), 11 deletions(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 82e88d2477..d4b7272dd3 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -66,7 +66,9 @@
"attach_view_link",
"prepared_report_section",
"enable_prepared_report_auto_deletion",
- "prepared_report_expiry_period"
+ "prepared_report_expiry_period",
+ "system_updates_section",
+ "disable_system_update_notification"
],
"fields": [
{
@@ -462,12 +464,23 @@
"fieldname": "encrypt_backup",
"fieldtype": "Check",
"label": "Encrypt Backups"
+ },
+ {
+ "fieldname": "system_updates_section",
+ "fieldtype": "Section Break",
+ "label": "System Updates"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_system_update_notification",
+ "fieldtype": "Check",
+ "label": "Disable System Update Notification"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2021-10-21 19:24:15.232430",
+ "modified": "2021-11-27 01:17:05.228959",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py
index 109778b87b..b4f54e1b20 100644
--- a/frappe/utils/change_log.py
+++ b/frappe/utils/change_log.py
@@ -155,11 +155,11 @@ def check_for_update():
for update_type in updates:
if github_version.__dict__[update_type] > instance_version.__dict__[update_type]:
updates[update_type].append(frappe._dict(
- current_version = str(instance_version),
+ current_version = str(instance_version),
available_version = str(github_version),
- org_name = org_name,
- app_name = app,
- title = apps[app]['title'],
+ org_name = org_name,
+ app_name = app,
+ title = apps[app]['title'],
))
break
if github_version.__dict__[update_type] < instance_version.__dict__[update_type]: break
@@ -242,10 +242,11 @@ def add_message_to_redis(update_json):
@frappe.whitelist()
def show_update_popup():
cache = frappe.cache()
- user = frappe.session.user
+ user = frappe.session.user
+ system_settings = frappe.get_single("System Settings")
update_info = cache.get_value("update-info")
- if not update_info:
+ if not update_info or system_settings.disable_system_update_notification:
return
updates = json.loads(update_info)
@@ -259,9 +260,9 @@ def show_update_popup():
app = frappe._dict(app)
release_links += "{title} : v{available_version} ".format(
available_version = app.available_version,
- org_name = app.org_name,
- app_name = app.app_name,
- title = app.title
+ org_name = app.org_name,
+ app_name = app.app_name,
+ title = app.title
)
if release_links:
message = _("New {} releases for the following apps are available").format(_(update_type))
From 5339008fef88cf691a28b0b1b3e23f721ff635d7 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Sat, 27 Nov 2021 14:35:58 +0530
Subject: [PATCH 071/246] feat: Added patch for replacing db level aggregation
calls
---
frappe/patches/v14_0/remove_db_aggregation.py | 25 +++++++++++++++++++
1 file changed, 25 insertions(+)
create mode 100644 frappe/patches/v14_0/remove_db_aggregation.py
diff --git a/frappe/patches/v14_0/remove_db_aggregation.py b/frappe/patches/v14_0/remove_db_aggregation.py
new file mode 100644
index 0000000000..373cfaf7db
--- /dev/null
+++ b/frappe/patches/v14_0/remove_db_aggregation.py
@@ -0,0 +1,25 @@
+import frappe
+import re
+
+
+def execute():
+ _sub_aggregation("frappe.db.max", "frappe.qb.max")
+ _sub_aggregation("frappe.db.min", "frappe.qb.min")
+ _sub_aggregation("frappe.db.sum", "frappe.qb.sum")
+ _sub_aggregation("frappe.db.avg", "frappe.qb.avg")
+
+
+def _sub_aggregation(function, subtitution):
+ scripts = frappe.get_all(
+ "Server Script",
+ filters={"script": ("like", f"%{function}%")},
+ fields=["name", "script"],
+ )
+ for script in scripts:
+ script.update(
+ {"script": re.sub(f"{function}", f"{subtitution}", script["script"])}
+ )
+ for script in scripts:
+ frappe.db.update(
+ "Server Script", {"name": script["name"]}, "script", script["script"]
+ )
From b1d0e574a0947fdb4133763f63f51677dc9c43ec Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 27 Nov 2021 17:17:05 +0530
Subject: [PATCH 072/246] feat(REST): OR filters in REST API
---
frappe/client.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/client.py b/frappe/client.py
index a3ed0fa37d..6641e471af 100644
--- a/frappe/client.py
+++ b/frappe/client.py
@@ -18,7 +18,7 @@ Requests via FrappeClient are also handled here.
@frappe.whitelist()
def get_list(doctype, fields=None, filters=None, order_by=None,
- limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True):
+ limit_start=None, limit_page_length=20, parent=None, debug=False, as_dict=True, or_filters=None):
'''Returns a list of records by filters, fields, ordering and limit
:param doctype: DocType of the data to be queried
@@ -34,6 +34,7 @@ def get_list(doctype, fields=None, filters=None, order_by=None,
doctype=doctype,
fields=fields,
filters=filters,
+ or_filters=or_filters,
order_by=order_by,
limit_start=limit_start,
limit_page_length=limit_page_length,
From 88c3d92662d4cf05eb1fe4aa1815fdb0f90d53a9 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 27 Nov 2021 19:23:37 +0530
Subject: [PATCH 073/246] fix: return self after submit/cancel
`Document.save` returns self but `submit` and `cancel` don't.
change:
For sake of consistency and better support for `run_method` via REST API, return the document.
ref: https://github.com/frappe/frappe/issues/14869
---
frappe/model/document.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 411d447d0f..6c85090ea9 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -915,7 +915,7 @@ class Document(BaseDocument):
def _submit(self):
"""Submit the document. Sets `docstatus` = 1, then saves."""
self.docstatus = 1
- self.save()
+ return self.save()
@whitelist.__func__
def _cancel(self):
@@ -925,17 +925,17 @@ class Document(BaseDocument):
new_name = gen_new_name_for_cancelled_doc(self)
frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
self.name = new_name
- self.save()
+ return self.save()
@whitelist.__func__
def submit(self):
"""Submit the document. Sets `docstatus` = 1, then saves."""
- self._submit()
+ return self._submit()
@whitelist.__func__
def cancel(self):
"""Cancel the document. Sets `docstatus` = 2, then saves."""
- self._cancel()
+ return self._cancel()
def delete(self, ignore_permissions=False):
"""Delete document."""
From 615db9b55eaa4ffde45e513594d1b8e9fd7940b3 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Sun, 28 Nov 2021 09:44:05 +0530
Subject: [PATCH 074/246] style: Fix sider alerts
---
frappe/desk/form/linked_with.py | 3 +--
frappe/tests/test_linked_with.py | 4 +++-
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py
index f44a350263..cbf459e8ae 100644
--- a/frappe/desk/form/linked_with.py
+++ b/frappe/desk/form/linked_with.py
@@ -2,7 +2,6 @@
# License: MIT. See LICENSE
import json
from collections import defaultdict
-from os import link
import itertools
from typing import List
@@ -263,7 +262,7 @@ def get_references_across_doctypes_by_dynamic_link_field(to_doctypes: List[str]=
for linked_to in frappe.db.get_all(doctype, pluck=doctype_fieldname, filters = filters, distinct=1):
if linked_to:
links_by_doctype[linked_to].append({'doctype': doctype, 'fieldname': fieldname, 'doctype_fieldname': doctype_fieldname})
- except frappe.db.ProgrammingError as e:
+ except frappe.db.ProgrammingError:
# TODO: FIXME
continue
return links_by_doctype
diff --git a/frappe/tests/test_linked_with.py b/frappe/tests/test_linked_with.py
index 64da8e51e0..ec461c7d5f 100644
--- a/frappe/tests/test_linked_with.py
+++ b/frappe/tests/test_linked_with.py
@@ -1,4 +1,6 @@
-import frappe, unittest
+import unittest
+
+import frappe
from frappe.core.doctype.doctype.test_doctype import new_doctype
from frappe.desk.form import linked_with
From 491ca2b7c7d7dd710dde3fbc0e98724d37ca9230 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Sun, 28 Nov 2021 09:57:55 +0530
Subject: [PATCH 075/246] chore: Fix CODEOWNERS formatting
---
CODEOWNERS | 24 ++++++++++++------------
1 file changed, 12 insertions(+), 12 deletions(-)
diff --git a/CODEOWNERS b/CODEOWNERS
index 69ca578b6c..f7d759c123 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,18 +3,18 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
-* @frappe/frappe-review-team
-templates/ @surajshetty3416
-www/ @surajshetty3416
-integrations/ @leela
-patches/ @surajshetty3416 @gavindsouza
-email/ @leela
-event_streaming/ @ruchamahabal
-data_import* @netchampfaris
-core/ @surajshetty3416
+* @frappe/frappe-review-team
+templates/ @surajshetty3416
+www/ @surajshetty3416
+integrations/ @leela
+patches/ @surajshetty3416 @gavindsouza
+email/ @leela
+event_streaming/ @ruchamahabal
+data_import* @netchampfaris
+core/ @surajshetty3416
database @gavindsouza
model @gavindsouza
-requirements.txt @gavindsouza
-query_builder/ @gavindsouza
-commands/ @gavindsouza
+requirements.txt @gavindsouza
+query_builder/ @gavindsouza
+commands/ @gavindsouza
workspace @shariquerik
From 4cae147aed134c358ba1b1ab9eb854aa17d45206 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Mon, 29 Nov 2021 09:23:52 +0530
Subject: [PATCH 076/246] fix: remove duplicate parent when child item option
selected (backport #15101) (#15110)
Co-authored-by: Bhavesh Maheshwari
---
frappe/public/js/frappe/form/multi_select_dialog.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js
index 37b7e08a80..161e4196b0 100644
--- a/frappe/public/js/frappe/form/multi_select_dialog.js
+++ b/frappe/public/js/frappe/form/multi_select_dialog.js
@@ -325,7 +325,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog {
let parent_names = this.child_datatable.rowmanager.checkMap.reduce((parent_names, checked, index) => {
if (checked == 1) {
const parent_name = this.child_results[index].parent;
- parent_names.push(parent_name);
+ if (!parent_names.includes(parent_name)) {
+ parent_names.push(parent_name);
+ }
}
return parent_names;
}, []);
From 2efed9d12e9c5785e7c45c2bf49035c171cea8d4 Mon Sep 17 00:00:00 2001
From: Your Name
Date: Wed, 1 Sep 2021 15:24:16 +0530
Subject: [PATCH 077/246] refactor: convert doctype user db.sql calls
---
frappe/core/doctype/user/user.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index b127cf5f0c..c00541ab5a 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -698,13 +698,11 @@ def has_email_account(email):
@frappe.whitelist(allow_guest=False)
def get_email_awaiting(user):
- waiting = frappe.db.sql("""select email_account,email_id
- from `tabUser Email`
- where awaiting_password = 1
- and parent = %(user)s""", {"user":user}, as_dict=1)
+ waiting = frappe.get_all("User Email", fields=["email_account", "email_id"], filters={"awaiting_password": 1, "parent": user})
if waiting:
return waiting
else:
+ # TODO
frappe.db.sql("""update `tabUser Email`
set awaiting_password =0
where parent = %(user)s""",{"user":user})
From ad1842e69d9ae9ba39c0cd5a224286f1a0267a9f Mon Sep 17 00:00:00 2001
From: abhishek
Date: Thu, 7 Oct 2021 13:24:21 +0530
Subject: [PATCH 078/246] refactor: convert doctype doctype db.sql call
change_modified_of_parent()
---
frappe/core/doctype/doctype/doctype.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 738fb73a34..1c8c1f9217 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -253,7 +253,7 @@ class DocType(Document):
parent_list = frappe.db.get_all('DocField', 'parent',
dict(fieldtype=['in', frappe.model.table_fields], options=self.name))
for p in parent_list:
- frappe.db.sql('UPDATE `tabDocType` SET modified=%s WHERE `name`=%s', (now(), p.parent))
+ frappe.db.update("DocType", p.parent, {}, for_update=False)
def scrub_field_names(self):
"""Sluggify fieldnames if not set from Label."""
From 7e84529dacc25eeafe87fc2b076639653c220e0b Mon Sep 17 00:00:00 2001
From: abhishek
Date: Thu, 7 Oct 2021 13:42:39 +0530
Subject: [PATCH 079/246] refactor: convert doctype file db.sql calls
update_existing_file_docs()
---
frappe/core/doctype/file/file.py | 22 ++++++++--------------
1 file changed, 8 insertions(+), 14 deletions(-)
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 0021240106..af9c8a48fa 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -940,20 +940,14 @@ def get_files_by_search_text(text):
def update_existing_file_docs(doc):
# Update is private and file url of all file docs that point to the same file
- frappe.db.sql("""
- UPDATE `tabFile`
- SET
- file_url = %(file_url)s,
- is_private = %(is_private)s
- WHERE
- content_hash = %(content_hash)s
- and name != %(file_name)s
- """, dict(
- file_url=doc.file_url,
- is_private=doc.is_private,
- content_hash=doc.content_hash,
- file_name=doc.name
- ))
+ file_doctype = frappe.qb.DocType("File")
+ (
+ frappe.qb.update(file_doctype)
+ .set(file_doctype.file_url, doc.file_url)
+ .set(file_doctype.is_private, doc.is_private)
+ .where(file_doctype.content_hash == doc.content_hash)
+ .where(file_doctype.name != doc.name)
+ ).run()
def attach_files_to_document(doc, event):
""" Runs on on_update hook of all documents.
From f7dcd781fb4632c3f757b1a1ca031addd1c4f922 Mon Sep 17 00:00:00 2001
From: abhishek
Date: Thu, 7 Oct 2021 13:57:10 +0530
Subject: [PATCH 080/246] refactor: convert doctype scheduled job type db.sql
calls
class TestScheduledJobType
---
.../core/doctype/scheduled_job_type/test_scheduled_job_type.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
index dc3353b176..a11966c47e 100644
--- a/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
+++ b/frappe/core/doctype/scheduled_job_type/test_scheduled_job_type.py
@@ -10,7 +10,7 @@ from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
class TestScheduledJobType(unittest.TestCase):
def setUp(self):
frappe.db.rollback()
- frappe.db.sql('truncate `tabScheduled Job Type`')
+ frappe.db.truncate("Scheduled Job Type")
sync_jobs()
frappe.db.commit()
From 5409be0a3440f1f2447ed249be79721975f0803f Mon Sep 17 00:00:00 2001
From: abhishek
Date: Thu, 7 Oct 2021 13:58:56 +0530
Subject: [PATCH 081/246] refactor: convert doctype server_script db.sql
setUpClass()
tearDownClass()
---
frappe/core/doctype/server_script/test_server_script.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py
index 3c091fec0b..bc92061f42 100644
--- a/frappe/core/doctype/server_script/test_server_script.py
+++ b/frappe/core/doctype/server_script/test_server_script.py
@@ -76,7 +76,7 @@ class TestServerScript(unittest.TestCase):
@classmethod
def setUpClass(cls):
frappe.db.commit()
- frappe.db.sql('truncate `tabServer Script`')
+ frappe.db.truncate("Server Script")
frappe.get_doc('User', 'Administrator').add_roles('Script Manager')
for script in scripts:
script_doc = frappe.get_doc(doctype ='Server Script')
@@ -88,7 +88,7 @@ class TestServerScript(unittest.TestCase):
@classmethod
def tearDownClass(cls):
frappe.db.commit()
- frappe.db.sql('truncate `tabServer Script`')
+ frappe.db.truncate("Server Script")
frappe.cache().delete_value('server_script_map')
def setUp(self):
From 1bb13fcee3d85d420fe17599f9a7f5202cebff72 Mon Sep 17 00:00:00 2001
From: abhishek
Date: Thu, 7 Oct 2021 15:24:50 +0530
Subject: [PATCH 082/246] refactor: convert doctype transaction log db.sql
get_current_index()
---
.../core/doctype/transaction_log/transaction_log.py | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/frappe/core/doctype/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py
index 6dc4340277..0a480f6660 100644
--- a/frappe/core/doctype/transaction_log/transaction_log.py
+++ b/frappe/core/doctype/transaction_log/transaction_log.py
@@ -9,6 +9,7 @@ from frappe.model.document import Document
from frappe.query_builder import DocType
from frappe.utils import cint, now_datetime
+
class TransactionLog(Document):
def before_insert(self):
index = get_current_index()
@@ -29,18 +30,15 @@ class TransactionLog(Document):
def hash_line(self):
sha = hashlib.sha256()
sha.update(
- frappe.safe_encode(str(self.row_index)) + \
- frappe.safe_encode(str(self.timestamp)) + \
- frappe.safe_encode(str(self.data))
+ frappe.safe_encode(str(self.row_index))
+ + frappe.safe_encode(str(self.timestamp))
+ + frappe.safe_encode(str(self.data))
)
return sha.hexdigest()
def hash_chain(self):
sha = hashlib.sha256()
- sha.update(
- frappe.safe_encode(str(self.transaction_hash)) + \
- frappe.safe_encode(str(self.previous_hash))
- )
+ sha.update(frappe.safe_encode(str(self.transaction_hash)) + frappe.safe_encode(str(self.previous_hash)))
return sha.hexdigest()
From d74f985728c9f44a76c21b011b7ac42e04662247 Mon Sep 17 00:00:00 2001
From: abhishek
Date: Thu, 7 Oct 2021 16:35:08 +0530
Subject: [PATCH 083/246] refactor: convert doctype user db.sql calls
has_desk_access()
---
frappe/core/doctype/user/user.py | 16 ++++++++++------
1 file changed, 10 insertions(+), 6 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index c00541ab5a..9694c11b87 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -213,15 +213,19 @@ class User(Document):
user_type_doc.update_modules_in_user(self)
def has_desk_access(self):
- '''Return true if any of the set roles has desk access'''
+ """Return true if any of the set roles has desk access"""
if not self.roles:
return False
- return len(frappe.db.sql("""select name
- from `tabRole` where desk_access=1
- and name in ({0}) limit 1""".format(', '.join(['%s'] * len(self.roles))),
- [d.role for d in self.roles]))
-
+ role_table = frappe.qb.DocType("Role")
+ return len(
+ frappe.qb.from_(role_table)
+ .select(role_table.name)
+ .where(role_table.desk_access == 1)
+ .where(role_table.name.isin([d.role for d in self.roles]))
+ .limit(1)
+ .run()
+ )
def share_with_self(self):
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
From e503d8117303eb787c0099f02d91b89eb23b6ce4 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Mon, 29 Nov 2021 13:07:31 +0530
Subject: [PATCH 084/246] perf: reduced no. of db calls
---
frappe/patches/v14_0/remove_db_aggregation.py | 39 ++++++++++++-------
1 file changed, 26 insertions(+), 13 deletions(-)
diff --git a/frappe/patches/v14_0/remove_db_aggregation.py b/frappe/patches/v14_0/remove_db_aggregation.py
index 373cfaf7db..231acccc6c 100644
--- a/frappe/patches/v14_0/remove_db_aggregation.py
+++ b/frappe/patches/v14_0/remove_db_aggregation.py
@@ -1,24 +1,37 @@
+from frappe.query_builder import DocType
import frappe
import re
def execute():
- _sub_aggregation("frappe.db.max", "frappe.qb.max")
- _sub_aggregation("frappe.db.min", "frappe.qb.min")
- _sub_aggregation("frappe.db.sum", "frappe.qb.sum")
- _sub_aggregation("frappe.db.avg", "frappe.qb.avg")
+ sub_aggregation()
-def _sub_aggregation(function, subtitution):
- scripts = frappe.get_all(
- "Server Script",
- filters={"script": ("like", f"%{function}%")},
- fields=["name", "script"],
- )
- for script in scripts:
- script.update(
- {"script": re.sub(f"{function}", f"{subtitution}", script["script"])}
+def sub_aggregation():
+ server_scripts = DocType("Server Script")
+ scripts = (
+ frappe.qb.from_(server_scripts)
+ .where(
+ server_scripts.script.like("%frappe.db.max%")
+ | server_scripts.script.like("%frappe.db.min%")
+ | server_scripts.script.like("%frappe.db.sum%")
+ | server_scripts.script.like("%frappe.db.avg%")
)
+ .select("name", "script")
+ .run(as_dict=True)
+ )
+
+ def _sub_aggregation(scripts, function, substitution):
+ for script in scripts:
+ script.update(
+ {"script": re.sub(f"{function}", f"{substitution}", script["script"])}
+ )
+
+ _sub_aggregation(scripts, "frappe.db.max", "frappe.qb.max")
+ _sub_aggregation(scripts, "frappe.db.min", "frappe.qb.min")
+ _sub_aggregation(scripts, "frappe.db.sum", "frappe.qb.sum")
+ _sub_aggregation(scripts, "frappe.db.avg", "frappe.qb.avg")
+
for script in scripts:
frappe.db.update(
"Server Script", {"name": script["name"]}, "script", script["script"]
From 43fc713dd7ef881f8f76632b266965d94c98de21 Mon Sep 17 00:00:00 2001
From: Gavin D'souza
Date: Mon, 29 Nov 2021 13:46:47 +0530
Subject: [PATCH 085/246] refactor: Remove Aggregation methods from DB API
* Make it DRY & make it "better"
* Add to patches.txt so the patch runs :')
* Style fixes - tabs > spaces for consistency, removed unnecessary
blocks
---
frappe/patches.txt | 1 +
frappe/patches/v14_0/remove_db_aggregation.py | 54 +++++++++----------
2 files changed, 25 insertions(+), 30 deletions(-)
diff --git a/frappe/patches.txt b/frappe/patches.txt
index b230c336b4..3078159c3d 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -186,3 +186,4 @@ frappe.patches.v14_0.rename_cancelled_documents
frappe.patches.v14_0.copy_mail_data #08.03.21
frappe.patches.v14_0.update_workspace2 # 20.09.2021
frappe.patches.v14_0.update_github_endpoints #08-11-2021
+frappe.patches.v14_0.remove_db_aggregation
diff --git a/frappe/patches/v14_0/remove_db_aggregation.py b/frappe/patches/v14_0/remove_db_aggregation.py
index 231acccc6c..25a170f362 100644
--- a/frappe/patches/v14_0/remove_db_aggregation.py
+++ b/frappe/patches/v14_0/remove_db_aggregation.py
@@ -1,38 +1,32 @@
-from frappe.query_builder import DocType
-import frappe
import re
+import frappe
+from frappe.query_builder import DocType
+
def execute():
- sub_aggregation()
+ """Replace temporarily available Database Aggregate APIs on frappe (develop)
+ APIs changed:
+ * frappe.db.max => frappe.qb.max
+ * frappe.db.min => frappe.qb.min
+ * frappe.db.sum => frappe.qb.sum
+ * frappe.db.avg => frappe.qb.avg
+ """
+ ServerScript = DocType("Server Script")
+ server_scripts = frappe.qb.from_(ServerScript).where(
+ ServerScript.script.like("%frappe.db.max(%")
+ | ServerScript.script.like("%frappe.db.min(%")
+ | ServerScript.script.like("%frappe.db.sum(%")
+ | ServerScript.script.like("%frappe.db.avg(%")
+ ).select(
+ "name", "script"
+ ).run(as_dict=True)
-def sub_aggregation():
- server_scripts = DocType("Server Script")
- scripts = (
- frappe.qb.from_(server_scripts)
- .where(
- server_scripts.script.like("%frappe.db.max%")
- | server_scripts.script.like("%frappe.db.min%")
- | server_scripts.script.like("%frappe.db.sum%")
- | server_scripts.script.like("%frappe.db.avg%")
- )
- .select("name", "script")
- .run(as_dict=True)
- )
+ for server_script in server_scripts:
+ name, script = server_script["name"], server_script["script"]
- def _sub_aggregation(scripts, function, substitution):
- for script in scripts:
- script.update(
- {"script": re.sub(f"{function}", f"{substitution}", script["script"])}
- )
+ for agg in ["avg", "max", "min", "sum"]:
+ script = re.sub(f"frappe.db.{agg}(", f"frappe.qb.{agg}(", script)
- _sub_aggregation(scripts, "frappe.db.max", "frappe.qb.max")
- _sub_aggregation(scripts, "frappe.db.min", "frappe.qb.min")
- _sub_aggregation(scripts, "frappe.db.sum", "frappe.qb.sum")
- _sub_aggregation(scripts, "frappe.db.avg", "frappe.qb.avg")
-
- for script in scripts:
- frappe.db.update(
- "Server Script", {"name": script["name"]}, "script", script["script"]
- )
+ frappe.db.update("Server Script", name, "script", script)
From 0e32d52e3a0a75500ac59f8fe5c875b37138009f Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Mon, 29 Nov 2021 16:38:15 +0530
Subject: [PATCH 086/246] fix: Use separate API to insert route history
---
frappe/deferred_insert.py | 1 -
.../doctype/route_history/route_history.py | 20 +++++++++++++++++++
frappe/public/js/frappe/router_history.js | 12 +++++------
3 files changed, 26 insertions(+), 7 deletions(-)
diff --git a/frappe/deferred_insert.py b/frappe/deferred_insert.py
index 499fc5e41b..b1338a73b0 100644
--- a/frappe/deferred_insert.py
+++ b/frappe/deferred_insert.py
@@ -5,7 +5,6 @@ from frappe.utils import cstr
queue_prefix = 'insert_queue_for_'
-@frappe.whitelist()
def deferred_insert(doctype, records):
frappe.cache().rpush(queue_prefix + doctype, records)
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index 01184fcc3a..489a3bd50a 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -1,9 +1,13 @@
# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
+import json
+
import frappe
+from frappe.deferred_insert import deferred_insert
from frappe.model.document import Document
+
class RouteHistory(Document):
pass
@@ -35,3 +39,19 @@ def flush_old_route_records():
"modified": ("<=", last_record_to_keep[0].modified),
"user": user
})
+
+@frappe.whitelist()
+def deferred_insert_route_history(routes):
+ routes_record = []
+
+ if isinstance(routes, str):
+ routes = json.loads(routes)
+
+ for route_doc in routes:
+ routes_record.append({
+ "user": frappe.session.user,
+ "route": route_doc.get("route"),
+ "creation": route_doc.get("creation")
+ })
+
+ deferred_insert("Route History", json.dumps(routes_record))
diff --git a/frappe/public/js/frappe/router_history.js b/frappe/public/js/frappe/router_history.js
index c64c3fc9f2..1fda6f4a1b 100644
--- a/frappe/public/js/frappe/router_history.js
+++ b/frappe/public/js/frappe/router_history.js
@@ -5,13 +5,14 @@ const save_routes = frappe.utils.debounce(() => {
if (frappe.session.user === 'Guest') return;
const routes = frappe.route_history_queue;
frappe.route_history_queue = [];
-
- frappe.xcall('frappe.deferred_insert.deferred_insert', {
- 'doctype': 'Route History',
- 'records': routes
+
+ if (!routes.length) return;
+
+ frappe.xcall('frappe.desk.doctype.route_history.route_history.deferred_insert_route_history', {
+ 'routes': routes
}).catch(() => {
frappe.route_history_queue.concat(routes);
- });
+ });
}, 10000);
@@ -19,7 +20,6 @@ frappe.router.on('change', () => {
const route = frappe.get_route();
if (is_route_useful(route)) {
frappe.route_history_queue.push({
- 'user': frappe.session.user,
'creation': frappe.datetime.now_datetime(),
'route': frappe.get_route_str()
});
From aa40abadb1277636e1f79d1ff787a7d615e218d5 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Mon, 29 Nov 2021 17:06:29 +0530
Subject: [PATCH 087/246] refactor: Simplify method naming
---
frappe/desk/doctype/route_history/route_history.py | 6 +++---
frappe/public/js/frappe/router_history.js | 2 +-
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index 489a3bd50a..fc87312950 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -4,7 +4,7 @@
import json
import frappe
-from frappe.deferred_insert import deferred_insert
+from frappe.deferred_insert import deferred_insert as _deferred_insert
from frappe.model.document import Document
@@ -41,7 +41,7 @@ def flush_old_route_records():
})
@frappe.whitelist()
-def deferred_insert_route_history(routes):
+def deferred_insert(routes):
routes_record = []
if isinstance(routes, str):
@@ -54,4 +54,4 @@ def deferred_insert_route_history(routes):
"creation": route_doc.get("creation")
})
- deferred_insert("Route History", json.dumps(routes_record))
+ _deferred_insert("Route History", json.dumps(routes_record))
diff --git a/frappe/public/js/frappe/router_history.js b/frappe/public/js/frappe/router_history.js
index 1fda6f4a1b..fb2d5790da 100644
--- a/frappe/public/js/frappe/router_history.js
+++ b/frappe/public/js/frappe/router_history.js
@@ -8,7 +8,7 @@ const save_routes = frappe.utils.debounce(() => {
if (!routes.length) return;
- frappe.xcall('frappe.desk.doctype.route_history.route_history.deferred_insert_route_history', {
+ frappe.xcall('frappe.desk.doctype.route_history.route_history.deferred_insert', {
'routes': routes
}).catch(() => {
frappe.route_history_queue.concat(routes);
From 2a08f35836efa010c3a9c100ec62f9a497cae9ca Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Sat, 27 Nov 2021 19:10:45 +0530
Subject: [PATCH 088/246] fix: allow cancelling by PUT docstatus=2
---
frappe/model/document.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 6c85090ea9..fcdadf48e6 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -306,6 +306,9 @@ class Document(BaseDocument):
self.check_permission("write", "save")
+ if self.docstatus == 2:
+ self._rename_doc_on_cancel()
+
self.set_user_and_timestamp()
self.set_docstatus()
self.check_if_latest()
@@ -922,9 +925,6 @@ class Document(BaseDocument):
"""Cancel the document. Sets `docstatus` = 2, then saves.
"""
self.docstatus = 2
- new_name = gen_new_name_for_cancelled_doc(self)
- frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
- self.name = new_name
return self.save()
@whitelist.__func__
@@ -1355,6 +1355,11 @@ class Document(BaseDocument):
from frappe.desk.doctype.tag.tag import DocTags
return DocTags(self.doctype).get_tags(self.name).split(",")[1:]
+ def _rename_doc_on_cancel(self):
+ new_name = gen_new_name_for_cancelled_doc(self)
+ frappe.rename_doc(self.doctype, self.name, new_name, force=True, show_alert=False)
+ self.name = new_name
+
def __repr__(self):
name = self.name or "unsaved"
doctype = self.__class__.__name__
From e751b07bde74046aeba9520ffd843f66e28336dc Mon Sep 17 00:00:00 2001
From: hrwx
Date: Mon, 29 Nov 2021 16:49:46 +0000
Subject: [PATCH 089/246] chore: make system timezone readonly
---
frappe/core/doctype/system_settings/system_settings.json | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 82e88d2477..2a06f58845 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -95,6 +95,7 @@
"fieldname": "time_zone",
"fieldtype": "Select",
"label": "Time Zone",
+ "read_only": 1,
"reqd": 1
},
{
@@ -467,7 +468,7 @@
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2021-10-21 19:24:15.232430",
+ "modified": "2021-11-29 17:49:20.950033",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
From db4476fc5fb8e0f99b5a9126c1f63def831db522 Mon Sep 17 00:00:00 2001
From: hrwx
Date: Mon, 29 Nov 2021 17:28:40 +0000
Subject: [PATCH 090/246] fix: reduce api call
---
.../system_settings/system_settings.json | 3 ++-
frappe/public/js/frappe/desk.js | 2 ++
frappe/utils/change_log.py | 21 +++++++++----------
3 files changed, 14 insertions(+), 12 deletions(-)
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index d4b7272dd3..dcec9b13c2 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -466,6 +466,7 @@
"label": "Encrypt Backups"
},
{
+ "collapsible": 1,
"fieldname": "system_updates_section",
"fieldtype": "Section Break",
"label": "System Updates"
@@ -480,7 +481,7 @@
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2021-11-27 01:17:05.228959",
+ "modified": "2021-11-29 18:09:53.601629",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 2855c6ae7c..4563875b91 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -531,6 +531,8 @@ frappe.Application = class Application {
}
show_update_available() {
+ if (frappe.boot.sysdefaults.disable_system_update_notification) return;
+
frappe.call({
"method": "frappe.utils.change_log.show_update_popup"
});
diff --git a/frappe/utils/change_log.py b/frappe/utils/change_log.py
index b4f54e1b20..5888166d5d 100644
--- a/frappe/utils/change_log.py
+++ b/frappe/utils/change_log.py
@@ -155,11 +155,11 @@ def check_for_update():
for update_type in updates:
if github_version.__dict__[update_type] > instance_version.__dict__[update_type]:
updates[update_type].append(frappe._dict(
- current_version = str(instance_version),
- available_version = str(github_version),
- org_name = org_name,
- app_name = app,
- title = apps[app]['title'],
+ current_version=str(instance_version),
+ available_version=str(github_version),
+ org_name=org_name,
+ app_name=app,
+ title=apps[app]['title'],
))
break
if github_version.__dict__[update_type] < instance_version.__dict__[update_type]: break
@@ -243,10 +243,9 @@ def add_message_to_redis(update_json):
def show_update_popup():
cache = frappe.cache()
user = frappe.session.user
- system_settings = frappe.get_single("System Settings")
update_info = cache.get_value("update-info")
- if not update_info or system_settings.disable_system_update_notification:
+ if not update_info:
return
updates = json.loads(update_info)
@@ -259,10 +258,10 @@ def show_update_popup():
for app in updates[update_type]:
app = frappe._dict(app)
release_links += "{title} : v{available_version} ".format(
- available_version = app.available_version,
- org_name = app.org_name,
- app_name = app.app_name,
- title = app.title
+ available_version=app.available_version,
+ org_name=app.org_name,
+ app_name=app.app_name,
+ title=app.title
)
if release_links:
message = _("New {} releases for the following apps are available").format(_(update_type))
From 7047cb83011f6472cd5686651aa177b2cc8990c1 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Tue, 30 Nov 2021 08:33:31 +0530
Subject: [PATCH 091/246] refactor: Deferred insert for route history (#15120)
---
.../doctype/route_history/route_history.py | 19 ++++++++-----------
frappe/public/js/frappe/router_history.js | 4 ++--
2 files changed, 10 insertions(+), 13 deletions(-)
diff --git a/frappe/desk/doctype/route_history/route_history.py b/frappe/desk/doctype/route_history/route_history.py
index fc87312950..a49d5d5418 100644
--- a/frappe/desk/doctype/route_history/route_history.py
+++ b/frappe/desk/doctype/route_history/route_history.py
@@ -42,16 +42,13 @@ def flush_old_route_records():
@frappe.whitelist()
def deferred_insert(routes):
- routes_record = []
-
- if isinstance(routes, str):
- routes = json.loads(routes)
-
- for route_doc in routes:
- routes_record.append({
+ routes = [
+ {
"user": frappe.session.user,
- "route": route_doc.get("route"),
- "creation": route_doc.get("creation")
- })
+ "route": route.get("route"),
+ "creation": route.get("creation"),
+ }
+ for route in frappe.parse_json(routes)
+ ]
- _deferred_insert("Route History", json.dumps(routes_record))
+ _deferred_insert("Route History", json.dumps(routes))
diff --git a/frappe/public/js/frappe/router_history.js b/frappe/public/js/frappe/router_history.js
index fb2d5790da..14b936f5e8 100644
--- a/frappe/public/js/frappe/router_history.js
+++ b/frappe/public/js/frappe/router_history.js
@@ -4,10 +4,10 @@ const routes_to_skip = ['Form', 'social', 'setup-wizard', 'recorder'];
const save_routes = frappe.utils.debounce(() => {
if (frappe.session.user === 'Guest') return;
const routes = frappe.route_history_queue;
- frappe.route_history_queue = [];
-
if (!routes.length) return;
+ frappe.route_history_queue = [];
+
frappe.xcall('frappe.desk.doctype.route_history.route_history.deferred_insert', {
'routes': routes
}).catch(() => {
From 72fa9a3c1980cbaf29d307928382a5c466a4aa51 Mon Sep 17 00:00:00 2001
From: leela
Date: Tue, 14 Sep 2021 16:35:15 +0530
Subject: [PATCH 092/246] fix: Move site setup into background to fix timeouts
Provide an option to trigger site setup as a background task.
---
frappe/desk/page/setup_wizard/setup_wizard.js | 5 ++++
frappe/desk/page/setup_wizard/setup_wizard.py | 23 +++++++++++++++----
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.js b/frappe/desk/page/setup_wizard/setup_wizard.js
index f44a57e339..7e90bc01ad 100644
--- a/frappe/desk/page/setup_wizard/setup_wizard.js
+++ b/frappe/desk/page/setup_wizard/setup_wizard.js
@@ -197,6 +197,8 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
callback: (r) => {
if (r.message.status === 'ok') {
this.post_setup_success();
+ } else if (r.message.status === 'registered') {
+ this.update_setup_message(__("starting the setup..."));
} else if (r.message.fail !== undefined) {
this.abort_setup(r.message.fail);
}
@@ -238,6 +240,9 @@ frappe.setup.SetupWizard = class SetupWizard extends frappe.ui.Slides {
if (data.fail_msg) {
this.abort_setup(data.fail_msg);
}
+ if (data.status === 'ok') {
+ this.post_setup_success();
+ }
})
}
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index c729c1d78b..83a5e16009 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -54,9 +54,17 @@ def setup_complete(args):
return {'status': 'ok'}
args = parse_args(args)
-
stages = get_setup_stages(args)
+ is_background_task = frappe.conf.get('trigger_site_setup_in_background')
+ if is_background_task:
+ process_setup_stages.enqueue(stages=stages, user_input=args, is_background_task=True)
+ return {'status': 'registered'}
+ else:
+ return process_setup_stages(stages, args)
+
+@frappe.task()
+def process_setup_stages(stages, user_input, is_background_task=False):
try:
frappe.flags.in_setup_wizard = True
current_task = None
@@ -68,11 +76,16 @@ def setup_complete(args):
current_task = task
task.get('fn')(task.get('args'))
except Exception:
- handle_setup_exception(args)
- return {'status': 'fail', 'fail': current_task.get('fail_msg')}
+ handle_setup_exception(user_input)
+ if not is_background_task:
+ return {'status': 'fail', 'fail': current_task.get('fail_msg')}
+ frappe.publish_realtime('setup_task',
+ {'status': 'fail', "fail_msg": current_task.get('fail_msg')}, user=frappe.session.user)
else:
- run_setup_success(args)
- return {'status': 'ok'}
+ run_setup_success(user_input)
+ if not is_background_task:
+ return {'status': 'ok'}
+ frappe.publish_realtime('setup_task', {"status": 'ok'}, user=frappe.session.user)
finally:
frappe.flags.in_setup_wizard = False
From 124407a70a9d18659e7c9032987f9a53b1942e4d Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 30 Nov 2021 11:15:52 +0530
Subject: [PATCH 093/246] fix: update EPS records when renaming doc
---
frappe/model/rename_doc.py | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index ee9044b73e..1db89493f2 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -78,6 +78,8 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F
rename_versions(doctype, old, new)
+ rename_eps_records(doctype, old, new)
+
# call after_rename
new_doc = frappe.get_doc(doctype, new)
@@ -177,6 +179,16 @@ def rename_versions(doctype, old, new):
frappe.db.sql("""UPDATE `tabVersion` SET `docname`=%s WHERE `ref_doctype`=%s AND `docname`=%s""",
(new, doctype, old))
+def rename_eps_records(doctype, old, new):
+ epl = frappe.qb.DocType("Energy Point Log")
+ (frappe.qb.update(epl)
+ .set(epl.reference_name, new)
+ .where(
+ (epl.reference_doctype == doctype)
+ & (epl.reference_name == old)
+ )
+ ).run()
+
def rename_parent_and_child(doctype, old, new, meta):
# rename the doc
frappe.db.sql("UPDATE `tab{0}` SET `name`={1} WHERE `name`={1}".format(doctype, '%s'), (new, old))
From 67b0293e6fc20eb449c79e26f67c4eb4d9341d46 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 30 Nov 2021 12:33:29 +0530
Subject: [PATCH 094/246] fix: Pretty date rendering based on user-timezone
---
frappe/public/js/frappe/utils/datetime.js | 25 +++++++++++---------
frappe/public/js/frappe/utils/pretty_date.js | 2 +-
2 files changed, 15 insertions(+), 12 deletions(-)
diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js
index c85cbd42e7..165636bad4 100644
--- a/frappe/public/js/frappe/utils/datetime.js
+++ b/frappe/public/js/frappe/utils/datetime.js
@@ -134,20 +134,23 @@ $.extend(frappe.datetime, {
},
str_to_user: function(val, only_time = false) {
- if(!val) return "";
+ if (!val) return "";
+ const user_time_fmt = frappe.datetime.get_user_time_fmt();
+ let date_obj = moment(val);
+ let user_format = user_time_fmt;
- var user_time_fmt = frappe.datetime.get_user_time_fmt();
- if(only_time) {
- return moment(val, frappe.defaultTimeFormat)
- .format(user_time_fmt);
- }
-
- var user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase();
- if(typeof val !== "string" || val.indexOf(" ")===-1) {
- return moment(val).format(user_date_fmt);
+ if (only_time) {
+ date_obj = moment(val, frappe.defaultTimeFormat);
} else {
- return moment(val, "YYYY-MM-DD HH:mm:ss").format(user_date_fmt + " " + user_time_fmt);
+ let user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase();
+ if (typeof val !== "string" || val.indexOf(" ")===-1) {
+ date_obj = moment(val);
+ } else {
+ date_obj = moment(val, "YYYY-MM-DD HH:mm:ss");
+ user_format = user_date_fmt + " " + user_time_fmt;
+ }
}
+ return date_obj.tz(frappe.boot.time_zone.user).format(user_format);
},
get_datetime_as_string: function(d) {
diff --git a/frappe/public/js/frappe/utils/pretty_date.js b/frappe/public/js/frappe/utils/pretty_date.js
index 3ebe2c1ae2..a5279682ce 100644
--- a/frappe/public/js/frappe/utils/pretty_date.js
+++ b/frappe/public/js/frappe/utils/pretty_date.js
@@ -6,7 +6,7 @@ function prettyDate(date, mini) {
date = new Date((date || "").replace(/-/g, "/").replace(/[TZ]/g, " ").replace(/\.[0-9]*/, ""));
}
- let diff = (((new Date()).getTime() - date.getTime()) / 1000);
+ let diff = (((new Date(frappe.datetime.now_datetime())).getTime() - date.getTime()) / 1000);
let day_diff = Math.floor(diff / 86400);
if (isNaN(day_diff) || day_diff < 0) return '';
From 8eea80e14ec4e86f619bb752f2ce81177596e459 Mon Sep 17 00:00:00 2001
From: abhishek
Date: Mon, 25 Oct 2021 14:13:14 +0530
Subject: [PATCH 095/246] refactor: convert doctype user db.sql calls
---
frappe/core/doctype/user/user.py | 56 +++++++++++++-------------------
1 file changed, 23 insertions(+), 33 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 9694c11b87..9561dcb737 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -217,15 +217,8 @@ class User(Document):
if not self.roles:
return False
- role_table = frappe.qb.DocType("Role")
- return len(
- frappe.qb.from_(role_table)
- .select(role_table.name)
- .where(role_table.desk_access == 1)
- .where(role_table.name.isin([d.role for d in self.roles]))
- .limit(1)
- .run()
- )
+ role_table = DocType("Role")
+ return frappe.db.count(role_table, ((role_table.desk_access == 1) & (role_table.name.isin([d.role for d in self.roles]))))
def share_with_self(self):
frappe.share.add(self.doctype, self.name, self.name, write=1, share=1,
@@ -283,12 +276,20 @@ class User(Document):
return link
def get_other_system_managers(self):
- return frappe.db.sql("""select distinct `user`.`name` from `tabHas Role` as `user_role`, `tabUser` as `user`
- where user_role.role='System Manager'
- and `user`.docstatus<2
- and `user`.enabled=1
- and `user_role`.parent = `user`.name
- and `user_role`.parent not in ('Administrator', %s) limit 1""", (self.name,))
+ user_doctype = DocType("User").as_("user")
+ user_role_doctype = DocType("Has Role").as_("user_role")
+ return (
+ frappe.qb.from_(user_doctype)
+ .from_(user_role_doctype)
+ .select(user_doctype.name)
+ .where(user_role_doctype.role == 'System Manager')
+ .where(user_doctype.docstatus < 2)
+ .where(user_doctype.enabled == 1)
+ .where(user_role_doctype.parent == user_doctype.name)
+ .where(user_role_doctype.parent.notin(["Administrator", self.name]))
+ .limit(1)
+ .distinct()
+ ).run()
def get_fullname(self):
"""get first_name space last_name"""
@@ -362,8 +363,8 @@ class User(Document):
# delete todos
frappe.db.delete("ToDo", {"owner": self.name})
- frappe.db.sql("""UPDATE `tabToDo` SET `assigned_by`=NULL WHERE `assigned_by`=%s""",
- (self.name,))
+ todo_table = DocType("ToDo")
+ frappe.qb.update(todo_table).set(todo_table.assigned_by, None).where(todo_table.assigned_by == self.name).run()
# delete events
frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
@@ -429,10 +430,7 @@ class User(Document):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
# set email
- table = DocType("User")
- frappe.qb.update(table).where(
- table.name == new_name
- ).set("email", new_name).run()
+ frappe.db.update("User", new_name, "email", new_name)
def append_roles(self, *roles):
"""Add roles to user"""
@@ -706,22 +704,15 @@ def get_email_awaiting(user):
if waiting:
return waiting
else:
- # TODO
- frappe.db.sql("""update `tabUser Email`
- set awaiting_password =0
- where parent = %(user)s""",{"user":user})
+ user_email_table = DocType("User Email")
+ frappe.qb.update(user_email_table).set(user_email_table.user_email_table, 0).where(user_email_table.parent == user).run()
return False
def ask_pass_update():
# update the sys defaults as to awaiting users
from frappe.utils import set_default
- doctype = DocType("User Email")
- users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select(
- doctype.parent.as_("user")
- ).distinct().run(as_dict=True)
-
- password_list = [ user.get("user") for user in users ]
+ password_list = frappe.get_all("User Email", filters={"awaiting_password": True}, pluck="parent", distinct=True)
set_default("email_user_password", u','.join(password_list))
def _get_user_for_update_password(key, old_password):
@@ -889,8 +880,7 @@ def get_active_users():
def get_website_users():
"""Returns total no. of website users"""
- return frappe.db.sql("""select count(*) from `tabUser`
- where enabled = 1 and user_type = 'Website User'""")[0][0]
+ return frappe.db.count("User", filters={"enabled": True, "user_type": "Website User"})
def get_active_website_users():
"""Returns No. of website users who logged in, in the last 3 days"""
From 1bb8dcf1767508088e593e61122da506456ec752 Mon Sep 17 00:00:00 2001
From: abhishek
Date: Sun, 14 Nov 2021 14:40:53 +0530
Subject: [PATCH 096/246] refactor: convert core notifications db.sql calls
get_unseen_likes()
get_unread_emails()
---
frappe/core/notifications.py | 52 +++++++++++++++++++++---------------
1 file changed, 30 insertions(+), 22 deletions(-)
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index b43d424df5..d091983d00 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -2,6 +2,9 @@
# License: MIT. See LICENSE
import frappe
+from frappe.query_builder import DocType, Interval
+from frappe.query_builder.functions import Now
+
def get_notification_config():
return {
@@ -39,28 +42,33 @@ def get_todays_events(as_list=False):
def get_unseen_likes():
"""Returns count of unseen likes"""
- return frappe.db.sql("""select count(*) from `tabComment`
- where
- comment_type='Like'
- and modified >= (NOW() - INTERVAL '1' YEAR)
- and owner is not null and owner!=%(user)s
- and reference_owner=%(user)s
- and seen=0
- """, {"user": frappe.session.user})[0][0]
+
+ comment_doctype = DocType("Comment")
+ return frappe.db.count(comment_doctype,
+ filters=(
+ (comment_doctype.comment_type == "Like")
+ & (comment_doctype.modified >= Now() - Interval(years=1))
+ & (comment_doctype.owner.notnull())
+ & (comment_doctype.owner != frappe.session.user)
+ & (comment_doctype.reference_owner == frappe.session.user)
+ & (comment_doctype.seen == 0)
+ )
+ )
+
def get_unread_emails():
- "returns unread emails for a user"
+ "returns count of unread emails for a user"
- return frappe.db.sql("""\
- SELECT count(*)
- FROM `tabCommunication`
- WHERE communication_type='Communication'
- AND communication_medium='Email'
- AND sent_or_received='Received'
- AND email_status not in ('Spam', 'Trash')
- AND email_account in (
- SELECT distinct email_account from `tabUser Email` WHERE parent=%(user)s
- )
- AND modified >= (NOW() - INTERVAL '1' YEAR)
- AND seen=0
- """, {"user": frappe.session.user})[0][0]
+ communication_doctype = DocType("Communication")
+ user_doctype = DocType("User")
+ distinct_email_accounts = frappe.qb.from_(user_doctype).select(user_doctype.email_account).where(user_doctype.parent == frappe.session.user).distinct()
+
+ return frappe.db.count(communication_doctype, filters=(
+ (communication_doctype.communication_type == "Communication")
+ & (communication_doctype.communication_medium == "Email")
+ & (communication_doctype.sent_or_received == "Received")
+ & (communication_doctype.email_status.notin(["spam", "Trash"]))
+ & (communication_doctype.email_account.isin(distinct_email_accounts))
+ & (communication_doctype.modified >= Now() - Interval(years=1))
+ & (communication_doctype.seen == 0)
+ ))
From 9f183e6cf2d709457e371674b7a38661644a6dfd Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Tue, 30 Nov 2021 14:16:13 +0530
Subject: [PATCH 097/246] style: follow simple indenting standard
---
frappe/core/doctype/user/user.py | 6 +++++-
frappe/core/notifications.py | 7 ++++++-
2 files changed, 11 insertions(+), 2 deletions(-)
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 9561dcb737..f36553593b 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -364,7 +364,11 @@ class User(Document):
# delete todos
frappe.db.delete("ToDo", {"owner": self.name})
todo_table = DocType("ToDo")
- frappe.qb.update(todo_table).set(todo_table.assigned_by, None).where(todo_table.assigned_by == self.name).run()
+ (
+ frappe.qb.update(todo_table)
+ .set(todo_table.assigned_by, None)
+ .where(todo_table.assigned_by == self.name)
+ ).run()
# delete events
frappe.db.delete("Event", {"owner": self.name, "event_type": "Private"})
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index d091983d00..a69c644383 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -61,7 +61,12 @@ def get_unread_emails():
communication_doctype = DocType("Communication")
user_doctype = DocType("User")
- distinct_email_accounts = frappe.qb.from_(user_doctype).select(user_doctype.email_account).where(user_doctype.parent == frappe.session.user).distinct()
+ distinct_email_accounts = (
+ frappe.qb.from_(user_doctype)
+ .select(user_doctype.email_account)
+ .where(user_doctype.parent == frappe.session.user)
+ .distinct()
+ )
return frappe.db.count(communication_doctype, filters=(
(communication_doctype.communication_type == "Communication")
From 23ce93be73a404326e43bcaad02d0da37d03bc47 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Tue, 30 Nov 2021 15:28:16 +0530
Subject: [PATCH 098/246] fix: reset lft rgt in copy doc
---
frappe/public/js/frappe/model/create_new.js | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js
index 1b09a451eb..d10af1932e 100644
--- a/frappe/public/js/frappe/model/create_new.js
+++ b/frappe/public/js/frappe/model/create_new.js
@@ -178,7 +178,7 @@ $.extend(frappe.model, {
user_default = user_defaults[0];
}
}
-
+
if (!user_default) {
user_default = frappe.defaults.get_user_default(df.fieldname);
} else if (
@@ -351,6 +351,8 @@ $.extend(frappe.model, {
newdoc.creation = "";
newdoc.modified_by = user;
newdoc.modified = "";
+ newdoc.lft = null;
+ newdoc.rgt = null;
return newdoc;
},
From 2410dc66c197d3fce2d0256c46382122e6028519 Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Tue, 30 Nov 2021 15:36:06 +0530
Subject: [PATCH 099/246] style: Fix indentation
---
frappe/core/notifications.py | 24 +++++++++++++-----------
1 file changed, 13 insertions(+), 11 deletions(-)
diff --git a/frappe/core/notifications.py b/frappe/core/notifications.py
index a69c644383..939cf52911 100644
--- a/frappe/core/notifications.py
+++ b/frappe/core/notifications.py
@@ -45,15 +45,15 @@ def get_unseen_likes():
comment_doctype = DocType("Comment")
return frappe.db.count(comment_doctype,
- filters=(
- (comment_doctype.comment_type == "Like")
- & (comment_doctype.modified >= Now() - Interval(years=1))
- & (comment_doctype.owner.notnull())
- & (comment_doctype.owner != frappe.session.user)
- & (comment_doctype.reference_owner == frappe.session.user)
- & (comment_doctype.seen == 0)
- )
- )
+ filters=(
+ (comment_doctype.comment_type == "Like")
+ & (comment_doctype.modified >= Now() - Interval(years=1))
+ & (comment_doctype.owner.notnull())
+ & (comment_doctype.owner != frappe.session.user)
+ & (comment_doctype.reference_owner == frappe.session.user)
+ & (comment_doctype.seen == 0)
+ )
+ )
def get_unread_emails():
@@ -68,7 +68,8 @@ def get_unread_emails():
.distinct()
)
- return frappe.db.count(communication_doctype, filters=(
+ return frappe.db.count(communication_doctype,
+ filters=(
(communication_doctype.communication_type == "Communication")
& (communication_doctype.communication_medium == "Email")
& (communication_doctype.sent_or_received == "Received")
@@ -76,4 +77,5 @@ def get_unread_emails():
& (communication_doctype.email_account.isin(distinct_email_accounts))
& (communication_doctype.modified >= Now() - Interval(years=1))
& (communication_doctype.seen == 0)
- ))
+ )
+ )
From dc522395c80fa9e320b63bf23827974ef0ef3488 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Tue, 30 Nov 2021 16:49:27 +0530
Subject: [PATCH 100/246] test: Fix test case for datetime
---
cypress/integration/datetime.js | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js
index b310526c7c..ef1952dc94 100644
--- a/cypress/integration/datetime.js
+++ b/cypress/integration/datetime.js
@@ -92,15 +92,15 @@ context('Control Date, Time and DateTime', () => {
date_format: 'dd.mm.yyyy',
time_format: 'HH:mm:ss',
value: ' 02.12.2019 11:00:12',
- doc_value: '2019-12-02 11:00:12',
- input_value: '02.12.2019 11:00:12'
+ doc_value: '2019-12-02 00:30:12', // system timezone (America/New_York)
+ input_value: '02.12.2019 11:00:12' // admin timezone (Asia/Kolkata)
},
{
date_format: 'mm-dd-yyyy',
time_format: 'HH:mm',
value: ' 12-02-2019 11:00:00',
- doc_value: '2019-12-02 11:00:00',
- input_value: '12-02-2019 11:00'
+ doc_value: '2019-12-02 00:30:00', // system timezone (America/New_York)
+ input_value: '12-02-2019 11:00' // admin timezone (Asia/Kolkata)
}
];
datetime_formats.forEach(d => {
From 62247431ca11efa09529c061f57d5329bc758a7f Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Tue, 30 Nov 2021 17:38:47 +0530
Subject: [PATCH 101/246] fix: scrolling issues after minimize global search
(backport #15127) (#15131)
Co-authored-by: Bhavesh Maheshwari
---
frappe/public/scss/common/modal.scss | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frappe/public/scss/common/modal.scss b/frappe/public/scss/common/modal.scss
index 54843290fc..ec582591f2 100644
--- a/frappe/public/scss/common/modal.scss
+++ b/frappe/public/scss/common/modal.scss
@@ -152,6 +152,8 @@ body.modal-open[style^="padding-right"] {
.modal-minimize {
position: initial;
+ height: 0;
+ width: 0;
.modal-dialog {
position: fixed;
From 5715bbda5077349575cb227e5576c0c193d0c638 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Tue, 30 Nov 2021 22:50:43 +0530
Subject: [PATCH 102/246] fix: multiple time global search minimize screen
freeze (backport #15133) (#15134)
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Co-authored-by: Bhavesh Maheshwari <34086262+bhavesh95863@users.noreply.github.com>
---
frappe/public/js/frappe/ui/dialog.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/frappe/public/js/frappe/ui/dialog.js b/frappe/public/js/frappe/ui/dialog.js
index b1a22c8929..e2e51ce501 100644
--- a/frappe/public/js/frappe/ui/dialog.js
+++ b/frappe/public/js/frappe/ui/dialog.js
@@ -198,6 +198,11 @@ frappe.ui.Dialog = class Dialog extends frappe.ui.FieldGroup {
this.$wrapper.removeClass('modal-minimize');
+ if (this.minimizable && this.is_minimized) {
+ $(".modal-backdrop").toggle();
+ this.is_minimized = false;
+ }
+
// clear any message
this.clear_message();
From d622b96e64312bd24d8606b4ad3511d6d88d5e92 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 1 Dec 2021 10:54:15 +0530
Subject: [PATCH 103/246] fix: request for account deletion flow
---
.../emails/account_deletion_notification.html | 3 +++
.../personal_data_deletion_request.py | 17 +++++++++++--
.../website_settings/website_settings.json | 25 ++++++++++++++++---
.../website_settings/website_settings.py | 4 +++
.../request_to_delete_data.js | 15 +++++++++--
.../request_to_delete_data.json | 10 +++++---
frappe/www/me.html | 3 +++
7 files changed, 66 insertions(+), 11 deletions(-)
create mode 100644 frappe/templates/emails/account_deletion_notification.html
diff --git a/frappe/templates/emails/account_deletion_notification.html b/frappe/templates/emails/account_deletion_notification.html
new file mode 100644
index 0000000000..17d6aa3c93
--- /dev/null
+++ b/frappe/templates/emails/account_deletion_notification.html
@@ -0,0 +1,3 @@
+{{_("Dear User,")}}
+{{_("As per your request, your account and data on {0} associated with email {1} has been permanently deleted").format(app_name, email)}}.
+
diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
index 8a72fa269f..c3e0d22063 100644
--- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
+++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
@@ -75,7 +75,7 @@ class PersonalDataDeletionRequest(Document):
frappe.sendmail(
recipients=self.email,
- subject=_("Confirm Deletion of Data"),
+ subject=_("Confirm Deletion of Account"),
template="delete_data_confirmation",
args={
"email": self.email,
@@ -83,7 +83,7 @@ class PersonalDataDeletionRequest(Document):
"host_name": frappe.local.site,
"link": url,
},
- header=[_("Confirm Deletion of Data"), "green"],
+ header=[_("Confirm Deletion of Account"), "green"],
)
def notify_system_managers(self):
@@ -109,6 +109,7 @@ class PersonalDataDeletionRequest(Document):
self.validate_data_anonymization()
self.disable_user()
self.anonymize_data()
+ self.notify_user_after_deletion()
def anonymize_data(self):
return frappe.enqueue_doc(
@@ -120,6 +121,18 @@ class PersonalDataDeletionRequest(Document):
now=frappe.flags.in_test,
)
+ def notify_user_after_deletion(self):
+ frappe.sendmail(
+ recipients=self.email,
+ subject=_("Your account has been deleted"),
+ template="account_deletion_notification",
+ args={
+ "email": self.email,
+ "app_name": frappe.db.get_single_value("Website Settings", "app_name"),
+ },
+ header=[_("Your account has been deleted"), "green"],
+ )
+
def add_deletion_steps(self):
if self.deletion_steps:
return
diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json
index 48f097e525..f39147d555 100644
--- a/frappe/website/doctype/website_settings/website_settings.json
+++ b/frappe/website/doctype/website_settings/website_settings.json
@@ -63,7 +63,10 @@
"subdomain",
"head_html",
"robots_txt",
- "route_redirects"
+ "route_redirects",
+ "account_deletion_settings_section",
+ "show_account_deletion_link",
+ "account_deletion_sla"
],
"fields": [
{
@@ -386,6 +389,22 @@
"fieldname": "app_logo",
"fieldtype": "Attach Image",
"label": "App Logo"
+ },
+ {
+ "fieldname": "account_deletion_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Account Deletion Settings"
+ },
+ {
+ "fieldname": "account_deletion_sla",
+ "fieldtype": "Int",
+ "label": "Account Deletion SLA (Days)"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_account_deletion_link",
+ "fieldtype": "Check",
+ "label": "Show Account Deletion Link in My Account Page"
}
],
"icon": "fa fa-cog",
@@ -394,7 +413,7 @@
"issingle": 1,
"links": [],
"max_attachments": 10,
- "modified": "2021-08-23 21:39:51.702248",
+ "modified": "2021-12-01 10:15:17.403155",
"modified_by": "Administrator",
"module": "Website",
"name": "Website Settings",
@@ -418,4 +437,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py
index 7a744eaf53..02eed9cc6d 100644
--- a/frappe/website/doctype/website_settings/website_settings.py
+++ b/frappe/website/doctype/website_settings/website_settings.py
@@ -177,3 +177,7 @@ def get_items(parentfield):
t['child_items'].append(d)
break
return top_items
+
+@frappe.whitelist(allow_guest=True)
+def get_account_deletion_sla():
+ return frappe.db.get_single_value("Website Settings", "account_deletion_sla")
diff --git a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
index 7da3f1fb41..9d19e36a05 100644
--- a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
+++ b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
@@ -1,3 +1,14 @@
frappe.ready(function() {
- // bind events here
-});
\ No newline at end of file
+ frappe.web_form.after_load = () => {
+ frappe.call({
+ method: "frappe.website.doctype.website_settings.website_settings.get_account_deletion_sla",
+ callback: (data) => {
+ if (data.message) {
+ const intro_wrapper = $('#introduction .ql-editor.read-mode');
+ const sla_description = `Note: Your request for account deletion will be fulfilled within ${data.message} days. `;
+ intro_wrapper.html(intro_wrapper.html() + sla_description);
+ }
+ }
+ })
+ }
+});
diff --git a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json
index b0180d833c..1113297df6 100644
--- a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json
+++ b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.json
@@ -10,24 +10,26 @@
"amount_based_on_field": 0,
"apply_document_permissions": 0,
"button_label": "Submit",
+ "client_script": "",
"creation": "2019-01-25 14:24:12.588810",
"currency": "INR",
+ "custom_css": "[data-doctype=\"Web Form\"] {\n width: 50%;\n margin: 6rem auto;\n}",
"doc_type": "Personal Data Deletion Request",
"docstatus": 0,
"doctype": "Web Form",
"idx": 0,
- "introduction_text": "Send a request to delete your personally identifiable information (PII) that is stored on our system. You will receive an email to verify your request. Once the request is verified we will take care of deleting your PII. If you just want to check what PII we have stored, you can request your data .
",
+ "introduction_text": "Send a request to delete your account and personally identifiable information (PII) that is stored on our system. You will receive an email to verify your request. Once the request is verified we will take care of deleting your PII. If you just want to check what PII we have stored, you can request your data .
",
"is_standard": 1,
"login_required": 0,
"max_attachment_size": 0,
- "modified": "2021-03-25 11:08:49.580621",
+ "modified": "2021-11-30 17:56:03.099870",
"modified_by": "Administrator",
"module": "Website",
"name": "request-to-delete-data",
"owner": "Administrator",
"payment_button_label": "Buy Now",
"published": 1,
- "route": "request-to-delete-data",
+ "route": "request-for-account-deletion",
"route_to_success_link": 0,
"show_attachments": 0,
"show_in_grid": 0,
@@ -35,7 +37,7 @@
"sidebar_items": [],
"success_message": "An email to verify your request has been sent to your email address. Please verify your request to complete the process.",
"success_url": "/",
- "title": "Request to Delete Data",
+ "title": "Request for Account Deletion",
"web_form_fields": [
{
"allow_read_on_all_link_options": 0,
diff --git a/frappe/www/me.html b/frappe/www/me.html
index eb97c566d8..4f9a59cac5 100644
--- a/frappe/www/me.html
+++ b/frappe/www/me.html
@@ -10,6 +10,9 @@
{{ _("Reset Password") }}
{{ _("Edit Profile") }}
{{ _("Manage Third Party Apps") }}
+ {% if frappe.db.get_single_value("Website Settings", "show_account_deletion_link") %}
+ {{ _("Request for Account Deletion") }}
+ {% endif %}
From a86f8d9640a7d01f88cc2d2beeac52572c00aa98 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 1 Dec 2021 11:27:23 +0530
Subject: [PATCH 104/246] fix: Do not guess timezone for only time
- Time value should be consistent across timezones
- Only worry about timezone when dealing with datetime
---
frappe/public/js/frappe/utils/datetime.js | 26 ++++++++++++-----------
1 file changed, 14 insertions(+), 12 deletions(-)
diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js
index 165636bad4..2fcfd75c5e 100644
--- a/frappe/public/js/frappe/utils/datetime.js
+++ b/frappe/public/js/frappe/utils/datetime.js
@@ -17,14 +17,15 @@ $.extend(frappe.datetime, {
// system time zone and then convert the string to user time zone(from User doctype).
let date_obj = null;
if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) {
- date_obj = moment.tz(date, frappe.boot.time_zone.system)
+ date_obj = moment(date)
+ .tz(frappe.boot.time_zone.system)
.clone()
.tz(frappe.boot.time_zone.user);
} else {
date_obj = moment(date);
}
- return format===false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
+ return format === false ? date_obj : date_obj.format(frappe.defaultDatetimeFormat);
},
convert_to_system_tz: function(date, format) {
@@ -113,11 +114,11 @@ $.extend(frappe.datetime, {
return moment().endOf("quarter").format();
},
- year_start: function(){
+ year_start: function() {
return moment().startOf("year").format();
},
- year_end: function(){
+ year_end: function() {
return moment().endOf("year").format();
},
@@ -135,22 +136,23 @@ $.extend(frappe.datetime, {
str_to_user: function(val, only_time = false) {
if (!val) return "";
+ const user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase();
const user_time_fmt = frappe.datetime.get_user_time_fmt();
- let date_obj = moment(val);
let user_format = user_time_fmt;
if (only_time) {
- date_obj = moment(val, frappe.defaultTimeFormat);
+ let date_obj = moment(val, frappe.defaultTimeFormat);
+ return date_obj.format(user_format);
} else {
- let user_date_fmt = frappe.datetime.get_user_date_fmt().toUpperCase();
- if (typeof val !== "string" || val.indexOf(" ")===-1) {
- date_obj = moment(val);
+ let date_obj = moment(val);
+ if (typeof val !== "string" || val.indexOf(" ") === -1) {
+ user_format = user_date_fmt;
} else {
date_obj = moment(val, "YYYY-MM-DD HH:mm:ss");
user_format = user_date_fmt + " " + user_time_fmt;
}
+ return date_obj.tz(frappe.boot.time_zone.user).format(user_format);
}
- return date_obj.tz(frappe.boot.time_zone.user).format(user_format);
},
get_datetime_as_string: function(d) {
@@ -217,9 +219,9 @@ $.extend(frappe.datetime, {
return as_obj ? frappe.datetime.moment_to_date_obj(date) : date.format(format);
},
- moment_to_date_obj: function(moment) {
+ moment_to_date_obj: function(moment_obj) {
const date_obj = new Date();
- const date_array = moment.toArray();
+ const date_array = moment_obj.toArray();
date_obj.setFullYear(date_array[0]);
date_obj.setMonth(date_array[1]);
date_obj.setDate(date_array[2]);
From 515dca29fda58c4979989d0590f59d30c6a6c595 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 1 Dec 2021 12:57:15 +0530
Subject: [PATCH 105/246] fix: Remove unnecessary code
- frappe.datetime.str_to_user(value, false) takes care of tz conversion
---
frappe/public/js/frappe/form/controls/datetime.js | 3 ---
1 file changed, 3 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index 3142f1bf0f..d8c67ce2e9 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -40,9 +40,6 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
format_for_input(value) {
if (!value) return "";
- if (!frappe.datetime.is_system_time_zone()) {
- value = frappe.datetime.convert_to_user_tz(value, true);
- }
return frappe.datetime.str_to_user(value, false);
}
From e2bf192633624669693d39a09bc9ac4ccb0cc940 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 1 Dec 2021 12:59:32 +0530
Subject: [PATCH 106/246] fix: Select date of datepicker while doing
set_formatted_input
- To avoid empty value when clicking datetime for first time
---
frappe/public/js/frappe/form/controls/datetime.js | 1 +
1 file changed, 1 insertion(+)
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index d8c67ce2e9..c7efc3e2c3 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -10,6 +10,7 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
}
this.$input && this.$input.val(this.format_for_input(value));
+ this.datepicker.selectDate(frappe.datetime.str_to_obj(value));
}
set_date_options() {
super.set_date_options();
From 62073d65a0aa948902c6f2f556a3f4971e7e72d7 Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Wed, 1 Dec 2021 13:10:49 +0530
Subject: [PATCH 107/246] fix: Set now date based on system timezone
- because set_formatted_value will convert value to user timezone down the line
---
frappe/public/js/frappe/form/controls/date.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js
index 9ad81c7e46..ba94531b0f 100644
--- a/frappe/public/js/frappe/form/controls/date.js
+++ b/frappe/public/js/frappe/form/controls/date.js
@@ -53,7 +53,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
let date_format = sysdefaults && sysdefaults.date_format
? sysdefaults.date_format : 'yyyy-mm-dd';
- let now_date = new Date();
+ let now_date = new Date(this.get_now_date());
this.today_text = __("Today");
this.date_format = frappe.defaultDateFormat;
@@ -112,7 +112,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
this.datepicker.update('position', position);
}
get_now_date() {
- return frappe.datetime.now_date(true);
+ return frappe.datetime.convert_to_system_tz(frappe.datetime.now_date(true));
}
set_t_for_today() {
var me = this;
From db101e5ec396ae1562608ed9a159c9f0877eab37 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 30 Nov 2021 16:25:20 +0530
Subject: [PATCH 108/246] fix(ux): ensure max_attachments is more than no of
attach fields
---
frappe/core/doctype/doctype/doctype.js | 22 +++++++++++++++++++++-
1 file changed, 21 insertions(+), 1 deletion(-)
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index 262a6efd90..291fb237a9 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -129,7 +129,23 @@ frappe.ui.form.on('DocType', {
}
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
- }
+ },
+
+ max_attachments: function(frm) {
+ if (!frm.doc.max_attachments) {
+ return;
+ }
+ const is_attach_field = (f) => ["Attach", "Attach Image"].includes(f.fieldtype);
+ const no_of_attach_fields = frm.doc.fields.filter(is_attach_field).length;
+
+ if (no_of_attach_fields > frm.doc.max_attachments) {
+ frm.set_value("max_attachments", no_of_attach_fields);
+ const label = frm.get_docfield("max_attachments").label;
+ frappe.show_alert(
+ __("Number of attachment fields are more than {}, limit updated to {}.", [label, no_of_attach_fields]));
+ }
+ },
+
});
frappe.ui.form.on("DocField", {
@@ -217,5 +233,9 @@ frappe.ui.form.on("DocField", {
$doctype_select.val(curr_value.doctype);
update_fieldname_options();
}
+ },
+
+ fieldtype: function(frm) {
+ frm.trigger("max_attachments");
}
});
From 2f6b57cc0ae2b87859bdcca8755f0e764f0ea16a Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Tue, 30 Nov 2021 12:18:52 +0530
Subject: [PATCH 109/246] fix(ux): validate max_attachment on doctype
controller
---
frappe/core/doctype/doctype/doctype.py | 17 +++++++++++++++++
frappe/model/__init__.py | 5 +++++
2 files changed, 22 insertions(+)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 1c8c1f9217..a6a81cb195 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -75,6 +75,7 @@ class DocType(Document):
self.make_repeatable()
self.validate_nestedset()
self.validate_website()
+ self.ensure_minimum_max_attachment_limit()
validate_links_table_fieldnames(self)
if not self.is_new():
@@ -246,6 +247,22 @@ class DocType(Document):
# clear website cache
clear_cache()
+ def ensure_minimum_max_attachment_limit(self):
+ """Ensure that max_attachments is *at least* bigger than number of attach fields."""
+ from frappe.model import attachment_fieldtypes
+
+
+ if not self.max_attachments:
+ return
+
+ total_attach_fields = len([d for d in self.fields if d.fieldtype in attachment_fieldtypes])
+ if total_attach_fields > self.max_attachments:
+ self.max_attachments = total_attach_fields
+ field_label = frappe.bold(self.meta.get_field("max_attachments").label)
+ frappe.msgprint(_("Number of attachment fields are more than {}, limit updated to {}.")
+ .format(field_label, total_attach_fields),
+ title=_("Insufficient attachment limit"), alert=True)
+
def change_modified_of_parent(self):
"""Change the timestamp of parent DocType if the current one is a child to clear caches."""
if frappe.flags.in_import:
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index b460db29a7..b50a0304a5 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -38,6 +38,11 @@ data_fieldtypes = (
'Icon'
)
+attachment_fieldtypes = (
+ 'Attach',
+ 'Attach Image',
+)
+
no_value_fields = (
'Section Break',
'Column Break',
From 3a34bfc52035b2ff23091d827d8f41a8c7f49f30 Mon Sep 17 00:00:00 2001
From: Manuel <57345036+mtraeber@users.noreply.github.com>
Date: Wed, 1 Dec 2021 13:41:27 +0100
Subject: [PATCH 110/246] Fix: copy_email_data patch #15106 (#15115)
Reload DocType `Email Account` in patch.
Changed `modified` in json files.
Removed linking of `imap_folder` in `Email Flag Queue` this connection
seems not necessary at this point. Also removed all parts that create
this connection.
---
.../doctype/communication/communication.json | 2 +-
.../doctype/communication/communication.py | 1 -
.../doctype/email_account/email_account.json | 2 +-
.../doctype/email_account/email_account.py | 3 +-
.../email_flag_queue/email_flag_queue.json | 256 ++++--------------
frappe/patches/v14_0/copy_mail_data.py | 6 +-
6 files changed, 59 insertions(+), 211 deletions(-)
diff --git a/frappe/core/doctype/communication/communication.json b/frappe/core/doctype/communication/communication.json
index 9e154146b3..175c64b9eb 100644
--- a/frappe/core/doctype/communication/communication.json
+++ b/frappe/core/doctype/communication/communication.json
@@ -395,7 +395,7 @@
"icon": "fa fa-comment",
"idx": 1,
"links": [],
- "modified": "2021-03-25 09:44:28.963538",
+ "modified": "2021-11-30 09:03:25.728637",
"modified_by": "Administrator",
"module": "Core",
"name": "Communication",
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index f2fbc26a22..3a78a6a599 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -42,7 +42,6 @@ class Communication(Document, CommunicationEmailMixin):
"action": "Read",
"communication": self.name,
"uid": self.uid,
- "imap_folder": self.imap_folder,
"email_account": self.email_account
}).insert(ignore_permissions=True)
frappe.db.commit()
diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json
index bf9a79529b..65053bab3d 100644
--- a/frappe/email/doctype/email_account/email_account.json
+++ b/frappe/email/doctype/email_account/email_account.json
@@ -581,7 +581,7 @@
"icon": "fa fa-inbox",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-09-21 16:44:25.728637",
+ "modified": "2021-11-30 09:03:25.728637",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Account",
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index 52623fb358..ef1d49302f 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -578,10 +578,9 @@ class EmailAccount(Document):
EmailFlagQ = frappe.qb.DocType("Email Flag Queue")
flags = (
frappe.qb.from_(EmailFlagQ)
- .select(EmailFlagQ.name, EmailFlagQ.communication, EmailFlagQ.uid, EmailFlagQ.action, EmailFlagQ.imap_folder)
+ .select(EmailFlagQ.name, EmailFlagQ.communication, EmailFlagQ.uid, EmailFlagQ.action)
.where(EmailFlagQ.is_completed == 0)
.where(EmailFlagQ.email_account == frappe.db.escape(self.name))
- .where(EmailFlagQ.folder_name == frappe.db.escape(folder_name))
).run(as_dict=True)
uid_list = { flag.get("uid", None): flag.get("action", "Read") for flag in flags }
diff --git a/frappe/email/doctype/email_flag_queue/email_flag_queue.json b/frappe/email/doctype/email_flag_queue/email_flag_queue.json
index 165e8f9ea9..14b1ec4f53 100644
--- a/frappe/email/doctype/email_flag_queue/email_flag_queue.json
+++ b/frappe/email/doctype/email_flag_queue/email_flag_queue.json
@@ -1,213 +1,67 @@
{
- "allow_copy": 1,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-04-20 15:29:39.785172",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 0,
+ "actions": [],
+ "allow_copy": 1,
+ "creation": "2016-04-20 15:29:39.785172",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "is_completed",
+ "communication",
+ "action",
+ "email_account",
+ "uid"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "is_completed",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Is Completed",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "default": "0",
+ "fieldname": "is_completed",
+ "fieldtype": "Check",
+ "label": "Is Completed",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "communication",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Communication",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "communication",
+ "fieldtype": "Data",
+ "label": "Communication"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "action",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Action",
- "length": 0,
- "no_copy": 0,
- "options": "Read\nUnread",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "action",
+ "fieldtype": "Select",
+ "label": "Action",
+ "options": "Read\nUnread"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "email_account",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Email Account",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "email_account",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Email Account"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "uid",
- "fieldtype": "Data",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "UID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "uid",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "UID"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 1,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-09-20 15:27:12.142079",
- "modified_by": "Administrator",
- "module": "Email",
- "name": "Email Flag Queue",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "in_create": 1,
+ "links": [],
+ "modified": "2021-11-30 09:51:34.489932",
+ "modified_by": "Administrator",
+ "module": "Email",
+ "name": "Email Flag Queue",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 0
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/frappe/patches/v14_0/copy_mail_data.py b/frappe/patches/v14_0/copy_mail_data.py
index 8780ab8630..362d23d0e1 100644
--- a/frappe/patches/v14_0/copy_mail_data.py
+++ b/frappe/patches/v14_0/copy_mail_data.py
@@ -3,6 +3,7 @@ import frappe
def execute():
+ frappe.reload_doc("email", "doctype", "email_account")
# patch for all Email Account with the flag use_imap
for email_account in frappe.get_list("Email Account", filters={"enable_incoming": 1, "use_imap": 1}):
# get all data from Email Account
@@ -19,8 +20,3 @@ def execute():
})
doc.save()
- EmailFlagQueue = frappe.qb.DocType("Email Flag Queue")
- frappe.qb.update(EmailFlagQueue) \
- .set(EmailFlagQueue.imap_folder, "INBOX") \
- .where(EmailFlagQueue.email_account == doc.name) \
- .where(EmailFlagQueue.imap_folder.isnull()).run()
From 121333874a07d18eb4e3aff83496d975c3f03bc3 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 1 Dec 2021 18:54:01 +0530
Subject: [PATCH 111/246] fix: send email immediately
---
frappe/templates/emails/delete_data_confirmation.html | 4 ++--
.../personal_data_deletion_request.py | 1 +
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/frappe/templates/emails/delete_data_confirmation.html b/frappe/templates/emails/delete_data_confirmation.html
index 126d8bcb4b..a5794abf96 100644
--- a/frappe/templates/emails/delete_data_confirmation.html
+++ b/frappe/templates/emails/delete_data_confirmation.html
@@ -7,6 +7,6 @@
{{ _("Confirm Request") }}
- {% set verification_link = '{{ _("Verification Link") }} ' %}
+ {% set verification_link = ' _("Verification Link") ' %}
{{_("You can also copy-paste this {0} to your browser").format(verification_link) }}
-
\ No newline at end of file
+
diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
index c3e0d22063..460f2cad53 100644
--- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
+++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
@@ -131,6 +131,7 @@ class PersonalDataDeletionRequest(Document):
"app_name": frappe.db.get_single_value("Website Settings", "app_name"),
},
header=[_("Your account has been deleted"), "green"],
+ now=True
)
def add_deletion_steps(self):
From f33be7592f21de80c55a48824c7e94885a3198cb Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Wed, 1 Dec 2021 18:59:03 +0530
Subject: [PATCH 112/246] refactor: common JS controller for DocType and
customize form
---
frappe/core/doctype/doctype/doctype.js | 28 ++-----------------
.../doctype/customize_form/customize_form.js | 1 +
frappe/public/js/form.bundle.js | 2 +-
frappe/public/js/frappe/doctype/index.js | 23 +++++++++++++++
4 files changed, 27 insertions(+), 27 deletions(-)
create mode 100644 frappe/public/js/frappe/doctype/index.js
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index 291fb237a9..1c52070063 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -1,16 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// MIT License. See license.txt
-// -------------
-// Menu Display
-// -------------
-
-// $(cur_frm.wrapper).on("grid-row-render", function(e, grid_row) {
-// if(grid_row.doc && grid_row.doc.fieldtype=="Section Break") {
-// $(grid_row.row).css({"font-weight": "bold"});
-// }
-// })
-
frappe.ui.form.on('DocType', {
refresh: function(frm) {
frm.set_query('role', 'permissions', function(doc) {
@@ -130,22 +120,6 @@ frappe.ui.form.on('DocType', {
frm.set_df_property('fields', 'reqd', frm.doc.autoname !== 'Prompt');
},
-
- max_attachments: function(frm) {
- if (!frm.doc.max_attachments) {
- return;
- }
- const is_attach_field = (f) => ["Attach", "Attach Image"].includes(f.fieldtype);
- const no_of_attach_fields = frm.doc.fields.filter(is_attach_field).length;
-
- if (no_of_attach_fields > frm.doc.max_attachments) {
- frm.set_value("max_attachments", no_of_attach_fields);
- const label = frm.get_docfield("max_attachments").label;
- frappe.show_alert(
- __("Number of attachment fields are more than {}, limit updated to {}.", [label, no_of_attach_fields]));
- }
- },
-
});
frappe.ui.form.on("DocField", {
@@ -239,3 +213,5 @@ frappe.ui.form.on("DocField", {
frm.trigger("max_attachments");
}
});
+
+extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 4e00456f0d..8ca6e0e54e 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -332,3 +332,4 @@ frappe.customize_form.clear_locals_and_refresh = function(frm) {
frm.refresh();
}
+extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm}));
diff --git a/frappe/public/js/form.bundle.js b/frappe/public/js/form.bundle.js
index 5bed5c2cb8..2719535599 100644
--- a/frappe/public/js/form.bundle.js
+++ b/frappe/public/js/form.bundle.js
@@ -14,4 +14,4 @@ import "./frappe/form/controls/control.js";
import "./frappe/views/formview.js";
import "./frappe/form/form.js";
import "./frappe/meta_tag.js";
-
+import "./frappe/doctype/"
diff --git a/frappe/public/js/frappe/doctype/index.js b/frappe/public/js/frappe/doctype/index.js
new file mode 100644
index 0000000000..9fe8957c60
--- /dev/null
+++ b/frappe/public/js/frappe/doctype/index.js
@@ -0,0 +1,23 @@
+frappe.provide("frappe.model");
+
+/*
+ Common class for handling client side interactions that
+ apply to both DocType form and customize form.
+*/
+frappe.model.DocTypeController = class DocTypeController extends frappe.ui.form.Controller {
+
+ max_attachments() {
+ if (!this.frm.doc.max_attachments) {
+ return;
+ }
+ const is_attach_field = (f) => ["Attach", "Attach Image"].includes(f.fieldtype);
+ const no_of_attach_fields = this.frm.doc.fields.filter(is_attach_field).length;
+
+ if (no_of_attach_fields > this.frm.doc.max_attachments) {
+ this.frm.set_value("max_attachments", no_of_attach_fields);
+ const label = this.frm.get_docfield("max_attachments").label;
+ frappe.show_alert(
+ __("Number of attachment fields are more than {}, limit updated to {}.", [label, no_of_attach_fields]));
+ }
+ }
+}
From abd6eafbad3321b71636940e20ee6549c12507aa Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 1 Dec 2021 19:44:19 +0530
Subject: [PATCH 113/246] fix: email content
---
frappe/templates/emails/account_deletion_notification.html | 3 ++-
frappe/templates/emails/delete_data_confirmation.html | 2 +-
.../personal_data_deletion_request.py | 6 +++---
3 files changed, 6 insertions(+), 5 deletions(-)
diff --git a/frappe/templates/emails/account_deletion_notification.html b/frappe/templates/emails/account_deletion_notification.html
index 17d6aa3c93..2e8d8b2e3a 100644
--- a/frappe/templates/emails/account_deletion_notification.html
+++ b/frappe/templates/emails/account_deletion_notification.html
@@ -1,3 +1,4 @@
{{_("Dear User,")}}
-{{_("As per your request, your account and data on {0} associated with email {1} has been permanently deleted").format(app_name, email)}}.
+
+{{_("As per your request, your account and data on {0} associated with email {1} has been permanently deleted").format(host_name, email)}}.
diff --git a/frappe/templates/emails/delete_data_confirmation.html b/frappe/templates/emails/delete_data_confirmation.html
index a5794abf96..bd81f92f40 100644
--- a/frappe/templates/emails/delete_data_confirmation.html
+++ b/frappe/templates/emails/delete_data_confirmation.html
@@ -7,6 +7,6 @@
{{ _("Confirm Request") }}
- {% set verification_link = ' _("Verification Link") ' %}
+ {% set verification_link = ' {0} '.format(_("Verification Link")) %}
{{_("You can also copy-paste this {0} to your browser").format(verification_link) }}
diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
index 460f2cad53..ef3856ad25 100644
--- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
+++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
@@ -80,7 +80,7 @@ class PersonalDataDeletionRequest(Document):
args={
"email": self.email,
"name": self.name,
- "host_name": frappe.local.site,
+ "host_name": frappe.utils.get_url(),
"link": url,
},
header=[_("Confirm Deletion of Account"), "green"],
@@ -128,7 +128,7 @@ class PersonalDataDeletionRequest(Document):
template="account_deletion_notification",
args={
"email": self.email,
- "app_name": frappe.db.get_single_value("Website Settings", "app_name"),
+ "host_name": frappe.utils.get_url(),
},
header=[_("Your account has been deleted"), "green"],
now=True
@@ -351,7 +351,7 @@ def confirm_deletion(email, name, host_name):
return
doc = frappe.get_doc("Personal Data Deletion Request", name)
- host_name = frappe.local.site
+ host_name = frappe.utils.get_url()
if doc.status == "Pending Verification":
doc.status = "Pending Approval"
From d26899662929d5d86cda26ab037f69a930599334 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Wed, 1 Dec 2021 21:18:52 +0530
Subject: [PATCH 114/246] fix: test and semicolon
---
.../test_personal_data_deletion_request.py | 2 +-
.../web_form/request_to_delete_data/request_to_delete_data.js | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py
index 240e37adef..8fc8f38512 100644
--- a/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py
+++ b/frappe/website/doctype/personal_data_deletion_request/test_personal_data_deletion_request.py
@@ -24,7 +24,7 @@ class TestPersonalDataDeletionRequest(unittest.TestCase):
email_queue = frappe.get_all("Email Queue", fields=["*"], order_by="creation desc", limit=1)
self.assertEqual(self.delete_request.status, "Pending Verification")
- self.assertTrue("Subject: Confirm Deletion of Data" in email_queue[0].message)
+ self.assertTrue("Subject: Confirm Deletion of Account" in email_queue[0].message)
def test_anonymized_data(self):
self.delete_request.status = "Pending Approval"
diff --git a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
index 9d19e36a05..9279302628 100644
--- a/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
+++ b/frappe/website/web_form/request_to_delete_data/request_to_delete_data.js
@@ -9,6 +9,6 @@ frappe.ready(function() {
intro_wrapper.html(intro_wrapper.html() + sla_description);
}
}
- })
- }
+ });
+ };
});
From 9e6cefba6c280dbbe800b9428212627d55ff24ac Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Thu, 2 Dec 2021 10:01:20 +0530
Subject: [PATCH 115/246] fix: Add fallback option for time format when system
defaults are not set
---
frappe/public/js/frappe/form/formatters.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 0a3c032a40..7e1f3fed06 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -172,7 +172,7 @@ frappe.form.formatters = {
m = m.tz(frappe.boot.sysdefaults.time_zone);
}
return m.format(frappe.boot.sysdefaults.date_format.toUpperCase()
- + ' ' + frappe.boot.sysdefaults.time_format);
+ + ' ' + (frappe.boot.sysdefaults.time_format || 'HH:mm:ss'));
} else {
return "";
}
From ba094e76d1fe0b4c093e4b4e0c16fce05c277687 Mon Sep 17 00:00:00 2001
From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com>
Date: Thu, 2 Dec 2021 10:58:36 +0530
Subject: [PATCH 116/246] fix: Add `xcall` to Frappe's web bundle (backport
#15146) (#15152)
Co-authored-by: Omar Younis
---
frappe/website/js/website.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js
index 824b9ae3bf..079924d014 100644
--- a/frappe/website/js/website.js
+++ b/frappe/website/js/website.js
@@ -46,6 +46,20 @@ $.extend(frappe, {
hide_message: function() {
$('.message-overlay').remove();
},
+ xcall: function(method, params) {
+ return new Promise((resolve, reject) => {
+ frappe.call({
+ method: method,
+ args: params,
+ callback: (r) => {
+ resolve(r.message);
+ },
+ error: (r) => {
+ reject(r.message);
+ }
+ });
+ });
+ },
call: function(opts) {
// opts = {"method": "PYTHON MODULE STRING", "args": {}, "callback": function(r) {}}
if (typeof arguments[0]==='string') {
From ecca7f287ef48575dacd380c42dcd4c957156c31 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 2 Dec 2021 11:09:33 +0530
Subject: [PATCH 117/246] chore: better message for system updates (#15154)
closes https://github.com/frappe/frappe/issues/10997
[skip ci]
---
frappe/utils/response.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/utils/response.py b/frappe/utils/response.py
index 3dc6fa0c80..104c48527c 100644
--- a/frappe/utils/response.py
+++ b/frappe/utils/response.py
@@ -220,6 +220,6 @@ def send_private_file(path):
def handle_session_stopped():
from frappe.website.serve import get_response
frappe.respond_as_web_page(_("Updating"),
- _("Your system is being updated. Please refresh again after a few moments."),
+ _("The system is being updated. Please refresh again after a few moments."),
http_status_code=503, indicator_color='orange', fullpage = True, primary_action=None)
return get_response("message", http_status_code=503)
From 2fe2db5f944157c5cc7fe163b9859e04ab63669c Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Thu, 2 Dec 2021 11:17:31 +0530
Subject: [PATCH 118/246] fix: form tour field in onboarding step
---
.../desk/doctype/onboarding_step/onboarding_step.js | 11 +++++++++++
.../desk/doctype/onboarding_step/onboarding_step.json | 11 ++++++++++-
frappe/public/js/frappe/widgets/onboarding_widget.js | 6 ++++--
3 files changed, 25 insertions(+), 3 deletions(-)
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.js b/frappe/desk/doctype/onboarding_step/onboarding_step.js
index 793e044d98..3c9bbab9ac 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.js
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.js
@@ -2,6 +2,17 @@
// For license information, please see license.txt
frappe.ui.form.on("Onboarding Step", {
+
+ setup: function(frm) {
+ frm.set_query("form_tour", function() {
+ return {
+ filters: {
+ reference_doctype: frm.doc.reference_document
+ }
+ };
+ });
+ },
+
refresh: function(frm) {
frappe.boot.developer_mode &&
frm.set_intro(
diff --git a/frappe/desk/doctype/onboarding_step/onboarding_step.json b/frappe/desk/doctype/onboarding_step/onboarding_step.json
index f71e821f65..b5d7851eca 100644
--- a/frappe/desk/doctype/onboarding_step/onboarding_step.json
+++ b/frappe/desk/doctype/onboarding_step/onboarding_step.json
@@ -20,6 +20,7 @@
"reference_document",
"show_full_form",
"show_form_tour",
+ "form_tour",
"is_single",
"reference_report",
"report_reference_doctype",
@@ -206,13 +207,21 @@
"fieldname": "show_form_tour",
"fieldtype": "Check",
"label": "Show Form Tour"
+ },
+ {
+ "depends_on": "show_form_tour",
+ "fieldname": "form_tour",
+ "fieldtype": "Link",
+ "label": "Form Tour",
+ "options": "Form Tour"
}
],
"links": [],
- "modified": "2020-10-30 14:54:06.646513",
+ "modified": "2021-12-02 10:56:04.448580",
"modified_by": "Administrator",
"module": "Desk",
"name": "Onboarding Step",
+ "naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 110d617f73..0c06bd3203 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -234,8 +234,9 @@ export default class OnboardingWidget extends Widget {
},
});
};
+ const tour_name = step.form_tour;
frm.tour
- .init({ on_finish })
+ .init({ tour_name, on_finish })
.then(() => frm.tour.start());
};
@@ -328,8 +329,9 @@ export default class OnboardingWidget extends Widget {
this.mark_complete(step);
};
};
+ const tour_name = step.form_tour
frm.tour
- .init({ on_finish })
+ .init({ tour_name, on_finish })
.then(() => frm.tour.start());
};
From a9b433e81944d626c6a4b18b84a9a619bcf19a20 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Thu, 2 Dec 2021 11:42:49 +0530
Subject: [PATCH 119/246] fix: semicolon
---
frappe/public/js/frappe/widgets/onboarding_widget.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/widgets/onboarding_widget.js b/frappe/public/js/frappe/widgets/onboarding_widget.js
index 0c06bd3203..7d379d4531 100644
--- a/frappe/public/js/frappe/widgets/onboarding_widget.js
+++ b/frappe/public/js/frappe/widgets/onboarding_widget.js
@@ -329,7 +329,7 @@ export default class OnboardingWidget extends Widget {
this.mark_complete(step);
};
};
- const tour_name = step.form_tour
+ const tour_name = step.form_tour;
frm.tour
.init({ tour_name, on_finish })
.then(() => frm.tour.start());
From 66321b075ef06dd73f4d6244f7fa1e99e940d065 Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 2 Dec 2021 16:19:23 +0530
Subject: [PATCH 120/246] fix: show "Button" field in user grid config
---
frappe/public/js/frappe/form/grid_row.js | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 311a5b7a1e..5903a46683 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -368,8 +368,13 @@ export default class GridRow {
prepare_columns_for_dialog(selected_fields) {
let fields = [];
+ const blocked_fields = frappe.model.no_value_type;
+ const always_allow = ["Button"];
+
+ const show_field = (f) => always_allow.includes(f) || !blocked_fields.includes(f);
+
this.docfields.forEach(column => {
- if (!column.hidden && !in_list(frappe.model.no_value_type, column.fieldtype)) {
+ if (!column.hidden && show_field(column.fieldtype)) {
fields.push({
label: column.label,
value: column.fieldname,
From e0fadef11baf91ac7647538c13e56dd2e0b63dde Mon Sep 17 00:00:00 2001
From: Ankush Menat
Date: Thu, 2 Dec 2021 16:19:38 +0530
Subject: [PATCH 121/246] feat: reset user grid config to default
---
frappe/public/js/frappe/form/grid_row.js | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js
index 5903a46683..dff5c216ee 100644
--- a/frappe/public/js/frappe/form/grid_row.js
+++ b/frappe/public/js/frappe/form/grid_row.js
@@ -291,6 +291,11 @@ export default class GridRow {
this.grid_settings_dialog.hide();
});
+ this.grid_settings_dialog.set_secondary_action_label(__("Reset to default"));
+ this.grid_settings_dialog.set_secondary_action(() => {
+ this.reset_user_settings_for_grid();
+ this.grid_settings_dialog.hide();
+ });
}
setup_columns_for_dialog() {
@@ -515,6 +520,14 @@ export default class GridRow {
});
}
+ reset_user_settings_for_grid() {
+ frappe.model.user_settings.save(this.frm.doctype, 'GridView', null)
+ .then((r) => {
+ frappe.model.user_settings[this.frm.doctype] = r.message || r;
+ this.grid.reset_grid();
+ });
+ }
+
setup_columns() {
this.focus_set = false;
From 51c4738a04ae5e2aa7bc23bd0fb685f691174cb7 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Thu, 2 Dec 2021 18:01:43 +0530
Subject: [PATCH 122/246] fix(newsletter): use md_to_html instead of markdown
because valid html is valid markdown and markdown method
doesn't convert markdown if it encounters some html tags
---
frappe/email/doctype/newsletter/newsletter.py | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 7c0e2dfe87..12fe160c9d 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -186,12 +186,13 @@ class Newsletter(WebsiteGenerator):
frappe.db.auto_commit_on_many_writes = is_auto_commit_set
def get_message(self) -> str:
- if self.content_type == "HTML":
- return frappe.render_template(self.message_html, {"doc": self.as_dict()})
+ message = self.message
if self.content_type == "Markdown":
- return frappe.utils.markdown(self.message_md)
- # fallback to Rich Text
- return self.message
+ message = frappe.utils.md_to_html(self.message_md)
+ if self.content_type == "HTML":
+ message = self.message_html
+
+ return frappe.render_template(message, {"doc": self.as_dict()})
def get_recipients(self) -> List[str]:
"""Get recipients from Email Group"""
From 3bdca7192434646014b2abe2251124afdc1e9908 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Fri, 3 Dec 2021 10:37:30 +0530
Subject: [PATCH 123/246] fix: delete Event Producer Last Update on trash event
of Event Producer
---
.../event_streaming/doctype/event_producer/event_producer.py | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py
index 05771a89d3..a6c2a257fa 100644
--- a/frappe/event_streaming/doctype/event_producer/event_producer.py
+++ b/frappe/event_streaming/doctype/event_producer/event_producer.py
@@ -54,6 +54,11 @@ class EventProducer(Document):
self.db_set('incoming_change', 0)
self.reload()
+ def on_trash(self):
+ last_update = frappe.db.get_value('Event Producer Last Update', dict(event_producer=self.name))
+ if last_update:
+ frappe.delete_doc('Event Producer Last Update', last_update)
+
def check_url(self):
valid_url_schemes = ("http", "https")
frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes)
From aea21c2fe68bcd5ba8700f2cf62ff12881a63526 Mon Sep 17 00:00:00 2001
From: Shariq Ansari
Date: Fri, 3 Dec 2021 12:48:36 +0530
Subject: [PATCH 124/246] fix: show unique records in list view
---
frappe/public/js/frappe/list/base_list.js | 2 ++
1 file changed, 2 insertions(+)
diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js
index 03e20ee6f5..f8d83019c1 100644
--- a/frappe/public/js/frappe/list/base_list.js
+++ b/frappe/public/js/frappe/list/base_list.js
@@ -492,6 +492,8 @@ frappe.views.BaseList = class BaseList {
} else {
this.data = this.data.concat(data);
}
+
+ this.data = this.data.uniqBy((d) => d.name);
}
freeze() {
From 2ca687dac8e591afe2021c1e877819d53a2c31cb Mon Sep 17 00:00:00 2001
From: Suraj Shetty
Date: Fri, 3 Dec 2021 13:26:13 +0530
Subject: [PATCH 125/246] fix: Fix timezone conversions
- User to System & vice-versa
- Fixed infinite loop of setting datepicker value
---
frappe/public/js/frappe/form/controls/date.js | 9 ++++++---
frappe/public/js/frappe/form/controls/datetime.js | 9 +++++++--
frappe/public/js/frappe/utils/datetime.js | 13 +++++++------
3 files changed, 20 insertions(+), 11 deletions(-)
diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js
index ba94531b0f..b964740ace 100644
--- a/frappe/public/js/frappe/form/controls/date.js
+++ b/frappe/public/js/frappe/form/controls/date.js
@@ -53,8 +53,6 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
let date_format = sysdefaults && sysdefaults.date_format
? sysdefaults.date_format : 'yyyy-mm-dd';
- let now_date = new Date(this.get_now_date());
-
this.today_text = __("Today");
this.date_format = frappe.defaultDateFormat;
this.datepicker_options = {
@@ -62,7 +60,7 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
autoClose: true,
todayButton: true,
dateFormat: date_format,
- startDate: now_date,
+ startDate: this.get_start_date(),
keyboardNav: false,
onSelect: () => {
this.$input.trigger('change');
@@ -76,6 +74,11 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
}
};
}
+
+ get_start_date() {
+ return new Date(this.get_now_date());
+ }
+
set_datepicker() {
this.$input.datepicker(this.datepicker_options);
this.datepicker = this.$input.data('datepicker');
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index c7efc3e2c3..5d0ecb9fe7 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -8,9 +8,14 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
} else if (value === "Today") {
value = this.get_now_date();
}
+ value = this.format_for_input(value);
+ this.$input && this.$input.val(value);
+ this.datepicker.selectDate(frappe.datetime.user_to_obj(value));
+ }
- this.$input && this.$input.val(this.format_for_input(value));
- this.datepicker.selectDate(frappe.datetime.str_to_obj(value));
+ get_start_date() {
+ let value = frappe.datetime.convert_to_user_tz(this.value);
+ return frappe.datetime.str_to_obj(value);
}
set_date_options() {
super.set_date_options();
diff --git a/frappe/public/js/frappe/utils/datetime.js b/frappe/public/js/frappe/utils/datetime.js
index 2fcfd75c5e..7bb6076b72 100644
--- a/frappe/public/js/frappe/utils/datetime.js
+++ b/frappe/public/js/frappe/utils/datetime.js
@@ -17,8 +17,7 @@ $.extend(frappe.datetime, {
// system time zone and then convert the string to user time zone(from User doctype).
let date_obj = null;
if (frappe.boot.time_zone && frappe.boot.time_zone.system && frappe.boot.time_zone.user) {
- date_obj = moment(date)
- .tz(frappe.boot.time_zone.system)
+ date_obj = moment.tz(date, frappe.boot.time_zone.system)
.clone()
.tz(frappe.boot.time_zone.user);
} else {
@@ -144,14 +143,16 @@ $.extend(frappe.datetime, {
let date_obj = moment(val, frappe.defaultTimeFormat);
return date_obj.format(user_format);
} else {
- let date_obj = moment(val);
+ let date_obj = moment.tz(val, frappe.boot.time_zone.system);
if (typeof val !== "string" || val.indexOf(" ") === -1) {
user_format = user_date_fmt;
} else {
- date_obj = moment(val, "YYYY-MM-DD HH:mm:ss");
user_format = user_date_fmt + " " + user_time_fmt;
}
- return date_obj.tz(frappe.boot.time_zone.user).format(user_format);
+ return date_obj
+ .clone()
+ .tz(frappe.boot.time_zone.user)
+ .format(user_format);
}
},
@@ -207,7 +208,7 @@ $.extend(frappe.datetime, {
_date: function(format, as_obj = false) {
/**
- * Whenever we are getting now_date/datetime, always make sure dates are fetched using usertime zone.
+ * Whenever we are getting now_date/datetime, always make sure dates are fetched using user time zone.
* This is to make sure that time is as per user time zone set in User doctype, If a user had to change the timezone,
* we will end up having multiple timezone by not honouring timezone in User doctype.
* This will make sure that at any point we know which timezone the user if following and not have random timezone
From 8a84ae4f0812d2dc5700b7e7011fa96141be204f Mon Sep 17 00:00:00 2001
From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
Date: Fri, 3 Dec 2021 13:57:57 +0530
Subject: [PATCH 126/246] ci: Use node version 14 to avoid node-sass failure in
patch testing build (#15176)
---
.github/workflows/patch-mariadb-tests.yml | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml
index 52fa987994..c8294886a0 100644
--- a/.github/workflows/patch-mariadb-tests.yml
+++ b/.github/workflows/patch-mariadb-tests.yml
@@ -32,6 +32,12 @@ jobs:
with:
python-version: '3.9'
+ - name: Setup Node
+ uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ check-latest: true
+
- name: Check if build should be run
id: check-build
run: |
From e862ae83da1e19abea889fcfe5e6366975201547 Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Thu, 2 Dec 2021 21:07:06 +0530
Subject: [PATCH 127/246] fix: fixed list of Field objects as fields in
get_values
tests: added test for list of field objects
---
frappe/database/database.py | 4 ++--
frappe/tests/test_db.py | 28 ++++++++++++++++++++++++----
2 files changed, 26 insertions(+), 6 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index f489cea7de..64f09c1835 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -583,7 +583,7 @@ class Database(object):
if not isinstance(fields, Criterion):
for field in fields:
- if "(" in field or " as " in field:
+ if "(" in str(field) or " as " in str(field):
field_objects.append(PseudoColumn(field))
else:
field_objects.append(field)
@@ -842,7 +842,7 @@ class Database(object):
cache_count = frappe.cache().get_value('doctype:count:{}'.format(dt))
if cache_count is not None:
return cache_count
- query = self.query.build_conditions(table=dt, filters=filters).select(Count("*"))
+ query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"))
if filters:
count = self.sql(query, debug=debug)[0][0]
return count
diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py
index 60c8db6ab6..6501d753ff 100644
--- a/frappe/tests/test_db.py
+++ b/frappe/tests/test_db.py
@@ -24,10 +24,30 @@ class TestDB(unittest.TestCase):
self.assertNotEqual(frappe.db.get_value("User", {"name": ["!=", "Guest"]}), "Guest")
self.assertEqual(frappe.db.get_value("User", {"name": ["<", "Adn"]}), "Administrator")
self.assertEqual(frappe.db.get_value("User", {"name": ["<=", "Administrator"]}), "Administrator")
- self.assertEqual(frappe.db.get_value("User", {}, ["Max(name)"], order_by=None), frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0])
- self.assertEqual(frappe.db.get_value("User", {}, "Min(name)", order_by=None), frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0])
- self.assertIn("for update", frappe.db.get_value("User", Field("name") == "Administrator", for_update=True, run=False).lower())
-
+ self.assertEqual(
+ frappe.db.get_value("User", {}, ["Max(name)"], order_by=None),
+ frappe.db.sql("SELECT Max(name) FROM tabUser")[0][0],
+ )
+ self.assertEqual(
+ frappe.db.get_value("User", {}, "Min(name)", order_by=None),
+ frappe.db.sql("SELECT Min(name) FROM tabUser")[0][0],
+ )
+ self.assertIn(
+ "for update",
+ frappe.db.get_value(
+ "User", Field("name") == "Administrator", for_update=True, run=False
+ ).lower(),
+ )
+ doctype = frappe.qb.DocType("User")
+ self.assertEqual(
+ frappe.qb.from_(doctype).select(doctype.name, doctype.email).run(),
+ frappe.db.get_values(
+ doctype,
+ filters={},
+ fieldname=[doctype.name, doctype.email],
+ order_by=None,
+ ),
+ )
self.assertEqual(frappe.db.sql("""SELECT name FROM `tabUser` WHERE name > 's' ORDER BY MODIFIED DESC""")[0][0],
frappe.db.get_value("User", {"name": [">", "s"]}))
From 6f7d030e82ae3b2664f56a6fd9989cfbfc1f1648 Mon Sep 17 00:00:00 2001
From: Saqib Ansari
Date: Fri, 3 Dec 2021 16:06:23 +0530
Subject: [PATCH 128/246] fix: IndexError while handling sql timeout error
---
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 f489cea7de..ea56acff27 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -171,10 +171,10 @@ class Database(object):
frappe.errprint(query)
elif self.is_deadlocked(e):
- raise frappe.QueryDeadlockError
+ raise frappe.QueryDeadlockError(e)
elif self.is_timedout(e):
- raise frappe.QueryTimeoutError
+ raise frappe.QueryTimeoutError(e)
if ignore_ddl and (self.is_missing_column(e) or self.is_missing_table(e) or self.cant_drop_field_or_key(e)):
pass
From db951d2369d8ca3a13f9f5667706b4a002185983 Mon Sep 17 00:00:00 2001
From: Summayya Hashmani <58825865+sumaiya2908@users.noreply.github.com>
Date: Fri, 3 Dec 2021 17:22:07 +0530
Subject: [PATCH 129/246] frefactor: padd separate padding (#15178)
Co-authored-by: Summayya
Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com>
---
frappe/public/scss/website/index.scss | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss
index 9c84e99a5a..58f5ca79c6 100644
--- a/frappe/public/scss/website/index.scss
+++ b/frappe/public/scss/website/index.scss
@@ -266,7 +266,8 @@ h5.modal-title {
.login-content.container {
background-color: var(--fg-color);
- padding: 45px 0px;
+ padding-bottom: 45px;
+ padding-top: 45px;
box-shadow: var(--shadow-base);
border-radius: var(--border-radius-md);
max-width: 400px;
From c5df17e3561c083a68f477ffe56403747aaf4d9c Mon Sep 17 00:00:00 2001
From: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
Date: Fri, 3 Dec 2021 15:34:41 +0100
Subject: [PATCH 130/246] fix: cannot uninstall app with virtual doctype
(#15136)
* Update installer.py
* fix: Drop table only if it exists
* revert: "Update installer.py"
This reverts commit 0e8370ede8a9c2b1c0687e5c216ecf67566da0f5.
Co-authored-by: Suraj Shetty
---
frappe/installer.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frappe/installer.py b/frappe/installer.py
index 9eed44ea15..cd6526c788 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -324,7 +324,7 @@ def _delete_doctypes(doctypes: List[str], dry_run: bool) -> None:
print(f"* dropping Table for '{doctype}'...")
if not dry_run:
frappe.delete_doc("DocType", doctype, ignore_on_trash=True)
- frappe.db.sql_ddl(f"drop table `tab{doctype}`")
+ frappe.db.sql_ddl(f"DROP TABLE IF EXISTS `tab{doctype}`")
def post_install(rebuild_website=False):
From 850cd54b890856af30403542289facdeec42fad4 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Fri, 3 Dec 2021 17:07:16 +0100
Subject: [PATCH 131/246] refactor: module profile
---
.../doctype/module_profile/module_profile.js | 17 +-
.../module_profile/module_profile.json | 10 +-
.../doctype/role_profile/role_profile.json | 227 +++++-------------
frappe/core/doctype/user/user.js | 4 +-
frappe/public/js/frappe/module_editor.js | 76 +++---
5 files changed, 133 insertions(+), 201 deletions(-)
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
index 9c92042dda..57b563157c 100644
--- a/frappe/core/doctype/module_profile/module_profile.js
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -1,19 +1,24 @@
// Copyright (c) 2020, Frappe Technologies and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Module Profile', {
- refresh: function(frm) {
+frappe.ui.form.on("Module Profile", {
+ refresh: function (frm) {
+ debugger;
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
- let module_area = $('')
- .appendTo(frm.fields_dict.module_html.wrapper);
-
+ const module_area = $(frm.fields_dict.module_html.wrapper);
frm.module_editor = new frappe.ModuleEditor(frm, module_area);
}
}
if (frm.module_editor) {
- frm.module_editor.refresh();
+ frm.module_editor.show();
+ }
+ },
+
+ validate: function (frm) {
+ if (frm.module_editor) {
+ frm.module_editor.set_modules_in_table();
}
}
});
diff --git a/frappe/core/doctype/module_profile/module_profile.json b/frappe/core/doctype/module_profile/module_profile.json
index 0e4e56962e..32bc757427 100644
--- a/frappe/core/doctype/module_profile/module_profile.json
+++ b/frappe/core/doctype/module_profile/module_profile.json
@@ -34,11 +34,17 @@
}
],
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2021-01-03 15:36:52.622696",
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_fieldname": "module_profile"
+ }
+ ],
+ "modified": "2021-12-03 15:47:21.296443",
"modified_by": "Administrator",
"module": "Core",
"name": "Module Profile",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/role_profile/role_profile.json b/frappe/core/doctype/role_profile/role_profile.json
index 4b3f35aa57..7cd60a16d1 100644
--- a/frappe/core/doctype/role_profile/role_profile.json
+++ b/frappe/core/doctype/role_profile/role_profile.json
@@ -1,175 +1,80 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "role_profile",
- "beta": 0,
- "creation": "2017-08-31 04:16:38.764465",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "role_profile",
+ "creation": "2017-08-31 04:16:38.764465",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "role_profile",
+ "roles_html",
+ "roles"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "role_profile",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Role Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
+ "fieldname": "role_profile",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Role Name",
+ "reqd": 1,
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "roles_html",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Roles HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "roles_html",
+ "fieldtype": "HTML",
+ "label": "Roles HTML",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "roles",
- "fieldtype": "Table",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Roles Assigned",
- "length": 0,
- "no_copy": 0,
- "options": "Has Role",
- "permlevel": 1,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "fieldname": "roles",
+ "fieldtype": "Table",
+ "hidden": 1,
+ "label": "Roles Assigned",
+ "options": "Has Role",
+ "permlevel": 1,
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2017-10-17 11:05:11.183066",
- "modified_by": "Administrator",
- "module": "Core",
- "name": "Role Profile",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [
+ {
+ "link_doctype": "User",
+ "link_fieldname": "role_profile_name"
+ }
+ ],
+ "modified": "2021-12-03 15:45:45.270963",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Role Profile",
+ "naming_rule": "Expression (old style)",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "role_profile",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "role_profile",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 2ce7413aa7..5b3a1affd9 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -50,7 +50,7 @@ frappe.ui.form.on('User', {
let d = frm.add_child("block_modules");
d.module = v.module;
});
- frm.module_editor && frm.module_editor.refresh();
+ frm.module_editor && frm.module_editor.show();
}
});
}
@@ -180,7 +180,7 @@ frappe.ui.form.on('User', {
frm.roles_editor.show();
}
- frm.module_editor && frm.module_editor.refresh();
+ frm.module_editor && frm.module_editor.show();
if(frappe.session.user==doc.name) {
// update display settings
diff --git a/frappe/public/js/frappe/module_editor.js b/frappe/public/js/frappe/module_editor.js
index 5e2ca4bc83..ff0cfc2426 100644
--- a/frappe/public/js/frappe/module_editor.js
+++ b/frappe/public/js/frappe/module_editor.js
@@ -1,38 +1,54 @@
frappe.ModuleEditor = class ModuleEditor {
constructor(frm, wrapper) {
- this.wrapper = $('
').appendTo(wrapper);
this.frm = frm;
- this.make();
- }
- make() {
- var me = this;
- this.frm.doc.__onload.all_modules.forEach(function(m) {
- $(repl('
', {module: m})).appendTo(me.wrapper);
- });
- this.bind();
- }
- refresh() {
- var me = this;
- this.wrapper.find(".block-module-check").prop("checked", true);
- $.each(this.frm.doc.block_modules, function(i, d) {
- me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false);
+ this.wrapper = wrapper;
+ const block_modules = this.frm.doc.block_modules.map(row => row.module);
+ this.multicheck = frappe.ui.form.make_control({
+ parent: wrapper,
+ df: {
+ fieldname: "block_modules",
+ fieldtype: "MultiCheck",
+ select_all: true,
+ columns: 3,
+ get_data: () => {
+ return this.frm.doc.__onload.all_modules.map(module => {
+ return {
+ label: __(module),
+ value: module,
+ checked: !block_modules.includes(module),
+ };
+ });
+ },
+ on_change: () => {
+ this.set_modules_in_table();
+ this.frm.dirty();
+ }
+ },
+ render_input: true
});
}
- bind() {
- var me = this;
- this.wrapper.on("change", ".block-module-check", function() {
- var module = $(this).attr('data-module');
- if ($(this).prop("checked")) {
- // remove from block_modules
- me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) {
- if (d.module != module) {
- return d;
- }
- });
- } else {
- me.frm.add_child("block_modules", {"module": module});
+
+ show() {
+ const block_modules = this.frm.doc.block_modules.map(row => row.module);
+ const all_modules = this.frm.doc.__onload.all_modules;
+ this.multicheck.selected_options = all_modules.filter(m => !block_modules.includes(m));
+ this.multicheck.refresh_input();
+ }
+
+ set_modules_in_table() {
+ let block_modules = this.frm.doc.block_modules || [];
+ let unchecked_options = this.multicheck.get_unchecked_options();
+
+ block_modules.map(module_doc => {
+ if (!unchecked_options.includes(module_doc.module)) {
+ frappe.model.clear_doc(module_doc.doctype, module_doc.name);
+ }
+ });
+
+ unchecked_options.map(module => {
+ if (!block_modules.find(d => d.module === module)) {
+ let module_doc = frappe.model.add_child(this.frm.doc, "Block Module", "block_modules");
+ module_doc.module = module;
}
});
}
From 504f8743c9ac818f1cacc7c1424340ebcc24c5a9 Mon Sep 17 00:00:00 2001
From: barredterra <14891507+barredterra@users.noreply.github.com>
Date: Fri, 3 Dec 2021 17:24:57 +0100
Subject: [PATCH 132/246] fix: remove debugger
---
frappe/core/doctype/module_profile/module_profile.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js
index 57b563157c..3714d31ade 100644
--- a/frappe/core/doctype/module_profile/module_profile.js
+++ b/frappe/core/doctype/module_profile/module_profile.js
@@ -3,7 +3,6 @@
frappe.ui.form.on("Module Profile", {
refresh: function (frm) {
- debugger;
if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) {
if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) {
const module_area = $(frm.fields_dict.module_html.wrapper);
From 8ead1d9c487b5f7f3e86016b0404a700cc11ba8c Mon Sep 17 00:00:00 2001
From: this-gavagai
Date: Mon, 6 Dec 2021 13:13:43 +0545
Subject: [PATCH 133/246] fix: Clarified docstatus transition exceptions
(#15194)
* [fix] Clarified docstatus transition exceptions
Exceptions issued by the document.py `check_docstatus_transition` method are potentially very misleading. In cases where an invalid docstatus is used, users receive an confusing exception stating "Cannot change docstatus from 0 to 2" or "Cannot change docstatus from 1 to 0".
This PR adds an additional exception message when an invalid docstatus is used.
* fix: Clarified docstatus transition exceptions
Added additional clarifications to exception messages
---
frappe/model/document.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/frappe/model/document.py b/frappe/model/document.py
index fcdadf48e6..2260406125 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -750,8 +750,10 @@ class Document(BaseDocument):
elif self.docstatus==1:
self._action = "submit"
self.check_permission("submit")
+ elif self.docstatus==2:
+ raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 0 (Draft) to 2 (Cancelled)"))
else:
- raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 0 to 2"))
+ raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
elif docstatus==1:
if self.docstatus==1:
@@ -760,8 +762,10 @@ class Document(BaseDocument):
elif self.docstatus==2:
self._action = "cancel"
self.check_permission("cancel")
+ elif self.docstatus==0:
+ raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 1 (Submitted) to 0 (Draft)"))
else:
- raise frappe.DocstatusTransitionError(_("Cannot change docstatus from 1 to 0"))
+ raise frappe.ValidationError(_("Invalid docstatus"), self.docstatus)
elif docstatus==2:
raise frappe.ValidationError(_("Cannot edit cancelled document"))
From 3243fb2083593fdc42b59f609935a758ae399bad Mon Sep 17 00:00:00 2001
From: Aradhya
Date: Mon, 6 Dec 2021 13:04:27 +0530
Subject: [PATCH 134/246] fix: misc fixes
---
frappe/database/database.py | 17 ++++++++---------
frappe/database/query.py | 5 ++---
frappe/query_builder/builder.py | 33 +++++++++++----------------------
3 files changed, 21 insertions(+), 34 deletions(-)
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 6a4e781b44..0f325a746e 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -511,14 +511,10 @@ class Database(object):
# Get coulmn and value of the single doctype Accounts Settings
account_settings = frappe.db.get_singles_dict("Accounts Settings")
"""
- result = self.sql("""
- SELECT field, value
- FROM `tabSingles`
- WHERE doctype = %s
- """, doctype)
-
+ result = self.query.get_sql(
+ "Singles", filters={"doctype": doctype}, fields=["field", "value"]
+ ).run()
dict_ = frappe._dict(result)
-
return dict_
@staticmethod
@@ -547,8 +543,11 @@ class Database(object):
if fieldname in self.value_cache[doctype]:
return self.value_cache[doctype][fieldname]
- val = self.sql("""select `value` from
- `tabSingles` where `doctype`=%s and `field`=%s""", (doctype, fieldname))
+ val = self.query.get_sql(
+ table="Singles",
+ filters={"doctype": doctype, "field": fieldname},
+ fields="value",
+ ).run()
val = val[0][0] if val else None
df = frappe.get_meta(doctype).get_field(fieldname)
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 69328cb206..6d2be5fa25 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -286,14 +286,13 @@ class Query:
):
criterion = self.build_conditions(table, filters, **kwargs)
if isinstance(fields, (list, tuple)):
- query = criterion.select(*kwargs.get("field_objects"))
+ query = criterion.select(*kwargs.get("field_objects", fields))
elif isinstance(fields, Criterion):
query = criterion.select(fields)
else:
- if fields=="*":
- query = criterion.select(fields)
+ query = criterion.select(fields)
return query
diff --git a/frappe/query_builder/builder.py b/frappe/query_builder/builder.py
index 630cfea222..a65d50fdeb 100644
--- a/frappe/query_builder/builder.py
+++ b/frappe/query_builder/builder.py
@@ -18,16 +18,6 @@ class Base:
table_name = get_table_name(table_name)
return Table(table_name, *args, **kwargs)
-
-class MariaDB(Base, MySQLQuery):
- Field = terms.Field
-
- @classmethod
- def from_(cls, table, *args, **kwargs):
- if isinstance(table, str):
- table = cls.DocType(table)
- return super().from_(table, *args, **kwargs)
-
@classmethod
def into(cls, table, *args, **kwargs):
if isinstance(table, str):
@@ -40,6 +30,17 @@ class MariaDB(Base, MySQLQuery):
table = cls.DocType(table)
return super().update(table, *args, **kwargs)
+
+class MariaDB(Base, MySQLQuery):
+ Field = terms.Field
+
+ @classmethod
+ def from_(cls, table, *args, **kwargs):
+ if isinstance(table, str):
+ table = cls.DocType(table)
+ return super().from_(table, *args, **kwargs)
+
+
class Postgres(Base, PostgreSQLQuery):
field_translation = {"table_name": "relname", "table_rows": "n_tup_ins"}
schema_translation = {"tables": "pg_stat_all_tables"}
@@ -69,15 +70,3 @@ class Postgres(Base, PostgreSQLQuery):
table = cls.DocType(table)
return super().from_(table, *args, **kwargs)
-
- @classmethod
- def into(cls, table, *args, **kwargs):
- if isinstance(table, str):
- table = cls.DocType(table)
- return super().into(table, *args, **kwargs)
-
- @classmethod
- def update(cls, table, *args, **kwargs):
- if isinstance(table, str):
- table = cls.DocType(table)
- return super().update(table, *args, **kwargs)
\ No newline at end of file
From a574c1ba88cd76d74065bfbcbf2c9c78dd208d0b Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Thu, 2 Dec 2021 23:20:40 +0530
Subject: [PATCH 135/246] chore: patching ValueWrapper
---
frappe/query_builder/terms.py | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
create mode 100644 frappe/query_builder/terms.py
diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py
new file mode 100644
index 0000000000..c221dcb28e
--- /dev/null
+++ b/frappe/query_builder/terms.py
@@ -0,0 +1,27 @@
+from typing import Any, Optional, Dict
+from pypika.terms import ValueWrapper
+from pypika.utils import format_alias_sql
+
+
+class NamedParameterWrapper():
+ def __init__(self, parameters: Dict[str, Any]):
+ self.parameters = parameters
+
+ def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
+ self.parameters[param_key[1:]] = param_value
+
+ def get_sql(self, **kwargs):
+ return f'@param{len(self.parameters) + 1}'
+
+
+class ParameterizedValueWrapper(ValueWrapper):
+ def get_sql(self, quote_char: Optional[str] = None, secondary_quote_char: str = "'", param_wrapper= None, **kwargs: Any) -> str:
+ if param_wrapper is None:
+ sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
+ return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
+ else:
+ value_sql = self.get_value_sql(quote_char=quote_char, **kwargs)
+ param_sql = param_wrapper.get_sql(**kwargs)
+ param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
+
+ return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
\ No newline at end of file
From 9fdacedfc80889c81c4887c2f2f2581e5d0d56f6 Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Thu, 2 Dec 2021 23:23:25 +0530
Subject: [PATCH 136/246] feat: sanitise frappe.qb
---
frappe/query_builder/__init__.py | 5 +++++
frappe/query_builder/terms.py | 8 ++++----
frappe/query_builder/utils.py | 14 ++++++++++----
3 files changed, 19 insertions(+), 8 deletions(-)
diff --git a/frappe/query_builder/__init__.py b/frappe/query_builder/__init__.py
index 9c7432142f..06d499678f 100644
--- a/frappe/query_builder/__init__.py
+++ b/frappe/query_builder/__init__.py
@@ -1,2 +1,7 @@
+from frappe.query_builder.terms import ParameterizedValueWrapper
+import pypika
+
+pypika.terms.ValueWrapper = ParameterizedValueWrapper
+
from pypika import *
from frappe.query_builder.utils import Column, DocType, get_query_builder, patch_query_execute, patch_query_aggregation
diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py
index c221dcb28e..c09d9595fb 100644
--- a/frappe/query_builder/terms.py
+++ b/frappe/query_builder/terms.py
@@ -1,4 +1,4 @@
-from typing import Any, Optional, Dict
+from typing import Any, List, Optional, Dict
from pypika.terms import ValueWrapper
from pypika.utils import format_alias_sql
@@ -8,10 +8,10 @@ class NamedParameterWrapper():
self.parameters = parameters
def update_parameters(self, param_key: Any, param_value: Any, **kwargs):
- self.parameters[param_key[1:]] = param_value
+ self.parameters[param_key[2:-2]] = param_value
def get_sql(self, **kwargs):
- return f'@param{len(self.parameters) + 1}'
+ return f'%(param{len(self.parameters) + 1})s'
class ParameterizedValueWrapper(ValueWrapper):
@@ -20,7 +20,7 @@ class ParameterizedValueWrapper(ValueWrapper):
sql = self.get_value_sql(quote_char=quote_char, secondary_quote_char=secondary_quote_char, **kwargs)
return format_alias_sql(sql, self.alias, quote_char=quote_char, **kwargs)
else:
- value_sql = self.get_value_sql(quote_char=quote_char, **kwargs)
+ value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) if not isinstance(self.value,int) else self.value
param_sql = param_wrapper.get_sql(**kwargs)
param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
diff --git a/frappe/query_builder/utils.py b/frappe/query_builder/utils.py
index a7f52df012..7922825725 100644
--- a/frappe/query_builder/utils.py
+++ b/frappe/query_builder/utils.py
@@ -10,6 +10,7 @@ import frappe
from .builder import MariaDB, Postgres
from pypika.terms import PseudoColumn
+from frappe.query_builder.terms import NamedParameterWrapper
class db_type_is(Enum):
MARIADB = "mariadb"
@@ -53,12 +54,16 @@ def patch_query_execute():
This excludes the use of `frappe.db.sql` method while
executing the query object
"""
-
def execute_query(query, *args, **kwargs):
- query = str(query)
+ query, params = prepare_query(query)
+ return frappe.db.sql(query, params, *args, **kwargs)
+
+ def prepare_query(query):
+ params = {}
+ query = query.get_sql(param_wrapper = NamedParameterWrapper(params))
if frappe.flags.in_safe_exec and not query.lower().strip().startswith("select"):
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
- return frappe.db.sql(query, *args, **kwargs)
+ return query, params
query_class = get_attr(str(frappe.qb).split("'")[1])
builder_class = get_type_hints(query_class._builder).get('return')
@@ -67,6 +72,7 @@ def patch_query_execute():
raise BuilderIdentificationFailed
builder_class.run = execute_query
+ builder_class.walk = prepare_query
def patch_query_aggregation():
@@ -77,4 +83,4 @@ def patch_query_aggregation():
frappe.qb.max = _max
frappe.qb.min = _min
frappe.qb.avg = _avg
- frappe.qb.sum = _sum
\ No newline at end of file
+ frappe.qb.sum = _sum
From 6120b4b3c1dbf17bc783be16ba41a5c73d5c5df1 Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Sat, 4 Dec 2021 20:12:48 +0530
Subject: [PATCH 137/246] fix: extend named parameters to frappe.qb.function
---
frappe/query_builder/__init__.py | 3 ++-
frappe/query_builder/terms.py | 28 +++++++++++++++++++++++++---
2 files changed, 27 insertions(+), 4 deletions(-)
diff --git a/frappe/query_builder/__init__.py b/frappe/query_builder/__init__.py
index 06d499678f..bf7be84c51 100644
--- a/frappe/query_builder/__init__.py
+++ b/frappe/query_builder/__init__.py
@@ -1,7 +1,8 @@
-from frappe.query_builder.terms import ParameterizedValueWrapper
+from frappe.query_builder.terms import ParameterizedValueWrapper, ParameterizedFunction
import pypika
pypika.terms.ValueWrapper = ParameterizedValueWrapper
+pypika.terms.Function = ParameterizedFunction
from pypika import *
from frappe.query_builder.utils import Column, DocType, get_query_builder, patch_query_execute, patch_query_aggregation
diff --git a/frappe/query_builder/terms.py b/frappe/query_builder/terms.py
index c09d9595fb..2032cd8497 100644
--- a/frappe/query_builder/terms.py
+++ b/frappe/query_builder/terms.py
@@ -1,5 +1,6 @@
-from typing import Any, List, Optional, Dict
-from pypika.terms import ValueWrapper
+from typing import Any, Dict, Optional
+
+from pypika.terms import Function, ValueWrapper
from pypika.utils import format_alias_sql
@@ -23,5 +24,26 @@ class ParameterizedValueWrapper(ValueWrapper):
value_sql = self.get_value_sql(quote_char=quote_char, **kwargs) if not isinstance(self.value,int) else self.value
param_sql = param_wrapper.get_sql(**kwargs)
param_wrapper.update_parameters(param_key=param_sql, param_value=value_sql, **kwargs)
+ return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
- return format_alias_sql(param_sql, self.alias, quote_char=quote_char, **kwargs)
\ No newline at end of file
+
+class ParameterizedFunction(Function):
+ def get_sql(self, **kwargs: Any) -> str:
+ with_alias = kwargs.pop("with_alias", False)
+ with_namespace = kwargs.pop("with_namespace", False)
+ quote_char = kwargs.pop("quote_char", None)
+ dialect = kwargs.pop("dialect", None)
+ param_wrapper = kwargs.pop("param_wrapper", None)
+
+ function_sql = self.get_function_sql(with_namespace=with_namespace, quote_char=quote_char, param_wrapper=param_wrapper, dialect=dialect)
+
+ if self.schema is not None:
+ function_sql = "{schema}.{function}".format(
+ schema=self.schema.get_sql(quote_char=quote_char, dialect=dialect, **kwargs),
+ function=function_sql,
+ )
+
+ if with_alias:
+ return format_alias_sql(function_sql, self.alias, quote_char=quote_char, **kwargs)
+
+ return function_sql
From aa855afe089d209d2a4796c8497d21ed5d12578a Mon Sep 17 00:00:00 2001
From: saxenabhishek
Date: Mon, 6 Dec 2021 12:12:56 +0530
Subject: [PATCH 138/246] test: test for patches through walk
---
frappe/tests/test_query_builder.py | 22 ++++++++++++++++++++--
1 file changed, 20 insertions(+), 2 deletions(-)
diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py
index 7a0935a63b..1d63d2041c 100644
--- a/frappe/tests/test_query_builder.py
+++ b/frappe/tests/test_query_builder.py
@@ -2,7 +2,7 @@ import unittest
from typing import Callable
import frappe
-from frappe.query_builder.functions import GroupConcat, Match
+from frappe.query_builder.functions import Coalesce, GroupConcat, Match
from frappe.query_builder.utils import db_type_is
@@ -49,6 +49,25 @@ class TestBuilderBase(object):
self.assertIsInstance(query.run, Callable)
self.assertIsInstance(data, list)
+ def test_walk(self):
+ DocType = frappe.qb.DocType('DocType')
+ query = (
+ frappe.qb.from_(DocType)
+ .select(DocType.name)
+ .where((DocType.owner == "Administrator' --")
+ & (Coalesce(DocType.search_fields == "subject"))
+ )
+ )
+ self.assertTrue("walk" in dir(query))
+ query, params = query.walk()
+
+ self.assertIn("%(param1)s", query)
+ self.assertIn("%(param2)s", query)
+ self.assertIn("param1",params)
+ self.assertEqual(params["param1"],"Administrator' --")
+ self.assertEqual(params["param2"],"subject")
+
+
@run_only_if(db_type_is.MARIADB)
class TestBuilderMaria(unittest.TestCase, TestBuilderBase):
def test_adding_tabs_in_from(self):
@@ -59,7 +78,6 @@ class TestBuilderMaria(unittest.TestCase, TestBuilderBase):
"SELECT * FROM `__Auth`", frappe.qb.from_("__Auth").select("*").get_sql()
)
-
@run_only_if(db_type_is.POSTGRES)
class TestBuilderPostgres(unittest.TestCase, TestBuilderBase):
def test_adding_tabs_in_from(self):
From 8340639afdeab4414ae54af67ef0b4e23d5a8a64 Mon Sep 17 00:00:00 2001
From: Rucha Mahabal
Date: Mon, 6 Dec 2021 14:51:37 +0530
Subject: [PATCH 139/246] fix: Allow Fetch From for a different link field of
the same DocType
---
frappe/core/doctype/doctype/doctype.js | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/frappe/core/doctype/doctype/doctype.js b/frappe/core/doctype/doctype/doctype.js
index 1c52070063..b907ebc0bc 100644
--- a/frappe/core/doctype/doctype/doctype.js
+++ b/frappe/core/doctype/doctype/doctype.js
@@ -143,11 +143,10 @@ frappe.ui.form.on("DocField", {
curr_value.doctype = doctype;
curr_value.fieldname = fieldname;
}
- let curr_df_link_doctype = row.fieldtype == "Link" ? row.options : null;
let doctypes = frm.doc.fields
.filter(df => df.fieldtype == "Link")
- .filter(df => df.options && df.options != curr_df_link_doctype)
+ .filter(df => df.options && df.fieldname != row.fieldname)
.map(df => ({
label: `${df.options} (${df.fieldname})`,
value: df.fieldname
From da4160e2dd0c9c47e207b51689d9a276221b50af Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Fri, 3 Dec 2021 18:22:25 +0530
Subject: [PATCH 140/246] fix: Newsletter enhancements and fixes
- Organize fields into sections
- Buttons for Send now and Scheduled sending
- Buttons to Send test email and to Check broken links
- Remove Test section
---
frappe/email/doctype/newsletter/newsletter.js | 144 ++++++++++++------
.../email/doctype/newsletter/newsletter.json | 110 +++++++------
frappe/email/doctype/newsletter/newsletter.py | 31 +++-
.../newsletter/templates/newsletter.html | 4 +-
4 files changed, 194 insertions(+), 95 deletions(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index 3277d8e9ee..a7cbcf702a 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -4,69 +4,123 @@
frappe.ui.form.on('Newsletter', {
refresh(frm) {
let doc = frm.doc;
- if (!doc.__islocal && !cint(doc.email_sent) && !doc.__unsaved
- && in_list(frappe.boot.user.can_write, doc.doctype)) {
- frm.add_custom_button(__('Send Now'), function() {
- frappe.confirm(__("Do you really want to send this email newsletter?"), function() {
- frm.call('send_emails').then(() => {
- frm.refresh();
- });
+ let can_write = in_list(frappe.boot.user.can_write, doc.doctype);
+ if (!frm.is_new() && !frm.is_dirty() && !doc.email_sent && can_write) {
+ frm.add_custom_button(__('Send a test email'), () => {
+ frm.events.send_test_email(frm);
+ }, __('Preview'));
+
+ frm.add_custom_button(__('Check broken links'), () => {
+ frm.call('find_broken_links').then(r => {
+ let links = r.message;
+ if (links) {
+ let html = '' + links.map(link => `${link} `).join('') + ' ';
+ frappe.msgprint({
+ title: __("Broken Links"),
+ message: __("Following links are broken in the email content: {0}", [html]),
+ indicator: "red"
+ })
+ } else {
+ frappe.msgprint({
+ title: _("No Broken Links"),
+ message: _("No broken links found in the email content"),
+ indicator: "green"
+ })
+ }
+ })
+ }, __('Preview'));
+
+ frm.add_custom_button(__('Send now'), () => {
+ frappe.confirm(__("Do you really want to send this email newsletter?"), function () {
+ frm.call('send_emails').then(() => frm.refresh());
});
- }, "fa fa-play", "btn-success");
+ }, __('Send'));
+
+ frm.add_custom_button(__('Schedule sending'), () => {
+ frm.events.schedule_send_dialog(frm);
+ }, __('Send'));
}
frm.events.setup_dashboard(frm);
- if (doc.__islocal && !doc.send_from) {
+ if (frm.is_new() && !doc.sender_email) {
let { fullname, email } = frappe.user_info(doc.owner);
- frm.set_value('send_from', `${fullname} <${email}>`);
+ frm.set_value('sender_email', email);
+ frm.set_value('sender_name', fullname);
}
},
- onload_post_render(frm) {
- frm.trigger('setup_schedule_send');
- },
-
- setup_schedule_send(frm) {
- let today = new Date();
-
- // setting datepicker options to set min date & min time
- today.setHours(today.getHours() + 1 );
- frm.get_field('schedule_send').$input.datepicker({
- maxMinutes: 0,
- minDate: today,
- timeFormat: 'hh:00:00',
- onSelect: function (fd, d, picker) {
- if (!d) return;
- var date = d.toDateString();
- if (date === today.toDateString()) {
- picker.update({
- minHours: (today.getHours() + 1)
- });
- } else {
- picker.update({
- minHours: 0
- });
- }
- frm.get_field('schedule_send').$input.trigger('change');
+ schedule_send_dialog(frm) {
+ let hours = frappe.utils.range(24);
+ let time_slots = hours.map(hour => {
+ return `${(hour + '').padStart(2, '0')}:00`;
+ });
+ let d = new frappe.ui.Dialog({
+ title: __('Schedule Newsletter'),
+ fields: [
+ {
+ label: __('Date'),
+ fieldname: 'date',
+ fieldtype: 'Date',
+ options: {
+ minDate: new Date()
+ }
+ },
+ {
+ label: __('Time'),
+ fieldname: 'time',
+ fieldtype: 'Select',
+ options: time_slots,
+ },
+ ],
+ primary_action_label: __('Schedule'),
+ primary_action({ date, time }) {
+ frm.set_value('schedule_sending', 1);
+ frm.set_value('schedule_send', `${date} ${time}`);
+ d.hide();
}
});
+ if (frm.doc.schedule_sending) {
+ let parts = frm.doc.schedule_send.split(' ');
+ if (parts.length === 2) {
+ let [date, time] = parts;
+ d.set_value('date', date);
+ d.set_value('time', time);
+ }
+ }
+ d.show();
+ },
-
- const $tp = frm.get_field('schedule_send').datepicker.timepicker;
- $tp.$minutes.parent().css('display', 'none');
- $tp.$minutesText.css('display', 'none');
- $tp.$minutesText.prev().css('display', 'none');
- $tp.$seconds.parent().css('display', 'none');
+ send_test_email(frm) {
+ let d = new frappe.ui.Dialog({
+ title: __('Send Test Email'),
+ fields: [
+ {
+ label: __('Email'),
+ fieldname: 'email',
+ fieldtype: 'Data',
+ options: 'Email',
+ }
+ ],
+ primary_action_label: __('Send'),
+ primary_action({ email }) {
+ d.get_primary_btn().text(__('Sending...')).prop('disabled', true);
+ frm.call('send_test_email', { email })
+ .then(() => {
+ d.get_primary_btn().text(__('Send again')).prop('disabled', false);
+ });
+ }
+ });
+ d.show();
},
setup_dashboard(frm) {
- if(!frm.doc.__islocal && cint(frm.doc.email_sent)
+ if (!frm.doc.__islocal && cint(frm.doc.email_sent)
&& frm.doc.__onload && frm.doc.__onload.status_count) {
var stat = frm.doc.__onload.status_count;
var total = frm.doc.scheduled_to_send;
- if(total) {
- $.each(stat, function(k, v) {
+ if (total) {
+ $.each(stat, function (k, v) {
stat[k] = flt(v * 100 / total, 2) + '%';
});
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index dcd19ed33c..ccb2ca8181 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -7,29 +7,33 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
+ "from_section",
+ "sender_name",
+ "column_break_5",
+ "sender_email",
+ "column_break_7",
"send_from",
- "schedule_sending",
- "schedule_send",
"recipients",
"email_group",
"email_sent",
- "newsletter_content",
+ "subject_section",
"subject",
+ "preview_text",
+ "newsletter_content",
"content_type",
"message",
"message_md",
"message_html",
- "section_break_13",
"send_unsubscribe_link",
"send_attachments",
- "column_break_9",
- "published",
"send_webview_link",
- "route",
- "test_the_newsletter",
- "test_email_id",
- "test_send",
- "scheduled_to_send"
+ "schedule_settings_section",
+ "scheduled_to_send",
+ "schedule_sending",
+ "schedule_send",
+ "publish_as_a_web_page_section",
+ "published",
+ "route"
],
"fields": [
{
@@ -43,7 +47,8 @@
"fieldname": "send_from",
"fieldtype": "Data",
"ignore_xss_filter": 1,
- "label": "Sender"
+ "label": "Sender",
+ "read_only": 1
},
{
"default": "0",
@@ -89,30 +94,9 @@
{
"fieldname": "route",
"fieldtype": "Data",
- "hidden": 1,
"label": "Route",
"read_only": 1
},
- {
- "collapsible": 1,
- "fieldname": "test_the_newsletter",
- "fieldtype": "Section Break",
- "label": "Testing"
- },
- {
- "description": "A Lead with this Email Address should exist",
- "fieldname": "test_email_id",
- "fieldtype": "Data",
- "label": "Test Email Address",
- "options": "Email"
- },
- {
- "depends_on": "eval: doc.test_email_id",
- "fieldname": "test_send",
- "fieldtype": "Button",
- "label": "Test",
- "options": "test_send"
- },
{
"fieldname": "scheduled_to_send",
"fieldtype": "Int",
@@ -122,13 +106,14 @@
{
"fieldname": "recipients",
"fieldtype": "Section Break",
- "label": "Recipients"
+ "label": "To"
},
{
"depends_on": "eval: doc.schedule_sending",
"fieldname": "schedule_send",
"fieldtype": "Datetime",
- "label": "Schedule Send",
+ "label": "Send Email At",
+ "read_only": 1,
"read_only_depends_on": "eval: doc.email_sent"
},
{
@@ -161,13 +146,9 @@
"default": "0",
"fieldname": "schedule_sending",
"fieldtype": "Check",
- "label": "Schedule Sending",
+ "label": "Schedule sending at a later time",
"read_only_depends_on": "eval: doc.email_sent"
},
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
{
"default": "0",
"depends_on": "published",
@@ -176,8 +157,51 @@
"label": "Send Web View Link"
},
{
- "fieldname": "section_break_13",
- "fieldtype": "Section Break"
+ "fieldname": "from_section",
+ "fieldtype": "Section Break",
+ "label": "From"
+ },
+ {
+ "fieldname": "sender_name",
+ "fieldtype": "Data",
+ "label": "Sender Name"
+ },
+ {
+ "fieldname": "sender_email",
+ "fieldtype": "Data",
+ "label": "Sender Email",
+ "options": "Email",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "subject_section",
+ "fieldtype": "Section Break",
+ "label": "Subject"
+ },
+ {
+ "description": "Preview Text appears in the inbox after the subject line",
+ "fieldname": "preview_text",
+ "fieldtype": "Data",
+ "label": "Preview Text"
+ },
+ {
+ "fieldname": "publish_as_a_web_page_section",
+ "fieldtype": "Section Break",
+ "label": "Publish as a web page"
+ },
+ {
+ "depends_on": "schedule_sending",
+ "fieldname": "schedule_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Scheduled Sending"
}
],
"has_web_view": 1,
@@ -187,7 +211,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2021-02-22 14:33:56.095380",
+ "modified": "2021-12-03 17:50:12.028162",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index 12fe160c9d..acaa1dbcc1 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -30,10 +30,30 @@ class Newsletter(WebsiteGenerator):
return self._recipients
@frappe.whitelist()
- def test_send(self):
- test_emails = frappe.utils.split_emails(self.test_email_id)
+ def send_test_email(self, email):
+ test_emails = frappe.utils.split_emails(email)
self.queue_all(test_emails=test_emails)
- frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id))
+ frappe.msgprint(_("Test email sent to {0}").format(email), alert=True)
+
+ @frappe.whitelist()
+ def find_broken_links(self):
+ from bs4 import BeautifulSoup
+ import requests
+
+ html = self.get_message()
+ soup = BeautifulSoup(html, "html.parser")
+ links = soup.find_all("a")
+ images = soup.find_all("img")
+ broken_links = []
+ for el in links + images:
+ url = el.attrs.get("href") or el.attrs.get("src")
+ try:
+ response = requests.head(url, verify=False, timeout=5)
+ if response.status_code >= 400:
+ broken_links.append(url)
+ except:
+ broken_links.append(url)
+ return broken_links
@frappe.whitelist()
def send_emails(self):
@@ -75,8 +95,9 @@ class Newsletter(WebsiteGenerator):
def validate_sender_address(self):
"""Validate self.send_from is a valid email address or not.
"""
- if self.send_from:
- frappe.utils.validate_email_address(self.send_from, throw=True)
+ if self.sender_email:
+ frappe.utils.validate_email_address(self.sender_email, throw=True)
+ self.send_from = f"{self.sender_name} <{self.sender_email}>" if self.sender_name else self.sender_email
def validate_recipient_address(self):
"""Validate if self.newsletter_recipients are all valid email addresses or not.
diff --git a/frappe/email/doctype/newsletter/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html
index 733c7df6af..11ee19a550 100644
--- a/frappe/email/doctype/newsletter/templates/newsletter.html
+++ b/frappe/email/doctype/newsletter/templates/newsletter.html
@@ -36,7 +36,7 @@
- {{ doc.message }}
+ {{ doc.get_message() }}
@@ -51,7 +51,7 @@
{% for attachment in attachments %}
-
+
{{ attachment.file_name }}
From f6379fdf40ff75e93bd672218a7171697e5e95ec Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Fri, 3 Dec 2021 18:23:28 +0530
Subject: [PATCH 141/246] fix: allow options in datepicker via df.options
ability to customize datepicker options via df.options
---
frappe/public/js/frappe/form/controls/date.js | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js
index 9ad81c7e46..b9945060cd 100644
--- a/frappe/public/js/frappe/form/controls/date.js
+++ b/frappe/public/js/frappe/form/controls/date.js
@@ -73,7 +73,8 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
.text(this.today_text);
this.update_datepicker_position();
- }
+ },
+ ...(this.get_df_options())
};
}
set_datepicker() {
@@ -150,4 +151,19 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
}
return value;
}
+ get_df_options() {
+ let options = {};
+ let df_options = this.df.options || '';
+ if (typeof df_options === 'string') {
+ try {
+ options = JSON.parse(df_options);
+ } catch (error) {
+ console.warn(`Invalid JSON in options of "${this.df.fieldname}"`);
+ }
+ }
+ else if (typeof df_options === 'object') {
+ options = df_options;
+ }
+ return options;
+ }
};
From 742d5e2e0691107657497b0574cef8c110782011 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Fri, 3 Dec 2021 18:23:51 +0530
Subject: [PATCH 142/246] fix: utility method to create a range of values
mimics python's range function
---
frappe/public/js/frappe/utils/utils.js | 13 +++++++++++++
1 file changed, 13 insertions(+)
diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js
index 2dbad5427d..5f546c22da 100644
--- a/frappe/public/js/frappe/utils/utils.js
+++ b/frappe/public/js/frappe/utils/utils.js
@@ -1376,5 +1376,18 @@ Object.assign(frappe.utils, {
return array;
}
return undefined;
+ },
+
+ // simple implementation of python's range
+ range(start, end) {
+ if (!end) {
+ end = start;
+ start = 0;
+ }
+ let arr = [];
+ for (let i = start; i < end; i++) {
+ arr.push(i);
+ }
+ return arr;
}
});
From 02759631b498ebed638b2764585027378e4326ca Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Mon, 6 Dec 2021 15:35:41 +0530
Subject: [PATCH 143/246] fix: handle falsy return values in document methods
problem: if a whitelisted document method returns a falsy value like
`[]`, `{}`, `0` then response.message is not set and not returned
in the response.
this change checks if the return value is `None` and falsy values
are returned properly in the response
---
frappe/handler.py | 2 +-
frappe/model/document.py | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/frappe/handler.py b/frappe/handler.py
index 42c17261b4..35063ee9d6 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -255,7 +255,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None):
response = doc.run_method(method, **args)
frappe.response.docs.append(doc)
- if not response:
+ if response is None:
return
# build output as csv
diff --git a/frappe/model/document.py b/frappe/model/document.py
index 2260406125..bbba9b1492 100644
--- a/frappe/model/document.py
+++ b/frappe/model/document.py
@@ -1130,12 +1130,16 @@ class Document(BaseDocument):
collated in one dict and returned. Ideally, don't return values in hookable
methods, set properties in the document."""
def add_to_return_value(self, new_return_value):
+ if new_return_value is None:
+ self._return_value = self.get("_return_value")
+ return
+
if isinstance(new_return_value, dict):
if not self.get("_return_value"):
self._return_value = {}
self._return_value.update(new_return_value)
else:
- self._return_value = new_return_value or self.get("_return_value")
+ self._return_value = new_return_value
def compose(fn, *hooks):
def runner(self, method, *args, **kwargs):
From 0d3bac55284ae3c6bff085211ea573d69e124e0a Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Mon, 6 Dec 2021 15:36:30 +0530
Subject: [PATCH 144/246] fix(ux): Show broken links as dashboard message
---
frappe/email/doctype/newsletter/newsletter.js | 19 ++++++++-----------
1 file changed, 8 insertions(+), 11 deletions(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index a7cbcf702a..e8978b8d0b 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -11,21 +11,18 @@ frappe.ui.form.on('Newsletter', {
}, __('Preview'));
frm.add_custom_button(__('Check broken links'), () => {
+ frm.dashboard.set_headline(__('Checking broken links...'));
frm.call('find_broken_links').then(r => {
+ frm.dashboard.set_headline('');
let links = r.message;
- if (links) {
+ if (links && links.length) {
let html = '' + links.map(link => `${link} `).join('') + ' ';
- frappe.msgprint({
- title: __("Broken Links"),
- message: __("Following links are broken in the email content: {0}", [html]),
- indicator: "red"
- })
+ frm.dashboard.set_headline(__("Following links are broken in the email content: {0}", [html]));
} else {
- frappe.msgprint({
- title: _("No Broken Links"),
- message: _("No broken links found in the email content"),
- indicator: "green"
- })
+ frm.dashboard.set_headline(__("No broken links found in the email content"));
+ setTimeout(() => {
+ frm.dashboard.set_headline('');
+ }, 3000);
}
})
}, __('Preview'));
From 9bdb5f2eb2404a4a6db855659d7c8f55cdb9b518 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Mon, 6 Dec 2021 17:04:24 +0530
Subject: [PATCH 145/246] fix: Explicit attachments table
The newsletter content may contain images that get "attached" to the
newsletter document. If this is the case, you can't selectively include
attachments in the newsletter as it attaches all the attachments.
An explicit attachments table solves this problem.
---
.../email/doctype/email_queue/email_queue.py | 14 ++++++---
.../email/doctype/newsletter/newsletter.json | 23 +++++---------
frappe/email/doctype/newsletter/newsletter.py | 13 +-------
.../doctype/newsletter_attachment/__init__.py | 0
.../newsletter_attachment.json | 31 +++++++++++++++++++
.../newsletter_attachment.py | 8 +++++
6 files changed, 58 insertions(+), 31 deletions(-)
create mode 100644 frappe/email/doctype/newsletter_attachment/__init__.py
create mode 100644 frappe/email/doctype/newsletter_attachment/newsletter_attachment.json
create mode 100644 frappe/email/doctype/newsletter_attachment/newsletter_attachment.py
diff --git a/frappe/email/doctype/email_queue/email_queue.py b/frappe/email/doctype/email_queue/email_queue.py
index 4489a68cac..077a5dd40b 100644
--- a/frappe/email/doctype/email_queue/email_queue.py
+++ b/frappe/email/doctype/email_queue/email_queue.py
@@ -283,9 +283,14 @@ class SendMailContext:
if attachment.get('fcontent'):
continue
- fid = attachment.get("fid")
- if fid:
- _file = frappe.get_doc("File", fid)
+ file_filters = {}
+ if attachment.get('fid'):
+ file_filters['name'] = attachment.get('fid')
+ elif attachment.get('file_url'):
+ file_filters['file_url'] = attachment.get('file_url')
+
+ if file_filters:
+ _file = frappe.get_doc("File", file_filters)
fcontent = _file.get_content()
attachment.update({
'fname': _file.file_name,
@@ -293,6 +298,7 @@ class SendMailContext:
'parent': message_obj
})
attachment.pop("fid", None)
+ attachment.pop("file_url", None)
add_attachment(**attachment)
elif attachment.get("print_format_attachment") == 1:
@@ -503,7 +509,7 @@ class QueueBuilder:
if self._attachments:
# store attachments with fid or print format details, to be attached on-demand later
for att in self._attachments:
- if att.get('fid'):
+ if att.get('fid') or att.get('file_url'):
attachments.append(att)
elif att.get("print_format_attachment") == 1:
if not att.get('lang', None):
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index ccb2ca8181..9d35671042 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -18,14 +18,13 @@
"email_sent",
"subject_section",
"subject",
- "preview_text",
"newsletter_content",
"content_type",
"message",
"message_md",
"message_html",
"send_unsubscribe_link",
- "send_attachments",
+ "attachments",
"send_webview_link",
"schedule_settings_section",
"scheduled_to_send",
@@ -116,12 +115,6 @@
"read_only": 1,
"read_only_depends_on": "eval: doc.email_sent"
},
- {
- "default": "0",
- "fieldname": "send_attachments",
- "fieldtype": "Check",
- "label": "Send Attachments"
- },
{
"fieldname": "content_type",
"fieldtype": "Select",
@@ -186,12 +179,6 @@
"fieldtype": "Section Break",
"label": "Subject"
},
- {
- "description": "Preview Text appears in the inbox after the subject line",
- "fieldname": "preview_text",
- "fieldtype": "Data",
- "label": "Preview Text"
- },
{
"fieldname": "publish_as_a_web_page_section",
"fieldtype": "Section Break",
@@ -202,6 +189,12 @@
"fieldname": "schedule_settings_section",
"fieldtype": "Section Break",
"label": "Scheduled Sending"
+ },
+ {
+ "fieldname": "attachments",
+ "fieldtype": "Table",
+ "label": "Attachments",
+ "options": "Newsletter Attachment"
}
],
"has_web_view": 1,
@@ -211,7 +204,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2021-12-03 17:50:12.028162",
+ "modified": "2021-12-06 17:01:32.353153",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index acaa1dbcc1..b2146ed100 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -164,18 +164,7 @@ class Newsletter(WebsiteGenerator):
def get_newsletter_attachments(self) -> List[Dict[str, str]]:
"""Get list of attachments on current Newsletter
"""
- attachments = []
-
- if self.send_attachments:
- files = frappe.get_all(
- "File",
- filters={"attached_to_doctype": "Newsletter", "attached_to_name": self.name},
- order_by="creation desc",
- pluck="name",
- )
- attachments.extend({"fid": file} for file in files)
-
- return attachments
+ return [{"file_url": row.attachment} for row in self.attachments]
def send_newsletter(self, emails: List[str]):
"""Trigger email generation for `emails` and add it in Email Queue.
diff --git a/frappe/email/doctype/newsletter_attachment/__init__.py b/frappe/email/doctype/newsletter_attachment/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/email/doctype/newsletter_attachment/newsletter_attachment.json b/frappe/email/doctype/newsletter_attachment/newsletter_attachment.json
new file mode 100644
index 0000000000..e2add0ed64
--- /dev/null
+++ b/frappe/email/doctype/newsletter_attachment/newsletter_attachment.json
@@ -0,0 +1,31 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-12-06 16:37:40.652468",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "attachment"
+ ],
+ "fields": [
+ {
+ "fieldname": "attachment",
+ "fieldtype": "Attach",
+ "in_list_view": 1,
+ "label": "Attachment",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-12-06 16:37:47.481057",
+ "modified_by": "Administrator",
+ "module": "Email",
+ "name": "Newsletter Attachment",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py b/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py
new file mode 100644
index 0000000000..7842badbe1
--- /dev/null
+++ b/frappe/email/doctype/newsletter_attachment/newsletter_attachment.py
@@ -0,0 +1,8 @@
+# Copyright (c) 2021, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+class NewsletterAttachment(Document):
+ pass
From 3caa93c2c438b09cb1b386730ba3a5f9bae9abf1 Mon Sep 17 00:00:00 2001
From: Jannat Patel
Date: Tue, 7 Dec 2021 11:14:11 +0530
Subject: [PATCH 146/246] chore: sla tracking
---
frappe/hooks.py | 3 ++-
.../personal_data_deletion_request.js | 8 +++++++
.../personal_data_deletion_request.json | 23 ++++++++++++++++++-
.../personal_data_deletion_request.py | 23 ++++++++++++++++++-
4 files changed, 54 insertions(+), 3 deletions(-)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 8bca5c066c..c204b01356 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -240,7 +240,8 @@ scheduler_events = {
"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.prepared_report.prepared_report.delete_expired_prepared_reports",
- "frappe.core.doctype.log_settings.log_settings.run_log_clean_up"
+ "frappe.core.doctype.log_settings.log_settings.run_log_clean_up",
+ "frappe.website.doctype.personal_data_deletion_request.personal_data_deletion_request.update_sla"
],
"daily_long": [
"frappe.integrations.doctype.dropbox_settings.dropbox_settings.take_backups_daily",
diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.js b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.js
index 1eb2e02f49..a6cb0b234f 100644
--- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.js
+++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.js
@@ -18,5 +18,13 @@ frappe.ui.form.on("Personal Data Deletion Request", {
});
});
}
+ },
+
+ before_load: function(frm) {
+ frappe.db.get_single_value("Website Settings", "account_deletion_sla").then((data) => {
+ if (data < 1) {
+ frm.set_df_property("sla_status", "hidden", 1);
+ }
+ });
}
});
diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json
index 0cb11068f5..dd4ae54a90 100644
--- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json
+++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json
@@ -7,6 +7,9 @@
"field_order": [
"email",
"status",
+ "column_break_3",
+ "sla_status",
+ "section_break_5",
"anonymization_matrix",
"deletion_steps"
],
@@ -42,10 +45,28 @@
"fieldtype": "Table",
"label": "Deletion Steps ",
"options": "Personal Data Deletion Step"
+ },
+ {
+ "default": "Open",
+ "fieldname": "sla_status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "SLA Status",
+ "options": "Open\nFulfilled\nFailed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
}
],
"links": [],
- "modified": "2021-04-23 13:25:53.629308",
+ "modified": "2021-12-07 10:48:06.194408",
"modified_by": "Administrator",
"module": "Website",
"name": "Personal Data Deletion Request",
diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
index ef3856ad25..6d943eb103 100644
--- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
+++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py
@@ -7,7 +7,7 @@ import re
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import get_fullname
+from frappe.utils import get_fullname, date_diff, get_datetime
from frappe.utils.user import get_system_managers
from frappe.utils.verified_command import get_signed_params, verify_request
import json
@@ -280,6 +280,13 @@ class PersonalDataDeletionRequest(Document):
frappe.rename_doc("User", email, anon, force=True, show_alert=False)
self.db_set("status", "Deleted")
+ account_deletion_sla = frappe.db.get_single_value("Website Settings", "account_deletion_sla")
+ if account_deletion_sla > 0 and self.sla_status == "Open":
+ if date_diff(get_datetime(), self.creation) > account_deletion_sla:
+ self.db_set("sla_status", "Failed")
+ elif date_diff(get_datetime(), self.creation) <= account_deletion_sla:
+ self.db_set("sla_status", "Fulfilled")
+
if commit:
frappe.db.commit()
@@ -344,6 +351,20 @@ def remove_unverified_record():
AND `creation` < (NOW() - INTERVAL '7' DAY)"""
)
+def update_sla():
+ account_deletion_sla = frappe.db.get_single_value("Website Settings", "account_deletion_sla")
+ if account_deletion_sla < 1:
+ return
+
+ requests = frappe.get_all("Personal Data Deletion Request",
+ filters = {
+ "sla_status": "Open"
+ },
+ fields = ["name", "creation", "status"])
+
+ for request in requests:
+ if date_diff(get_datetime(), request.creation) > account_deletion_sla and request.status != "Deleted":
+ frappe.db.set_value("Personal Data Deletion Request", request.name, "sla_status", "Failed")
@frappe.whitelist(allow_guest=True)
def confirm_deletion(email, name, host_name):
From 330677bb0a8960ab4624c3813bc15e1a6091544c Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 7 Dec 2021 15:40:55 +0530
Subject: [PATCH 147/246] fix: better sending status
- show email sending progress in form dashboard
---
frappe/email/doctype/newsletter/newsletter.js | 46 +++++++++++++++++++
frappe/email/doctype/newsletter/newsletter.py | 17 +++++++
2 files changed, 63 insertions(+)
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index e8978b8d0b..d68736a00f 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -39,6 +39,7 @@ frappe.ui.form.on('Newsletter', {
}
frm.events.setup_dashboard(frm);
+ frm.events.setup_sending_status(frm);
if (frm.is_new() && !doc.sender_email) {
let { fullname, email } = frappe.user_info(doc.owner);
@@ -145,5 +146,50 @@ frappe.ui.form.on('Newsletter', {
]);
}
}
+ },
+
+ setup_sending_status(frm) {
+ frm.call('get_sending_status').then(r => {
+ if (r.message) {
+ frm.events.update_sending_progress(frm, r.message.sent, r.message.total);
+ }
+ if (r.message.sent >= r.message.total) {
+ return;
+ }
+ if (frm.sending_status) return;
+
+ frm.sending_status = setInterval(() => {
+ if (frm.doc.email_sent && frm.$wrapper.is(':visible')) {
+ frm.call('get_sending_status').then(r => {
+ if (r.message) {
+ let { sent, total } = r.message;
+ frm.events.update_sending_progress(frm, sent, total);
+
+ if (sent >= total) {
+ clearInterval(frm.sending_status);
+ frm.sending_status = null;
+ return;
+ }
+ }
+ });
+ }
+ }, 5000);
+ });
+ },
+
+ update_sending_progress(frm, sent, total) {
+ if (sent >= total) {
+ frm.dashboard.hide_progress();
+ return;
+ }
+ frm.dashboard.show_progress(__('Sending emails'), sent * 100 / total, __("{0} of {1} sent", [sent, total]));
+ },
+
+ on_hide(frm) {
+ if (frm.sending_status) {
+ clearInterval(frm.sending_status);
+ frm.sending_status = null;
+ }
+ },
}
});
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index b2146ed100..d84953db93 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -29,6 +29,23 @@ class Newsletter(WebsiteGenerator):
self._recipients = self.get_recipients()
return self._recipients
+ @frappe.whitelist()
+ def get_sending_status(self):
+ count_by_status = frappe.get_all("Email Queue",
+ filters={"reference_doctype": self.doctype, "reference_name": self.name},
+ fields=["status", "count(name) as count"],
+ group_by="status",
+ order_by="status"
+ )
+ sent = 0
+ total = 0
+ for row in count_by_status:
+ if row.status == "Sent":
+ sent = row.count
+ total += row.count
+
+ return {'sent': sent, 'total': total}
+
@frappe.whitelist()
def send_test_email(self, email):
test_emails = frappe.utils.split_emails(email)
From 1bb3c2d3f4797b76c400beb95982f7d51be0dde7 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 7 Dec 2021 15:41:38 +0530
Subject: [PATCH 148/246] fix: add on_hide event in form
can be used to clearing events, for e.g., clearing setInterval
---
frappe/public/js/frappe/form/form.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js
index 27281d8927..e789b7241c 100644
--- a/frappe/public/js/frappe/form/form.js
+++ b/frappe/public/js/frappe/form/form.js
@@ -75,6 +75,10 @@ frappe.ui.form.Form = class FrappeForm {
this.page = this.wrapper.page;
this.layout_main = this.page.main.get(0);
+ this.$wrapper.on("hide", () => {
+ this.script_manager.trigger("on_hide");
+ });
+
this.toolbar = new frappe.ui.form.Toolbar({
frm: this,
page: this.page
From de7d0337a60be9ca3d6e59ab4d6141a595e8f2fe Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 7 Dec 2021 15:48:35 +0530
Subject: [PATCH 149/246] fix: various newsletter form ux fixes
- Cancel Scheduling button
- Show dashboard message if newsletter is scheduled
---
frappe/email/doctype/newsletter/newsletter.js | 30 +++++++++++++++++--
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.js b/frappe/email/doctype/newsletter/newsletter.js
index d68736a00f..72b8fa6993 100644
--- a/frappe/email/doctype/newsletter/newsletter.js
+++ b/frappe/email/doctype/newsletter/newsletter.js
@@ -28,7 +28,13 @@ frappe.ui.form.on('Newsletter', {
}, __('Preview'));
frm.add_custom_button(__('Send now'), () => {
- frappe.confirm(__("Do you really want to send this email newsletter?"), function () {
+ if (frm.doc.schedule_send) {
+ frappe.confirm(__("This newsletter was scheduled to send on a later date. Are you sure you want to send it now?"), function () {
+ frm.call('send_emails').then(() => frm.refresh());
+ });
+ return;
+ }
+ frappe.confirm(__("Are you sure you want to send this newsletter now?"), function () {
frm.call('send_emails').then(() => frm.refresh());
});
}, __('Send'));
@@ -46,6 +52,8 @@ frappe.ui.form.on('Newsletter', {
frm.set_value('sender_email', email);
frm.set_value('sender_name', fullname);
}
+
+ frm.trigger('update_schedule_message');
},
schedule_send_dialog(frm) {
@@ -74,8 +82,16 @@ frappe.ui.form.on('Newsletter', {
primary_action_label: __('Schedule'),
primary_action({ date, time }) {
frm.set_value('schedule_sending', 1);
- frm.set_value('schedule_send', `${date} ${time}`);
+ frm.set_value('schedule_send', `${date} ${time}:00`);
d.hide();
+ frm.save();
+ },
+ secondary_action_label: __('Cancel Scheduling'),
+ secondary_action() {
+ frm.set_value('schedule_sending', 0);
+ frm.set_value('schedule_send', '');
+ d.hide();
+ frm.save();
}
});
if (frm.doc.schedule_sending) {
@@ -83,7 +99,7 @@ frappe.ui.form.on('Newsletter', {
if (parts.length === 2) {
let [date, time] = parts;
d.set_value('date', date);
- d.set_value('time', time);
+ d.set_value('time', time.slice(0, 5));
}
}
d.show();
@@ -191,5 +207,13 @@ frappe.ui.form.on('Newsletter', {
frm.sending_status = null;
}
},
+
+ update_schedule_message(frm) {
+ if (!frm.doc.email_sent && frm.doc.schedule_send) {
+ let datetime = frappe.datetime.global_date_format(frm.doc.schedule_send);
+ frm.dashboard.set_headline_alert(__('This newsletter is scheduled to be sent on {0}', [datetime.bold()]));
+ } else {
+ frm.dashboard.clear_headline();
+ }
}
});
From 606a0d3809f868a5f55e3be02f5264b9929d6f26 Mon Sep 17 00:00:00 2001
From: Faris Ansari
Date: Tue, 7 Dec 2021 15:52:25 +0530
Subject: [PATCH 150/246] fix: various fixes
- show published newsletters in list view
- show published newsletter as web page
- show status section after newsletter is sent
- add email_sent_at and total_recipients field
---
.../email/doctype/newsletter/newsletter.json | 45 +++++-
frappe/email/doctype/newsletter/newsletter.py | 103 ++++----------
.../newsletter/templates/newsletter.html | 10 +-
.../newsletter_email_group.json | 134 +++++-------------
4 files changed, 103 insertions(+), 189 deletions(-)
diff --git a/frappe/email/doctype/newsletter/newsletter.json b/frappe/email/doctype/newsletter/newsletter.json
index 9d35671042..baabd4991e 100644
--- a/frappe/email/doctype/newsletter/newsletter.json
+++ b/frappe/email/doctype/newsletter/newsletter.json
@@ -7,6 +7,12 @@
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
+ "status_section",
+ "email_sent_at",
+ "column_break_3",
+ "total_recipients",
+ "column_break_12",
+ "email_sent",
"from_section",
"sender_name",
"column_break_5",
@@ -15,7 +21,6 @@
"send_from",
"recipients",
"email_group",
- "email_sent",
"subject_section",
"subject",
"newsletter_content",
@@ -23,8 +28,8 @@
"message",
"message_md",
"message_html",
- "send_unsubscribe_link",
"attachments",
+ "send_unsubscribe_link",
"send_webview_link",
"schedule_settings_section",
"scheduled_to_send",
@@ -39,8 +44,9 @@
"fieldname": "email_group",
"fieldtype": "Table",
"in_standard_filter": 1,
- "label": "Email Group",
- "options": "Newsletter Email Group"
+ "label": "Audience",
+ "options": "Newsletter Email Group",
+ "reqd": 1
},
{
"fieldname": "send_from",
@@ -53,6 +59,7 @@
"default": "0",
"fieldname": "email_sent",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Email Sent",
"no_copy": 1,
"read_only": 1
@@ -91,6 +98,7 @@
"label": "Published"
},
{
+ "depends_on": "published",
"fieldname": "route",
"fieldtype": "Data",
"label": "Route",
@@ -144,7 +152,6 @@
},
{
"default": "0",
- "depends_on": "published",
"fieldname": "send_webview_link",
"fieldtype": "Check",
"label": "Send Web View Link"
@@ -195,6 +202,32 @@
"fieldtype": "Table",
"label": "Attachments",
"options": "Newsletter Attachment"
+ },
+ {
+ "fieldname": "email_sent_at",
+ "fieldtype": "Datetime",
+ "label": "Email Sent At",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_recipients",
+ "fieldtype": "Int",
+ "label": "Total Recipients",
+ "read_only": 1
+ },
+ {
+ "depends_on": "email_sent",
+ "fieldname": "status_section",
+ "fieldtype": "Section Break",
+ "label": "Status"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
}
],
"has_web_view": 1,
@@ -204,7 +237,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 3,
- "modified": "2021-12-06 17:01:32.353153",
+ "modified": "2021-12-06 20:09:37.963141",
"modified_by": "Administrator",
"module": "Email",
"name": "Newsletter",
diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py
index d84953db93..aa6fa2c40a 100644
--- a/frappe/email/doctype/newsletter/newsletter.py
+++ b/frappe/email/doctype/newsletter/newsletter.py
@@ -15,13 +15,11 @@ from .exceptions import NewsletterAlreadySentError, NoRecipientFoundError, Newsl
class Newsletter(WebsiteGenerator):
- def onload(self):
- self.setup_newsletter_status()
-
def validate(self):
self.route = f"newsletters/{self.name}"
self.validate_sender_address()
self.validate_recipient_address()
+ self.validate_publishing()
@property
def newsletter_recipients(self) -> List[str]:
@@ -48,8 +46,8 @@ class Newsletter(WebsiteGenerator):
@frappe.whitelist()
def send_test_email(self, email):
- test_emails = frappe.utils.split_emails(email)
- self.queue_all(test_emails=test_emails)
+ test_emails = frappe.utils.validate_email_address(email, throw=True)
+ self.send_newsletter(emails=test_emails)
frappe.msgprint(_("Test email sent to {0}").format(email), alert=True)
@frappe.whitelist()
@@ -74,22 +72,11 @@ class Newsletter(WebsiteGenerator):
@frappe.whitelist()
def send_emails(self):
- """send emails to leads and customers"""
+ """queue sending emails to recipients"""
+ self.schedule_sending = False
+ self.schedule_send = None
self.queue_all()
- frappe.msgprint(_("Email queued to {0} recipients").format(len(self.newsletter_recipients)))
-
- def setup_newsletter_status(self):
- """Setup analytical status for current Newsletter. Can be accessible from desk.
- """
- if self.email_sent:
- status_count = frappe.get_all("Email Queue",
- filters={"reference_doctype": self.doctype, "reference_name": self.name},
- fields=["status", "count(name)"],
- group_by="status",
- order_by="status",
- as_list=True,
- )
- self.get("__onload").status_count = dict(status_count)
+ frappe.msgprint(_("Email queued to {0} recipients").format(self.total_recipients))
def validate_send(self):
"""Validate if Newsletter can be sent.
@@ -122,6 +109,10 @@ class Newsletter(WebsiteGenerator):
for recipient in self.newsletter_recipients:
frappe.utils.validate_email_address(recipient, throw=True)
+ def validate_publishing(self):
+ if self.send_webview_link and not self.published:
+ frappe.throw(_("Newsletter must be published to send webview link in email"))
+
def get_linked_email_queue(self) -> List[str]:
"""Get list of email queue linked to this newsletter.
"""
@@ -154,29 +145,19 @@ class Newsletter(WebsiteGenerator):
x for x in self.newsletter_recipients if x not in self.get_success_recipients()
]
- def queue_all(self, test_emails: List[str] = None):
- """Queue Newsletter to all the recipients generated from the `Email Group`
- table
-
- Args:
- test_email (List[str], optional): Send test Newsletter to the passed set of emails.
- Defaults to None.
+ def queue_all(self):
+ """Queue Newsletter to all the recipients generated from the `Email Group` table
"""
- if test_emails:
- for test_email in test_emails:
- frappe.utils.validate_email_address(test_email, throw=True)
- else:
- self.validate()
- self.validate_send()
+ self.validate()
+ self.validate_send()
- newsletter_recipients = test_emails or self.get_pending_recipients()
- self.send_newsletter(emails=newsletter_recipients)
+ recipients = self.get_pending_recipients()
+ self.send_newsletter(emails=recipients)
- if not test_emails:
- self.email_sent = True
- self.schedule_send = frappe.utils.now_datetime()
- self.scheduled_to_send = len(newsletter_recipients)
- self.save()
+ self.email_sent = True
+ self.email_sent_at = frappe.utils.now()
+ self.total_recipients = len(recipients)
+ self.save()
def get_newsletter_attachments(self) -> List[Dict[str, str]]:
"""Get list of attachments on current Newsletter
@@ -251,21 +232,6 @@ class Newsletter(WebsiteGenerator):
},
)
- def get_context(self, context):
- newsletters = get_newsletter_list("Newsletter", None, None, 0)
- if newsletters:
- newsletter_list = [d.name for d in newsletters]
- if self.name not in newsletter_list:
- frappe.redirect_to_message(
- _("Permission Error"), _("You are not permitted to view the newsletter.")
- )
- frappe.local.flags.redirect_location = frappe.local.response.location
- raise frappe.Redirect
- else:
- context.attachments = self.get_attachments()
- context.no_cache = 1
- context.show_sidebar = True
-
@frappe.whitelist(allow_guest=True)
def confirmed_unsubscribe(email, group):
@@ -348,35 +314,14 @@ def confirm_subscription(email, email_group=_("Website")):
def get_list_context(context=None):
context.update({
- "show_sidebar": True,
"show_search": True,
- 'no_breadcrumbs': True,
- "title": _("Newsletter"),
- "get_list": get_newsletter_list,
+ "no_breadcrumbs": True,
+ "title": _("Newsletters"),
+ "filters": {"published": 1},
"row_template": "email/doctype/newsletter/templates/newsletter_row.html",
})
-def get_newsletter_list(doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"):
- email_group_list = frappe.db.sql('''SELECT eg.name
- FROM `tabEmail Group` eg, `tabEmail Group Member` egm
- WHERE egm.unsubscribed=0
- AND eg.name=egm.email_group
- AND egm.email = %s''', frappe.session.user)
- email_group_list = [d[0] for d in email_group_list]
-
- if email_group_list:
- return frappe.db.sql('''SELECT n.name, n.subject, n.message, n.modified
- FROM `tabNewsletter` n, `tabNewsletter Email Group` neg
- WHERE n.name = neg.parent
- AND n.email_sent=1
- AND n.published=1
- AND neg.email_group in ({0})
- ORDER BY n.modified DESC LIMIT {1} OFFSET {2}
- '''.format(','.join(['%s'] * len(email_group_list)),
- limit_page_length, limit_start), email_group_list, as_dict=1)
-
-
def send_scheduled_email():
"""Send scheduled newsletter to the recipients."""
scheduled_newsletter = frappe.get_all(
diff --git a/frappe/email/doctype/newsletter/templates/newsletter.html b/frappe/email/doctype/newsletter/templates/newsletter.html
index 11ee19a550..1244f4c49a 100644
--- a/frappe/email/doctype/newsletter/templates/newsletter.html
+++ b/frappe/email/doctype/newsletter/templates/newsletter.html
@@ -1,6 +1,6 @@
{% extends "templates/web.html" %}
-{% block title %} {{ _("Newsletter") }} {% endblock %}
+{% block title %} {{ doc.subject }} {% endblock %}
{% block page_content %}