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 = "" + df_options_str = "" 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 %}