diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js
index 5154adb634..efa1959969 100644
--- a/esbuild/esbuild.js
+++ b/esbuild/esbuild.js
@@ -258,12 +258,17 @@ function get_watch_config() {
async function clean_dist_folders(apps) {
for (let app of apps) {
let public_path = get_public_path(app);
- await fs.promises.rmdir(path.resolve(public_path, "dist", "js"), {
- recursive: true
- });
- await fs.promises.rmdir(path.resolve(public_path, "dist", "css"), {
- recursive: true
- });
+ let paths = [
+ path.resolve(public_path, "dist", "js"),
+ path.resolve(public_path, "dist", "css")
+ ];
+ for (let target of paths) {
+ if (fs.existsSync(target)) {
+ // rmdir is deprecated in node 16, this will work in both node 14 and 16
+ let rmdir = fs.promises.rm || fs.promises.rmdir;
+ await rmdir(target, { recursive: true });
+ }
+ }
}
}
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py
index 998e73a42c..d2afda1553 100644
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.py
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py
@@ -333,7 +333,7 @@ class AutoRepeat(Document):
if self.reference_doctype and self.reference_document:
res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
- email_ids = list(set([d.email_id for d in res]))
+ email_ids = {d.email_id for d in res}
if not email_ids:
frappe.msgprint(_('No contacts linked to document'), alert=True)
else:
diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py
index f21819ad98..77305168c1 100644
--- a/frappe/contacts/address_and_contact.py
+++ b/frappe/contacts/address_and_contact.py
@@ -153,7 +153,7 @@ def filter_dynamic_link_doctypes(doctype, txt, searchfield, start, page_len, fil
doctypes = frappe.db.get_all("DocField", filters=filters, fields=["parent"],
distinct=True, as_list=True)
- doctypes = tuple([d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE)])
+ doctypes = tuple(d for d in doctypes if re.search(txt+".*", _(d[0]), re.IGNORECASE))
filters.update({
"dt": ("not in", [d[0] for d in doctypes])
diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py
index bfcf91427d..755bc63064 100644
--- a/frappe/contacts/doctype/address/address.py
+++ b/frappe/contacts/doctype/address/address.py
@@ -257,7 +257,7 @@ def address_query(doctype, txt, searchfield, start, page_len, filters):
def get_condensed_address(doc):
fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country"]
- return ", ".join([doc.get(d) for d in fields if doc.get(d)])
+ return ", ".join(doc.get(d) for d in fields if doc.get(d))
def update_preferred_address(address, field):
frappe.db.set_value('Address', address, field, 0)
diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py
index fed90b75ce..bb922f1f5d 100644
--- a/frappe/core/doctype/data_import/importer.py
+++ b/frappe/core/doctype/data_import/importer.py
@@ -449,7 +449,7 @@ class ImportFile:
for row in data_without_first_row:
row_values = row.get_values(parent_column_indexes)
# if the row is blank, it's a child row doc
- if all([v in INVALID_VALUES for v in row_values]):
+ if all(v in INVALID_VALUES for v in row_values):
rows.append(row)
continue
# if we encounter a row which has values in parent columns,
@@ -606,7 +606,7 @@ class Row:
if df.fieldtype == "Select":
select_options = get_select_options(df)
if select_options and value not in select_options:
- options_string = ", ".join([frappe.bold(d) for d in select_options])
+ options_string = ", ".join(frappe.bold(d) for d in select_options)
msg = _("Value must be one of {0}").format(options_string)
self.warnings.append(
{"row": self.row_number, "field": df_as_json(df), "message": msg,}
@@ -902,7 +902,7 @@ class Column:
if self.df.fieldtype == "Link":
# find all values that dont exist
- values = list(set([cstr(v) for v in self.column_values[1:] if v]))
+ values = list({cstr(v) for v in self.column_values[1:] if v})
exists = [
d.name for d in frappe.db.get_all(self.df.options, filters={"name": ("in", values)})
]
@@ -935,11 +935,11 @@ class Column:
elif self.df.fieldtype == "Select":
options = get_select_options(self.df)
if options:
- values = list(set([cstr(v) for v in self.column_values[1:] if v]))
- invalid = list(set(values) - set(options))
+ values = {cstr(v) for v in self.column_values[1:] if v}
+ invalid = values - set(options)
if invalid:
- valid_values = ", ".join([frappe.bold(o) for o in options])
- invalid_values = ", ".join([frappe.bold(i) for i in invalid])
+ valid_values = ", ".join(frappe.bold(o) for o in options)
+ invalid_values = ", ".join(frappe.bold(i) for i in invalid)
self.warnings.append(
{
"col": self.column_number,
diff --git a/frappe/core/doctype/data_import_legacy/importer.py b/frappe/core/doctype/data_import_legacy/importer.py
index 4080e70418..ceefff4410 100644
--- a/frappe/core/doctype/data_import_legacy/importer.py
+++ b/frappe/core/doctype/data_import_legacy/importer.py
@@ -177,7 +177,7 @@ def upload(rows = None, submit_after_import=None, ignore_encoding_errors=False,
if d.get("name") and d["name"].startswith('"'):
d["name"] = d["name"][1:-1]
- if sum([0 if not val else 1 for val in d.values()]):
+ if sum(0 if not val else 1 for val in d.values()):
d['doctype'] = dt
if dt == doctype:
doc.update(d)
@@ -533,6 +533,6 @@ def get_parent_field(doctype, parenttype):
def delete_child_rows(rows, doctype):
"""delete child rows for all parents"""
- for p in list(set([r[1] for r in rows])):
+ for p in list(set(r[1] for r in rows)):
if p:
frappe.db.sql("""delete from `tab{0}` where parent=%s""".format(doctype), p)
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 4de2352864..3cdc45ea08 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -193,7 +193,7 @@ class DocType(Document):
self.flags.update_fields_to_fetch_queries = []
- if set(old_fields_to_fetch) != set([df.fieldname for df in new_meta.get_fields_to_fetch()]):
+ if set(old_fields_to_fetch) != set(df.fieldname for df in new_meta.get_fields_to_fetch()):
for df in new_meta.get_fields_to_fetch():
if df.fieldname not in old_fields_to_fetch:
link_fieldname, source_fieldname = df.fetch_from.split('.', 1)
@@ -757,7 +757,7 @@ def validate_fields(meta):
invalid_fields = ('doctype',)
if fieldname in invalid_fields:
frappe.throw(_("{0}: Fieldname cannot be one of {1}")
- .format(docname, ", ".join([frappe.bold(d) for d in invalid_fields])))
+ .format(docname, ", ".join(frappe.bold(d) for d in invalid_fields)))
def check_unique_fieldname(docname, fieldname):
duplicates = list(filter(None, map(lambda df: df.fieldname==fieldname and str(df.idx) or None, fields)))
@@ -991,7 +991,7 @@ def validate_fields(meta):
if docfield.options and (docfield.options not in data_field_options):
df_str = frappe.bold(_(docfield.label))
text_str = _("{0} is an invalid Data field.").format(df_str) + "
" * 2 + _("Only Options allowed for Data field are:") + "
"
- df_options_str = "
- " + "
- ".join([_(x) for x in data_field_options]) + "
"
+ df_options_str = "- " + "
- ".join(_(x) for x in data_field_options) + "
"
frappe.msgprint(text_str + df_options_str, title="Invalid Data Field", raise_exception=True)
diff --git a/frappe/core/doctype/domain/domain.py b/frappe/core/doctype/domain/domain.py
index 681824bb02..bbd20f3b70 100644
--- a/frappe/core/doctype/domain/domain.py
+++ b/frappe/core/doctype/domain/domain.py
@@ -110,7 +110,7 @@ class Domain(Document):
# enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=1
- where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.allow_sidebar_items])))
+ where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.allow_sidebar_items)))
if self.data.remove_sidebar_items:
# disable all
@@ -118,4 +118,4 @@ class Domain(Document):
# enable
frappe.db.sql('''update `tabPortal Menu Item` set enabled=0
- where route in ({0})'''.format(', '.join(['"{0}"'.format(d) for d in self.data.remove_sidebar_items])))
+ where route in ({0})'''.format(', '.join('"{0}"'.format(d) for d in self.data.remove_sidebar_items)))
diff --git a/frappe/core/doctype/feedback/__init__.py b/frappe/core/doctype/feedback/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/core/doctype/feedback/feedback.js b/frappe/core/doctype/feedback/feedback.js
new file mode 100644
index 0000000000..131f0e19d8
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Feedback', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/frappe/core/doctype/feedback/feedback.json b/frappe/core/doctype/feedback/feedback.json
new file mode 100644
index 0000000000..cf8a180e27
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.json
@@ -0,0 +1,86 @@
+{
+ "actions": [],
+ "creation": "2021-06-03 19:02:55.328423",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "reference_doctype",
+ "reference_name",
+ "column_break_3",
+ "email",
+ "rating",
+ "section_break_6",
+ "feedback"
+ ],
+ "fields": [
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "label": "Email",
+ "reqd": 1
+ },
+ {
+ "fieldname": "rating",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Rating",
+ "precision": "1",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "feedback",
+ "fieldtype": "Small Text",
+ "label": "Feedback",
+ "reqd": 1
+ },
+ {
+ "fieldname": "reference_doctype",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Reference Document Type",
+ "options": "\nBlog Post"
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "label": "Reference Name",
+ "options": "reference_doctype",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-06-14 15:11:26.005805",
+ "modified_by": "Administrator",
+ "module": "Core",
+ "name": "Feedback",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "reference_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/feedback/feedback.py b/frappe/core/doctype/feedback/feedback.py
new file mode 100644
index 0000000000..655bed6eb1
--- /dev/null
+++ b/frappe/core/doctype/feedback/feedback.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 Feedback(Document):
+ pass
diff --git a/frappe/core/doctype/feedback/test_feedback.py b/frappe/core/doctype/feedback/test_feedback.py
new file mode 100644
index 0000000000..702f9d8ac1
--- /dev/null
+++ b/frappe/core/doctype/feedback/test_feedback.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2021, Frappe Technologies and Contributors
+# See license.txt
+
+import frappe
+import unittest
+
+class TestFeedback(unittest.TestCase):
+ def test_feedback_creation_updation(self):
+ from frappe.website.doctype.blog_post.test_blog_post import make_test_blog
+ test_blog = make_test_blog()
+
+ frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+
+ from frappe.templates.includes.feedback.feedback import add_feedback, update_feedback
+ feedback = add_feedback('Blog Post', test_blog.name, 5, 'New feedback','test@test.com')
+
+ self.assertEqual(feedback.feedback, 'New feedback')
+ self.assertEqual(feedback.rating, 5)
+
+ updated_feedback = update_feedback('Blog Post', test_blog.name, 6, 'Updated feedback', 'test@test.com')
+
+ self.assertEqual(updated_feedback.feedback, 'Updated feedback')
+ self.assertEqual(updated_feedback.rating, 6)
+
+ frappe.db.sql("delete from `tabFeedback` where reference_doctype = 'Blog Post'")
+
+ test_blog.delete()
\ No newline at end of file
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 9124eb031f..5b605504e8 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -931,7 +931,7 @@ def user_query(doctype, txt, searchfield, start, page_len, filters):
LIMIT %(page_len)s OFFSET %(start)s
""".format(
user_type_condition = user_type_condition,
- standard_users=", ".join([frappe.db.escape(u) for u in STANDARD_USERS]),
+ standard_users=", ".join(frappe.db.escape(u) for u in STANDARD_USERS),
key=searchfield,
fcond=get_filters_cond(doctype, filters, conditions),
mcond=get_match_cond(doctype)
diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py
index 42ca4d7a14..4aa5797c7f 100644
--- a/frappe/core/doctype/user_permission/user_permission.py
+++ b/frappe/core/doctype/user_permission/user_permission.py
@@ -16,11 +16,11 @@ class UserPermission(Document):
self.validate_default_permission()
def on_update(self):
- frappe.cache().delete_value('user_permissions')
+ frappe.cache().hdel('user_permissions', self.user)
frappe.publish_realtime('update_user_permissions')
def on_trash(self): # pylint: disable=no-self-use
- frappe.cache().delete_value('user_permissions')
+ frappe.cache().hdel('user_permissions', self.user)
frappe.publish_realtime('update_user_permissions')
def validate_user_permission(self):
diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py
index e7d06c45f2..82ffb090f1 100644
--- a/frappe/core/doctype/user_type/user_type.py
+++ b/frappe/core/doctype/user_type/user_type.py
@@ -112,7 +112,7 @@ class UserType(Document):
self.select_doctypes = []
select_doctypes = []
- user_doctypes = tuple([row.document_type for row in self.user_doctypes])
+ user_doctypes = [row.document_type for row in self.user_doctypes]
for doctype in user_doctypes:
doc = frappe.get_meta(doctype)
@@ -265,4 +265,4 @@ def apply_permissions_for_non_standard_user_type(doc, method=None):
user_doc.update_children()
add_user_permission(doc.doctype, doc.name, doc.get(data[1]))
else:
- frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))
\ No newline at end of file
+ frappe.db.set_value('User Permission', perm_data[0], 'user', doc.get(data[1]))
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 8bcc6cf059..1b8977acc4 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -355,9 +355,9 @@ class CustomizeForm(Document):
def delete_custom_fields(self):
meta = frappe.get_meta(self.doc_type)
- fields_to_remove = (set([df.fieldname for df in meta.get("fields")])
- - set(df.fieldname for df in self.get("fields")))
-
+ fields_to_remove = (
+ {df.fieldname for df in meta.get("fields")} - {df.fieldname for df in self.get("fields")}
+ )
for fieldname in fields_to_remove:
df = meta.get("fields", {"fieldname": fieldname})[0]
if df.get("is_custom_field"):
diff --git a/frappe/database/database.py b/frappe/database/database.py
index 7e8d2da43b..81e24cc7ad 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -335,7 +335,7 @@ class Database(object):
values[key] = value[1]
if isinstance(value[1], (tuple, list)):
# value is a list in tuple ("in", ("A", "B"))
- _rhs = " ({0})".format(", ".join([self.escape(v) for v in value[1]]))
+ _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1]))
del values[key]
if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]:
@@ -1010,7 +1010,7 @@ class Database(object):
:params values: list of list of values
"""
insert_list = []
- fields = ", ".join(["`"+field+"`" for field in fields])
+ fields = ", ".join("`"+field+"`" for field in fields)
for idx, value in enumerate(values):
insert_list.append(tuple(value))
diff --git a/frappe/desk/doctype/global_search_settings/global_search_settings.py b/frappe/desk/doctype/global_search_settings/global_search_settings.py
index 28a1ed8239..9112349c1b 100644
--- a/frappe/desk/doctype/global_search_settings/global_search_settings.py
+++ b/frappe/desk/doctype/global_search_settings/global_search_settings.py
@@ -21,7 +21,7 @@ class GlobalSearchSettings(Document):
dts.append(dt.document_type)
if core_dts:
- core_dts = (", ".join([frappe.bold(dt) for dt in core_dts]))
+ core_dts = ", ".join(frappe.bold(dt) for dt in core_dts)
frappe.throw(_("Core Modules {0} cannot be searched in Global Search.").format(core_dts))
if repeated_dts:
@@ -60,7 +60,7 @@ def update_global_search_doctypes():
if search_doctypes.get(domain):
global_search_doctypes.extend(search_doctypes.get(domain))
- doctype_list = set([dt.name for dt in frappe.get_all("DocType")])
+ doctype_list = {dt.name for dt in frappe.get_all("DocType")}
allowed_in_global_search = []
for dt in global_search_doctypes:
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index 3c67bb4668..4ea5c9cd7e 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -131,7 +131,7 @@ def update_tags(doc, tags):
:param doc: Document to be added to global tags
"""
- new_tags = list(set([tag.strip() for tag in tags.split(",") if tag]))
+ new_tags = {tag.strip() for tag in tags.split(",") if tag}
for tag in new_tags:
if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}):
@@ -186,4 +186,4 @@ def get_documents_for_tag(tag):
@frappe.whitelist()
def get_tags_list_for_awesomebar():
- return [t.name for t in frappe.get_list("Tag")]
\ No newline at end of file
+ return [t.name for t in frappe.get_list("Tag")]
diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py
index 0329e0f7d2..0b5babc8d9 100644
--- a/frappe/desk/doctype/workspace/workspace.py
+++ b/frappe/desk/doctype/workspace/workspace.py
@@ -55,8 +55,7 @@ class Workspace(Document):
for link in self.links:
link = link.as_dict()
if link.type == "Card Break":
-
- if card_links:
+ if card_links and (not current_card.only_for or current_card.only_for == frappe.get_system_settings('country')):
current_card['links'] = card_links
cards.append(current_card)
diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py
index a714fd6e45..ecd59f42bb 100755
--- a/frappe/email/doctype/email_account/email_account.py
+++ b/frappe/email/doctype/email_account/email_account.py
@@ -1,32 +1,25 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-import frappe
+import email.utils
+import functools
import imaplib
-import re
-import json
import socket
import time
-import functools
-
-import email.utils
-
-from frappe import _, are_emails_muted, safe_encode
-from frappe.model.document import Document
-from frappe.utils import (validate_email_address, cint, cstr, get_datetime,
- DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr)
-from frappe.utils.user import is_system_user
-from frappe.utils.jinja import render_template
-from frappe.email.smtp import SMTPServer
-from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError
-from poplib import error_proto
-from dateutil.relativedelta import relativedelta
from datetime import datetime, timedelta
+from poplib import error_proto
+
+import frappe
+from frappe import _, are_emails_muted, safe_encode
from frappe.desk.form import assign_to
-from frappe.utils.user import get_system_managers
-from frappe.utils.background_jobs import enqueue, get_jobs
-from frappe.utils.html_utils import clean_email_html
-from frappe.utils.error import raise_error_on_no_output
+from frappe.email.receive import EmailServer, InboundMail, SentEmailInInboxError
+from frappe.email.smtp import SMTPServer
from frappe.email.utils import get_port
+from frappe.model.document import Document
+from frappe.utils import cint, comma_or, cstr, parse_addr, validate_email_address
+from frappe.utils.background_jobs import enqueue, get_jobs
+from frappe.utils.error import raise_error_on_no_output
+from frappe.utils.jinja import render_template
+from frappe.utils.user import get_system_managers
OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account")
@@ -577,8 +570,8 @@ class EmailAccount(Document):
email_server.update_flag(uid_list=uid_list)
# mark communication as read
- docnames = ",".join([ "'%s'"%flag.get("communication") for flag in flags \
- if flag.get("action") == "Read" ])
+ docnames = ",".join("'%s'"%flag.get("communication") for flag in flags \
+ if flag.get("action") == "Read")
self.set_communication_seen_status(docnames, seen=1)
# mark communication as unread
diff --git a/frappe/email/doctype/email_group/email_group.json b/frappe/email/doctype/email_group/email_group.json
index c49de841e6..cb74249143 100644
--- a/frappe/email/doctype/email_group/email_group.json
+++ b/frappe/email/doctype/email_group/email_group.json
@@ -1,6 +1,7 @@
{
"actions": [],
"allow_import": 1,
+ "allow_rename": 1,
"autoname": "field:title",
"creation": "2015-03-18 06:08:32.729800",
"doctype": "DocType",
@@ -50,7 +51,7 @@
"link_fieldname": "email_group"
}
],
- "modified": "2020-09-24 16:41:55.286377",
+ "modified": "2021-06-15 11:25:13.556201",
"modified_by": "Administrator",
"module": "Email",
"name": "Email Group",
diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py
index cfd0df53a9..3abd339ed9 100644
--- a/frappe/email/doctype/newsletter/test_newsletter.py
+++ b/frappe/email/doctype/newsletter/test_newsletter.py
@@ -42,7 +42,7 @@ class TestNewsletter(unittest.TestCase):
email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")]
self.assertEqual(len(email_queue_list), 4)
- recipients = set([e.recipients[0].recipient for e in email_queue_list])
+ recipients = {e.recipients[0].recipient for e in email_queue_list}
self.assertTrue(set(emails).issubset(recipients))
def test_unsubscribe(self):
diff --git a/frappe/email/inbox.py b/frappe/email/inbox.py
index 5f8f516772..c6020e14e4 100644
--- a/frappe/email/inbox.py
+++ b/frappe/email/inbox.py
@@ -18,7 +18,7 @@ def get_email_accounts(user=None):
"all_accounts": ""
}
- all_accounts = ",".join([ account.get("email_account") for account in accounts ])
+ all_accounts = ",".join(account.get("email_account") for account in accounts)
if len(accounts) > 1:
email_accounts.append({
"email_account": all_accounts,
diff --git a/frappe/email/receive.py b/frappe/email/receive.py
index 9ad560aa4a..2e42008951 100644
--- a/frappe/email/receive.py
+++ b/frappe/email/receive.py
@@ -802,7 +802,7 @@ class InboundMail(Email):
except frappe.DuplicateEntryError:
# try and find matching parent
parent_name = frappe.db.get_value(self.email_account.append_to,
- {email_fileds.sender_field: email.from_email}
+ {email_fileds.sender_field: self.from_email}
)
if parent_name:
parent.name = parent_name
diff --git a/frappe/installer.py b/frappe/installer.py
index b81d0a03b4..d4d8117fcb 100755
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -537,7 +537,7 @@ def is_downgrade(sql_file_path, verbose=False):
def is_partial(sql_file_path):
with open(sql_file_path) as f:
- header = " ".join([f.readline() for _ in range(5)])
+ header = " ".join(f.readline() for _ in range(5))
if "Partial Backup" in header:
return True
return False
diff --git a/frappe/integrations/doctype/ldap_settings/ldap_settings.py b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
index 122096cf6f..acc8b96679 100644
--- a/frappe/integrations/doctype/ldap_settings/ldap_settings.py
+++ b/frappe/integrations/doctype/ldap_settings/ldap_settings.py
@@ -79,7 +79,7 @@ class LDAPSettings(Document):
def sync_roles(self, user, additional_groups=None):
- current_roles = set([d.role for d in user.get("roles")])
+ current_roles = set(d.role for d in user.get("roles"))
needed_roles = set()
needed_roles.add(self.default_role)
diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py
index dd93fbcc18..75122f5aba 100644
--- a/frappe/model/__init__.py
+++ b/frappe/model/__init__.py
@@ -165,7 +165,7 @@ def delete_fields(args_dict, delete=0):
frappe.db.sql("""
DELETE FROM `tabSingles`
WHERE doctype='%s' AND field IN (%s)
- """ % (dt, ", ".join(["'{}'".format(f) for f in fields])))
+ """ % (dt, ", ".join("'{}'".format(f) for f in fields)))
else:
existing_fields = frappe.db.multisql({
"mariadb": "DESC `tab%s`" % dt,
@@ -188,7 +188,7 @@ def delete_fields(args_dict, delete=0):
frappe.db.commit()
query = "ALTER TABLE `tab%s` " % dt + \
- ", ".join(["DROP COLUMN `%s`" % f for f in fields_need_to_delete])
+ ", ".join("DROP COLUMN `%s`" % f for f in fields_need_to_delete)
frappe.db.sql(query)
if frappe.db.db_type == 'postgres':
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 2f5154cfd9..af696e116d 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -354,7 +354,7 @@ class BaseDocument(object):
frappe.db.sql("""INSERT INTO `tab{doctype}` ({columns})
VALUES ({values})""".format(
doctype = self.doctype,
- columns = ", ".join(["`"+c+"`" for c in columns]),
+ columns = ", ".join("`"+c+"`" for c in columns),
values = ", ".join(["%s"] * len(columns))
), list(d.values()))
except Exception as e:
@@ -397,7 +397,7 @@ class BaseDocument(object):
frappe.db.sql("""UPDATE `tab{doctype}`
SET {values} WHERE `name`=%s""".format(
doctype = self.doctype,
- values = ", ".join(["`"+c+"`=%s" for c in columns])
+ values = ", ".join("`"+c+"`=%s" for c in columns)
), list(d.values()) + [name])
except Exception as e:
if frappe.db.is_unique_key_violation(e):
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index 1acccdc142..7ed681644f 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -43,8 +43,14 @@ class DatabaseQuery(object):
# filters and fields swappable
# its hard to remember what comes first
- if (isinstance(fields, dict)
- or (isinstance(fields, list) and fields and isinstance(fields[0], list))):
+ if (
+ isinstance(fields, dict)
+ or (
+ fields
+ and isinstance(fields, list)
+ and isinstance(fields[0], list)
+ )
+ ):
# if fields is given as dict/list of list, its probably filters
filters, fields = fields, filters
@@ -56,10 +62,7 @@ class DatabaseQuery(object):
if fields:
self.fields = fields
else:
- if pluck:
- self.fields = ["`tab{0}`.`{1}`".format(self.doctype, pluck)]
- else:
- self.fields = ["`tab{0}`.`name`".format(self.doctype)]
+ self.fields = [f"`tab{self.doctype}`.`{pluck or 'name'}`"]
if start: limit_start = start
if page_length: limit_page_length = page_length
@@ -70,7 +73,7 @@ class DatabaseQuery(object):
self.docstatus = docstatus or []
self.group_by = group_by
self.order_by = order_by
- self.limit_start = 0 if (limit_start is False) else cint(limit_start)
+ self.limit_start = cint(limit_start)
self.limit_page_length = cint(limit_page_length) if limit_page_length else None
self.with_childnames = with_childnames
self.debug = debug
@@ -157,11 +160,10 @@ class DatabaseQuery(object):
# left join parent, child tables
for child in self.tables[1:]:
- args.tables += " {join} {child} on ({child}.parent = {main}.name)".format(join=self.join,
- child=child, main=self.tables[0])
+ args.tables += f" {self.join} {child} on ({child}.parent = {self.tables[0]}.name)"
if self.grouped_or_conditions:
- self.conditions.append("({0})".format(" or ".join(self.grouped_or_conditions)))
+ self.conditions.append(f"({' or '.join(self.grouped_or_conditions)})")
args.conditions = ' and '.join(self.conditions)
@@ -186,9 +188,9 @@ class DatabaseQuery(object):
fields.append(field)
elif "as" in field.lower().split(" "):
col, _, new = field.split()
- fields.append("`{0}` as {1}".format(col, new))
+ fields.append(f"`{col}` as {new}")
else:
- fields.append("`{0}`".format(field))
+ fields.append(f"`{field}`")
args.fields = ", ".join(fields)
@@ -260,10 +262,10 @@ class DatabaseQuery(object):
if any(keyword in field.lower().split() for keyword in blacklisted_keywords):
_raise_exception()
- if any("({0}".format(keyword) in field.lower() for keyword in blacklisted_keywords):
+ if any(f"({keyword}" in field.lower() for keyword in blacklisted_keywords):
_raise_exception()
- if any("{0}(".format(keyword) in field.lower() for keyword in blacklisted_functions):
+ if any(f"{keyword}(" in field.lower() for keyword in blacklisted_functions):
_raise_exception()
if '@' in field.lower():
@@ -287,22 +289,30 @@ class DatabaseQuery(object):
def extract_tables(self):
"""extract tables from fields"""
- self.tables = ['`tab' + self.doctype + '`']
-
+ self.tables = [f"`tab{self.doctype}`"]
+ sql_functions = [
+ "dayofyear(",
+ "extract(",
+ "locate(",
+ "strpos(",
+ "count(",
+ "sum(",
+ "avg(",
+ ]
# add tables from fields
if self.fields:
- for f in self.fields:
- if ( not ("tab" in f and "." in f) ) or ("locate(" in f) or ("strpos(" in f) or \
- ("count(" in f) or ("avg(" in f) or ("sum(" in f) or ("extract(" in f) or ("dayofyear(" in f):
+ for field in self.fields:
+ if not ("tab" in field and "." in field) or any(x for x in sql_functions if x in field):
continue
- table_name = f.split('.')[0]
+ table_name = field.split('.')[0]
+
if table_name.lower().startswith('group_concat('):
table_name = table_name[13:]
if table_name.lower().startswith('ifnull('):
table_name = table_name[7:]
if not table_name[0]=='`':
- table_name = '`' + table_name + '`'
+ table_name = f"`{table_name}`"
if not table_name in self.tables:
self.append_table(table_name)
@@ -311,8 +321,7 @@ class DatabaseQuery(object):
doctype = table_name[4:-1]
ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read'
- if (not self.flags.ignore_permissions) and\
- (not frappe.has_permission(doctype, ptype=ptype)):
+ if not self.flags.ignore_permissions and not frappe.has_permission(doctype, ptype=ptype):
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype))
raise frappe.PermissionError(doctype)
@@ -326,7 +335,7 @@ class DatabaseQuery(object):
if len(self.tables) > 1:
for idx, field in enumerate(self.fields):
if '.' not in field and not _in_standard_sql_methods(field):
- self.fields[idx] = '{0}.{1}'.format(self.tables[0], field)
+ self.fields[idx] = f"{self.tables[0]}.{field}"
def get_table_columns(self):
try:
@@ -375,7 +384,7 @@ class DatabaseQuery(object):
if not self.flags.ignore_permissions:
match_conditions = self.build_match_conditions()
if match_conditions:
- self.conditions.append("(" + match_conditions + ")")
+ self.conditions.append(f"({match_conditions})")
def build_filter_conditions(self, filters, conditions, ignore_permissions=None):
"""build conditions from user filters"""
@@ -407,8 +416,7 @@ class DatabaseQuery(object):
if 'ifnull(' in f.fieldname:
column_name = f.fieldname
else:
- column_name = '{tname}.{fname}'.format(tname=tname,
- fname=f.fieldname)
+ column_name = f"{tname}.{f.fieldname}"
can_be_null = True
@@ -450,7 +458,7 @@ class DatabaseQuery(object):
fallback = "''"
value = [frappe.db.escape((v.name or '').strip(), percent=False) for v in result]
if len(value):
- value = "({0})".format(", ".join(value))
+ value = f"({', '.join(value)})"
else:
value = "('')"
# changing operator to IN as the above code fetches all the parent / child values and convert into tuple
@@ -466,7 +474,7 @@ class DatabaseQuery(object):
fallback = "''"
value = [frappe.db.escape((v or '').strip(), percent=False) for v in values]
if len(value):
- value = "({0})".format(", ".join(value))
+ value = f"({', '.join(value)})"
else:
value = "('')"
else:
@@ -503,7 +511,7 @@ class DatabaseQuery(object):
can_be_null = True
if 'ifnull' not in column_name:
- column_name = 'ifnull({}, {})'.format(column_name, fallback)
+ column_name = f'ifnull({column_name}, {fallback})'
elif df and df.fieldtype=="Date":
value = frappe.db.format_date(f.value)
@@ -540,21 +548,19 @@ class DatabaseQuery(object):
# escape value
if isinstance(value, str) and not f.operator.lower() == 'between':
- value = "{0}".format(frappe.db.escape(value, percent=False))
+ value = f"{frappe.db.escape(value, percent=False)}"
- if (self.ignore_ifnull
+ if (
+ self.ignore_ifnull
or not can_be_null
or (f.value and f.operator.lower() in ('=', 'like'))
- or 'ifnull(' in column_name.lower()):
+ or 'ifnull(' in column_name.lower()
+ ):
if f.operator.lower() == 'like' and frappe.conf.get('db_type') == 'postgres':
f.operator = 'ilike'
- condition = '{column_name} {operator} {value}'.format(
- column_name=column_name, operator=f.operator,
- value=value)
+ condition = f'{column_name} {f.operator} {value}'
else:
- condition = 'ifnull({column_name}, {fallback}) {operator} {value}'.format(
- column_name=column_name, fallback=fallback, operator=f.operator,
- value=value)
+ condition = f'ifnull({column_name}, {fallback}) {f.operator} {value}'
return condition
@@ -572,10 +578,12 @@ class DatabaseQuery(object):
role_permissions = frappe.permissions.get_role_permissions(meta, user=self.user)
self.shared = frappe.share.get_shared(self.doctype, self.user)
- if (not meta.istable and
+ if (
+ not meta.istable and
not (role_permissions.get("select") or role_permissions.get("read")) and
not self.flags.ignore_permissions and
- not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)):
+ not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)
+ ):
only_if_shared = True
if not self.shared:
frappe.throw(_("No permission to read {0}").format(self.doctype), frappe.PermissionError)
@@ -585,8 +593,10 @@ class DatabaseQuery(object):
else:
#if has if_owner permission skip user perm check
if role_permissions.get("has_if_owner_enabled") and role_permissions.get("if_owner", {}):
- self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype,
- frappe.db.escape(self.user, percent=False)))
+ self.match_conditions.append(
+ f"`tab{self.doctype}`.`owner` = {frappe.db.escape(self.user, percent=False)}"
+ )
+
# add user permission only if role has read perm
elif role_permissions.get("read") or role_permissions.get("select"):
# get user permissions
@@ -605,8 +615,7 @@ class DatabaseQuery(object):
# share is an OR condition, if there is a role permission
if not only_if_shared and self.shared and conditions:
- conditions = "({conditions}) or ({shared_condition})".format(
- conditions=conditions, shared_condition=self.get_share_condition())
+ conditions = f"({conditions}) or ({self.get_share_condition()})"
return conditions
@@ -614,8 +623,7 @@ class DatabaseQuery(object):
return self.match_filters
def get_share_condition(self):
- return """`tab{0}`.name in ({1})""".format(self.doctype, ", ".join(["%s"] * len(self.shared))) % \
- tuple([frappe.db.escape(s, percent=False) for s in self.shared])
+ return f"`tab{self.doctype}`.name in ({', '.join(frappe.db.escape(s, percent=False) for s in self.shared)})"
def add_user_permissions(self, user_permissions):
meta = frappe.get_meta(self.doctype)
@@ -640,9 +648,7 @@ class DatabaseQuery(object):
if frappe.get_system_settings("apply_strict_user_permissions"):
condition = ""
else:
- empty_value_condition = "ifnull(`tab{doctype}`.`{fieldname}`, '')=''".format(
- doctype=self.doctype, fieldname=df.get('fieldname')
- )
+ empty_value_condition = f"ifnull(`tab{self.doctype}`.`{df.get('fieldname')}`, '')=''"
condition = empty_value_condition + " or "
for permission in user_permission_values:
@@ -650,9 +656,7 @@ class DatabaseQuery(object):
docs.append(permission.get('doc'))
# append docs based on user permission applicable on reference doctype
-
# this is useful when getting list of docs from a link field
-
# in this case parent doctype of the link
# will be the reference doctype
@@ -664,14 +668,9 @@ class DatabaseQuery(object):
docs.append(permission.get('doc'))
if docs:
- condition += "`tab{doctype}`.`{fieldname}` in ({values})".format(
- doctype=self.doctype,
- fieldname=df.get('fieldname'),
- values=", ".join(
- [(frappe.db.escape(doc, percent=False)) for doc in docs])
- )
-
- match_conditions.append("({condition})".format(condition=condition))
+ values = ", ".join(frappe.db.escape(doc, percent=False) for doc in docs)
+ condition += f"`tab{self.doctype}`.`{df.get('fieldname')}` in ({values})"
+ match_conditions.append(f"({condition})")
match_filters[df.get('options')] = docs
if match_conditions:
@@ -721,17 +720,17 @@ class DatabaseQuery(object):
# `idx desc, modified desc`
# will covert to
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
- args.order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(self.doctype,
- f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
+ args.order_by = ', '.join(
+ f"`tab{self.doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(',')
+ )
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
-
- args.order_by = "`tab{0}`.`{1}` {2}".format(self.doctype, sort_field or "modified", sort_order or "desc")
+ 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 = "`tab{0}`.docstatus asc, {1}".format(self.doctype, args.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"""
@@ -802,17 +801,16 @@ def get_order_by(doctype, meta):
# `idx desc, modified desc`
# will covert to
# `tabItem`.`idx` desc, `tabItem`.`modified` desc
- order_by = ', '.join(['`tab{0}`.`{1}` {2}'.format(doctype,
- f.split()[0].strip(), f.split()[1].strip()) for f in meta.sort_field.split(',')])
+ order_by = ', '.join(f"`tab{doctype}`.`{f.split()[0].strip()}` {f.split()[1].strip()}" for f in meta.sort_field.split(','))
+
else:
sort_field = meta.sort_field or 'modified'
sort_order = (meta.sort_field and meta.sort_order) or 'desc'
-
- order_by = "`tab{0}`.`{1}` {2}".format(doctype, sort_field or "modified", sort_order or "desc")
+ order_by = f"`tab{doctype}`.`{sort_field or 'modified'}` {sort_order or 'desc'}"
# draft docs always on top
if meta.is_submittable:
- order_by = "`tab{0}`.docstatus asc, {1}".format(doctype, order_by)
+ order_by = f"`tab{doctype}`.docstatus asc, {order_by}"
return order_by
diff --git a/frappe/model/meta.py b/frappe/model/meta.py
index b67c41c990..b212324208 100644
--- a/frappe/model/meta.py
+++ b/frappe/model/meta.py
@@ -664,7 +664,7 @@ def trim_tables(doctype=None):
and not f.startswith("_")]
if columns_to_remove:
print(doctype, "columns removed:", columns_to_remove)
- columns_to_remove = ", ".join(["drop `{0}`".format(c) for c in columns_to_remove])
+ columns_to_remove = ", ".join("drop `{0}`".format(c) for c in columns_to_remove)
query = """alter table `tab{doctype}` {columns}""".format(
doctype=doctype, columns=columns_to_remove)
frappe.db.sql_ddl(query)
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index fc5b3ca9fe..9b8ac2574d 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -141,7 +141,7 @@ def update_user_settings(old, new, link_fields):
if not link_fields: return
# find the user settings for the linked doctypes
- linked_doctypes = set([d.parent for d in link_fields if not d.issingle])
+ linked_doctypes = {d.parent for d in link_fields if not d.issingle}
user_settings_details = frappe.db.sql('''SELECT `user`, `doctype`, `data`
FROM `__UserSettings`
WHERE `data` like %s
diff --git a/frappe/permissions.py b/frappe/permissions.py
index c25a7c3947..07b4a2e68f 100644
--- a/frappe/permissions.py
+++ b/frappe/permissions.py
@@ -308,7 +308,7 @@ def has_controller_permissions(doc, ptype, user=None):
return None
def get_doctypes_with_read():
- return list(set([p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()]))
+ return list({p.parent if type(p.parent) == str else p.parent.encode('UTF8') for p in get_valid_perms()})
def get_valid_perms(doctype=None, user=None):
'''Get valid permissions for the current user from DocPerm and Custom DocPerm'''
diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js
index c1c95d94cf..eb7a6edc5d 100644
--- a/frappe/public/js/frappe/form/dashboard.js
+++ b/frappe/public/js/frappe/form/dashboard.js
@@ -5,6 +5,7 @@ frappe.ui.form.Dashboard = class FormDashboard {
constructor(opts) {
$.extend(this, opts);
this.setup_dashboard_sections();
+ this.set_open_count = frappe.utils.throttle(this.set_open_count, 500);
}
setup_dashboard_sections() {
diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js
index 89c34ed80c..b9a838688d 100644
--- a/frappe/public/js/frappe/form/formatters.js
+++ b/frappe/public/js/frappe/form/formatters.js
@@ -221,9 +221,13 @@ frappe.form.formatters = {
Tag: function(value) {
var html = "";
$.each((value || "").split(","), function(i, v) {
- if(v) html+= ''+v +'';
+ if (v) html += `
+
+ ${v}
+ `;
});
return html;
},
@@ -310,6 +314,7 @@ frappe.form.get_formatter = function(fieldtype) {
frappe.format = function(value, df, options, doc) {
if(!df) df = {"fieldtype":"Data"};
+ if (df.fieldname == '_user_tags') df.fieldtype = 'Tag';
var fieldtype = df.fieldtype || "Data";
// format Dynamic Link as a Link
diff --git a/frappe/public/js/frappe/ui/group_by/group_by.js b/frappe/public/js/frappe/ui/group_by/group_by.js
index 3ebf9c9d3d..692d675c62 100644
--- a/frappe/public/js/frappe/ui/group_by/group_by.js
+++ b/frappe/public/js/frappe/ui/group_by/group_by.js
@@ -381,10 +381,11 @@ frappe.ui.GroupBy = class {
this.group_by_fields = {};
this.all_fields = {};
- let fields = this.report_view.meta.fields.filter((f) =>
+ const fields = this.report_view.meta.fields.filter((f) =>
['Select', 'Link', 'Data', 'Int', 'Check'].includes(f.fieldtype)
);
- this.group_by_fields[this.doctype] = fields;
+ const tag_field = {fieldname: '_user_tags', fieldtype: 'Data', label: __('Tags')};
+ this.group_by_fields[this.doctype] = fields.concat(tag_field);
this.all_fields[this.doctype] = this.report_view.meta.fields;
const standard_fields_filter = (df) =>
diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js
index b29b6b87e6..6a324f6034 100644
--- a/frappe/public/js/frappe/views/reports/report_view.js
+++ b/frappe/public/js/frappe/views/reports/report_view.js
@@ -410,7 +410,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
x_fields.push({
label: col.content,
fieldname: col.id,
- value: col.id,
+ value: col.id,
});
// numeric values in y
@@ -1024,8 +1024,12 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView {
return docfield.fieldtype === 'Date' ? 'right' : 'left';
})();
+ let id = fieldname;
+
// child table column
- const id = doctype !== this.doctype ? `${doctype}:${fieldname}` : fieldname;
+ if (doctype !== this.doctype && fieldname !== '_aggregate_column') {
+ id = `${doctype}:${fieldname}`;
+ }
let width = (docfield ? cint(docfield.width) : null) || null;
if (this.report_doc) {
diff --git a/frappe/public/scss/common/controls.scss b/frappe/public/scss/common/controls.scss
index 83fc4461d6..c939c6de39 100644
--- a/frappe/public/scss/common/controls.scss
+++ b/frappe/public/scss/common/controls.scss
@@ -241,6 +241,7 @@ textarea.form-control {
// rating
.rating {
+ cursor: pointer;
--star-fill: var(--gray-300);
.star-hover {
--star-fill: var(--yellow-100);
@@ -248,6 +249,24 @@ textarea.form-control {
.star-click {
--star-fill: var(--yellow-300);
}
+
+ .rating-box {
+ background-color: var(--gray-300);
+ border-radius: 5px;
+ font-size: 14px;
+ text-align: center;
+ padding: 2px;
+ cursor: pointer;
+ width: 25px;
+ height: 25px;
+ margin: 4px 2px;
+ }
+ .rating-hover {
+ background-color: var(--yellow-100);
+ }
+ .rating-click {
+ background-color: var(--yellow-300);
+ }
}
.frappe-control .control-value {
diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss
index aac949b1bf..57d0583b35 100644
--- a/frappe/public/scss/common/grid.scss
+++ b/frappe/public/scss/common/grid.scss
@@ -53,7 +53,12 @@
display: none;
}
+.form-grid .grid-heading-row .template-row {
+ margin-left: 20px;
+}
+
.form-grid .template-row {
+ width: calc(100% - 30px);
padding: 8px 15px;
}
diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss
index 6f6e09dc70..12706d6b7f 100644
--- a/frappe/public/scss/common/quill.scss
+++ b/frappe/public/scss/common/quill.scss
@@ -176,6 +176,7 @@
}
.ql-editor.read-mode {
+ height: unset;
padding: 0;
.mention {
--user-mention-bg-color: var(--control-bg);
diff --git a/frappe/public/scss/website/markdown.scss b/frappe/public/scss/website/markdown.scss
index c5f44d20d8..6f009df393 100644
--- a/frappe/public/scss/website/markdown.scss
+++ b/frappe/public/scss/website/markdown.scss
@@ -48,7 +48,6 @@ $font-sizes-mobile: (
}
li {
- text-indent: 0.25rem;
padding-top: 1px;
padding-bottom: 1px;
}
diff --git a/frappe/templates/includes/comments/comments.html b/frappe/templates/includes/comments/comments.html
index c490bedd72..935fa5367e 100644
--- a/frappe/templates/includes/comments/comments.html
+++ b/frappe/templates/includes/comments/comments.html
@@ -49,8 +49,10 @@
{% endif %}
\ No newline at end of file
diff --git a/frappe/templates/includes/feedback/feedback.py b/frappe/templates/includes/feedback/feedback.py
new file mode 100644
index 0000000000..1830a3e09e
--- /dev/null
+++ b/frappe/templates/includes/feedback/feedback.py
@@ -0,0 +1,63 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+from __future__ import unicode_literals
+
+import frappe
+
+from frappe import _
+
+@frappe.whitelist(allow_guest=True)
+def add_feedback(reference_doctype, reference_name, rating, feedback, feedback_email):
+ doc = frappe.get_doc(reference_doctype, reference_name)
+ if doc.disable_feedback == 1:
+ return
+
+ doc = frappe.new_doc('Feedback')
+ doc.reference_doctype = reference_doctype
+ doc.reference_name = reference_name
+ doc.rating = rating
+ doc.feedback = feedback
+ doc.email = feedback_email
+ doc.save(ignore_permissions=True)
+
+ subject = _('New Feedback on {0}: {1}').format(reference_doctype, reference_name)
+ send_mail(doc, subject)
+ return doc
+
+@frappe.whitelist()
+def update_feedback(reference_doctype, reference_name, rating, feedback, feedback_email):
+ doc = frappe.get_doc(reference_doctype, reference_name)
+ if doc.disable_feedback == 1:
+ return
+
+ filters = {
+ "email": feedback_email,
+ "reference_doctype": reference_doctype,
+ "reference_name": reference_name
+ }
+ d = frappe.get_all('Feedback', filters=filters, limit=1)
+ doc = frappe.get_doc('Feedback', d[0].name)
+ doc.rating = rating
+ doc.feedback = feedback
+ doc.save(ignore_permissions=True)
+
+ subject = _('Feedback updated on {0}: {1}').format(reference_doctype, reference_name)
+ send_mail(doc, subject)
+ return doc
+
+def send_mail(feedback, subject):
+ doc = frappe.get_doc(feedback.reference_doctype, feedback.reference_name)
+
+ message = ("{0} ({1})
".format(feedback.feedback, feedback.rating)
+ + "{2}
".format(frappe.utils.get_request_site_address(),
+ feedback.name,
+ _("View Feedback")))
+
+ # notify creator
+ frappe.sendmail(
+ recipients=frappe.db.get_value('User', doc.owner, 'email') or doc.owner,
+ subject=subject,
+ message=message,
+ reference_doctype=doc.doctype,
+ reference_name=doc.name
+ )
diff --git a/frappe/test_runner.py b/frappe/test_runner.py
index 1f99e55fb8..0c30fbbd00 100644
--- a/frappe/test_runner.py
+++ b/frappe/test_runner.py
@@ -175,6 +175,7 @@ def run_tests_for_module(module, verbose=False, tests=(), profile=False, junit_x
for doctype in module.test_dependencies:
make_test_records(doctype, verbose=verbose)
+ frappe.db.commit()
return _run_unittest(module, verbose=verbose, tests=tests, profile=profile, junit_xml_output=junit_xml_output)
def _run_unittest(modules, verbose=False, tests=(), profile=False, junit_xml_output=False):
diff --git a/frappe/tests/test_api.py b/frappe/tests/test_api.py
index 7e77aab779..29939fea1c 100644
--- a/frappe/tests/test_api.py
+++ b/frappe/tests/test_api.py
@@ -39,6 +39,11 @@ class TestResourceAPI(unittest.TestCase):
for name in self.GENERATED_DOCUMENTS:
frappe.delete_doc_if_exists(self.DOCTYPE, name)
+ def setUp(self):
+ # commit to ensure consistency in session (postgres CI randomly fails)
+ if frappe.conf.db_type == "postgres":
+ frappe.db.commit()
+
@property
def sid(self):
if not getattr(self, "_sid", None):
diff --git a/frappe/tests/test_commands.py b/frappe/tests/test_commands.py
index b6cd0b575c..07bdf8791e 100644
--- a/frappe/tests/test_commands.py
+++ b/frappe/tests/test_commands.py
@@ -80,7 +80,7 @@ def exists_in_backup(doctypes, file):
)
with gzip.open(file, "rb") as f:
content = f.read().decode("utf8")
- return all([predicate.format(doctype).lower() in content.lower() for doctype in doctypes])
+ return all(predicate.format(doctype).lower() in content.lower() for doctype in doctypes)
class BaseTestCommands(unittest.TestCase):
@@ -355,12 +355,12 @@ class TestCommands(BaseTestCommands):
# test 2: bare functionality for single site
self.execute("bench --site {site} list-apps")
self.assertEqual(self.returncode, 0)
- list_apps = set([
+ list_apps = set(
_x.split()[0] for _x in self.stdout.split("\n")
- ])
+ )
doctype = frappe.get_single("Installed Applications").installed_applications
if doctype:
- installed_apps = set([x.app_name for x in doctype])
+ installed_apps = set(x.app_name for x in doctype)
else:
installed_apps = set(frappe.get_installed_apps())
self.assertSetEqual(list_apps, installed_apps)
diff --git a/frappe/tests/test_db_query.py b/frappe/tests/test_db_query.py
index 42ebd05b67..89975b46d6 100644
--- a/frappe/tests/test_db_query.py
+++ b/frappe/tests/test_db_query.py
@@ -21,6 +21,18 @@ class TestReportview(unittest.TestCase):
def test_basic(self):
self.assertTrue({"name":"DocType"} in DatabaseQuery("DocType").execute(limit_page_length=None))
+ def test_extract_tables(self):
+ db_query = DatabaseQuery("DocType")
+ add_custom_field("DocType", 'test_tab_field', 'Data')
+
+ db_query.fields = ["tabNote.creation", "test_tab_field", "tabDocType.test_tab_field"]
+ db_query.extract_tables()
+ self.assertIn("`tabNote`", db_query.tables)
+ self.assertIn("`tabDocType`", db_query.tables)
+ self.assertNotIn("test_tab_field", db_query.tables)
+
+ clear_custom_fields("DocType")
+
def test_build_match_conditions(self):
clear_user_permissions_for_doctype('Blog Post', 'test2@example.com')
diff --git a/frappe/translate.py b/frappe/translate.py
index a407ad9579..4ff50d3fd0 100644
--- a/frappe/translate.py
+++ b/frappe/translate.py
@@ -284,8 +284,8 @@ def clear_cache():
def get_messages_for_app(app, deduplicate=True):
"""Returns all messages (list) for a specified `app`"""
messages = []
- modules = ", ".join(['"{}"'.format(m.title().replace("_", " ")) \
- for m in frappe.local.app_modules[app]])
+ modules = ", ".join('"{}"'.format(m.title().replace("_", " ")) \
+ for m in frappe.local.app_modules[app])
# doctypes
if modules:
diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py
index 3e7dc48304..af9d5de1ee 100644
--- a/frappe/utils/__init__.py
+++ b/frappe/utils/__init__.py
@@ -189,7 +189,7 @@ def random_string(length):
"""generate a random string"""
import string
from random import choice
- return ''.join([choice(string.ascii_letters + string.digits) for i in range(length)])
+ return ''.join(choice(string.ascii_letters + string.digits) for i in range(length))
def has_gravatar(email):
@@ -311,7 +311,7 @@ def make_esc(esc_chars):
"""
Function generator for Escaping special characters
"""
- return lambda s: ''.join(['\\' + c if c in esc_chars else c for c in s])
+ return lambda s: ''.join('\\' + c if c in esc_chars else c for c in s)
# esc / unescape characters -- used for command line
def esc(s, esc_chars):
diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py
index b21efc5e89..908be52452 100644
--- a/frappe/utils/backups.py
+++ b/frappe/utils/backups.py
@@ -307,8 +307,8 @@ class BackupGenerator:
backup_summary = self.get_summary()
print("Backup Summary for {0} at {1}".format(frappe.local.site, now()))
- title = max([len(x) for x in backup_summary])
- path = max([len(x["path"]) for x in backup_summary.values()])
+ title = max(len(x) for x in backup_summary)
+ path = max(len(x["path"]) for x in backup_summary.values())
for _type, info in backup_summary.items():
template = "{{0:{0}}}: {{1:{1}}} {{2}}".format(title, path)
@@ -381,7 +381,7 @@ class BackupGenerator:
"",
])
- generated_header = "\n".join([f"-- {x}" for x in database_header_content]) + "\n"
+ generated_header = "\n".join(f"-- {x}" for x in database_header_content) + "\n"
with gzip.open(args.backup_path_db, "wt") as f:
f.write(generated_header)
diff --git a/frappe/utils/bot.py b/frappe/utils/bot.py
index 572723c056..c75b48ab49 100644
--- a/frappe/utils/bot.py
+++ b/frappe/utils/bot.py
@@ -38,10 +38,10 @@ class BotParser(object):
def format_list(self, data):
'''Format list as markdown'''
- return _('I found these: ') + ', '.join([' [{title}](/app/Form/{doctype}/{name})'.format(
+ return _('I found these:') + ' ' + ', '.join(' [{title}](/app/Form/{doctype}/{name})'.format(
title = d.title or d.name,
doctype=self.get_doctype(),
- name=d.name) for d in data])
+ name=d.name) for d in data)
def get_doctype(self):
'''returns the doctype name from self.tables'''
@@ -56,8 +56,8 @@ class ShowNotificationBot(BotParser):
if open_items:
return ("Following items need your attention:\n\n"
- + "\n\n".join(["{0} [{1}](/app/List/{1})".format(d[1], d[0])
- for d in open_items if d[1] > 0]))
+ + "\n\n".join("{0} [{1}](/app/List/{1})".format(d[1], d[0])
+ for d in open_items if d[1] > 0))
else:
return 'Take it easy, nothing urgent needs your attention'
diff --git a/frappe/utils/jinja_globals.py b/frappe/utils/jinja_globals.py
index 924b8bb8b9..2c14249672 100644
--- a/frappe/utils/jinja_globals.py
+++ b/frappe/utils/jinja_globals.py
@@ -10,10 +10,10 @@ def resolve_class(classes):
return classes
if isinstance(classes, (list, tuple)):
- return " ".join([resolve_class(c) for c in classes]).strip()
+ return " ".join(resolve_class(c) for c in classes).strip()
if isinstance(classes, dict):
- return " ".join([classname for classname in classes if classes[classname]]).strip()
+ return " ".join(classname for classname in classes if classes[classname]).strip()
return classes
diff --git a/frappe/website/doctype/blog_post/blog_post.json b/frappe/website/doctype/blog_post/blog_post.json
index 909cecf867..c2491ee5a4 100644
--- a/frappe/website/doctype/blog_post/blog_post.json
+++ b/frappe/website/doctype/blog_post/blog_post.json
@@ -18,6 +18,7 @@
"featured",
"hide_cta",
"disable_comments",
+ "disable_feedback",
"section_break_5",
"blog_intro",
"content_type",
@@ -191,6 +192,13 @@
"fieldtype": "Data",
"label": "Meta Title",
"length": 60
+ },
+ {
+ "default": "0",
+ "description": "Feedback on this blog post will be disabled if checked.",
+ "fieldname": "disable_feedback",
+ "fieldtype": "Check",
+ "label": "Disable Feedback"
}
],
"has_web_view": 1,
@@ -200,7 +208,7 @@
"is_published_field": "published",
"links": [],
"max_attachments": 5,
- "modified": "2020-12-23 14:28:36.311389",
+ "modified": "2021-06-14 13:50:02.109719",
"modified_by": "Administrator",
"module": "Website",
"name": "Blog Post",
diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py
index 1fd8947db2..965fc8e3e0 100644
--- a/frappe/website/doctype/blog_post/blog_post.py
+++ b/frappe/website/doctype/blog_post/blog_post.py
@@ -97,12 +97,14 @@ class BlogPost(WebsiteGenerator):
context.metatags["image"] = self.meta_image or image or None
self.load_comments(context)
+ self.load_feedback(context)
context.category = frappe.db.get_value("Blog Category",
context.doc.blog_category, ["title", "route"], as_dict=1)
context.parents = [{"name": _("Home"), "route":"/"},
{"name": "Blog", "route": "/blog"},
{"label": context.category.title, "route":context.category.route}]
+ context.guest_allowed = True
def fetch_cta(self):
if frappe.db.get_single_value("Blog Settings", "show_cta_in_blog", cache=True):
@@ -144,6 +146,17 @@ class BlogPost(WebsiteGenerator):
else:
context.comment_text = _('{0} comments').format(len(context.comment_list))
+ def load_feedback(self, context):
+ feedback = frappe.get_all('Feedback',
+ fields=['email', 'feedback', 'rating'],
+ filters=dict(
+ reference_doctype=self.doctype,
+ reference_name=self.name,
+ email=frappe.session.user
+ )
+ )
+ context.user_feedback = feedback[0] if feedback else ''
+
def set_read_time(self):
content = self.content or self.content_html or ''
if self.content_type == "Markdown":
diff --git a/frappe/website/doctype/blog_post/templates/blog_post.html b/frappe/website/doctype/blog_post/templates/blog_post.html
index dad8b97164..4678622062 100644
--- a/frappe/website/doctype/blog_post/templates/blog_post.html
+++ b/frappe/website/doctype/blog_post/templates/blog_post.html
@@ -65,6 +65,11 @@
{% include 'templates/includes/comments/comments.html' %}
{% endif %}
+ {% if not disable_feedback %}
+
+ {% include 'templates/includes/feedback/feedback.html' %}
+
+ {% endif %}