From 15b3cc890f9c14c78b2fb6932e698ce206c54c95 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Sat, 6 Mar 2021 08:55:58 +0000 Subject: [PATCH 001/112] feat: allow query/custom reports to save custom data in the json field --- .../prepared_report/prepared_report.py | 5 ++++- frappe/desk/query_report.py | 14 +++++++++---- frappe/patches.txt | 1 + frappe/patches/v13_0/queryreport_columns.py | 21 +++++++++++++++++++ 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 frappe/patches/v13_0/queryreport_columns.py diff --git a/frappe/core/doctype/prepared_report/prepared_report.py b/frappe/core/doctype/prepared_report/prepared_report.py index e947cee8ed..c27853f460 100644 --- a/frappe/core/doctype/prepared_report/prepared_report.py +++ b/frappe/core/doctype/prepared_report/prepared_report.py @@ -37,7 +37,10 @@ def run_background(prepared_report): custom_report_doc = report reference_report = custom_report_doc.reference_report report = frappe.get_doc("Report", reference_report) - report.custom_columns = custom_report_doc.json + if custom_report_doc.json: + data = json.loads(custom_report_doc.json) + if data: + report.custom_columns = data["columns"] result = generate_report_result( report=report, diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py index 22d47d1120..9589507ca6 100644 --- a/frappe/desk/query_report.py +++ b/frappe/desk/query_report.py @@ -36,7 +36,10 @@ def get_report_doc(report_name): reference_report = custom_report_doc.reference_report doc = frappe.get_doc("Report", reference_report) doc.custom_report = report_name - doc.custom_columns = custom_report_doc.json + if custom_report_doc.json: + data = json.loads(custom_report_doc.json) + if data: + doc.custom_columns = data["columns"] doc.is_custom_report = True if not doc.is_permitted(): @@ -83,7 +86,7 @@ def generate_report_result(report, filters=None, user=None, custom_columns=None) if report.custom_columns: # saved columns (with custom columns / with different column order) - columns = json.loads(report.custom_columns) + columns = report.custom_columns # unsaved custom_columns if custom_columns: @@ -524,9 +527,12 @@ def save_report(reference_report, report_name, columns): "report_type": "Custom Report", }, ) + if docname: report = frappe.get_doc("Report", docname) - report.update({"json": columns}) + existing_jd = json.loads(report.json) + existing_jd["columns"] = json.loads(columns) + report.update({"json": json.dumps(existing_jd, separators=(',', ':'))}) report.save() frappe.msgprint(_("Report updated successfully")) @@ -536,7 +542,7 @@ def save_report(reference_report, report_name, columns): { "doctype": "Report", "report_name": report_name, - "json": columns, + "json": f'{{"columns":{columns}}}', "ref_doctype": report_doc.ref_doctype, "is_standard": "No", "report_type": "Custom Report", diff --git a/frappe/patches.txt b/frappe/patches.txt index 5251b3da30..516ddb6094 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -334,3 +334,4 @@ frappe.patches.v13_0.delete_package_publish_tool frappe.patches.v13_0.rename_list_view_setting_to_list_view_settings frappe.patches.v13_0.remove_twilio_settings frappe.patches.v12_0.rename_uploaded_files_with_proper_name +frappe.patches.v13_0.queryreport_columns diff --git a/frappe/patches/v13_0/queryreport_columns.py b/frappe/patches/v13_0/queryreport_columns.py new file mode 100644 index 0000000000..cc09bae37c --- /dev/null +++ b/frappe/patches/v13_0/queryreport_columns.py @@ -0,0 +1,21 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe, json + +def execute(): + """Convert Query Report json to support other content""" + records = frappe.get_all('Report', + filters={ + "json": ["!=", ""] + }, + fields=["name", "json"] + ) + for record in records: + jstr = record["json"] + data = json.loads(jstr) + if isinstance(data, list): + # double escape braces + jstr = f'{{"columns":{jstr}}}' + frappe.db.update('Report', record["name"], "json", jstr) From 49a79e4d0076a940a32763191dcfa45e846dd014 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Fri, 9 Apr 2021 00:27:02 +0000 Subject: [PATCH 002/112] chore: fix sider --- frappe/patches/v13_0/queryreport_columns.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/patches/v13_0/queryreport_columns.py b/frappe/patches/v13_0/queryreport_columns.py index cc09bae37c..6c2a1b1219 100644 --- a/frappe/patches/v13_0/queryreport_columns.py +++ b/frappe/patches/v13_0/queryreport_columns.py @@ -2,7 +2,8 @@ # MIT License. See license.txt from __future__ import unicode_literals -import frappe, json +import frappe +import json def execute(): """Convert Query Report json to support other content""" From 4c787effc67f9c4912599a0b4e028c7e57de4102 Mon Sep 17 00:00:00 2001 From: Anupam Date: Sun, 11 Apr 2021 01:52:43 +0530 Subject: [PATCH 003/112] fix: don't allow user to remove/change data source file in data import --- frappe/core/doctype/data_import/data_import.json | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json index 8b1b6c4e07..fe6fb90481 100644 --- a/frappe/core/doctype/data_import/data_import.json +++ b/frappe/core/doctype/data_import/data_import.json @@ -53,7 +53,8 @@ "fieldname": "import_file", "fieldtype": "Attach", "in_list_view": 1, - "label": "Import File" + "label": "Import File", + "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" }, { "fieldname": "import_preview", @@ -156,10 +157,11 @@ "description": "Must be a publicly accessible Google Sheets URL", "fieldname": "google_sheets_url", "fieldtype": "Data", - "label": "Import from Google Sheets" + "label": "Import from Google Sheets", + "read_only_depends_on": "eval: ['Success', 'Partial Success'].includes(doc.status)" }, { - "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved", + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved && ['Success', 'Partial Success'].includes(doc.status)", "fieldname": "refresh_google_sheet", "fieldtype": "Button", "label": "Refresh Google Sheet" @@ -167,7 +169,7 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2020-06-24 14:33:03.173876", + "modified": "2021-04-11 01:50:42.074623", "modified_by": "Administrator", "module": "Core", "name": "Data Import", From 1a075a97ac433b9f51a17598e4c13fb556a12229 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 9 Apr 2021 21:36:31 +0530 Subject: [PATCH 004/112] chore: static analysis hack for globals --- frappe/__init__.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frappe/__init__.py b/frappe/__init__.py index 785d5ee7e5..bc2c933f5a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -15,6 +15,7 @@ from __future__ import unicode_literals, print_function from six import iteritems, binary_type, text_type, string_types, PY2 from werkzeug.local import Local, release_local import os, sys, importlib, inspect, json +import typing from past.builtins import cmp import click @@ -133,6 +134,14 @@ message_log = local("message_log") lang = local("lang") +# This if block is never executed when running the code. It is only used for +# telling static code analyzer where to find dynamically defined attributes. +if typing.TYPE_CHECKING: + from frappe.database.mariadb.database import MariaDBDatabase + from frappe.database.postgres.database import PostgresDatabase + db: typing.Union[MariaDBDatabase, PostgresDatabase] +# end: static analysis hack + def init(site, sites_path=None, new_site=False): """Initialize frappe for the current site. Reset thread locals `frappe.local`""" if getattr(local, "initialised", None): From a1190a0c4a9e2f1943d30a3206420367a451340b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Mon, 12 Apr 2021 16:35:11 +0530 Subject: [PATCH 005/112] feat: Add option to mention a group of users --- frappe/boot.py | 2 + frappe/core/doctype/user/user.py | 5 ++ frappe/core/doctype/user_group/__init__.py | 0 .../doctype/user_group/test_user_group.py | 10 ++++ frappe/core/doctype/user_group/user_group.js | 8 +++ .../core/doctype/user_group/user_group.json | 50 +++++++++++++++++++ frappe/core/doctype/user_group/user_group.py | 10 ++++ .../doctype/user_group_member/__init__.py | 0 .../test_user_group_member.py | 10 ++++ .../user_group_member/user_group_member.js | 8 +++ .../user_group_member/user_group_member.json | 32 ++++++++++++ .../user_group_member/user_group_member.py | 10 ++++ .../controls/quill-mention/blots/mention.js | 2 + .../controls/quill-mention/quill.mention.js | 2 + frappe/public/js/frappe/utils/utils.js | 9 ++++ frappe/public/scss/common/quill.scss | 6 ++- frappe/public/scss/desk/timeline.scss | 3 +- frappe/utils/html_utils.py | 2 +- 18 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 frappe/core/doctype/user_group/__init__.py create mode 100644 frappe/core/doctype/user_group/test_user_group.py create mode 100644 frappe/core/doctype/user_group/user_group.js create mode 100644 frappe/core/doctype/user_group/user_group.json create mode 100644 frappe/core/doctype/user_group/user_group.py create mode 100644 frappe/core/doctype/user_group_member/__init__.py create mode 100644 frappe/core/doctype/user_group_member/test_user_group_member.py create mode 100644 frappe/core/doctype/user_group_member/user_group_member.js create mode 100644 frappe/core/doctype/user_group_member/user_group_member.json create mode 100644 frappe/core/doctype/user_group_member/user_group_member.py diff --git a/frappe/boot.py b/frappe/boot.py index 0dfcb8d1b4..65a07b15e5 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -42,6 +42,8 @@ def get_bootinfo(): bootinfo.user_info = get_user_info() bootinfo.sid = frappe.session['sid'] + bootinfo.user_groups = frappe.get_all('User Group', pluck="name") + bootinfo.modules = {} bootinfo.module_list = [] load_desktop_data(bootinfo) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 04d087e82a..035333cbe9 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1018,8 +1018,13 @@ def extract_mentions(txt): soup = BeautifulSoup(txt, 'html.parser') emails = [] for mention in soup.find_all(class_='mention'): + if mention.get('data-is-group'): + user_group = frappe.get_cached_doc('User Group', mention['data-id']) + emails += [d.user for d in user_group.user_group_members] + continue email = mention['data-id'] emails.append(email) + return emails def handle_password_test_fail(result): diff --git a/frappe/core/doctype/user_group/__init__.py b/frappe/core/doctype/user_group/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_group/test_user_group.py b/frappe/core/doctype/user_group/test_user_group.py new file mode 100644 index 0000000000..c7e28f3d31 --- /dev/null +++ b/frappe/core/doctype/user_group/test_user_group.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestUserGroup(unittest.TestCase): + pass diff --git a/frappe/core/doctype/user_group/user_group.js b/frappe/core/doctype/user_group/user_group.js new file mode 100644 index 0000000000..2aa9b68658 --- /dev/null +++ b/frappe/core/doctype/user_group/user_group.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('User Group', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/user_group/user_group.json b/frappe/core/doctype/user_group/user_group.json new file mode 100644 index 0000000000..3d859dcc9a --- /dev/null +++ b/frappe/core/doctype/user_group/user_group.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "autoname": "field:group_name", + "creation": "2021-04-12 15:17:24.751710", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "group_name", + "user_group_members" + ], + "fields": [ + { + "fieldname": "group_name", + "fieldtype": "Data", + "label": "Group Name", + "unique": 1 + }, + { + "fieldname": "user_group_members", + "fieldtype": "Table MultiSelect", + "label": "User Group Members", + "options": "User Group Member" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-04-12 15:17:24.751710", + "modified_by": "Administrator", + "module": "Core", + "name": "User Group", + "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", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py new file mode 100644 index 0000000000..6fc6db4620 --- /dev/null +++ b/frappe/core/doctype/user_group/user_group.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UserGroup(Document): + pass diff --git a/frappe/core/doctype/user_group_member/__init__.py b/frappe/core/doctype/user_group_member/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/core/doctype/user_group_member/test_user_group_member.py b/frappe/core/doctype/user_group_member/test_user_group_member.py new file mode 100644 index 0000000000..38aade4608 --- /dev/null +++ b/frappe/core/doctype/user_group_member/test_user_group_member.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestUserGroupMember(unittest.TestCase): + pass diff --git a/frappe/core/doctype/user_group_member/user_group_member.js b/frappe/core/doctype/user_group_member/user_group_member.js new file mode 100644 index 0000000000..0b2dbe0d46 --- /dev/null +++ b/frappe/core/doctype/user_group_member/user_group_member.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('User Group Member', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/core/doctype/user_group_member/user_group_member.json b/frappe/core/doctype/user_group_member/user_group_member.json new file mode 100644 index 0000000000..d2ff149366 --- /dev/null +++ b/frappe/core/doctype/user_group_member/user_group_member.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2021-04-12 15:16:29.279107", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "in_list_view": 1, + "label": "User", + "options": "User", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-04-12 15:17:18.773046", + "modified_by": "Administrator", + "module": "Core", + "name": "User Group Member", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user_group_member/user_group_member.py b/frappe/core/doctype/user_group_member/user_group_member.py new file mode 100644 index 0000000000..4d0656913d --- /dev/null +++ b/frappe/core/doctype/user_group_member/user_group_member.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class UserGroupMember(Document): + pass diff --git a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js index 1c5787f854..d6907158f9 100644 --- a/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js +++ b/frappe/public/js/frappe/form/controls/quill-mention/blots/mention.js @@ -15,6 +15,7 @@ class MentionBlot extends Embed { node.dataset.id = data.id; node.dataset.value = data.value; node.dataset.denotationChar = data.denotationChar; + node.dataset.isGroup = data.isGroup; if (data.link) { node.dataset.link = data.link; } @@ -27,6 +28,7 @@ class MentionBlot extends Embed { value: domNode.dataset.value, link: domNode.dataset.link || null, denotationChar: domNode.dataset.denotationChar, + isGroup: domNode.dataset.isGroup, }; } } diff --git a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js index ac1b9697f0..4b5326271e 100644 --- a/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js +++ b/frappe/public/js/frappe/form/controls/quill-mention/quill.mention.js @@ -149,6 +149,7 @@ class Mention { this.mentionList.childNodes[this.itemIndex].dataset.value, link: itemLink || null, denotationChar: this.mentionList.childNodes[this.itemIndex].dataset.denotationChar, + isGroup: this.mentionList.childNodes[this.itemIndex].dataset.isGroup, }; } @@ -197,6 +198,7 @@ class Mention { li.dataset.index = i; li.dataset.id = data[i].id; li.dataset.value = data[i].value; + li.dataset.isGroup = Boolean(data[i].is_group); li.dataset.denotationChar = mentionChar; if (data[i].link) { li.dataset.link = data[i].link; diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 302ebceeda..e1e4f437cd 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1285,6 +1285,15 @@ Object.assign(frappe.utils, { value: frappe.boot.user_info[user].fullname, }; }); + + frappe.boot.user_groups && frappe.boot.user_groups.map(group => { + names_for_mentions.push({ + id: group, + value: group, + is_group: true + }); + }); + return names_for_mentions; }, print(doctype, docname, print_format, letterhead, lang_code) { diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index e5303be5cf..c4011e2d6b 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -190,4 +190,8 @@ .mention>span { margin: 0 3px; -} \ No newline at end of file +} + +.mention[data-is-group="true"] { + background-color: var(--purple-100) !important; +} diff --git a/frappe/public/scss/desk/timeline.scss b/frappe/public/scss/desk/timeline.scss index 4bb3cbec78..a7e5d3dd9c 100644 --- a/frappe/public/scss/desk/timeline.scss +++ b/frappe/public/scss/desk/timeline.scss @@ -77,6 +77,7 @@ $threshold: 34; } } .document-email-link-container { + @extend .ellipsis; position: relative; padding: var(--padding-sm); font-size: var(--text-sm); @@ -141,4 +142,4 @@ $threshold: 34; --icon-stroke: var(--text-color); } } -} \ No newline at end of file +} diff --git a/frappe/utils/html_utils.py b/frappe/utils/html_utils.py index 81e5f2de3e..f3f86dcad2 100644 --- a/frappe/utils/html_utils.py +++ b/frappe/utils/html_utils.py @@ -177,7 +177,7 @@ acceptable_attributes = [ 'data-value', 'role', 'frameborder', 'allowfullscreen', 'spellcheck', 'data-mode', 'data-gramm', 'data-placeholder', 'data-comment', 'data-id', 'data-denotation-char', 'itemprop', 'itemscope', - 'itemtype', 'itemid', 'itemref', 'datetime' + 'itemtype', 'itemid', 'itemref', 'datetime', 'data-is-group' ] mathml_attributes = [ From 322a9f7623031bd2167cf00fe1fe9053815f05d6 Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Mon, 12 Apr 2021 17:28:27 +0530 Subject: [PATCH 006/112] fix: none type error for empty lines --- frappe/email/doctype/auto_email_report/auto_email_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index d82caa7bd4..6f1cd8eebd 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -252,7 +252,7 @@ def make_links(columns, data): elif col.fieldtype == "Dynamic Link": if col.options and row.get(col.fieldname) and row.get(col.options): row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) - elif col.fieldtype == "Currency": + elif col.fieldtype == "Currency" and row.get(col.fieldname): row[col.fieldname] = frappe.format_value(row[col.fieldname], col) return columns, data From 1f70959fa1d36ad7f008e13459b23a72ea6302b0 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 14 Apr 2021 05:39:31 +0530 Subject: [PATCH 007/112] feat: Add Enable/Disable Webhook via Check - Added a new field to Webhook DocType - Modfied the webhook run logic to only fetch enabled webhooks --- frappe/integrations/doctype/webhook/__init__.py | 4 +++- frappe/integrations/doctype/webhook/webhook.json | 9 ++++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py index 8b08db5f68..0524cd609b 100644 --- a/frappe/integrations/doctype/webhook/__init__.py +++ b/frappe/integrations/doctype/webhook/__init__.py @@ -21,7 +21,9 @@ def run_webhooks(doc, method): if webhooks is None: # query webhooks webhooks_list = frappe.get_all('Webhook', - fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"]) + fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"], + filters={"enabled": "1"} + ) # make webhooks map for cache webhooks = {} diff --git a/frappe/integrations/doctype/webhook/webhook.json b/frappe/integrations/doctype/webhook/webhook.json index 9f979099c9..85895c052c 100644 --- a/frappe/integrations/doctype/webhook/webhook.json +++ b/frappe/integrations/doctype/webhook/webhook.json @@ -11,6 +11,7 @@ "webhook_doctype", "cb_doc_events", "webhook_docevent", + "enabled", "sb_condition", "condition", "cb_condition", @@ -147,10 +148,16 @@ "fieldname": "webhook_secret", "fieldtype": "Password", "label": "Webhook Secret" + }, + { + "default": "1", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" } ], "links": [], - "modified": "2020-01-13 01:53:04.459968", + "modified": "2021-04-14 05:35:28.532049", "modified_by": "Administrator", "module": "Integrations", "name": "Webhook", From cdd3bb37613418c3695a1596f047082d19a31cbf Mon Sep 17 00:00:00 2001 From: Anupam Date: Wed, 14 Apr 2021 12:29:02 +0530 Subject: [PATCH 008/112] fix: allow to override dashboard chart properties type/color --- frappe/public/js/frappe/form/dashboard.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index ed3ad5ea09..9b6d15c1fc 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -535,14 +535,14 @@ frappe.ui.form.Dashboard = class FormDashboard { render_graph(args) { this.chart_area.show(); this.chart_area.body.empty(); - $.extend(args, { + $.extend({ type: 'line', colors: ['green'], truncateLegends: 1, axisOptions: { shortenYAxisNumbers: 1 } - }); + }, args); this.show(); this.chart = new frappe.Chart('.form-graph', args); From a373c00abd1db85a4a8304fecc299890c74e163e Mon Sep 17 00:00:00 2001 From: thebachy1 Date: Wed, 14 Apr 2021 12:39:46 -0500 Subject: [PATCH 009/112] fix: Load server translations in boot --- frappe/translate.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/frappe/translate.py b/frappe/translate.py index cdcaa31920..62ee733f5f 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -109,6 +109,13 @@ def get_dict(fortype, name=None): elif fortype=="jsfile": messages = get_messages_from_file(name) elif fortype=="boot": + messages = [] + apps = frappe.get_all_apps(True) + for app in apps: + messages.extend(get_server_messages(app)) + messages = deduplicate_messages(messages) + + messages += frappe.db.sql("""select "navbar", item_label from `tabNavbar Item` where item_label is not null""") messages = get_messages_from_include_files() messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`") messages += frappe.db.sql("select 'DocType:', name from tabDocType") From 2133777c4449532263f824c39c2d57452bf63b06 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 15 Apr 2021 07:46:39 +0530 Subject: [PATCH 010/112] test: Execute enabled webhook trigger --- .../integrations/doctype/webhook/__init__.py | 2 +- .../doctype/webhook/test_webhook.py | 65 +++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/webhook/__init__.py b/frappe/integrations/doctype/webhook/__init__.py index 0524cd609b..19233bd175 100644 --- a/frappe/integrations/doctype/webhook/__init__.py +++ b/frappe/integrations/doctype/webhook/__init__.py @@ -22,7 +22,7 @@ def run_webhooks(doc, method): # query webhooks webhooks_list = frappe.get_all('Webhook', fields=["name", "`condition`", "webhook_docevent", "webhook_doctype"], - filters={"enabled": "1"} + filters={"enabled": True} ) # make webhooks map for cache diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index fa7f9534e1..6c6e4c1a4d 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -10,6 +10,44 @@ from frappe.integrations.doctype.webhook.webhook import get_webhook_headers, get class TestWebhook(unittest.TestCase): + @classmethod + def setUpClass(cls): + # delete any existing webhooks + frappe.db.sql("DELETE FROM tabWebhook") + # create test webhooks + cls.create_sample_webhooks() + + @classmethod + def create_sample_webhooks(cls): + samples_webhooks_data = [ + { + "webhook_doctype": "User", + "webhook_docevent": "after_insert", + "request_url": "https://httpbin.org/post", + "condition": "doc.email", + "enabled": True + }, + { + "webhook_doctype": "User", + "webhook_docevent": "after_insert", + "request_url": "https://httpbin.org/post", + "condition": "doc.first_name", + "enabled": False + } + ] + + cls.sample_webhooks = [] + for wh_fields in samples_webhooks_data: + wh = frappe.new_doc("Webhook") + wh.update(wh_fields) + wh.insert() + cls.sample_webhooks.append(wh) + + @classmethod + def tearDownClass(cls): + # delete any existing webhooks + frappe.db.sql("DELETE FROM tabWebhook") + def setUp(self): # retrieve or create a User webhook for `after_insert` webhook_fields = { @@ -30,10 +68,37 @@ class TestWebhook(unittest.TestCase): self.user.email = frappe.mock("email") self.user.save() + # Create another test user specific to this test + self.test_user = frappe.new_doc("User") + self.test_user.email = "user1@integration.webhooks.test.com" + self.test_user.first_name = "user1" + def tearDown(self) -> None: self.user.delete() + self.test_user.delete() super().tearDown() + def test_webhook_trigger_with_enabled_webhooks(self): + """Test webhook trigger for enabled webhooks""" + + frappe.cache().delete_value('webhooks') + frappe.flags.webhooks = None + + # Insert the user to db + self.test_user.insert() + + self.assertTrue("User" in frappe.flags.webhooks) + # only 1 hook (enabled) must be queued + self.assertEqual( + len(frappe.flags.webhooks.get("User")), + 1 + ) + self.assertTrue(self.test_user.email in frappe.flags.webhooks_executed) + self.assertEqual( + frappe.flags.webhooks_executed.get(self.test_user.email)[0], + self.sample_webhooks[0].name + ) + def test_validate_doc_events(self): "Test creating a submit-related webhook for a non-submittable DocType" From 38ca6fe621a03ba4fa33aefef677c9711f46d945 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 15 Apr 2021 10:23:09 +0530 Subject: [PATCH 011/112] style: Remove extra space --- frappe/integrations/doctype/webhook/test_webhook.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/webhook/test_webhook.py b/frappe/integrations/doctype/webhook/test_webhook.py index 6c6e4c1a4d..acf2f609e7 100644 --- a/frappe/integrations/doctype/webhook/test_webhook.py +++ b/frappe/integrations/doctype/webhook/test_webhook.py @@ -38,7 +38,7 @@ class TestWebhook(unittest.TestCase): cls.sample_webhooks = [] for wh_fields in samples_webhooks_data: - wh = frappe.new_doc("Webhook") + wh = frappe.new_doc("Webhook") wh.update(wh_fields) wh.insert() cls.sample_webhooks.append(wh) From 9830406afa14f94be65d6e3c7c506e0d461857ab Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 11:55:48 +0530 Subject: [PATCH 012/112] test: Add test to check group mentions --- frappe/core/doctype/user/test_user.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index 5b16c72775..deea241617 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -229,6 +229,28 @@ class TestUser(unittest.TestCase): self.assertEqual(extract_mentions(comment)[0], "test_user@example.com") self.assertEqual(extract_mentions(comment)[1], "test.again@example1.com") + doc = frappe.get_doc({ + 'doctype': 'User Group', + 'group_name': 'Team', + 'user_group_members': [{ + 'user': 'test@example.com' + }, { + 'user': 'test1@example.com' + }] + }) + doc.insert(ignore_if_duplicate=True) + + comment = ''' +
+ Testing comment for + + @Team + + please check +
+ ''' + self.assertListEqual(extract_mentions(comment), ['test@example.com', 'test1@example.com']) + def test_rate_limiting_for_reset_password(self): # Allow only one reset request for a day frappe.db.set_value("System Settings", "System Settings", "password_reset_limit", 1) From ea2a2772f287e6038b3895e6b15d83a9a06bc248 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 15 Apr 2021 12:39:15 +0530 Subject: [PATCH 013/112] test: which email Id to pick for sending emails (#12550) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/email/test_smtp.py | 55 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py index 869d708430..0b11c559a2 100644 --- a/frappe/email/test_smtp.py +++ b/frappe/email/test_smtp.py @@ -2,7 +2,9 @@ # License: The MIT License import unittest +import frappe from frappe.email.smtp import SMTPServer +from frappe.email.smtp import get_outgoing_email_account class TestSMTP(unittest.TestCase): def test_smtp_ssl_session(self): @@ -13,6 +15,57 @@ class TestSMTP(unittest.TestCase): for port in [None, 0, 587, "587"]: make_server(port, 0, 1) + def test_get_email_account(self): + existing_email_accounts = frappe.get_all("Email Account", fields = ["name", "enable_outgoing", "default_outgoing", "append_to"]) + unset_details = { + "enable_outgoing": 0, + "default_outgoing": 0, + "append_to": None + } + for email_account in existing_email_accounts: + frappe.db.set_value('Email Account', email_account['name'], unset_details) + + # remove mail_server config so that test@example.com is not created + mail_server = frappe.conf.get('mail_server') + del frappe.conf['mail_server'] + + frappe.local.outgoing_email_account = {} + + frappe.local.outgoing_email_account = {} + # lowest preference given to email account with default incoming enabled + create_email_account(email_id="default_outgoing_enabled@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1) + self.assertEqual(get_outgoing_email_account().email_id, "default_outgoing_enabled@gmail.com") + + frappe.local.outgoing_email_account = {} + # highest preference given to email account with append_to matching + create_email_account(email_id="append_to@gmail.com", password="***", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post") + self.assertEqual(get_outgoing_email_account(append_to="Blog Post").email_id, "append_to@gmail.com") + + # add back the mail_server + frappe.conf['mail_server'] = mail_server + for email_account in existing_email_accounts: + set_details = { + "enable_outgoing": email_account['enable_outgoing'], + "default_outgoing": email_account['default_outgoing'], + "append_to": email_account['append_to'] + } + frappe.db.set_value('Email Account', email_account['name'], set_details) + +def create_email_account(email_id, password, enable_outgoing, default_outgoing=0, append_to=None): + email_dict = { + "email_id": email_id, + "passsword": password, + "enable_outgoing":enable_outgoing , + "default_outgoing":default_outgoing , + "enable_incoming": 1, + "append_to":append_to, + "is_dummy_password": 1, + "smtp_server": "localhost" + } + + email_account = frappe.new_doc('Email Account') + email_account.update(email_dict) + email_account.save() def make_server(port, ssl, tls): server = SMTPServer( @@ -22,4 +75,4 @@ def make_server(port, ssl, tls): use_tls = tls ) - server.sess \ No newline at end of file + server.sess From c7c2362f9c7d007d07b927223663f551e557f03e Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 15 Apr 2021 12:44:36 +0530 Subject: [PATCH 014/112] fix: Redirect Web Form user directly to success URL, if no amount is due (backport #12661) (#12856) If there is no amount due when using WebForms which accept payments, redirect the user directly to the success URL. This is useful for free items. (cherry picked from commit f8f1301b796180bca6b860bde5e01fa85a632c1f) Co-authored-by: ci2014 --- frappe/website/doctype/web_form/web_form.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/website/doctype/web_form/web_form.py b/frappe/website/doctype/web_form/web_form.py index 49cbad5658..f78aaac934 100644 --- a/frappe/website/doctype/web_form/web_form.py +++ b/frappe/website/doctype/web_form/web_form.py @@ -215,6 +215,11 @@ def get_context(context): amount = self.amount if self.amount_based_on_field: amount = doc.get(self.amount_field) + + from decimal import Decimal + if amount is None or Decimal(amount) <= 0: + return frappe.utils.get_url(self.success_url or self.route) + payment_details = { "amount": amount, "title": title, From 219b60a6b101664865925ab5b046a9d5364da462 Mon Sep 17 00:00:00 2001 From: leela Date: Thu, 15 Apr 2021 13:10:16 +0530 Subject: [PATCH 015/112] fix: add field type change check in custom field validation We have fieldtype change check in customizeform but not in custom field. Added the same for customfield. --- .../doctype/custom_field/custom_field.py | 8 ++++- .../doctype/customize_form/customize_form.py | 34 +++++++++++-------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index ee6e3b9c61..3126326636 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -40,6 +40,8 @@ class CustomField(Document): frappe.throw(_("A field with the name '{}' already exists in doctype {}.").format(self.fieldname, self.dt)) def validate(self): + from frappe.custom.doctype.customize_form.customize_form import CustomizeForm + meta = frappe.get_meta(self.dt, cached=False) fieldnames = [df.fieldname for df in meta.get("fields")] @@ -49,7 +51,11 @@ class CustomField(Document): if self.insert_after and self.insert_after in fieldnames: self.idx = fieldnames.index(self.insert_after) + 1 - self._old_fieldtype = self.db_get('fieldtype') + old_fieldtype = self.db_get('fieldtype') + is_fieldtype_changed = (not self.is_new()) and (old_fieldtype != self.fieldtype) + + if is_fieldtype_changed and not CustomizeForm.allow_fieldtype_change(old_fieldtype, self.fieldtype): + frappe.throw(_("Fieldtype cannot be changed from {0} to {1}").format(old_fieldtype, self.fieldtype)) if not self.fieldname: frappe.throw(_("Fieldname not set for Custom Field")) diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index c79c965aae..9f6996a660 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -401,22 +401,18 @@ class CustomizeForm(Document): return property_value def validate_fieldtype_change(self, df, old_value, new_value): - allowed = False - self.check_length_for_fieldtypes = [] - for allowed_changes in ALLOWED_FIELDTYPE_CHANGE: - if (old_value in allowed_changes and new_value in allowed_changes): - allowed = True - old_value_length = cint(frappe.db.type_map.get(old_value)[1]) - new_value_length = cint(frappe.db.type_map.get(new_value)[1]) + allowed = self.allow_fieldtype_change(old_value, new_value) + if allowed: + old_value_length = cint(frappe.db.type_map.get(old_value)[1]) + new_value_length = cint(frappe.db.type_map.get(new_value)[1]) - # Ignore fieldtype check validation if new field type has unspecified maxlength - # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated - if new_value_length and (old_value_length > new_value_length): - self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value}) - self.validate_fieldtype_length() - else: - self.flags.update_db = True - break + # Ignore fieldtype check validation if new field type has unspecified maxlength + # Changes like DATA to TEXT, where new_value_lenth equals 0 will not be validated + if new_value_length and (old_value_length > new_value_length): + self.check_length_for_fieldtypes.append({'df': df, 'old_value': old_value}) + self.validate_fieldtype_length() + else: + self.flags.update_db = True if not allowed: frappe.throw(_("Fieldtype cannot be changed from {0} to {1} in row {2}").format(old_value, new_value, df.idx)) @@ -458,6 +454,14 @@ class CustomizeForm(Document): reset_customization(self.doc_type) self.fetch_to_customize() + @classmethod + def allow_fieldtype_change(self, old_type: str, new_type: str) -> bool: + """ allow type change, if both old_type and new_type are in same field group. + field groups are defined in ALLOWED_FIELDTYPE_CHANGE variables. + """ + in_field_group = lambda group: (old_type in group) and (new_type in group) + return any(map(in_field_group, ALLOWED_FIELDTYPE_CHANGE)) + def reset_customization(doctype): setters = frappe.get_all("Property Setter", filters={ 'doc_type': doctype, From 6d20983eb27424505e0874bf317b0d586fdbcdb8 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 15 Apr 2021 14:15:28 +0530 Subject: [PATCH 016/112] fix(minor): Make language select optional and fix breakpoint issues in website --- frappe/public/scss/website/index.scss | 7 +++ frappe/public/scss/website/navbar.scss | 14 ++++- frappe/templates/base.html | 36 +------------ frappe/templates/includes/navbar/navbar.html | 4 +- .../website_settings/website_settings.json | 21 ++++++-- .../website_settings/website_settings.py | 7 +-- frappe/website/js/website.js | 53 ++++++++++++++----- 7 files changed, 85 insertions(+), 57 deletions(-) diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 1fb5badc6c..823ec9b08a 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -90,6 +90,13 @@ margin: 2rem 0; } +@media (max-width: map-get($grid-breakpoints, "lg")) { + .page-content-wrapper .container { + padding-left: 1rem; + padding-right: 1rem; + } +} + .breadcrumb-container { margin-top: 1rem; padding-top: 0.25rem; diff --git a/frappe/public/scss/website/navbar.scss b/frappe/public/scss/website/navbar.scss index 4d2ccfece9..3496a8907c 100644 --- a/frappe/public/scss/website/navbar.scss +++ b/frappe/public/scss/website/navbar.scss @@ -1,3 +1,15 @@ +.navbar { + padding-left: 0; + padding-right: 0; +} + +@media (max-width: map-get($grid-breakpoints, "lg")) { + .navbar { + padding-left: 1rem; + padding-right: 1rem; + } +} + .navbar-light { border-bottom: 1px solid $border-color; background: $navbar-bg; @@ -96,4 +108,4 @@ @extend .ellipsis; max-width: 100%; vertical-align: middle; -} \ No newline at end of file +} diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 18c9e9d99a..78aa573c99 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -56,6 +56,8 @@ } window.dev_server = {{ dev_server }}; window.socketio_port = {{ (frappe.socketio_port or 'null') }}; + window.show_language_picker = {{ show_language_picker }}; + window.is_chat_enabled = {{ chat_enable }}; @@ -110,39 +112,5 @@ {%- endblock %} {%- block body_include %}{{ body_include or "" }}{% endblock -%} - diff --git a/frappe/templates/includes/navbar/navbar.html b/frappe/templates/includes/navbar/navbar.html index 7856413602..1fb4ae9fb0 100644 --- a/frappe/templates/includes/navbar/navbar.html +++ b/frappe/templates/includes/navbar/navbar.html @@ -21,8 +21,8 @@ -
- +
+
diff --git a/frappe/website/doctype/website_settings/website_settings.json b/frappe/website/doctype/website_settings/website_settings.json index 3ca02e2a37..9e04cf3795 100644 --- a/frappe/website/doctype/website_settings/website_settings.json +++ b/frappe/website/doctype/website_settings/website_settings.json @@ -25,9 +25,11 @@ "set_banner_from_image", "favicon", "top_bar", - "navbar_search", - "hide_login", "top_bar_items", + "hide_login", + "navbar_search", + "show_language_picker", + "navbar_template_section", "navbar_template", "navbar_template_values", "edit_navbar_template_values", @@ -410,6 +412,19 @@ "fieldname": "google_analytics_anonymize_ip", "fieldtype": "Check", "label": "Google Analytics Anonymize IP" + }, + { + "default": "0", + "fieldname": "show_language_picker", + "fieldtype": "Check", + "label": "Show Language Picker" + }, + { + "collapsible": 1, + "collapsible_depends_on": "navbar_template", + "fieldname": "navbar_template_section", + "fieldtype": "Section Break", + "label": "Navbar Template" } ], "icon": "fa fa-cog", @@ -418,7 +433,7 @@ "issingle": 1, "links": [], "max_attachments": 10, - "modified": "2021-04-13 10:22:51.888788", + "modified": "2021-04-14 17:39:56.609771", "modified_by": "Administrator", "module": "Website", "name": "Website Settings", diff --git a/frappe/website/doctype/website_settings/website_settings.py b/frappe/website/doctype/website_settings/website_settings.py index 89def9bf8d..f7f22aa2df 100644 --- a/frappe/website/doctype/website_settings/website_settings.py +++ b/frappe/website/doctype/website_settings/website_settings.py @@ -121,7 +121,8 @@ def get_website_settings(context=None): "facebook_share", "google_plus_one", "twitter_share", "linked_in_share", "disable_signup", "hide_footer_signup", "head_html", "title_prefix", "navbar_template", "footer_template", "navbar_search", "enable_view_tracking", - "footer_logo", "call_to_action", "call_to_action_url"]: + "footer_logo", "call_to_action", "call_to_action_url", "show_language_picker", + "chat_enable"]: if hasattr(settings, k): context[k] = settings.get(k) @@ -178,7 +179,3 @@ def get_items(parentfield): t['child_items'].append(d) break return top_items - -@frappe.whitelist(allow_guest=True) -def is_chat_enabled(): - return bool(frappe.db.get_single_value('Website Settings', 'chat_enable')) diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js index b8360e68ca..99d3fbf650 100644 --- a/frappe/website/js/website.js +++ b/frappe/website/js/website.js @@ -376,6 +376,39 @@ $.extend(frappe, { // Start observing an element io.observe(el); }); + }, + show_language_picker() { + if (frappe.session.user === 'Guest' && window.show_language_picker) { + frappe.call("frappe.translate.get_all_languages", { + with_language_name: true + }).then(res => { + let language_list = res.message; + let language = frappe.get_cookie('preferred_language'); + let language_codes = []; + let language_switcher = $("#language-switcher .form-control") + language_list.forEach(language_doc => { + language_codes.push(language_doc.language_code) + language_switcher + .append( + $("") + .attr("value", language_doc.language_code) + .text(language_doc.language_name) + ); + }); + $("#language-switcher").removeClass('hide'); + language = language || (language_codes.includes(navigator.language) ? navigator.language : 'en'); + language_switcher.val(language); + document.documentElement.lang = language; + language_switcher.change((e) => { + let lang = language_switcher.val(); + frappe.call("frappe.translate.set_preferred_language_cookie", { + "preferred_language": lang + }).then(() => { + window.location.reload(); + }); + }); + }); + } } }); @@ -599,17 +632,13 @@ $(document).on("page-change", function() { frappe.ready(function() { - frappe.call({ - method: 'frappe.website.doctype.website_settings.website_settings.is_chat_enabled', - callback: (r) => { - if (r.message) { - frappe.require(['/assets/js/moment-bundle.min.js', "/assets/css/frappe-chat-web.css", "/assets/frappe/js/lib/socket.io.min.js"], () => { - frappe.require('/assets/js/chat.js', () => { - frappe.chat.setup(); - }); - }); - } - } - }); + frappe.show_language_picker(); + if (window.is_chat_enabled) { + frappe.require(['/assets/js/moment-bundle.min.js', "/assets/css/frappe-chat-web.css", "/assets/frappe/js/lib/socket.io.min.js"], () => { + frappe.require('/assets/js/chat.js', () => { + frappe.chat.setup(); + }); + }); + } frappe.socketio.init(window.socketio_port); }); From 8a5e8b53e8f10aa8acb8b4a2d5f89c9a7a7a647b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 14:22:52 +0530 Subject: [PATCH 017/112] fix: Make group_name and user group members mandatory --- frappe/core/doctype/user/user.py | 7 +++++-- frappe/core/doctype/user_group/user_group.json | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 035333cbe9..b9889026fe 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1019,8 +1019,11 @@ def extract_mentions(txt): emails = [] for mention in soup.find_all(class_='mention'): if mention.get('data-is-group'): - user_group = frappe.get_cached_doc('User Group', mention['data-id']) - emails += [d.user for d in user_group.user_group_members] + try: + user_group = frappe.get_cached_doc('User Group', mention['data-id']) + emails += [d.user for d in user_group.user_group_members] + except frappe.DoesNotExistError: + pass continue email = mention['data-id'] emails.append(email) diff --git a/frappe/core/doctype/user_group/user_group.json b/frappe/core/doctype/user_group/user_group.json index 3d859dcc9a..64f2801e15 100644 --- a/frappe/core/doctype/user_group/user_group.json +++ b/frappe/core/doctype/user_group/user_group.json @@ -13,19 +13,22 @@ { "fieldname": "group_name", "fieldtype": "Data", + "in_list_view": 1, "label": "Group Name", + "reqd": 1, "unique": 1 }, { "fieldname": "user_group_members", "fieldtype": "Table MultiSelect", "label": "User Group Members", - "options": "User Group Member" + "options": "User Group Member", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-04-12 15:17:24.751710", + "modified": "2021-04-15 12:17:04.625640", "modified_by": "Administrator", "module": "Core", "name": "User Group", From 4f8ced5b1b2f1d8cabddd8d4d51e60d4df067b4b Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 14:27:41 +0530 Subject: [PATCH 018/112] fix: Remove unnecessary field --- frappe/core/doctype/user/test_user.py | 2 +- frappe/core/doctype/user_group/user_group.json | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index deea241617..5bea767934 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -231,7 +231,7 @@ class TestUser(unittest.TestCase): doc = frappe.get_doc({ 'doctype': 'User Group', - 'group_name': 'Team', + 'name': 'Team', 'user_group_members': [{ 'user': 'test@example.com' }, { diff --git a/frappe/core/doctype/user_group/user_group.json b/frappe/core/doctype/user_group/user_group.json index 64f2801e15..21d74ccddf 100644 --- a/frappe/core/doctype/user_group/user_group.json +++ b/frappe/core/doctype/user_group/user_group.json @@ -1,23 +1,14 @@ { "actions": [], - "autoname": "field:group_name", + "autoname": "Prompt", "creation": "2021-04-12 15:17:24.751710", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "group_name", "user_group_members" ], "fields": [ - { - "fieldname": "group_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Group Name", - "reqd": 1, - "unique": 1 - }, { "fieldname": "user_group_members", "fieldtype": "Table MultiSelect", @@ -28,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-04-15 12:17:04.625640", + "modified": "2021-04-15 14:24:23.168485", "modified_by": "Administrator", "module": "Core", "name": "User Group", From 27ed3dd37303c1678f006b92a006e269900786d7 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 15 Apr 2021 14:31:42 +0530 Subject: [PATCH 019/112] fix(minor): linting --- frappe/website/js/website.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/website/js/website.js b/frappe/website/js/website.js index 99d3fbf650..ea0b9aedfa 100644 --- a/frappe/website/js/website.js +++ b/frappe/website/js/website.js @@ -385,9 +385,9 @@ $.extend(frappe, { let language_list = res.message; let language = frappe.get_cookie('preferred_language'); let language_codes = []; - let language_switcher = $("#language-switcher .form-control") + let language_switcher = $("#language-switcher .form-control"); language_list.forEach(language_doc => { - language_codes.push(language_doc.language_code) + language_codes.push(language_doc.language_code); language_switcher .append( $("") @@ -399,7 +399,7 @@ $.extend(frappe, { language = language || (language_codes.includes(navigator.language) ? navigator.language : 'en'); language_switcher.val(language); document.documentElement.lang = language; - language_switcher.change((e) => { + language_switcher.change(() => { let lang = language_switcher.val(); frappe.call("frappe.translate.set_preferred_language_cookie", { "preferred_language": lang From 8678c09e910b193fa9b1239fae75373bed62a1c8 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 15 Apr 2021 14:34:23 +0530 Subject: [PATCH 020/112] feat: Add log_error and FrappeClient to restricted python (#12857) --- frappe/utils/safe_exec.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index 29bded6fc8..6c1fa21685 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -12,6 +12,7 @@ from frappe.modules import scrub from frappe.www.printview import get_visible_columns import frappe.exceptions import frappe.integrations.utils +from frappe.frappeclient import FrappeClient class ServerScriptNotEnabled(frappe.PermissionError): pass @@ -104,8 +105,10 @@ def get_safe_globals(): make_post_request = frappe.integrations.utils.make_post_request, socketio_port=frappe.conf.socketio_port, get_hooks=frappe.get_hooks, - sanitize_html=frappe.utils.sanitize_html + sanitize_html=frappe.utils.sanitize_html, + log_error=frappe.log_error ), + FrappeClient=FrappeClient, style=frappe._dict( border_color='#d1d8dd' ), @@ -297,4 +300,4 @@ VALID_UTILS = ( "formatdate", "get_user_info_for_avatar", "get_abbr" -) \ No newline at end of file +) From f71a6c09f503bac8028f16eccd6fd2016c7d12be Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 14:53:01 +0530 Subject: [PATCH 021/112] fix: Sync user groups realtime --- frappe/core/doctype/user_group/user_group.py | 7 ++++++- frappe/public/js/frappe/desk.js | 13 +++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py index 6fc6db4620..64bffa06d0 100644 --- a/frappe/core/doctype/user_group/user_group.py +++ b/frappe/core/doctype/user_group/user_group.py @@ -5,6 +5,11 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document +import frappe class UserGroup(Document): - pass + def after_insert(self): + frappe.publish_realtime('user_group_added', self.name) + + def on_trash(self): + frappe.publish_realtime('user_group_deleted', self.name) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 250d308b7e..6ceac48a8c 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -113,7 +113,7 @@ frappe.Application = Class.extend({ dialog.get_close_btn().toggle(false); }); - this.setup_social_listeners(); + this.setup_user_group_listeners(); // listen to build errors this.setup_build_error_listener(); @@ -592,11 +592,12 @@ frappe.Application = Class.extend({ } }, - setup_social_listeners() { - frappe.realtime.on('mention', (message) => { - if (frappe.get_route()[0] !== 'social') { - frappe.show_alert(message); - } + setup_user_group_listeners() { + frappe.realtime.on('user_group_added', (user_group) => { + frappe.boot.user_groups && frappe.boot.user_groups.push(user_group); + }); + frappe.realtime.on('user_group_deleted', (user_group) => { + frappe.boot.user_groups = (frappe.boot.user_groups || []).filter(el => el !== user_group); }); }, From b2649eda1ee350c957d4899672ea40c12e817203 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 16:07:03 +0530 Subject: [PATCH 022/112] fix: Use css variables for mention colors --- frappe/public/scss/common/quill.scss | 6 +++--- frappe/public/scss/desk/css_variables.scss | 4 ++++ frappe/public/scss/desk/dark.scss | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index c4011e2d6b..ce4a10d190 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -119,7 +119,7 @@ border: 1px solid var(--border-color); padding: 2px 5px; font-size: var(--text-sm); - background-color: var(--fg-color); + background-color: var(--user-mention-bg-color); } // table @@ -174,7 +174,7 @@ .ql-editor.read-mode { padding: 0; .mention { - background-color: var(--control-bg); + --user-mention-bg-color: var(--control-bg); } } @@ -193,5 +193,5 @@ } .mention[data-is-group="true"] { - background-color: var(--purple-100) !important; + background-color: var(--group-mention-bg-color); } diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss index 21b4ac6c1d..5aca23a0b0 100644 --- a/frappe/public/scss/desk/css_variables.scss +++ b/frappe/public/scss/desk/css_variables.scss @@ -59,6 +59,10 @@ $input-height: 28px !default; --timeline-content-max-width: 700px; --timeline-left-padding: calc(var(--padding-xl) + var(--timeline-item-icon-size) / 2); + // mentions + --user-mention-bg-color: var(--fg-color); + --group-mention-bg-color: var(--bg-purple); + // skeleton --skeleton-bg: var(--gray-100); diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index 743107af47..5817e33ca0 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -99,7 +99,7 @@ .ql-editor { color: var(--text-on-gray); &.read-mode { - span, + span:not(.mention), p, u, strong { From 1bfeafca5f862eeeb0848357e0fbae567483796d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 16:08:18 +0530 Subject: [PATCH 023/112] fix: Check if `data-is-group` should be `true` before getting doc --- frappe/core/doctype/user/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index b9889026fe..0462de8643 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1018,7 +1018,7 @@ def extract_mentions(txt): soup = BeautifulSoup(txt, 'html.parser') emails = [] for mention in soup.find_all(class_='mention'): - if mention.get('data-is-group'): + if mention.get('data-is-group') == 'true': try: user_group = frappe.get_cached_doc('User Group', mention['data-id']) emails += [d.user for d in user_group.user_group_members] From 7d335a9beffef41d736bd7d92057b7b66e1fb655 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 16:22:17 +0530 Subject: [PATCH 024/112] fix: Make group mention clickable --- frappe/core/doctype/user_group/user_group.json | 6 +++++- frappe/public/js/frappe/utils/utils.js | 3 ++- frappe/public/scss/common/quill.scss | 3 +++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user_group/user_group.json b/frappe/core/doctype/user_group/user_group.json index 21d74ccddf..e807372061 100644 --- a/frappe/core/doctype/user_group/user_group.json +++ b/frappe/core/doctype/user_group/user_group.json @@ -19,7 +19,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-04-15 14:24:23.168485", + "modified": "2021-04-15 16:12:31.455401", "modified_by": "Administrator", "module": "Core", "name": "User Group", @@ -36,6 +36,10 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "read": 1, + "role": "All" } ], "sort_field": "modified", diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index e1e4f437cd..7ce30a525c 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1290,7 +1290,8 @@ Object.assign(frappe.utils, { names_for_mentions.push({ id: group, value: group, - is_group: true + is_group: true, + link: frappe.utils.get_form_link('User Group', group) }); }); diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index ce4a10d190..d15ca7e036 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -120,6 +120,9 @@ padding: 2px 5px; font-size: var(--text-sm); background-color: var(--user-mention-bg-color); + a[href] { + text-decoration: none; + } } // table From ccadda21d501867f8a2be1a6457eca3912dcc949 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 15 Apr 2021 16:33:07 +0530 Subject: [PATCH 025/112] fix: Multi-column paste in grid (#12861) --- frappe/public/js/frappe/form/controls/table.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 075608aa8c..c40f471939 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -45,9 +45,12 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ } else { // no column header, map to the existing visible columns const visible_columns = grid_rows[0].get_visible_columns(); + let target_column_matched = false; visible_columns.forEach(column => { - if (column.fieldname === $(e.target).data('fieldname')) { + // consider all columns after the target column. + if (target_column_matched || column.fieldname === $(e.target).data('fieldname')) { fieldnames.push(column.fieldname); + target_column_matched = true; } }); } From d216acaaaec3e872fc78bf1e4ef5c6d5f9bb9ce1 Mon Sep 17 00:00:00 2001 From: walstanb Date: Thu, 15 Apr 2021 19:28:47 +0530 Subject: [PATCH 026/112] fix: attachment pill lock icon redirects to File --- .../public/js/frappe/form/sidebar/attachments.js | 16 +++------------- .../js/frappe/form/templates/attachment.html | 10 ---------- 2 files changed, 3 insertions(+), 23 deletions(-) delete mode 100644 frappe/public/js/frappe/form/templates/attachment.html diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index 9e1ea30c6e..ffd0b513a2 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -1,8 +1,6 @@ // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // MIT License. See license.txt - - frappe.ui.form.Attachments = Class.extend({ init: function(opts) { $.extend(this, opts); @@ -84,17 +82,9 @@ frappe.ui.form.Attachments = Class.extend({ }; } - let icon; - // REDESIGN-TODO: set icon using frappe.utils.icon - if (attachment.is_private) { - icon = `
- -
`; - } else { - icon = `
- -
`; - } + const icon = ` + ${frappe.utils.icon(attachment.is_private ? 'lock' : 'unlock', 'sm ml-0')} + `; $(`
  • `) .append(frappe.get_data_pill( diff --git a/frappe/public/js/frappe/form/templates/attachment.html b/frappe/public/js/frappe/form/templates/attachment.html deleted file mode 100644 index c1fe3f3c85..0000000000 --- a/frappe/public/js/frappe/form/templates/attachment.html +++ /dev/null @@ -1,10 +0,0 @@ -
  • - × - - - - - {{ file_name }} - -
  • - From f9742944843965d71e050b9f676e87f9b3e2d9f7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 15 Apr 2021 22:12:48 +0530 Subject: [PATCH 027/112] feat: Copy DocType / Documents without boundaries --- frappe/public/js/frappe/desk.js | 31 +++++++++++++++++++++++++ frappe/public/js/frappe/form/toolbar.js | 7 +++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 250d308b7e..c6190230e4 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -51,6 +51,7 @@ frappe.Application = Class.extend({ this.set_fullwidth_if_enabled(); this.add_browser_class(); this.setup_energy_point_listeners(); + this.copy_doc_listeners(); frappe.ui.keys.setup(); @@ -605,6 +606,36 @@ frappe.Application = Class.extend({ frappe.show_alert(message); }); }, + + copy_doc_listeners() { + $('body').on('paste', (e) => { + try { + let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; + let pasted_data = clipboard_data.getData('Text'); + let doc = JSON.parse(pasted_data); + if (doc.doctype) { + e.preventDefault(); + let sleep = (time) => { + return new Promise((resolve) => setTimeout(resolve, time)); + }; + frappe.dom.freeze(__('Copying {0}', [doc.doctype])); + + sleep(500).then(() => { + frappe.model.with_doctype(doc.doctype, () => { + let newdoc = frappe.model.copy_doc(doc); + newdoc.__newname = doc.name; + newdoc.idx = null; + newdoc.__run_link_triggers = false; + frappe.set_route('Form', newdoc.doctype, newdoc.name); + frappe.dom.unfreeze(); + }); + }); + } + } catch (e) { + // + } + }); + } }); frappe.get_module = function(m, default_module) { diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 2f5b84fb1a..080ff504f0 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -278,13 +278,18 @@ frappe.ui.form.Toolbar = class Toolbar { }, true) } - // copy + // duplicate if(in_list(frappe.boot.user.can_create, me.frm.doctype) && !me.frm.meta.allow_copy) { this.page.add_menu_item(__("Duplicate"), function() { me.frm.copy_doc(); }, true); } + // copy doc to clipboard + this.page.add_menu_item(__("Copy {0}", [me.frm.doc.doctype]), function() { + frappe.utils.copy_to_clipboard(JSON.stringify(me.frm.doc)); + }, true); + // rename if(this.can_rename()) { this.page.add_menu_item(__("Rename"), function() { From de7087722871b18c2fa56d680638b9b5a6e981a0 Mon Sep 17 00:00:00 2001 From: nikhilponnuru Date: Thu, 15 Apr 2021 21:08:02 +0530 Subject: [PATCH 028/112] fix: Newly created Workspace not being accessible unless a shortcut under Shortcuts is created --- frappe/desk/desktop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 5b6e2fdd21..d1b5e27a2f 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -63,7 +63,7 @@ class Workspace: for section in cards: links = loads(section.get('links')) if isinstance(section.get('links'), string_types) else section.get('links') for item in links: - if self.is_item_allowed(item.get('name'), item.get('type')): + if self.is_item_allowed(item.get('link_to'), item.get('link_type')): return True def _in_active_domains(item): From 2bbe4e6461f7e59563c9508e268e24ccdb1f4f72 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 16 Apr 2021 20:53:27 +0530 Subject: [PATCH 029/112] refactor: Use better naming --- frappe/public/js/frappe/desk.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index c6190230e4..82987a5bb2 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -51,7 +51,7 @@ frappe.Application = Class.extend({ this.set_fullwidth_if_enabled(); this.add_browser_class(); this.setup_energy_point_listeners(); - this.copy_doc_listeners(); + this.setup_copy_doc_listener(); frappe.ui.keys.setup(); @@ -607,7 +607,7 @@ frappe.Application = Class.extend({ }); }, - copy_doc_listeners() { + setup_copy_doc_listener() { $('body').on('paste', (e) => { try { let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; From 7e3b930138f1ac974192871b178c8b198e633d3f Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 16 Apr 2021 22:14:41 +0530 Subject: [PATCH 030/112] fix: Update labels --- frappe/public/js/frappe/desk.js | 7 +++++-- frappe/public/js/frappe/form/toolbar.js | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 82987a5bb2..5b95421e1e 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -618,10 +618,12 @@ frappe.Application = Class.extend({ let sleep = (time) => { return new Promise((resolve) => setTimeout(resolve, time)); }; - frappe.dom.freeze(__('Copying {0}', [doc.doctype])); + frappe.dom.freeze(__('Creating {0}', [doc.doctype]) + '...'); + // to avoid abrupt UX + // wait for activity feedback sleep(500).then(() => { - frappe.model.with_doctype(doc.doctype, () => { + let res = frappe.model.with_doctype(doc.doctype, () => { let newdoc = frappe.model.copy_doc(doc); newdoc.__newname = doc.name; newdoc.idx = null; @@ -629,6 +631,7 @@ frappe.Application = Class.extend({ frappe.set_route('Form', newdoc.doctype, newdoc.name); frappe.dom.unfreeze(); }); + res && res.fail(frappe.dom.unfreeze); }); } } catch (e) { diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 080ff504f0..145b8d3eed 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -286,7 +286,7 @@ frappe.ui.form.Toolbar = class Toolbar { } // copy doc to clipboard - this.page.add_menu_item(__("Copy {0}", [me.frm.doc.doctype]), function() { + this.page.add_menu_item(__("Copy to Clipboard"), function() { frappe.utils.copy_to_clipboard(JSON.stringify(me.frm.doc)); }, true); From eb45e8775a3d49ccc60c95aca27df0ee550bd7ce Mon Sep 17 00:00:00 2001 From: walstanb Date: Sat, 17 Apr 2021 15:20:12 +0530 Subject: [PATCH 031/112] feat(UI): standardardized themed scrollbar --- frappe/public/scss/common/css_variables.scss | 3 ++ frappe/public/scss/desk/dark.scss | 5 ++++ frappe/public/scss/desk/desktop.scss | 21 ++++++++++++++ frappe/public/scss/desk/index.scss | 1 + frappe/public/scss/desk/scrollbar.scss | 29 ++++++++++++++++++++ 5 files changed, 59 insertions(+) create mode 100644 frappe/public/scss/desk/scrollbar.scss diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index 701a0d09e9..8f4af36389 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -169,6 +169,9 @@ // Other Colors --sidebar-select-color: var(--gray-200); + --scrollbar-thumb-color: var(--gray-400); + --scrollbar-track-color: var(--gray-200); + --shadow-inset: inset 0px -1px 0px var(--gray-300); --border-color: var(--gray-100); --dark-border-color: var(--gray-300); diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index 5817e33ca0..4e83f4db47 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -65,6 +65,9 @@ --sidebar-select-color: var(--gray-800); + --scrollbar-thumb-color: var(--gray-600); + --scrollbar-track-color: var(--gray-700); + --shadow-inset: var(--fg-color); --border-color: var(--gray-700); --dark-border-color: var(--gray-600); @@ -75,6 +78,8 @@ // input --input-disabled-bg: none; + color-scheme: dark; + .frappe-card { .btn-default { background-color: var(--bg-color); diff --git a/frappe/public/scss/desk/desktop.scss b/frappe/public/scss/desk/desktop.scss index 1bb91090e6..ac3b1a4f7c 100644 --- a/frappe/public/scss/desk/desktop.scss +++ b/frappe/public/scss/desk/desktop.scss @@ -754,7 +754,28 @@ body { .layout-side-section, .layout-main-section-wrapper { height: 100%; overflow-y: auto; + padding-right: 25px; + scrollbar-color: var(--gray-200) transparent; + [data-theme="dark"] & { + scrollbar-color: var(--gray-800) transparent; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--gray-200); + [data-theme="dark"] & { + background: var(--gray-800); + } + } } + + .layout-side-section { + margin-right: 20px; + } + .desk-sidebar { margin-bottom: var(--margin-2xl); } diff --git a/frappe/public/scss/desk/index.scss b/frappe/public/scss/desk/index.scss index 31eae63776..d0d968df63 100644 --- a/frappe/public/scss/desk/index.scss +++ b/frappe/public/scss/desk/index.scss @@ -10,6 +10,7 @@ @import "mobile"; @import "form"; @import "print_preview"; +@import "scrollbar"; @import "navbar"; @import "../common/modal"; @import "slides"; diff --git a/frappe/public/scss/desk/scrollbar.scss b/frappe/public/scss/desk/scrollbar.scss new file mode 100644 index 0000000000..806ffd13eb --- /dev/null +++ b/frappe/public/scss/desk/scrollbar.scss @@ -0,0 +1,29 @@ +/* Works on Firefox */ +* { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb-color) var(--scrollbar-track-color); +} + +html { + scrollbar-width: auto; +} + +/* Works on Chrome, Edge, and Safari */ +*::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +*::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb-color); +} + +*::-webkit-scrollbar-track, +*::-webkit-scrollbar-corner { + background: var(--scrollbar-track-color); +} + +body::-webkit-scrollbar { + width: unset; + height: unset; +} From eac90c26d1c4ef69566277bd074124f9cc5b5fa8 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 21 Jan 2021 19:06:33 +0100 Subject: [PATCH 032/112] feat: default_email_template --- frappe/core/doctype/doctype/doctype.json | 13 ++++++++++++- frappe/public/js/frappe/views/communication.js | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 276ce7bee7..3333c50f45 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -56,6 +56,8 @@ "show_preview_popup", "show_name_in_global_search", "email_settings_sb", + "default_email_template", + "column_break_51", "email_append_to", "sender_field", "subject_field", @@ -535,6 +537,15 @@ "fieldname": "is_virtual", "fieldtype": "Check", "label": "Is Virtual" + }, + { + "fieldname": "default_email_template", + "fieldtype": "Data", + "label": "Default Email Template" + }, + { + "fieldname": "column_break_51", + "fieldtype": "Column Break" } ], "icon": "fa fa-bolt", @@ -650,4 +661,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 3a4da2a0b4..da08268b60 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -196,6 +196,7 @@ frappe.views.CommunicationComposer = Class.extend({ this.setup_last_edited_communication(); this.setup_email_template(); + this.dialog.set_value("email_template", this.frm.meta.default_email_template || ''); this.dialog.set_value("recipients", this.recipients || ''); this.dialog.set_value("cc", this.cc || ''); this.dialog.set_value("bcc", this.bcc || ''); From 46c5e301851022ae96140ec8c0840b2b0cca6a9d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 27 Jan 2021 18:55:54 +0100 Subject: [PATCH 033/112] feat: add default_email_template to Customize Form --- .../doctype/customize_form/customize_form.json | 13 ++++++++++++- .../custom/doctype/customize_form/customize_form.py | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 77f62b3ec3..22fc56c903 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -33,6 +33,8 @@ "show_preview_popup", "image_view", "email_settings_section", + "default_email_template", + "column_break_26", "email_append_to", "sender_field", "subject_field", @@ -275,6 +277,15 @@ "fieldname": "autoname", "fieldtype": "Data", "label": "Auto Name" + }, + { + "fieldname": "default_email_template", + "fieldtype": "Data", + "label": "Default Email Template" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, @@ -304,4 +315,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 9f6996a660..be0dded99c 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -491,6 +491,7 @@ doctype_properties = { 'allow_auto_repeat': 'Check', 'allow_import': 'Check', 'show_preview_popup': 'Check', + 'default_email_template': 'Data', 'email_append_to': 'Check', 'subject_field': 'Data', 'sender_field': 'Data', From 90819667cfcc87d2bcaec6f7a0a4594c29bfb730 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 27 Jan 2021 18:59:00 +0100 Subject: [PATCH 034/112] fix: check if frm is available Prevents error when creating new Communication from list view. --- frappe/public/js/frappe/views/communication.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index da08268b60..9264d0b057 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -196,7 +196,10 @@ frappe.views.CommunicationComposer = Class.extend({ this.setup_last_edited_communication(); this.setup_email_template(); - this.dialog.set_value("email_template", this.frm.meta.default_email_template || ''); + if ('frm' in this) { + this.dialog.set_value("email_template", this.frm.meta.default_email_template || ''); + } + this.dialog.set_value("recipients", this.recipients || ''); this.dialog.set_value("cc", this.cc || ''); this.dialog.set_value("bcc", this.bcc || ''); From d31ed278d6491e05a4bebc14bab9aa1901093d48 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 27 Jan 2021 18:59:47 +0100 Subject: [PATCH 035/112] fix: check if email_template is set Prevents error on empty email_template. --- frappe/public/js/frappe/views/communication.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 9264d0b057..141f4f4ec0 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -271,6 +271,9 @@ frappe.views.CommunicationComposer = Class.extend({ this.dialog.fields_dict["email_template"].df.onchange = () => { var email_template = me.dialog.fields_dict.email_template.get_value(); + if (email_template === '') { + return; + } var prepend_reply = function(reply) { if(me.reply_added===email_template) { From bde85dcb72beeaccab0b0fcc2b106d2ec54bf190 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 4 Feb 2021 12:20:12 +0100 Subject: [PATCH 036/112] fix: don't apply default email template for reply --- frappe/public/js/frappe/views/communication.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 141f4f4ec0..0c294d5869 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -196,10 +196,6 @@ frappe.views.CommunicationComposer = Class.extend({ this.setup_last_edited_communication(); this.setup_email_template(); - if ('frm' in this) { - this.dialog.set_value("email_template", this.frm.meta.default_email_template || ''); - } - this.dialog.set_value("recipients", this.recipients || ''); this.dialog.set_value("cc", this.cc || ''); this.dialog.set_value("bcc", this.bcc || ''); @@ -212,6 +208,11 @@ frappe.views.CommunicationComposer = Class.extend({ ); this.setup_earlier_reply(); + + if ('frm' in this && !this.is_a_reply) { + // set default email template for the first email in a document + this.dialog.set_value("email_template", this.frm.meta.default_email_template || ''); + } }, setup_subject_and_recipients: function() { From a720efdbd0f2d0dcab3f4df93718f5ba52c56239 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 8 Feb 2021 12:51:47 +0100 Subject: [PATCH 037/112] fix: signature should be an empty string by default (would become undefined if the server message was empty) --- frappe/public/js/frappe/views/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 0c294d5869..6e70284619 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -726,7 +726,7 @@ frappe.views.CommunicationComposer = Class.extend({ if (!signature) { const res = await this.get_default_outgoing_email_account_signature(); - signature = "" + res.message.signature; + signature = res.message.signature ? "" + res.message.signature : ""; } if (signature && !frappe.utils.is_html(signature)) { From 72dc556bb388110d888f44b188e6cd955658d0f7 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 22 Mar 2021 12:31:58 +0100 Subject: [PATCH 038/112] fix: make Default Email Template a link field --- frappe/core/doctype/doctype/doctype.json | 7 ++++--- frappe/custom/doctype/customize_form/customize_form.json | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json index 3333c50f45..ceaa1240f2 100644 --- a/frappe/core/doctype/doctype/doctype.json +++ b/frappe/core/doctype/doctype/doctype.json @@ -540,8 +540,9 @@ }, { "fieldname": "default_email_template", - "fieldtype": "Data", - "label": "Default Email Template" + "fieldtype": "Link", + "label": "Default Email Template", + "options": "Email Template" }, { "fieldname": "column_break_51", @@ -627,7 +628,7 @@ "link_fieldname": "reference_doctype" } ], - "modified": "2021-02-17 20:18:06.212232", + "modified": "2021-03-22 12:26:41.031135", "modified_by": "Administrator", "module": "Core", "name": "DocType", diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 22fc56c903..50abcf9b64 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -280,8 +280,9 @@ }, { "fieldname": "default_email_template", - "fieldtype": "Data", - "label": "Default Email Template" + "fieldtype": "Link", + "label": "Default Email Template", + "options": "Email Template" }, { "fieldname": "column_break_26", @@ -294,7 +295,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-02-16 15:22:11.108256", + "modified": "2021-03-22 12:27:15.462727", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", From e42b4e73119aa89be14eac0bdb3a1cfde85023d7 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Sat, 27 Mar 2021 15:01:37 +0100 Subject: [PATCH 039/112] fix: add back column break that was lost in merge --- .../custom/doctype/customize_form/customize_form.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 50abcf9b64..8d6a6a8ca7 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -266,6 +266,16 @@ "label": "Actions", "options": "DocType Action" }, + { + "fieldname": "default_email_template", + "fieldtype": "Link", + "label": "Default Email Template", + "options": "Email Template" + }, + { + "fieldname": "column_break_26", + "fieldtype": "Column Break" + }, { "collapsible": 1, "fieldname": "naming_section", From 3b4e40d6b1238e602371284c028fec05f2e2682d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 01:34:11 +0530 Subject: [PATCH 040/112] refactor: frappe.views.CommunicationComposer --- .../js/frappe/form/controls/multiselect.js | 2 +- .../public/js/frappe/views/communication.js | 521 +++++++++--------- 2 files changed, 258 insertions(+), 265 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/multiselect.js b/frappe/public/js/frappe/form/controls/multiselect.js index 64ca4fc83d..bbd7aef822 100644 --- a/frappe/public/js/frappe/form/controls/multiselect.js +++ b/frappe/public/js/frappe/form/controls/multiselect.js @@ -68,7 +68,7 @@ frappe.ui.form.ControlMultiSelect = frappe.ui.form.ControlAutocomplete.extend({ let data; if(this.df.get_data) { data = this.df.get_data(); - this.set_data(data); + if (data) this.set_data(data); } else { data = this._super(); } diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 6e70284619..cd7f0197c8 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -2,16 +2,15 @@ // MIT License. See license.txt frappe.last_edited_communication = {}; -frappe.standard_replies = {}; -frappe.separator_element = '
    ---
    '; +const separator_element = '
    ---
    '; frappe.views.CommunicationComposer = Class.extend({ - init: function(opts) { + init(opts) { $.extend(this, opts); this.make(); }, - make: function() { - var me = this; + make() { + const me = this; this.dialog = new frappe.ui.Dialog({ title: (this.title || this.subject || __("New Email")), @@ -19,56 +18,35 @@ frappe.views.CommunicationComposer = Class.extend({ fields: this.get_fields(), primary_action_label: __("Send"), size: 'large', - primary_action: function() { - me.delete_saved_draft(); + primary_action() { me.send_action(); + me.clear_cache(); + }, + secondary_action_label: __("Discard"), + secondary_action() { + me.dialog.hide(); + me.clear_cache(); }, minimizable: true }); this.dialog.sections[0].wrapper.addClass('to_section'); - ['recipients', 'cc', 'bcc'].forEach(field => { - this.dialog.fields_dict[field].get_data = function() { - const data = me.dialog.fields_dict[field].get_value(); - const txt = data.match(/[^,\s*]*$/)[0] || ''; - let options = []; - - frappe.call({ - method: "frappe.email.get_contact_list", - args: { - txt: txt, - }, - callback: (r) => { - options = r.message; - me.dialog.fields_dict[field].set_data(options); - } - }); - return options; - } - }); - this.prepare(); this.dialog.show(); if (this.frm) { $(document).trigger('form-typing', [this.frm]); } - - if (this.cc || this.bcc) { - this.toggle_more_options(true); - } }, - get_fields: function() { - let contactList = []; - let fields = [ + get_fields() { + const fields = [ { label: __("To"), fieldtype: "MultiSelect", reqd: 0, fieldname: "recipients", - options: contactList }, { fieldtype: "Button", @@ -87,13 +65,11 @@ frappe.views.CommunicationComposer = Class.extend({ label: __("CC"), fieldtype: "MultiSelect", fieldname: "cc", - options: contactList }, { label: __("BCC"), fieldtype: "MultiSelect", fieldname: "bcc", - options: contactList }, { label: __("Email Template"), @@ -163,18 +139,16 @@ frappe.views.CommunicationComposer = Class.extend({ ); }); - if (frappe.boot.email_accounts && email_accounts.length > 1) { - fields = [ - { - label: __("From"), - fieldtype: "Select", - reqd: 1, - fieldname: "sender", - options: email_accounts.map(function(e) { - return e.email_id; - }) - } - ].concat(fields); + if (email_accounts.length > 1) { + fields.unshift({ + label: __("From"), + fieldtype: "Select", + reqd: 1, + fieldname: "sender", + options: email_accounts.map(function(e) { + return e.email_id; + }) + }); } return fields; @@ -183,56 +157,58 @@ frappe.views.CommunicationComposer = Class.extend({ toggle_more_options(show_options) { show_options = show_options || this.dialog.fields_dict.more_options.df.hidden; this.dialog.set_df_property('more_options', 'hidden', !show_options); - let label = frappe.utils.icon(show_options ? 'up-line': 'down'); + + const label = frappe.utils.icon(show_options ? 'up-line': 'down'); this.dialog.get_field('option_toggle_button').set_label(label); }, - prepare: function() { + prepare() { + this.setup_multiselect_queries(); this.setup_subject_and_recipients(); this.setup_print_language(); this.setup_print(); this.setup_attach(); this.setup_email(); - this.setup_last_edited_communication(); this.setup_email_template(); - - this.dialog.set_value("recipients", this.recipients || ''); - this.dialog.set_value("cc", this.cc || ''); - this.dialog.set_value("bcc", this.bcc || ''); - - if(this.dialog.fields_dict.sender) { - this.dialog.fields_dict.sender.set_value(this.sender || ''); - } - this.dialog.fields_dict.subject.set_value( - frappe.utils.html2text(this.subject) || '' - ); - - this.setup_earlier_reply(); - - if ('frm' in this && !this.is_a_reply) { - // set default email template for the first email in a document - this.dialog.set_value("email_template", this.frm.meta.default_email_template || ''); - } + this.setup_last_edited_communication(); + this.set_values(); }, - setup_subject_and_recipients: function() { + setup_multiselect_queries() { + ['recipients', 'cc', 'bcc'].forEach(field => { + this.dialog.fields_dict[field].get_data = () => { + const data = this.dialog.fields_dict[field].get_value(); + const txt = data.match(/[^,\s*]*$/)[0] || ''; + + frappe.call({ + method: "frappe.email.get_contact_list", + args: {txt}, + callback: (r) => { + this.dialog.fields_dict[field].set_data(r.message); + } + }); + } + }); + }, + + setup_subject_and_recipients() { this.subject = this.subject || ""; - if(!this.forward && !this.recipients && this.last_email) { + if (!this.forward && !this.recipients && this.last_email) { this.recipients = this.last_email.sender; this.cc = this.last_email.cc; this.bcc = this.last_email.bcc; } - if(!this.forward && !this.recipients) { + if (!this.forward && !this.recipients) { this.recipients = this.frm && this.frm.timeline.get_recipient(); } - if(!this.subject && this.frm) { + if (!this.subject && this.frm) { // get subject from last communication - var last = this.frm.timeline.get_last_email(); + const last = this.frm.timeline.get_last_email(); - if(last) { + if (last) { this.subject = last.subject; if(!this.recipients) { this.recipients = last.sender; @@ -256,7 +232,7 @@ frappe.views.CommunicationComposer = Class.extend({ // always add an identifier to catch a reply // some email clients (outlook) may not send the message id to identify // the thread. So as a backup we use the name of the document as identifier - let identifier = `#${this.frm.doc.name}`; + const identifier = `#${this.frm.doc.name}`; if (!this.subject.includes(identifier)) { this.subject = `${this.subject} (${identifier})`; } @@ -267,34 +243,23 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - setup_email_template: function() { - var me = this; + setup_email_template() { + const me = this; this.dialog.fields_dict["email_template"].df.onchange = () => { - var email_template = me.dialog.fields_dict.email_template.get_value(); - if (email_template === '') { - return; - } + const email_template = me.dialog.fields_dict.email_template.get_value(); + if (!email_template) return; - var prepend_reply = function(reply) { - if(me.reply_added===email_template) { - return; - } - var content_field = me.dialog.fields_dict.content; - var subject_field = me.dialog.fields_dict.subject; - var content = content_field.get_value() || ""; - var subject = subject_field.get_value() || ""; + function prepend_reply(reply) { + if (me.reply_added === email_template) return; - var parts = content.split(''); + const content_field = me.dialog.fields_dict.content; + const subject_field = me.dialog.fields_dict.subject; - if(parts.length===2) { - content = [reply.message, "
    ", parts[1]]; - } else { - content = [reply.message, "
    ", content]; - } - - content_field.set_value(content.join('')); + let content = content_field.get_value() || ""; + content = content.split('')[1] || content; + content_field.set_value(`${reply.message}
    ${content}`); subject_field.set_value(reply.subject); me.reply_added = email_template; @@ -307,83 +272,105 @@ frappe.views.CommunicationComposer = Class.extend({ doc: me.frm.doc, _lang: me.dialog.get_value("language_sel") }, - callback: function(r) { + callback(r) { prepend_reply(r.message); }, }); } }, - setup_last_edited_communication: function() { - var me = this; - if (!this.doc){ - if (cur_frm){ - this.doc = cur_frm.doctype; - }else{ - this.doc = "Inbox"; - } - } - if (cur_frm && cur_frm.docname) { - this.key = cur_frm.docname; + setup_last_edited_communication() { + if (this.frm) { + this.doctype = this.frm.doctype; + this.key = this.frm.docname; } else { - this.key = "Inbox"; + this.doctype = this.key = "Inbox"; } - if(this.last_email) { + + if (this.last_email) { this.key = this.key + ":" + this.last_email.name; } - if(this.subject){ + + if (this.subject) { this.key = this.key + ":" + this.subject; } - this.dialog.onhide = function() { - var last_edited_communication = me.get_last_edited_communication(); - $.extend(last_edited_communication, { - sender: me.dialog.get_value("sender"), - recipients: me.dialog.get_value("recipients"), - cc: me.dialog.get_value("cc"), - bcc: me.dialog.get_value("bcc"), - subject: me.dialog.get_value("subject"), - content: me.dialog.get_value("content"), - }); - if (me.frm) { - $(document).trigger("form-stopped-typing", [me.frm]); + this.dialog.on_hide = () => { + $.extend( + this.get_last_edited_communication(true), + this.dialog.get_values(true) + ); + + if (this.frm) { + $(document).trigger("form-stopped-typing", [this.frm]); } } + }, - this.dialog.on_page_show = function() { - if (!me.txt) { - var last_edited_communication = me.get_last_edited_communication(); - if(last_edited_communication.content) { - me.dialog.set_value("sender", last_edited_communication.sender || ""); - me.dialog.set_value("subject", last_edited_communication.subject || ""); - me.dialog.set_value("recipients", last_edited_communication.recipients || ""); - me.dialog.set_value("cc", last_edited_communication.cc || ""); - me.dialog.set_value("bcc", last_edited_communication.bcc || ""); - me.dialog.set_value("content", last_edited_communication.content || ""); - } + get_last_edited_communication(clear) { + if (!frappe.last_edited_communication[this.doctype]) { + frappe.last_edited_communication[this.doctype] = {}; + } + + if (clear || !frappe.last_edited_communication[this.doctype][this.key]) { + frappe.last_edited_communication[this.doctype][this.key] = {}; + console.log('cleared!'); + } + + return frappe.last_edited_communication[this.doctype][this.key]; + }, + + set_values: async function () { + for (const fieldname of ["recipients", "cc", "bcc", "sender"]) { + await this.dialog.set_value(fieldname, this[fieldname] || ""); + } + + const subject = frappe.utils.html2text(this.subject) || ''; + await this.dialog.set_value("subject", subject); + + await this.set_values_from_last_edited_communication(); + await this.set_content(); + + // set default email template for the first email in a document + if (this.frm && !this.is_a_reply && !this.content_set) { + const email_template = this.frm.meta.default_email_template || ''; + await this.dialog.set_value("email_template", email_template); + } + + for (const fieldname of ['email_template', 'cc', 'bcc']) { + if (this.dialog.get_value(fieldname)) { + this.toggle_more_options(true); + break; } - } - }, - get_last_edited_communication: function() { - if (!frappe.last_edited_communication[this.doc]) { - frappe.last_edited_communication[this.doc] = {}; + set_values_from_last_edited_communication: async function () { + if (this.txt) return; + + const last_edited = this.get_last_edited_communication(); + if (!last_edited.content) return; + + // prevent re-triggering of email template + if (last_edited.email_template) { + const template_field = this.dialog.fields_dict.email_template; + await template_field.set_model_value(last_edited.email_template); + delete last_edited.email_template; } - if(!frappe.last_edited_communication[this.doc][this.key]) { - frappe.last_edited_communication[this.doc][this.key] = {}; - } - - return frappe.last_edited_communication[this.doc][this.key]; + await this.dialog.set_values(last_edited); + this.content_set = true; }, - selected_format: function() { - return this.dialog.fields_dict.select_print_format.input.value || (this.frm && this.frm.meta.default_print_format) || "Standard"; + selected_format() { + return ( + this.dialog.fields_dict.select_print_format.input.value + || this.frm && this.frm.meta.default_print_format + || "Standard" + ); }, - get_print_format: function(format) { + get_print_format(format) { if (!format) { format = this.selected_format(); } @@ -395,9 +382,9 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - setup_print_language: function() { - var doc = this.doc || cur_frm.doc; - var fields = this.dialog.fields_dict; + setup_print_language() { + const doc = this.frm && this.frm.doc; + const fields = this.dialog.fields_dict; //Load default print language from doctype this.lang_code = doc.language @@ -407,7 +394,7 @@ frappe.views.CommunicationComposer = Class.extend({ } //On selection of language retrieve language code - var me = this; + const me = this; $(fields.language_sel.input).change(function(){ me.lang_code = this.value }) @@ -422,9 +409,9 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - setup_print: function() { + setup_print() { // print formats - var fields = this.dialog.fields_dict; + const fields = this.dialog.fields_dict; // toggle print format $(fields.attach_document_print.input).click(function() { @@ -434,8 +421,8 @@ frappe.views.CommunicationComposer = Class.extend({ // select print format $(fields.select_print_format.wrapper).toggle(false); - if (cur_frm) { - const print_formats = frappe.meta.get_print_formats(cur_frm.meta.name); + if (this.frm) { + const print_formats = frappe.meta.get_print_formats(this.frm.meta.name); $(fields.select_print_format.input) .empty() .add_options(print_formats) @@ -446,9 +433,9 @@ frappe.views.CommunicationComposer = Class.extend({ }, - setup_attach: function() { - var fields = this.dialog.fields_dict; - var attach = $(fields.select_attachments.wrapper); + setup_attach() { + const fields = this.dialog.fields_dict; + const attach = $(fields.select_attachments.wrapper); if (!this.attachments) { this.attachments = []; @@ -493,7 +480,7 @@ frappe.views.CommunicationComposer = Class.extend({ this.render_attachment_rows(); }, - render_attachment_rows: function(attachment) { + render_attachment_rows(attachment) { const select_attachments = this.dialog.fields_dict.select_attachments; const attachment_rows = $(select_attachments.wrapper).find(".attach-list"); if (attachment) { @@ -536,9 +523,9 @@ frappe.views.CommunicationComposer = Class.extend({

    `); }, - setup_email: function() { + setup_email() { // email - var fields = this.dialog.fields_dict; + const fields = this.dialog.fields_dict; if(this.attach_document_print) { $(fields.attach_document_print.input).click(); @@ -547,21 +534,20 @@ frappe.views.CommunicationComposer = Class.extend({ $(fields.send_me_a_copy.input).on('click', () => { // update send me a copy (make it sticky) - let val = fields.send_me_a_copy.get_value(); + const val = fields.send_me_a_copy.get_value(); frappe.db.set_value('User', frappe.session.user, 'send_me_a_copy', val); frappe.boot.user.send_me_a_copy = val; }); }, - send_action: function() { - var me = this; - var btn = me.dialog.get_primary_btn(); - - var form_values = this.get_values(); + send_action() { + const me = this; + const btn = me.dialog.get_primary_btn(); + const form_values = this.get_values(); if(!form_values) return; - var selected_attachments = + const selected_attachments = $.map($(me.dialog.wrapper).find("[data-file-name]:checked"), function (element) { return $(element).attr("data-file-name"); }); @@ -574,16 +560,16 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - get_values: function() { - var form_values = this.dialog.get_values(); + get_values() { + const form_values = this.dialog.get_values(); // cc - for ( var i=0, l=this.dialog.fields.length; i < l; i++ ) { - var df = this.dialog.fields[i]; + for (let i = 0, l = this.dialog.fields.length; i < l; i++) { + const df = this.dialog.fields[i]; - if ( df.is_cc_checkbox ) { + if (df.is_cc_checkbox) { // concat in cc - if ( form_values[df.fieldname] ) { + if (form_values[df.fieldname]) { form_values.cc = ( form_values.cc ? (form_values.cc + ", ") : "" ) + df.fieldname; form_values.bcc = ( form_values.bcc ? (form_values.bcc + ", ") : "" ) + df.fieldname; } @@ -595,35 +581,40 @@ frappe.views.CommunicationComposer = Class.extend({ return form_values; }, - save_as_draft: function() { + save_as_draft() { if (this.dialog && this.frm) { let message = this.dialog.get_value('content'); - message = message.split(frappe.separator_element)[0]; + message = message.split(separator_element)[0]; localforage.setItem(this.frm.doctype + this.frm.docname, message).catch(e => { if (e) { // silently fail console.log(e); // eslint-disable-line - console.warn('[Communication] localStorage is full. Cannot save message as draft'); // eslint-disable-line + console.warn('[Communication] IndexedDB is full. Cannot save message as draft'); // eslint-disable-line } }); } }, + clear_cache() { + this.delete_saved_draft(); + this.get_last_edited_communication(true); + }, + delete_saved_draft() { if (this.dialog && this.frm) { localforage.removeItem(this.frm.doctype + this.frm.docname).catch(e => { if (e) { // silently fail console.log(e); // eslint-disable-line - console.warn('[Communication] localStorage is full. Cannot save message as draft'); // eslint-disable-line + console.warn('[Communication] IndexedDB is full. Cannot save message as draft'); // eslint-disable-line } }); } }, - send_email: function(btn, form_values, selected_attachments, print_html, print_format) { - var me = this; + send_email(btn, form_values, selected_attachments, print_html, print_format) { + const me = this; me.dialog.hide(); if(!form_values.recipients) { @@ -637,7 +628,7 @@ frappe.views.CommunicationComposer = Class.extend({ } - if(cur_frm && !frappe.model.can_email(me.doc.doctype, cur_frm)) { + if(this.frm && !frappe.model.can_email(me.doc.doctype, this.frm)) { frappe.msgprint(__("You are not allowed to send emails related to this document")); return; } @@ -658,15 +649,17 @@ frappe.views.CommunicationComposer = Class.extend({ send_me_a_copy: form_values.send_me_a_copy, print_format: print_format, sender: form_values.sender, - sender_full_name: form_values.sender?frappe.user.full_name():undefined, + sender_full_name: form_values.sender + ? frappe.user.full_name() + : undefined, email_template: form_values.email_template, attachments: selected_attachments, _lang : me.lang_code, read_receipt:form_values.send_read_receipt, print_letterhead: me.is_print_letterhead_checked(), }, - btn: btn, - callback: function(r) { + btn, + callback(r) { if(!r.exc) { frappe.utils.play_sound("email"); @@ -678,8 +671,8 @@ frappe.views.CommunicationComposer = Class.extend({ if ((frappe.last_edited_communication[me.doc] || {})[me.key]) { delete frappe.last_edited_communication[me.doc][me.key]; } - if (cur_frm) { - cur_frm.reload_doc(); + if (this.frm) { + this.frm.reload_doc(); } // try the success callback if it exists @@ -707,7 +700,7 @@ frappe.views.CommunicationComposer = Class.extend({ }); }, - is_print_letterhead_checked: function() { + is_print_letterhead_checked() { if (this.frm && $(this.frm.wrapper).find('.form-print-wrapper').is(':visible')){ return $(this.frm.wrapper).find('.print-letterhead').prop('checked') ? 1 : 0; } else { @@ -716,96 +709,96 @@ frappe.views.CommunicationComposer = Class.extend({ } }, - get_default_outgoing_email_account_signature: function() { - return frappe.db.get_value('Email Account', { 'default_outgoing': 1, 'add_signature': 1 }, 'signature'); - }, + set_content: async function() { + if (this.content_set) return; - setup_earlier_reply: async function() { - let fields = this.dialog.fields_dict; - let signature = frappe.boot.user.email_signature || ""; - - if (!signature) { - const res = await this.get_default_outgoing_email_account_signature(); - signature = res.message.signature ? "" + res.message.signature : ""; + let message = this.txt || ""; + if (!message && this.frm) { + const { doctype, docname } = this.frm; + message = await localforage.getItem(doctype + docname) || ""; } - if (signature && !frappe.utils.is_html(signature)) { - signature = signature.replace(/\n/g, "
    "); + if (message) { + this.content_set = true; } - if(this.txt) { - this.message = this.txt + (this.message ? ("

    " + this.message) : ""); - } else { - // saved draft in localStorage - const { doctype, docname } = this.frm || {}; - if (doctype && docname) { - this.message = await localforage.getItem(doctype + docname) || ''; - } - } - - if(this.real_name) { - this.message = '

    '+__('Dear') +' ' - + this.real_name + ",


    " + (this.message || ""); - } - - if(this.message && signature && this.message.includes(signature)) { - signature = ""; - } - - let reply = (this.message || "") + (signature ? ("
    " + signature) : ""); - let content = ''; - - if (this.is_a_reply === 'undefined') { - this.is_a_reply = true; + message += await this.get_signature(); + if (this.real_name && !message.includes("")) { + message = `

    ${__('Dear')} ${this.real_name},

    +
    ${message}`; } if (this.is_a_reply) { - let last_email = this.last_email; - - if (!last_email) { - last_email = this.frm && this.frm.timeline.get_last_email(true); - } - - if (!last_email) return; - - let last_email_content = last_email.original_comment || last_email.content; - - // convert the email context to text as we are enclosing - // this inside
    - last_email_content = this.html2text(last_email_content).replace(/\n/g, '
    '); - - // clip last email for a maximum of 20k characters - // to prevent the email content from getting too large - if (last_email_content.length > 20 * 1024) { - last_email_content += '
    ' + __('Message clipped') + '
    ' + last_email_content; - last_email_content = last_email_content.slice(0, 20 * 1024); - } - - let communication_date = last_email.communication_date || last_email.creation; - content = ` - ${reply} -

    - ${frappe.separator_element || ''} -

    ${__("On {0}, {1} wrote:", [frappe.datetime.global_date_format(communication_date) , last_email.sender])}

    -
    - ${last_email_content} -
    - `; - } else { - content = reply; + message += this.get_earlier_reply(); } - fields.content.set_value(content); + + await this.dialog.set_value("content", message); }, - html2text: function(html) { + get_signature: async function () { + let signature = frappe.boot.user.email_signature; + + if (!signature) { + const response = await frappe.db.get_value( + 'Email Account', + {'default_outgoing': 1, 'add_signature': 1}, + 'signature' + ); + + signature = response.message.signature; + } + + if (!signature) return ""; + + if (!frappe.utils.is_html(signature)) { + signature = signature.replace(/\n/g, "
    "); + } + + return "
    " + signature; + }, + + get_earlier_reply() { + const last_email = ( + this.last_email + || this.frm && this.frm.timeline.get_last_email(true) + ); + + if (!last_email) return ""; + let last_email_content = last_email.original_comment || last_email.content; + + // convert the email context to text as we are enclosing + // this inside
    + last_email_content = this.html2text(last_email_content).replace(/\n/g, '
    '); + + // clip last email for a maximum of 20k characters + // to prevent the email content from getting too large + if (last_email_content.length > 20 * 1024) { + last_email_content += '
    ' + __('Message clipped') + '
    ' + last_email_content; + last_email_content = last_email_content.slice(0, 20 * 1024); + } + + const communication_date = last_email.communication_date || last_email.creation; + return ` +

    + ${separator_element || ''} +

    ${__("On {0}, {1} wrote:", [ + frappe.datetime.global_date_format(communication_date), + last_email.sender + ])}

    +
    + ${last_email_content} +
    + `; + }, + + html2text(html) { // convert HTML to text and try and preserve whitespace - var d = document.createElement( 'div' ); + const d = document.createElement( 'div' ); d.innerHTML = html.replace(/<\/div>/g, '
    ') // replace end of blocks .replace(/<\/p>/g, '

    ') // replace end of paragraphs .replace(/
    /g, '\n'); - let text = d.textContent; // replace multiple empty lines with just one - return text.replace(/\n{3,}/g, '\n\n'); + return d.textContent.replace(/\n{3,}/g, '\n\n'); } }); From 686a2c53ad26747476b2eeee21a76845ec5afc3d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 02:12:12 +0530 Subject: [PATCH 041/112] fix: sider issues --- .../public/js/frappe/views/communication.js | 41 ++++++++++--------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index cd7f0197c8..ce097f69de 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -187,7 +187,7 @@ frappe.views.CommunicationComposer = Class.extend({ this.dialog.fields_dict[field].set_data(r.message); } }); - } + }; }); }, @@ -210,12 +210,12 @@ frappe.views.CommunicationComposer = Class.extend({ if (last) { this.subject = last.subject; - if(!this.recipients) { + if (!this.recipients) { this.recipients = last.sender; } // prepend "Re:" - if(strip(this.subject.toLowerCase().split(":")[0])!="re") { + if (strip(this.subject.toLowerCase().split(":")[0])!="re") { this.subject = __("Re: {0}", [this.subject]); } } @@ -304,7 +304,7 @@ frappe.views.CommunicationComposer = Class.extend({ if (this.frm) { $(document).trigger("form-stopped-typing", [this.frm]); } - } + }; }, get_last_edited_communication(clear) { @@ -314,7 +314,6 @@ frappe.views.CommunicationComposer = Class.extend({ if (clear || !frappe.last_edited_communication[this.doctype][this.key]) { frappe.last_edited_communication[this.doctype][this.key] = {}; - console.log('cleared!'); } return frappe.last_edited_communication[this.doctype][this.key]; @@ -527,7 +526,7 @@ frappe.views.CommunicationComposer = Class.extend({ // email const fields = this.dialog.fields_dict; - if(this.attach_document_print) { + if (this.attach_document_print) { $(fields.attach_document_print.input).click(); $(fields.select_print_format.wrapper).toggle(true); } @@ -545,7 +544,7 @@ frappe.views.CommunicationComposer = Class.extend({ const me = this; const btn = me.dialog.get_primary_btn(); const form_values = this.get_values(); - if(!form_values) return; + if (!form_values) return; const selected_attachments = $.map($(me.dialog.wrapper).find("[data-file-name]:checked"), function (element) { @@ -553,7 +552,7 @@ frappe.views.CommunicationComposer = Class.extend({ }); - if(form_values.attach_document_print) { + if (form_values.attach_document_print) { me.send_email(btn, form_values, selected_attachments, null, form_values.select_print_format || ""); } else { me.send_email(btn, form_values, selected_attachments); @@ -617,18 +616,18 @@ frappe.views.CommunicationComposer = Class.extend({ const me = this; me.dialog.hide(); - if(!form_values.recipients) { + if (!form_values.recipients) { frappe.msgprint(__("Enter Email Recipient(s)")); return; } - if(!form_values.attach_document_print) { + if (!form_values.attach_document_print) { print_html = null; print_format = null; } - if(this.frm && !frappe.model.can_email(me.doc.doctype, this.frm)) { + if (this.frm && !frappe.model.can_email(me.doc.doctype, this.frm)) { frappe.msgprint(__("You are not allowed to send emails related to this document")); return; } @@ -660,10 +659,10 @@ frappe.views.CommunicationComposer = Class.extend({ }, btn, callback(r) { - if(!r.exc) { + if (!r.exc) { frappe.utils.play_sound("email"); - if(r.message["emails_not_sent_to"]) { + if (r.message["emails_not_sent_to"]) { frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)", [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) ); } @@ -680,7 +679,7 @@ frappe.views.CommunicationComposer = Class.extend({ try { me.success(r); } catch (e) { - console.log(e); + console.log(e); // eslint-disable-line } } @@ -692,7 +691,7 @@ frappe.views.CommunicationComposer = Class.extend({ try { me.error(r); } catch (e) { - console.log(e); + console.log(e); // eslint-disable-line } } } @@ -777,14 +776,16 @@ frappe.views.CommunicationComposer = Class.extend({ last_email_content = last_email_content.slice(0, 20 * 1024); } - const communication_date = last_email.communication_date || last_email.creation; + const communication_date = frappe.datetime.global_date_format( + last_email.communication_date || last_email.creation + ); + return `

    ${separator_element || ''} -

    ${__("On {0}, {1} wrote:", [ - frappe.datetime.global_date_format(communication_date), - last_email.sender - ])}

    +

    + ${__("On {0}, {1} wrote:", [communication_date, last_email.sender])} +

    ${last_email_content}
    From 050b8eaafa4c3e98579c914fba3b61e772d8f809 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 02:36:52 +0530 Subject: [PATCH 042/112] fix: set lang to frappe.boot.lang by default --- frappe/public/js/frappe/views/communication.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index ce097f69de..4389b14301 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -387,10 +387,8 @@ frappe.views.CommunicationComposer = Class.extend({ //Load default print language from doctype this.lang_code = doc.language - - if (!this.lang_code && this.get_print_format().default_print_language) { - this.lang_code = this.get_print_format().default_print_language; - } + || this.get_print_format().default_print_language + || frappe.boot.lang; //On selection of language retrieve language code const me = this; From e94d15c5c150ece0a204be6a4225948c0a7a759d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 03:24:01 +0530 Subject: [PATCH 043/112] fix: clear_cache only on success; use me instead of this --- frappe/public/js/frappe/views/communication.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 4389b14301..5286bcc895 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -17,16 +17,15 @@ frappe.views.CommunicationComposer = Class.extend({ no_submit_on_enter: true, fields: this.get_fields(), primary_action_label: __("Send"), - size: 'large', primary_action() { me.send_action(); - me.clear_cache(); }, secondary_action_label: __("Discard"), secondary_action() { me.dialog.hide(); me.clear_cache(); }, + size: 'large', minimizable: true }); @@ -665,11 +664,10 @@ frappe.views.CommunicationComposer = Class.extend({ [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) ); } - if ((frappe.last_edited_communication[me.doc] || {})[me.key]) { - delete frappe.last_edited_communication[me.doc][me.key]; - } - if (this.frm) { - this.frm.reload_doc(); + me.clear_cache(); + + if (me.frm) { + me.frm.reload_doc(); } // try the success callback if it exists From 4c4cb68fdcb98bfc72ca26a67e6b10610a6f3651 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 12:21:19 +0530 Subject: [PATCH 044/112] style: use ES6 class --- .../public/js/frappe/views/communication.js | 75 ++++++++++--------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 5286bcc895..0cb3389713 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -4,11 +4,12 @@ frappe.last_edited_communication = {}; const separator_element = '
    ---
    '; -frappe.views.CommunicationComposer = Class.extend({ - init(opts) { +frappe.views.CommunicationComposer = class { + constructor(opts) { $.extend(this, opts); this.make(); - }, + } + make() { const me = this; @@ -37,7 +38,7 @@ frappe.views.CommunicationComposer = Class.extend({ if (this.frm) { $(document).trigger('form-typing', [this.frm]); } - }, + } get_fields() { const fields = [ @@ -151,7 +152,7 @@ frappe.views.CommunicationComposer = Class.extend({ } return fields; - }, + } toggle_more_options(show_options) { show_options = show_options || this.dialog.fields_dict.more_options.df.hidden; @@ -159,7 +160,7 @@ frappe.views.CommunicationComposer = Class.extend({ const label = frappe.utils.icon(show_options ? 'up-line': 'down'); this.dialog.get_field('option_toggle_button').set_label(label); - }, + } prepare() { this.setup_multiselect_queries(); @@ -171,7 +172,7 @@ frappe.views.CommunicationComposer = Class.extend({ this.setup_email_template(); this.setup_last_edited_communication(); this.set_values(); - }, + } setup_multiselect_queries() { ['recipients', 'cc', 'bcc'].forEach(field => { @@ -188,7 +189,7 @@ frappe.views.CommunicationComposer = Class.extend({ }); }; }); - }, + } setup_subject_and_recipients() { this.subject = this.subject || ""; @@ -240,7 +241,7 @@ frappe.views.CommunicationComposer = Class.extend({ if (this.frm && !this.recipients) { this.recipients = this.frm.doc[this.frm.email_field]; } - }, + } setup_email_template() { const me = this; @@ -276,7 +277,7 @@ frappe.views.CommunicationComposer = Class.extend({ }, }); } - }, + } setup_last_edited_communication() { if (this.frm) { @@ -304,7 +305,7 @@ frappe.views.CommunicationComposer = Class.extend({ $(document).trigger("form-stopped-typing", [this.frm]); } }; - }, + } get_last_edited_communication(clear) { if (!frappe.last_edited_communication[this.doctype]) { @@ -316,9 +317,9 @@ frappe.views.CommunicationComposer = Class.extend({ } return frappe.last_edited_communication[this.doctype][this.key]; - }, + } - set_values: async function () { + async set_values() { for (const fieldname of ["recipients", "cc", "bcc", "sender"]) { await this.dialog.set_value(fieldname, this[fieldname] || ""); } @@ -341,9 +342,9 @@ frappe.views.CommunicationComposer = Class.extend({ break; } } - }, + } - set_values_from_last_edited_communication: async function () { + async set_values_from_last_edited_communication() { if (this.txt) return; const last_edited = this.get_last_edited_communication(); @@ -358,7 +359,7 @@ frappe.views.CommunicationComposer = Class.extend({ await this.dialog.set_values(last_edited); this.content_set = true; - }, + } selected_format() { return ( @@ -366,7 +367,7 @@ frappe.views.CommunicationComposer = Class.extend({ || this.frm && this.frm.meta.default_print_format || "Standard" ); - }, + } get_print_format(format) { if (!format) { @@ -378,7 +379,7 @@ frappe.views.CommunicationComposer = Class.extend({ } else { return {}; } - }, + } setup_print_language() { const doc = this.frm && this.frm.doc; @@ -403,7 +404,7 @@ frappe.views.CommunicationComposer = Class.extend({ if (this.lang_code) { $(fields.language_sel.input).val(this.lang_code); } - }, + } setup_print() { // print formats @@ -427,7 +428,7 @@ frappe.views.CommunicationComposer = Class.extend({ $(fields.attach_document_print.wrapper).toggle(false); } - }, + } setup_attach() { const fields = this.dialog.fields_dict; @@ -474,7 +475,7 @@ frappe.views.CommunicationComposer = Class.extend({ .find(".add-more-attachments button") .on('click', () => new frappe.ui.FileUploader(args)); this.render_attachment_rows(); - }, + } render_attachment_rows(attachment) { const select_attachments = this.dialog.fields_dict.select_attachments; @@ -500,7 +501,7 @@ frappe.views.CommunicationComposer = Class.extend({ }); } } - }, + } get_attachment_row(attachment, checked) { return $(`

    @@ -517,7 +518,7 @@ frappe.views.CommunicationComposer = Class.extend({ ${frappe.utils.icon('link-url')}

    `); - }, + } setup_email() { // email @@ -535,7 +536,7 @@ frappe.views.CommunicationComposer = Class.extend({ frappe.boot.user.send_me_a_copy = val; }); - }, + } send_action() { const me = this; @@ -554,7 +555,7 @@ frappe.views.CommunicationComposer = Class.extend({ } else { me.send_email(btn, form_values, selected_attachments); } - }, + } get_values() { const form_values = this.dialog.get_values(); @@ -575,7 +576,7 @@ frappe.views.CommunicationComposer = Class.extend({ } return form_values; - }, + } save_as_draft() { if (this.dialog && this.frm) { @@ -590,12 +591,12 @@ frappe.views.CommunicationComposer = Class.extend({ }); } - }, + } clear_cache() { this.delete_saved_draft(); this.get_last_edited_communication(true); - }, + } delete_saved_draft() { if (this.dialog && this.frm) { @@ -607,7 +608,7 @@ frappe.views.CommunicationComposer = Class.extend({ } }); } - }, + } send_email(btn, form_values, selected_attachments, print_html, print_format) { const me = this; @@ -693,7 +694,7 @@ frappe.views.CommunicationComposer = Class.extend({ } } }); - }, + } is_print_letterhead_checked() { if (this.frm && $(this.frm.wrapper).find('.form-print-wrapper').is(':visible')){ @@ -702,9 +703,9 @@ frappe.views.CommunicationComposer = Class.extend({ return (frappe.model.get_doc(":Print Settings", "Print Settings") || { with_letterhead: 1 }).with_letterhead ? 1 : 0; } - }, + } - set_content: async function() { + async set_content() { if (this.content_set) return; let message = this.txt || ""; @@ -728,9 +729,9 @@ frappe.views.CommunicationComposer = Class.extend({ } await this.dialog.set_value("content", message); - }, + } - get_signature: async function () { + async get_signature() { let signature = frappe.boot.user.email_signature; if (!signature) { @@ -750,7 +751,7 @@ frappe.views.CommunicationComposer = Class.extend({ } return "
    " + signature; - }, + } get_earlier_reply() { const last_email = ( @@ -786,7 +787,7 @@ frappe.views.CommunicationComposer = Class.extend({ ${last_email_content}
    `; - }, + } html2text(html) { // convert HTML to text and try and preserve whitespace @@ -798,4 +799,4 @@ frappe.views.CommunicationComposer = Class.extend({ // replace multiple empty lines with just one return d.textContent.replace(/\n{3,}/g, '\n\n'); } -}); +}; From 26ff01d68f98266ae5627b95296a0783724e267d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 12:24:00 +0530 Subject: [PATCH 045/112] fix: sider issue --- frappe/public/js/frappe/views/communication.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 0cb3389713..99f2e2f42f 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -276,7 +276,7 @@ frappe.views.CommunicationComposer = class { prepend_reply(r.message); }, }); - } + }; } setup_last_edited_communication() { From 96d5b141e4c55b4844da9239655c08e70af0153f Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 13:12:19 +0530 Subject: [PATCH 046/112] test: no need to blur text editor --- cypress/integration/form.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/form.js b/cypress/integration/form.js index 5302ed0964..20ed7a61cd 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -8,7 +8,7 @@ context('Form', () => { }); it('create a new form', () => { cy.visit('/app/todo/new'); - cy.fill_field('description', 'this is a test todo', 'Text Editor').blur(); + cy.fill_field('description', 'this is a test todo', 'Text Editor'); cy.wait(300); cy.get('.page-title').should('contain', 'Not Saved'); cy.intercept({ From 6c8f04ea1b7b37762b7da55b9f3105923360cf41 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 17 Apr 2021 03:53:41 +0530 Subject: [PATCH 047/112] fix: Cannot read property `current` of undefined --- frappe/public/js/frappe/form/form_viewers.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/public/js/frappe/form/form_viewers.js b/frappe/public/js/frappe/form/form_viewers.js index 3d488e4729..964576ef8a 100644 --- a/frappe/public/js/frappe/form/form_viewers.js +++ b/frappe/public/js/frappe/form/form_viewers.js @@ -7,6 +7,11 @@ frappe.ui.form.FormViewers = class FormViewers { refresh() { let users = this.frm.get_docinfo()['viewers']; + if (!users || !users.current || !users.current.length) { + this.parent.empty(); + return; + } + let currently_viewing = users.current.filter(user => user != frappe.session.user); let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true}); this.parent.empty().append(avatar_group); From 50f9a7e1d87acb5f602f5f9eda98b2cf3b6263ad Mon Sep 17 00:00:00 2001 From: leela Date: Fri, 16 Apr 2021 16:21:19 +0530 Subject: [PATCH 048/112] fix: kanban board sync issue Recent refactoring introduced an issue of not syncing board data(comes from reference doctype) into kanban board columns db. Changed to sync it at time of creating kanban board. --- frappe/public/js/frappe/views/kanban/kanban_board.js | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/js/frappe/views/kanban/kanban_board.js b/frappe/public/js/frappe/views/kanban/kanban_board.js index f563f64cb4..bbc2051e4c 100644 --- a/frappe/public/js/frappe/views/kanban/kanban_board.js +++ b/frappe/public/js/frappe/views/kanban/kanban_board.js @@ -306,6 +306,7 @@ frappe.provide("frappe.views"); store.on('change:cur_list', setup_restore_columns); store.on('change:columns', setup_restore_columns); store.on('change:empty_state', show_empty_state); + fluxify.doAction('update_order'); } function prepare() { From d8dca020b314b8ad06e0fdcc4d3abb4c877621bf Mon Sep 17 00:00:00 2001 From: mustafaelagamey Date: Sun, 18 Apr 2021 06:46:19 +0200 Subject: [PATCH 049/112] fix: Remove `cmd` only if exist (#12886) * only remove cmd if exist When calling this function from backend this line raises key error as there's no such key called cmd * style: Simplify code Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/desk/treeview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index 12fdb0dadc..d479b71b52 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -66,7 +66,7 @@ def add_node(): doc.save() def make_tree_args(**kwarg): - del kwarg['cmd'] + kwarg.pop('cmd', None) doctype = kwarg['doctype'] parent_field = 'parent_' + doctype.lower().replace(' ', '_') From 1d9865197624f6571260b288b2a09e666bd5e9c6 Mon Sep 17 00:00:00 2001 From: Ernesto Ruiz Date: Sat, 17 Apr 2021 22:54:12 -0600 Subject: [PATCH 050/112] fix: Make strings translatable (#12877) Make strings translatable. --- .../desk/doctype/notification_settings/notification_settings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/notification_settings/notification_settings.js b/frappe/desk/doctype/notification_settings/notification_settings.js index 88dc145be2..cc2fd95204 100644 --- a/frappe/desk/doctype/notification_settings/notification_settings.js +++ b/frappe/desk/doctype/notification_settings/notification_settings.js @@ -19,7 +19,7 @@ frappe.ui.form.on('Notification Settings', { refresh: (frm) => { if (frappe.user.has_role('System Manager')) { - frm.add_custom_button('Go to Notification Settings List', () => { + frm.add_custom_button(__('Go to Notification Settings List'), () => { frappe.set_route('List', 'Notification Settings'); }); } From bc1b928d739c4ca1f8725d524bf8e4bf2b277000 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sun, 18 Apr 2021 12:38:19 +0530 Subject: [PATCH 051/112] fix: cannot read property `doc` of undefined (#12891) --- .../doctype/communication/communication_list.js | 2 +- frappe/public/js/frappe/views/communication.js | 13 ++++++++----- frappe/public/js/frappe/views/inbox/inbox_view.js | 4 +--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frappe/core/doctype/communication/communication_list.js b/frappe/core/doctype/communication/communication_list.js index 454897b865..315b74a39c 100644 --- a/frappe/core/doctype/communication/communication_list.js +++ b/frappe/core/doctype/communication/communication_list.js @@ -20,6 +20,6 @@ frappe.listview_settings['Communication'] = { }, primary_action: function() { - new frappe.views.CommunicationComposer({ doc: {} }); + new frappe.views.CommunicationComposer(); } }; diff --git a/frappe/public/js/frappe/views/communication.js b/frappe/public/js/frappe/views/communication.js index 99f2e2f42f..487b411854 100755 --- a/frappe/public/js/frappe/views/communication.js +++ b/frappe/public/js/frappe/views/communication.js @@ -7,6 +7,10 @@ const separator_element = '
    ---
    '; frappe.views.CommunicationComposer = class { constructor(opts) { $.extend(this, opts); + if (!this.doc) { + this.doc = this.frm && this.frm.doc || {}; + } + this.make(); } @@ -269,7 +273,7 @@ frappe.views.CommunicationComposer = class { method: 'frappe.email.doctype.email_template.email_template.get_email_template', args: { template_name: email_template, - doc: me.frm.doc, + doc: me.doc, _lang: me.dialog.get_value("language_sel") }, callback(r) { @@ -382,11 +386,10 @@ frappe.views.CommunicationComposer = class { } setup_print_language() { - const doc = this.frm && this.frm.doc; const fields = this.dialog.fields_dict; //Load default print language from doctype - this.lang_code = doc.language + this.lang_code = this.doc.language || this.get_print_format().default_print_language || frappe.boot.lang; @@ -612,7 +615,7 @@ frappe.views.CommunicationComposer = class { send_email(btn, form_values, selected_attachments, print_html, print_format) { const me = this; - me.dialog.hide(); + this.dialog.hide(); if (!form_values.recipients) { frappe.msgprint(__("Enter Email Recipient(s)")); @@ -625,7 +628,7 @@ frappe.views.CommunicationComposer = class { } - if (this.frm && !frappe.model.can_email(me.doc.doctype, this.frm)) { + if (this.frm && !frappe.model.can_email(this.doc.doctype, this.frm)) { frappe.msgprint(__("You are not allowed to send emails related to this document")); return; } diff --git a/frappe/public/js/frappe/views/inbox/inbox_view.js b/frappe/public/js/frappe/views/inbox/inbox_view.js index 1085e93e6c..8b53bd49a9 100644 --- a/frappe/public/js/frappe/views/inbox/inbox_view.js +++ b/frappe/public/js/frappe/views/inbox/inbox_view.js @@ -204,9 +204,7 @@ frappe.views.InboxView = class InboxView extends frappe.views.ListView { }; frappe.new_doc('Email Account'); } else { - new frappe.views.CommunicationComposer({ - doc: {} - }); + new frappe.views.CommunicationComposer(); } } }; From f16a53a2badbe787f7ed75880ea7368738f3c64c Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Sun, 18 Apr 2021 11:58:20 +0530 Subject: [PATCH 052/112] fix(minor): Add a delete trigger in grid, and use it to refresh labels in Website Settings --- frappe/public/js/frappe/form/grid.js | 6 +++++- .../website_settings/website_settings.js | 18 +++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index b211476e63..86feefed7a 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -194,7 +194,10 @@ export default class Grid { } tasks.push(() => { - if (dirty) this.refresh(); + if (dirty) { + this.refresh(); + this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); + } }); frappe.run_serially(tasks); @@ -210,6 +213,7 @@ export default class Grid { this.frm.doc[this.df.fieldname] = []; $(this.parent).find('.rows').empty(); this.grid_rows = []; + this.frm.script_manager.trigger(this.df.fieldname + "_delete", this.doctype); this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0); this.refresh(); diff --git a/frappe/website/doctype/website_settings/website_settings.js b/frappe/website/doctype/website_settings/website_settings.js index 422deb244e..2f15b4c00e 100644 --- a/frappe/website/doctype/website_settings/website_settings.js +++ b/frappe/website/doctype/website_settings/website_settings.js @@ -33,20 +33,12 @@ frappe.ui.form.on('Website Settings', { frm.fields_dict.top_bar_items.grid.update_docfield_property( 'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items") ); - - if ($(frm.fields_dict.top_bar_items.grid.wrapper).find(".grid-row-open")) { - frm.fields_dict.top_bar_items.grid.refresh(); - } }, set_parent_label_options_footer: function(frm) { frm.fields_dict.footer_items.grid.update_docfield_property( - 'parent_label', 'options', frm.events.get_parent_options(frm, "top_bar_items") + 'parent_label', 'options', frm.events.get_parent_options(frm, "footer_items") ); - - if ($(frm.fields_dict.footer_items.grid.wrapper).find(".grid-row-open")) { - frm.fields_dict.footer_items.grid.refresh(); - } }, authorize_api_indexing_access: function(frm) { @@ -122,10 +114,18 @@ frappe.ui.form.on('Website Settings', { }); frappe.ui.form.on('Top Bar Item', { + top_bar_items_delete(frm) { + frm.events.set_parent_label_options(frm); + }, + footer_items_add(frm, cdt, cdn) { frappe.model.set_value(cdt, cdn, 'right', 0); }, + footer_items_delete(frm) { + frm.events.set_parent_label_options_footer(frm); + }, + parent_label: function(frm, doctype, name) { frm.events.set_parent_options(frm, doctype, name); }, From 8b8bd2a4634df5098ee9419378aaa21209ffc698 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 19 Apr 2021 10:45:54 +0530 Subject: [PATCH 053/112] chore: Upgrade frappe-charts to rc13 (#12896) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 3c8da66242..6e82890617 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "driver.js": "^0.9.8", "express": "^4.17.1", "fast-deep-equal": "^2.0.1", - "frappe-charts": "^2.0.0-rc11", + "frappe-charts": "^2.0.0-rc13", "frappe-datatable": "^1.15.3", "frappe-gantt": "^0.5.0", "fuse.js": "^3.4.6", diff --git a/yarn.lock b/yarn.lock index 4f6f62ac0a..8ac348011d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2699,10 +2699,10 @@ fragment-cache@^0.2.1: dependencies: map-cache "^0.2.2" -frappe-charts@^2.0.0-rc11: - version "2.0.0-rc11" - resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc11.tgz#0724fa0d43593362c075c3805ebbbe1a608fcef7" - integrity sha512-DY3tThT1lNGcJlRMOtIhnILtSm5h1iKysWhZAyj7yrGiOnOWbZpYx/NZzXZYwtRrWwMlYiLX2ylV76qo31ONsg== +frappe-charts@^2.0.0-rc13: + version "2.0.0-rc13" + resolved "https://registry.yarnpkg.com/frappe-charts/-/frappe-charts-2.0.0-rc13.tgz#fdb251d7ae311c41e38f90a3ae108070ec6b9072" + integrity sha512-Bv7IfllIrjRbKWHn5b769dOSenqdBixAr6m5kurf8ZUOJSLOgK4HOXItJ7BA8n9PvviH9/k5DaloisjLM2Bm1w== frappe-datatable@^1.15.3: version "1.15.3" From e81d2567ff01ed238dbf774f71ee782404058982 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sun, 18 Apr 2021 13:22:43 +0530 Subject: [PATCH 054/112] test: multiple cypress fixes --- cypress/integration/relative_time_filters.js | 3 --- cypress/integration/table_multiselect.js | 2 +- frappe/commands/utils.py | 11 +++++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js index 80e6387d99..cbb0524c24 100644 --- a/cypress/integration/relative_time_filters.js +++ b/cypress/integration/relative_time_filters.js @@ -1,7 +1,4 @@ context('Relative Timeframe', () => { - beforeEach(() => { - cy.login(); - }); before(() => { cy.login(); cy.visit('/app/website'); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index faa72d63a5..25cab78ba2 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -1,5 +1,5 @@ context('Table MultiSelect', () => { - beforeEach(() => { + before(() => { cy.login(); }); diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 5ff66171fc..a203c8c6d9 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -11,7 +11,7 @@ import click import frappe from frappe.commands import get_site, pass_context from frappe.exceptions import SiteNotSpecifiedError -from frappe.utils import get_bench_path, update_progress_bar +from frappe.utils import get_bench_path, update_progress_bar, cint @click.command('build') @@ -567,11 +567,14 @@ def run_ui_tests(context, app, headless=False): node_bin = subprocess.getoutput("npm bin") cypress_path = "{0}/cypress".format(node_bin) - plugin_path = "{0}/cypress-file-upload".format(node_bin) + plugin_path = "{0}/../cypress-file-upload".format(node_bin) # check if cypress in path...if not, install it. - if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \ - or not subprocess.getoutput("npm view cypress version").startswith("6."): + if not ( + os.path.exists(cypress_path) + and os.path.exists(plugin_path) + and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6 + ): # install cypress click.secho("Installing Cypress...", fg="yellow") frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile") From 83ee74c074993f195344a2de28cc0acf6c1e4c5a Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 19 Apr 2021 14:15:30 +0530 Subject: [PATCH 055/112] fix: Typo in get_all_language_with_name (#12902) --- frappe/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 62ee733f5f..a65a1c28c1 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -827,7 +827,7 @@ def get_all_languages(with_language_name=False): return frappe.db.sql_list('select name from tabLanguage') def get_all_language_with_name(): - return frappe.db.get_all('language', ['language_code', 'language_name']) + return frappe.db.get_all('Language', ['language_code', 'language_name']) if not frappe.db: frappe.connect() From 3142723d418c4443aa6516d2417f5f2aeafa56aa Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Fri, 2 Apr 2021 15:33:12 +0530 Subject: [PATCH 056/112] feat: manage Python 3 compatiblity with dependencies --- .github/workflows/ci-tests.yml | 6 +- frappe/commands/site.py | 6 +- .../scheduled_job_type/scheduled_job_type.py | 11 +- frappe/database/mariadb/database.py | 42 +++-- frappe/email/receive.py | 33 ++-- .../dropbox_settings/dropbox_settings.py | 54 ++++--- .../google_calendar/google_calendar.py | 34 ++-- .../google_contacts/google_contacts.py | 23 +-- .../doctype/google_drive/google_drive.py | 37 +++-- frappe/utils/xlsxutils.py | 21 +-- .../website_settings/google_indexing.py | 24 +-- requirements.txt | 152 +++++++++--------- 12 files changed, 238 insertions(+), 205 deletions(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index bfe2002f69..08a2823dca 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -149,9 +149,9 @@ jobs: run: | cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE} cd ${GITHUB_WORKSPACE} - pip install coveralls==2.2.0 - pip install coverage==4.5.4 - coveralls + pip install coveralls==3.0.1 + pip install coverage==5.5 + coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 0fadf2a294..0102d3ac40 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -676,10 +676,8 @@ def start_ngrok(context): frappe.init(site=site) port = frappe.conf.http_port or frappe.conf.webserver_port - public_url = ngrok.connect(port=port, options={ - 'host_header': site - }) - print(f'Public URL: {public_url}') + tunnel = ngrok.connect(addr=str(port), host_header=site) + print(f'Public URL: {tunnel.public_url}') print('Inspect logs at http://localhost:4040') ngrok_process = ngrok.get_ngrok_process() diff --git a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py index 92493a593a..59089d12ad 100644 --- a/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py +++ b/frappe/core/doctype/scheduled_job_type/scheduled_job_type.py @@ -2,14 +2,15 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals +import json +from datetime import datetime from typing import Dict, List -import frappe, json -from frappe.model.document import Document -from frappe.utils import now_datetime, get_datetime -from datetime import datetime from croniter import croniter + +import frappe +from frappe.model.document import Document +from frappe.utils import get_datetime, now_datetime from frappe.utils.background_jobs import enqueue, get_jobs diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index f9997d1526..7d1d92408c 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -1,17 +1,13 @@ -from __future__ import unicode_literals - -import frappe import warnings import pymysql -from pymysql.times import TimeDelta -from pymysql.constants import ER, FIELD_TYPE -from pymysql.converters import conversions +from pymysql.constants import ER, FIELD_TYPE +from pymysql.converters import conversions, escape_string -from frappe.utils import get_datetime, cstr, UnicodeWithAttrs +import frappe from frappe.database.database import Database -from six import PY2, binary_type, text_type, string_types from frappe.database.mariadb.schema import MariaDBTable +from frappe.utils import UnicodeWithAttrs, cstr, get_datetime class MariaDBDatabase(Database): @@ -72,22 +68,20 @@ class MariaDBDatabase(Database): conversions.update({ FIELD_TYPE.NEWDECIMAL: float, FIELD_TYPE.DATETIME: get_datetime, - UnicodeWithAttrs: conversions[text_type] + UnicodeWithAttrs: conversions[str] }) - if PY2: - conversions.update({ - TimeDelta: conversions[binary_type] - }) - - if usessl: - conn = pymysql.connect(self.host, self.user or '', self.password or '', - port=self.port, charset='utf8mb4', use_unicode = True, ssl=ssl_params, - conv = conversions, local_infile = frappe.conf.local_infile) - else: - conn = pymysql.connect(self.host, self.user or '', self.password or '', - port=self.port, charset='utf8mb4', use_unicode = True, conv = conversions, - local_infile = frappe.conf.local_infile) + conn = pymysql.connect( + user=self.user or '', + password=self.password or '', + host=self.host, + port=self.port, + charset='utf8mb4', + use_unicode=True, + ssl=ssl_params if usessl else None, + conv=conversions, + local_infile=frappe.conf.local_infile + ) # MYSQL_OPTION_MULTI_STATEMENTS_OFF = 1 # # self._conn.set_server_option(MYSQL_OPTION_MULTI_STATEMENTS_OFF) @@ -111,7 +105,7 @@ class MariaDBDatabase(Database): def escape(s, percent=True): """Excape quotes and percent in given string.""" # pymysql expects unicode argument to escape_string with Python 3 - s = frappe.as_unicode(pymysql.escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`") + s = frappe.as_unicode(escape_string(frappe.as_unicode(s)), "utf-8").replace("`", "\\`") # NOTE separating % escape, because % escape should only be done when using LIKE operator # or when you use python format string to generate query that already has a %s @@ -260,7 +254,7 @@ class MariaDBDatabase(Database): ADD INDEX `%s`(%s)""" % (table_name, index_name, ", ".join(fields))) def add_unique(self, doctype, fields, constraint_name=None): - if isinstance(fields, string_types): + if isinstance(fields, str): fields = [fields] if not constraint_name: constraint_name = "unique_" + "_".join(fields) diff --git a/frappe/email/receive.py b/frappe/email/receive.py index cf6c13ee76..949da4a343 100644 --- a/frappe/email/receive.py +++ b/frappe/email/receive.py @@ -1,18 +1,27 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals -import six -from six import iteritems, text_type -from six.moves import range -import time, _socket, poplib, imaplib, email, email.utils, datetime, chardet, re -from email_reply_parser import EmailReplyParser +import datetime +import email +import email.utils +import imaplib +import poplib +import re +import time from email.header import decode_header + +import _socket +import chardet +import six +from email_reply_parser import EmailReplyParser + import frappe from frappe import _, safe_decode, safe_encode -from frappe.utils import (extract_email_id, convert_utc_to_user_timezone, now, - cint, cstr, strip, markdown, parse_addr) -from frappe.core.doctype.file.file import get_random_filename, MaxFileSizeReachedError +from frappe.core.doctype.file.file import (MaxFileSizeReachedError, + get_random_filename) +from frappe.utils import (cint, convert_utc_to_user_timezone, cstr, + extract_email_id, markdown, now, parse_addr, strip) + class EmailSizeExceededError(frappe.ValidationError): pass class EmailTimeoutError(frappe.ValidationError): pass @@ -337,7 +346,7 @@ class EmailServer: return self.imap.select("Inbox") - for uid, operation in iteritems(uid_list): + for uid, operation in uid_list.items(): if not uid: continue op = "+FLAGS" if operation == "Read" else "-FLAGS" @@ -473,7 +482,7 @@ class Email: self.html_content += markdown(text_content) def get_charset(self, part): - """Detect chartset.""" + """Detect charset.""" charset = part.get_content_charset() if not charset: charset = chardet.detect(safe_encode(cstr(part)))['encoding'] @@ -484,7 +493,7 @@ class Email: charset = self.get_charset(part) try: - return text_type(part.get_payload(decode=True), str(charset), "ignore") + return str(part.get_payload(decode=True), str(charset), "ignore") except LookupError: return part.get_payload() diff --git a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py index 09da1ecc42..53f0935c80 100644 --- a/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py +++ b/frappe/integrations/doctype/dropbox_settings/dropbox_settings.py @@ -2,22 +2,23 @@ # Copyright (c) 2015, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import dropbox import json -import frappe import os -from frappe import _ -from frappe.model.document import Document -from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size, get_chunk_site -from frappe.integrations.utils import make_post_request -from frappe.utils import (cint, get_request_site_address, - get_files_path, get_backups_path, get_url, encode) -from frappe.utils.backups import new_backup -from frappe.utils.background_jobs import enqueue -from six.moves.urllib.parse import urlparse, parse_qs +from urllib.parse import parse_qs, urlparse + +import dropbox from rq.timeouts import JobTimeoutException -from six import text_type + +import frappe +from frappe import _ +from frappe.integrations.offsite_backup_utils import (get_chunk_site, + get_latest_backup_file, send_email, validate_file_size) +from frappe.integrations.utils import make_post_request +from frappe.model.document import Document +from frappe.utils import (cint, encode, get_backups_path, get_files_path, + get_request_site_address, get_url) +from frappe.utils.background_jobs import enqueue +from frappe.utils.backups import new_backup ignore_list = [".DS_Store"] @@ -91,7 +92,10 @@ def backup_to_dropbox(upload_db_backup=True): dropbox_settings['access_token'] = access_token['oauth2_token'] set_dropbox_access_token(access_token['oauth2_token']) - dropbox_client = dropbox.Dropbox(dropbox_settings['access_token'], timeout=None) + dropbox_client = dropbox.Dropbox( + oauth2_access_token=dropbox_settings['access_token'], + timeout=None + ) if upload_db_backup: if frappe.flags.create_new_backup: @@ -127,7 +131,7 @@ def upload_from_folder(path, is_private, dropbox_folder, dropbox_client, did_not else: response = frappe._dict({"entries": []}) - path = text_type(path) + path = str(path) for f in frappe.get_all("File", filters={"is_folder": 0, "is_private": is_private, "uploaded_to_dropbox": 0}, fields=['file_url', 'name', 'file_name']): @@ -286,11 +290,11 @@ def get_redirect_url(): def get_dropbox_authorize_url(): app_details = get_dropbox_settings(redirect_uri=True) dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( - app_details["app_key"], - app_details["app_secret"], - app_details["redirect_uri"], - {}, - "dropbox-auth-csrf-token" + consumer_key=app_details["app_key"], + redirect_uri=app_details["redirect_uri"], + session={}, + csrf_token_session_key="dropbox-auth-csrf-token", + consumer_secret=app_details["app_secret"] ) auth_url = dropbox_oauth_flow.start() @@ -307,13 +311,13 @@ def dropbox_auth_finish(return_access_token=False): close = '

    ' + _('Please close this window') + '

    ' dropbox_oauth_flow = dropbox.DropboxOAuth2Flow( - app_details["app_key"], - app_details["app_secret"], - app_details["redirect_uri"], - { + consumer_key=app_details["app_key"], + redirect_uri=app_details["redirect_uri"], + session={ 'dropbox-auth-csrf-token': callback.state }, - "dropbox-auth-csrf-token" + csrf_token_session_key="dropbox-auth-csrf-token", + consumer_secret=app_details["app_secret"] ) if callback.state or callback.code: diff --git a/frappe/integrations/doctype/google_calendar/google_calendar.py b/frappe/integrations/doctype/google_calendar/google_calendar.py index fbedd75029..f93be35aa7 100644 --- a/frappe/integrations/doctype/google_calendar/google_calendar.py +++ b/frappe/integrations/doctype/google_calendar/google_calendar.py @@ -2,22 +2,23 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -import requests -import googleapiclient.discovery -import google.oauth2.credentials -from frappe import _ -from frappe.model.document import Document -from frappe.utils import get_request_site_address -from googleapiclient.errors import HttpError -from frappe.utils.password import set_encrypted_password -from frappe.utils import add_days, get_datetime, get_weekdays, now_datetime, add_to_date, get_time_zone -from dateutil import parser from datetime import datetime, timedelta -from six.moves.urllib.parse import quote +from urllib.parse import quote + +import google.oauth2.credentials +import requests +from dateutil import parser +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +import frappe +from frappe import _ from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.model.document import Document +from frappe.utils import (add_days, add_to_date, get_datetime, + get_request_site_address, get_time_zone, get_weekdays, now_datetime) +from frappe.utils.password import set_encrypted_password SCOPES = "https://www.googleapis.com/auth/calendar" @@ -171,7 +172,12 @@ def get_google_calendar_object(g_calendar): } credentials = google.oauth2.credentials.Credentials(**credentials_dict) - google_calendar = googleapiclient.discovery.build("calendar", "v3", credentials=credentials) + google_calendar = build( + serviceName="calendar", + version="v3", + credentials=credentials, + static_discovery=False + ) check_google_calendar(account, google_calendar) diff --git a/frappe/integrations/doctype/google_contacts/google_contacts.py b/frappe/integrations/doctype/google_contacts/google_contacts.py index 4c8c3b67f6..1705f98e91 100644 --- a/frappe/integrations/doctype/google_contacts/google_contacts.py +++ b/frappe/integrations/doctype/google_contacts/google_contacts.py @@ -2,17 +2,17 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -import requests -import googleapiclient.discovery -import google.oauth2.credentials -from frappe.model.document import Document -from frappe import _ +import google.oauth2.credentials +import requests +from googleapiclient.discovery import build from googleapiclient.errors import HttpError -from frappe.utils import get_request_site_address + +import frappe +from frappe import _ from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.model.document import Document +from frappe.utils import get_request_site_address SCOPES = "https://www.googleapis.com/auth/contacts" @@ -118,7 +118,12 @@ def get_google_contacts_object(g_contact): } credentials = google.oauth2.credentials.Credentials(**credentials_dict) - google_contacts = googleapiclient.discovery.build("people", "v1", credentials=credentials) + google_contacts = build( + serviceName="people", + version="v1", + credentials=credentials, + static_discovery=False + ) return google_contacts, account diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 859c769018..93b6fa3f8d 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -2,27 +2,29 @@ # Copyright (c) 2019, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -import requests -import googleapiclient.discovery -import google.oauth2.credentials import os +from urllib.parse import quote -from frappe import _ -from googleapiclient.errors import HttpError -from frappe.model.document import Document -from frappe.utils import get_request_site_address -from frappe.utils.background_jobs import enqueue -from six.moves.urllib.parse import quote +import google.oauth2.credentials +import requests from apiclient.http import MediaFileUpload -from frappe.utils import get_backups_path, get_bench_path -from frappe.utils.backups import new_backup +from googleapiclient.discovery import build +from googleapiclient.errors import HttpError + +import frappe +from frappe import _ from frappe.integrations.doctype.google_settings.google_settings import get_auth_url -from frappe.integrations.offsite_backup_utils import get_latest_backup_file, send_email, validate_file_size +from frappe.integrations.offsite_backup_utils import (get_latest_backup_file, + send_email, validate_file_size) +from frappe.model.document import Document +from frappe.utils import (get_backups_path, get_bench_path, + get_request_site_address) +from frappe.utils.background_jobs import enqueue +from frappe.utils.backups import new_backup SCOPES = "https://www.googleapis.com/auth/drive" + class GoogleDrive(Document): def validate(self): @@ -126,7 +128,12 @@ def get_google_drive_object(): } credentials = google.oauth2.credentials.Credentials(**credentials_dict) - google_drive = googleapiclient.discovery.build("drive", "v3", credentials=credentials) + google_drive = build( + serviceName="drive", + version="v3", + credentials=credentials, + static_discovery=False + ) return google_drive, account diff --git a/frappe/utils/xlsxutils.py b/frappe/utils/xlsxutils.py index 3c7b027470..356e2ddfdb 100644 --- a/frappe/utils/xlsxutils.py +++ b/frappe/utils/xlsxutils.py @@ -1,18 +1,19 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals - -import frappe +import re +from io import BytesIO import openpyxl import xlrd -import re -from openpyxl.styles import Font from openpyxl import load_workbook +from openpyxl.styles import Font from openpyxl.utils import get_column_letter -from six import BytesIO, string_types + +import frappe ILLEGAL_CHARACTERS_RE = re.compile(r'[\000-\010]|[\013-\014]|[\016-\037]') + + # return xlsx file object def make_xlsx(data, sheet_name, wb=None, column_widths=None): column_widths = column_widths or [] @@ -31,12 +32,12 @@ def make_xlsx(data, sheet_name, wb=None, column_widths=None): for row in data: clean_row = [] for item in row: - if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']): + if isinstance(item, str) and (sheet_name not in ['Data Import Template', 'Data Export']): value = handle_html(item) else: value = item - if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None): + if isinstance(item, str) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None): # Remove illegal characters from the string value = re.sub(ILLEGAL_CHARACTERS_RE, '', value) @@ -80,12 +81,12 @@ def handle_html(data): return value + def read_xlsx_file_from_attached_file(file_url=None, fcontent=None, filepath=None): if file_url: _file = frappe.get_doc("File", {"file_url": file_url}) filename = _file.get_full_path() elif fcontent: - from io import BytesIO filename = BytesIO(fcontent) elif filepath: filename = filepath @@ -102,6 +103,7 @@ def read_xlsx_file_from_attached_file(file_url=None, fcontent=None, filepath=Non rows.append(tmp_list) return rows + def read_xls_file_from_attached_file(content): book = xlrd.open_workbook(file_contents=content) sheets = book.sheets() @@ -111,6 +113,7 @@ def read_xls_file_from_attached_file(content): rows.append(sheet.row_values(i)) return rows + def build_xlsx_response(data, filename): xlsx_file = make_xlsx(data, filename) # write out response as a xlsx type diff --git a/frappe/website/doctype/website_settings/google_indexing.py b/frappe/website/doctype/website_settings/google_indexing.py index 599de5a2b6..75095bd7df 100644 --- a/frappe/website/doctype/website_settings/google_indexing.py +++ b/frappe/website/doctype/website_settings/google_indexing.py @@ -2,17 +2,18 @@ # Copyright (c) 2020, Frappe Technologies and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -import requests -import googleapiclient.discovery -import google.oauth2.credentials -from frappe import _ +from urllib.parse import quote + +import google.oauth2.credentials +import requests +from googleapiclient.discovery import build from googleapiclient.errors import HttpError -from frappe.utils import get_request_site_address -from six.moves.urllib.parse import quote + +import frappe +from frappe import _ from frappe.integrations.doctype.google_settings.google_settings import get_auth_url +from frappe.utils import get_request_site_address SCOPES = "https://www.googleapis.com/auth/indexing" @@ -82,7 +83,12 @@ def get_google_indexing_object(): } credentials = google.oauth2.credentials.Credentials(**credentials_dict) - google_indexing = googleapiclient.discovery.build("indexing", "v3", credentials=credentials) + google_indexing = build( + serviceName="indexing", + version="v3", + credentials=credentials, + static_discovery=False + ) return google_indexing diff --git a/requirements.txt b/requirements.txt index 0f88a48f73..8cbe0e800b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,79 +1,79 @@ -Babel==2.6.0 -beautifulsoup4==4.8.2 -bleach-whitelist==0.0.10 -bleach==3.3.0 -boto3==1.10.18 -braintree==3.57.1 -chardet==3.0.4 -Click==7.0 -coverage==4.5.4 -croniter==0.3.31 -cryptography==3.3.2 -dropbox==9.1.0 -email-reply-parser==0.5.9 -Faker==2.0.4 +Babel~=2.9.0 +beautifulsoup4~=4.9.3 +bleach-whitelist~=0.0.11 +bleach~=3.3.0 +boto3~=1.17.48 +braintree~=4.8.0 +chardet~=4.0.0 +Click~=7.1.2 +coverage~=5.5 +croniter~=1.0.11 +cryptography~=3.4.7 +dropbox~=11.6.0 +email-reply-parser~=0.5.12 +Faker~=8.1.0 future==0.18.2 -gitdb2==2.0.6;python_version<'3.4' -GitPython==2.1.15 -git-url-parse==1.2.2 -google-api-python-client==1.9.3 -google-auth-httplib2==0.0.3 -google-auth-oauthlib==0.4.1 -google-auth==1.18.0 -googlemaps==3.1.1 -gunicorn==19.10.0 -html2text==2016.9.19 -html5lib==1.0.1 -ipython==7.14.0 -jedi==0.17.2 # not directly required. Pinned to fix upstream issue with ipython. -Jinja2==2.11.3 -ldap3==2.7 -markdown2==2.4.0 +git-url-parse~=1.2.2 +gitdb~=4.0.7 +GitPython~=3.1.14 +google-api-python-client~=2.2.0 +google-auth-httplib2~=0.1.0 +google-auth-oauthlib~=0.4.4 +google-auth~=1.28.1 +googlemaps~=4.4.5 +gunicorn~=20.1.0 +html2text==2020.1.16 +html5lib~=1.1 +ipython~=7.16.1 +jedi==0.17.2 # not directly required. Pinned to fix upstream IPython issue (https://github.com/ipython/ipython/issues/12740) +Jinja2~=2.11.3 +ldap3~=2.9 +markdown2~=2.4.0 maxminddb-geolite2==2018.703 -ndg-httpsclient==0.5.1 -num2words==0.5.10 -oauthlib==3.1.0 -openpyxl==2.6.4 -passlib==1.7.3 -pdfkit==0.6.1 -Pillow>=8.0.0 -premailer==3.6.1 -psutil==5.7.2 -psycopg2-binary==2.8.4 -pyasn1==0.4.8 -PyJWT==1.7.1 -PyMySQL==0.9.3 -pyngrok==4.1.6 -pyOpenSSL==19.1.0 -pyotp==2.3.0 -PyPDF2==1.26.0 -pypng==0.0.20 -PyQRCode==1.2.1 -python-dateutil==2.8.1 -pytz==2019.3 -PyYAML==5.4 -rauth==0.7.3 -redis==3.5.3 -requests-oauthlib==1.3.0 -requests==2.23.0 -RestrictedPython==5.0 -rq>=1.1.0 -schedule==0.6.0 -semantic-version==2.8.4 -simple-chalk==0.1.0 -six==1.14.0 -sqlparse==0.2.4 -stripe==2.40.0 -terminaltables==3.1.0 -unittest-xml-reporting==2.5.2 -urllib3==1.25.9 -watchdog==0.8.0 -Werkzeug==0.16.1 -Whoosh==2.7.4 -xlrd==1.2.0 -zxcvbn-python==4.4.24 -pycryptodome==3.9.8 -paytmchecksum==1.7.0 -wrapt==1.10.11 -razorpay==1.2.0 +ndg-httpsclient~=0.5.1 +num2words~=0.5.10 +oauthlib~=3.1.0 +openpyxl~=3.0.7 +passlib~=1.7.4 +paytmchecksum~=1.7.0 +pdfkit~=0.6.1 +Pillow~=8.2.0 +premailer~=3.7.0 +psutil~=5.8.0 +psycopg2-binary~=2.8.6 +pyasn1~=0.4.8 +pycryptodome~=3.10.1 +PyJWT~=1.7.1 +PyMySQL~=1.0.2 +pyngrok~=5.0.5 +pyOpenSSL~=20.0.1 +pyotp~=2.6.0 +PyPDF2~=1.26.0 +pypng~=0.0.20 +PyQRCode~=1.2.1 +python-dateutil~=2.8.1 +pytz==2021.1 +PyYAML~=5.4.1 +rauth~=0.7.3 +razorpay~=1.2.0 +redis~=3.5.3 +requests-oauthlib~=1.3.0 +requests~=2.25.1 +RestrictedPython~=5.1 +rq~=1.8.0 rsa>=4.1 # not directly required, pinned by Snyk to avoid a vulnerability +schedule~=1.1.0 +semantic-version~=2.8.5 +simple-chalk~=0.1.0 +six~=1.15.0 +sqlparse~=0.4.1 +stripe~=2.56.0 +terminaltables~=3.1.0 +unittest-xml-reporting~=3.0.4 +urllib3~=1.26.4 +watchdog~=2.0.2 +Werkzeug~=0.16.1 +Whoosh~=2.7.4 +wrapt~=1.12.1 +xlrd~=2.0.1 +zxcvbn-python~=4.4.24 From 919a7b7218fa6a45a8ad63b6eae88374c5a1f888 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 19 Apr 2021 12:51:48 +0530 Subject: [PATCH 057/112] fix: update dependencies --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8cbe0e800b..91f235159f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,14 +2,14 @@ Babel~=2.9.0 beautifulsoup4~=4.9.3 bleach-whitelist~=0.0.11 bleach~=3.3.0 -boto3~=1.17.48 +boto3~=1.17.53 braintree~=4.8.0 chardet~=4.0.0 Click~=7.1.2 coverage~=5.5 croniter~=1.0.11 cryptography~=3.4.7 -dropbox~=11.6.0 +dropbox~=11.7.0 email-reply-parser~=0.5.12 Faker~=8.1.0 future==0.18.2 @@ -19,7 +19,7 @@ GitPython~=3.1.14 google-api-python-client~=2.2.0 google-auth-httplib2~=0.1.0 google-auth-oauthlib~=0.4.4 -google-auth~=1.28.1 +google-auth~=1.29.0 googlemaps~=4.4.5 gunicorn~=20.1.0 html2text==2020.1.16 @@ -38,7 +38,7 @@ passlib~=1.7.4 paytmchecksum~=1.7.0 pdfkit~=0.6.1 Pillow~=8.2.0 -premailer~=3.7.0 +premailer~=3.8.0 psutil~=5.8.0 psycopg2-binary~=2.8.6 pyasn1~=0.4.8 From 86aa060da50ba86f6a80ff6652676afe8395d374 Mon Sep 17 00:00:00 2001 From: leela Date: Mon, 19 Apr 2021 14:43:44 +0530 Subject: [PATCH 058/112] refactor: removed unused code --- frappe/www/login.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frappe/www/login.py b/frappe/www/login.py index 76b232f8ee..1ce25a81d9 100644 --- a/frappe/www/login.py +++ b/frappe/www/login.py @@ -95,14 +95,6 @@ def login_via_frappe(code, state): def login_via_office365(code, state): login_via_oauth2_id_token("office_365", code, state, decoder=decoder_compat) -@frappe.whitelist(allow_guest=True) -def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=None, generate_login_token=False): - if not ((data and provider and state) or (email_id and key)): - frappe.respond_as_web_page(_("Invalid Request"), _("Missing parameters for login"), http_status_code=417) - return - - _login_oauth_user(data, provider, state, email_id, key, generate_login_token) - @frappe.whitelist(allow_guest=True) def login_via_token(login_token): sid = frappe.cache().get_value("login_token:{0}".format(login_token), expires=True) From 3fd5f75606c97eaff4b6846ca734e682470ae955 Mon Sep 17 00:00:00 2001 From: leela Date: Mon, 19 Apr 2021 14:45:38 +0530 Subject: [PATCH 059/112] fix: remove the token validation check Let token be part of state to make state dynamic. But there is no need to have validation for token. --- frappe/utils/oauth.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frappe/utils/oauth.py b/frappe/utils/oauth.py index 6596701ee3..6a92737a0d 100644 --- a/frappe/utils/oauth.py +++ b/frappe/utils/oauth.py @@ -64,8 +64,6 @@ def get_oauth2_authorize_url(provider, redirect_to): state = { "site": frappe.utils.get_url(), "token": frappe.generate_hash(), "redirect_to": redirect_to } - frappe.cache().set_value("{0}:{1}".format(provider, state["token"]), True, expires_in_sec=120) - # relative to absolute url data = { "redirect_uri": get_redirect_uri(provider), @@ -176,11 +174,6 @@ def login_oauth_user(data=None, provider=None, state=None, email_id=None, key=No frappe.respond_as_web_page(_("Invalid Request"), _("Token is missing"), http_status_code=417) return - token = frappe.cache().get_value("{0}:{1}".format(provider, state["token"]), expires=True) - if not token: - frappe.respond_as_web_page(_("Invalid Request"), _("Invalid Token"), http_status_code=417) - return - user = get_email(data) if not user: From ff047ef943b612114d50bab57050d404c1d6596f Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 19 Apr 2021 16:05:01 +0530 Subject: [PATCH 060/112] fix: Grid Form buttons Insert Above, Insert Below not hidden when cannot_add_rows is true --- frappe/public/js/frappe/form/grid_row.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 5e3a2b8ccd..0a88beaa37 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -555,6 +555,15 @@ export default class GridRow { this.grid_form.render(); this.row.toggle(false); // this.form_panel.toggle(true); + + if (this.grid.cannot_add_rows || (this.grid.df && this.grid.df.cannot_add_rows)) { + this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row') + .addClass('hidden') + } else { + this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row') + .removeClass('hidden') + } + frappe.dom.freeze("", "dark"); if (cur_frm) cur_frm.cur_grid = this; this.wrapper.addClass("grid-row-open"); From d34bb3b633828a0323af2e2c0153b91d0d19d292 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 19 Apr 2021 16:58:26 +0530 Subject: [PATCH 061/112] fix: sider fix --- frappe/public/js/frappe/form/grid_row.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 0a88beaa37..e0fe1b3b54 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -558,10 +558,10 @@ export default class GridRow { if (this.grid.cannot_add_rows || (this.grid.df && this.grid.df.cannot_add_rows)) { this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row') - .addClass('hidden') + .addClass('hidden'); } else { this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row') - .removeClass('hidden') + .removeClass('hidden'); } frappe.dom.freeze("", "dark"); From 61d0da5ca54ef5aefc8cccad8922bda2c3608b09 Mon Sep 17 00:00:00 2001 From: prssanna Date: Mon, 19 Apr 2021 15:12:00 +0530 Subject: [PATCH 062/112] fix: do not empty viewers parent on form refresh --- frappe/public/js/frappe/form/form.js | 1 + frappe/public/js/frappe/form/toolbar.js | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index ef728e730e..de9331a726 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -360,6 +360,7 @@ frappe.ui.form.Form = class FrappeForm { grid_obj.grid.grid_pagination.go_to_page(1, true); }); frappe.ui.form.close_grid_form(); + this.viewers && this.viewers.parent.empty(); this.docname = docname; this.setup_docinfo_change_listener(); } diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 145b8d3eed..22787b70c1 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -211,7 +211,6 @@ frappe.ui.form.Toolbar = class Toolbar { make_viewers() { if (this.frm.viewers) { - this.frm.viewers.parent.empty(); return; } this.frm.viewers = new frappe.ui.form.FormViewers({ From df1a0cd0cda53e5cb799479ae6a90eb1fae63635 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sat, 17 Apr 2021 18:31:43 +0200 Subject: [PATCH 063/112] fix: test_token_cache PermissionError --- frappe/integrations/doctype/token_cache/test_token_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py index 73c9f38fce..7aa069647d 100644 --- a/frappe/integrations/doctype/token_cache/test_token_cache.py +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -13,7 +13,7 @@ class TestTokenCache(unittest.TestCase): def setUp(self): self.token_cache = frappe.get_last_doc('Token Cache') self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name}) - self.token_cache.save() + self.token_cache.save(ignore_permissions=True) def test_get_auth_header(self): self.token_cache.get_auth_header() From 936934b8136a2446f6574347c90b6f3f761619be Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 19 Apr 2021 20:01:52 +0530 Subject: [PATCH 064/112] fix: id_token format decode bytes to utf-8 string --- frappe/integrations/oauth2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index c444964a16..3ebaaffcff 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -133,7 +133,7 @@ def get_token(*args, **kwargs): } id_token_encoded = jwt.encode(id_token, client_secret, algorithm='HS256', headers=id_token_header) - out.update({"id_token": str(id_token_encoded)}) + out.update({"id_token": frappe.safe_decode(id_token_encoded)}) frappe.local.response = out From 9028964494d6798eebdfc229ee22db6936f46e9e Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Mon, 19 Apr 2021 23:27:38 +0530 Subject: [PATCH 065/112] fix: whitelist login method to fetch session remotely --- frappe/auth.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/auth.py b/frappe/auth.py index ca97bbc17d..73cb8e8c15 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -120,6 +120,7 @@ class LoginManager: self.make_session() self.set_user_info() + @frappe.whitelist() def login(self): # clear cache frappe.clear_cache(user = frappe.form_dict.get('usr')) From a9f9c16a9f3134ced5be8f2113e571eba9030750 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 20 Apr 2021 14:21:25 +0530 Subject: [PATCH 066/112] fix: Pass aggregate_on_doctype to properly create the query --- frappe/desk/reportview.py | 13 +++++++------ frappe/public/js/frappe/ui/group_by/group_by.js | 3 ++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 3d04c171a7..86f8ec0aa7 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -126,13 +126,14 @@ def setup_group_by(data): if data.group_by: if data.aggregate_function.lower() not in ('count', 'sum', 'avg'): frappe.throw(_('Invalid aggregate function')) - if '`' in data.aggregate_on: - raise_invalid_field(data.aggregate_on) - data.fields.append('{aggregate_function}(`tab{doctype}`.`{aggregate_on}`) AS _aggregate_column'.format(**data)) - if data.aggregate_on: - data.fields.append(data.aggregate_on) - data.pop('aggregate_on') + if frappe.db.has_column(data.aggregate_on_doctype, data.aggregate_on_field): + data.fields.append('{aggregate_function}(`tab{aggregate_on_doctype}`.`{aggregate_on_field}`) AS _aggregate_column'.format(**data)) + else: + raise_invalid_field(data.aggregate_on_field) + + data.pop('aggregate_on_doctype') + data.pop('aggregate_on_field') data.pop('aggregate_function') def raise_invalid_field(fieldname): 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 53e4914f0d..3ebf9c9d3d 100644 --- a/frappe/public/js/frappe/ui/group_by/group_by.js +++ b/frappe/public/js/frappe/ui/group_by/group_by.js @@ -313,7 +313,8 @@ frappe.ui.GroupBy = class { Object.assign(args, { with_comment_count: false, - aggregate_on: this.aggregate_on || 'name', + aggregate_on_field: this.aggregate_on_field || 'name', + aggregate_on_doctype: this.aggregate_on_doctype || this.doctype, aggregate_function: this.aggregate_function || 'count', group_by: this.report_view.group_by || null, order_by: '_aggregate_column desc', From 26b42fc794cf5833f3197b2df917fb1a1cb850cc Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 20 Apr 2021 14:54:40 +0530 Subject: [PATCH 067/112] fix(treeview): Accept filters as kwargs to avoid TypeError (#12920) --- frappe/desk/treeview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/treeview.py b/frappe/desk/treeview.py index d479b71b52..6f0d7d3d5f 100644 --- a/frappe/desk/treeview.py +++ b/frappe/desk/treeview.py @@ -36,7 +36,7 @@ def get_all_nodes(doctype, label, parent, tree_method, **filters): return out @frappe.whitelist() -def get_children(doctype, parent=''): +def get_children(doctype, parent='', **filters): return _get_children(doctype, parent) def _get_children(doctype, parent='', ignore_permissions=False): From 5fd856db279d8d0b0e4697279dbbe91713cbaa62 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 20 Apr 2021 14:36:41 +0530 Subject: [PATCH 068/112] fix: aggregate column in auto email report --- frappe/core/doctype/report/report.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index af2c4e5dc2..8a0f9a99f5 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -325,9 +325,8 @@ def get_group_by_field(args, doctype): if args['aggregate_function'] == 'count': group_by_field = 'count(*) as _aggregate_column' else: - group_by_field = '{0}(`tab{1}`.{2}) as _aggregate_column'.format( + group_by_field = '{0}({1}) as _aggregate_column'.format( args.aggregate_function, - doctype, args.aggregate_on ) From bcd2a3a98686df24c87aeeb70e35a8f517581ac2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 20 Apr 2021 21:39:59 +0530 Subject: [PATCH 069/112] fix: Get defaults from user_defaults based on fieldname --- frappe/public/js/frappe/model/create_new.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index dc6ee56fca..1b09a451eb 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -177,7 +177,9 @@ $.extend(frappe.model, { // Use User Permission value when only when it has a single value user_default = user_defaults[0]; } - } else if (!user_default) { + } + + if (!user_default) { user_default = frappe.defaults.get_user_default(df.fieldname); } else if ( !user_default && From 43f3ba3cba261ecde62c7f016d9eac0e3bfdb810 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Sat, 17 Apr 2021 23:09:16 +0530 Subject: [PATCH 070/112] fix: Add autocompletion items in Server Script - API to add autocompletion items in Code field --- frappe/cache_manager.py | 2 +- .../doctype/server_script/server_script.js | 6 +++ .../doctype/server_script/server_script.py | 23 ++++++++- frappe/public/js/frappe/form/controls/code.js | 50 +++++++++++++++++++ 4 files changed, 79 insertions(+), 2 deletions(-) diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index bad879d2fa..4e0fe0cf44 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -18,7 +18,7 @@ global_cache_keys = ("app_hooks", "installed_apps", 'all_apps', 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', 'domain_restricted_doctypes', 'domain_restricted_pages', 'information_schema:counts', - 'sitemap_routes', 'db_tables') + doctype_map_keys + 'sitemap_routes', 'db_tables', 'server_script_autocompletion_items') + doctype_map_keys user_cache_keys = ("bootinfo", "user_recent", "roles", "user_doc", "lang", "defaults", "user_permissions", "home_page", "linked_with", diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index 95a63780f8..e12200b6fc 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -9,6 +9,12 @@ frappe.ui.form.on('Server Script', { if (frm.doc.script_type != 'Scheduler Event') { frm.dashboard.hide(); } + + frm.call('get_autocompletion_items') + .then(r => r.message) + .then(items => { + frm.set_df_property('script', 'autocompletions', items) + }); }, setup_help(frm) { diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 8838d9e954..6a8eb59c3a 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -5,11 +5,12 @@ from __future__ import unicode_literals import ast +from types import FunctionType, ModuleType from typing import Dict, List import frappe from frappe.model.document import Document -from frappe.utils.safe_exec import safe_exec +from frappe.utils.safe_exec import get_safe_globals, safe_exec, NamespaceDict from frappe import _ @@ -122,6 +123,26 @@ class ServerScript(Document): if locals["conditions"]: return locals["conditions"] + @frappe.whitelist() + def get_autocompletion_items(self): + def get_keys(obj): + out = [] + for key in obj: + if key.startswith('_'): + continue + value = obj[key] + if isinstance(value, (FunctionType, ModuleType)): + out.append(key) + elif isinstance(value, (NamespaceDict, dict)): + out += [f'{key}.{subkey}' for subkey in get_keys(value)] + return out + + items = frappe.cache().get_value('server_script_autocompletion_items') + if not items: + items = get_keys(get_safe_globals()) + frappe.cache().set_value('server_script_autocompletion_items', items) + return items + @frappe.whitelist() def setup_scheduler_events(script_name, frequency): diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index eec450b390..8d2609b836 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -31,6 +31,56 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ const input_value = this.get_input_value(); this.parse_validate_and_set_in_model(input_value); }, 300)); + + // setup autocompletion when it is set the first time + Object.defineProperty(this.df, 'autocompletions', { + get() { + return this._autocompletions || []; + }, + set: (value) => { + this.setup_autocompletion(); + this.df._autocompletions = value; + } + }); + }, + + setup_autocompletion() { + if (this._autocompletion_setup) return; + + const ace = window.ace; + const get_autocompletions = () => this.df.autocompletions; + + ace.config.loadModule("ace/ext/language_tools", langTools => { + this.editor.setOptions({ + enableBasicAutocompletion: true, + enableSnippets: true, + enableLiveAutocompletion: true + }); + + let completer = { + getCompletions: function(editor, session, pos, prefix, callback) { + if (prefix.length === 0) { + callback(null, []); + return; + } + let autocompletions = get_autocompletions(); + if (autocompletions.length) { + callback( + null, + autocompletions.map(a => ({ + name: 'frappe', + value: a, + score: 100, + meta: 'Frappe API' + })) + ); + } + } + } + langTools.addCompleter(completer); + }); + + this._autocompletion_setup = true; }, refresh_height() { From a87774726101347c3950cbca16aa01fc5d7c54f1 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 20 Apr 2021 16:01:56 +0530 Subject: [PATCH 071/112] fix: Include all keys and sort by score --- .../doctype/server_script/server_script.js | 2 +- .../doctype/server_script/server_script.py | 40 +++++++++++++++---- frappe/public/js/frappe/form/controls/code.js | 7 +--- 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.js b/frappe/core/doctype/server_script/server_script.js index e12200b6fc..dda39115bf 100644 --- a/frappe/core/doctype/server_script/server_script.js +++ b/frappe/core/doctype/server_script/server_script.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Server Script', { frm.call('get_autocompletion_items') .then(r => r.message) .then(items => { - frm.set_df_property('script', 'autocompletions', items) + frm.set_df_property('script', 'autocompletions', items); }); }, diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 6a8eb59c3a..ea27a2ac83 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import ast -from types import FunctionType, ModuleType +from types import FunctionType, MethodType, ModuleType from typing import Dict, List import frappe @@ -125,21 +125,47 @@ class ServerScript(Document): @frappe.whitelist() def get_autocompletion_items(self): + """Generates a list of a autocompletion strings from the context dict + that is used while executing a Server Script. + + Returns: + list: Returns list of autocompletion items. + For e.g., ["frappe.utils.cint", "frappe.db.get_all", ...] + """ def get_keys(obj): out = [] for key in obj: if key.startswith('_'): continue value = obj[key] - if isinstance(value, (FunctionType, ModuleType)): - out.append(key) - elif isinstance(value, (NamespaceDict, dict)): - out += [f'{key}.{subkey}' for subkey in get_keys(value)] + if isinstance(value, (NamespaceDict, dict)) and value: + if key == 'form_dict': + out.append(['form_dict', 3]) + continue + for subkey, score in get_keys(value): + fullkey = f'{key}.{subkey}' + out.append([fullkey, score]) + else: + if isinstance(value, ModuleType): + score = 0 + elif isinstance(value, (FunctionType, MethodType)): + score = 1 + elif isinstance(value, type) and issubclass(value, Exception): + score = 9 + elif isinstance(value, type): + score = 2 + elif isinstance(value, dict): + score = 3 + else: + score = 4 + out.append([key, score]) return out items = frappe.cache().get_value('server_script_autocompletion_items') - if not items: - items = get_keys(get_safe_globals()) + if not items or True: + unsorted_items = get_keys(get_safe_globals()) + sorted_items = sorted(unsorted_items, key=lambda k: k[1]) + items = [d[0] for d in sorted_items] frappe.cache().set_value('server_script_autocompletion_items', items) return items diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 8d2609b836..635146563e 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -53,11 +53,10 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ ace.config.loadModule("ace/ext/language_tools", langTools => { this.editor.setOptions({ enableBasicAutocompletion: true, - enableSnippets: true, enableLiveAutocompletion: true }); - let completer = { + langTools.addCompleter({ getCompletions: function(editor, session, pos, prefix, callback) { if (prefix.length === 0) { callback(null, []); @@ -76,10 +75,8 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ ); } } - } - langTools.addCompleter(completer); + }); }); - this._autocompletion_setup = true; }, From 58f1db0417352b076354aa963a6ee92200a68cbf Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 20 Apr 2021 16:08:11 +0530 Subject: [PATCH 072/112] fix: remove hardcoded value --- frappe/core/doctype/server_script/server_script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index ea27a2ac83..b791997a8b 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -162,7 +162,7 @@ class ServerScript(Document): return out items = frappe.cache().get_value('server_script_autocompletion_items') - if not items or True: + if not items: unsorted_items = get_keys(get_safe_globals()) sorted_items = sorted(unsorted_items, key=lambda k: k[1]) items = [d[0] for d in sorted_items] From b2302bab092436edb5c3a509b30e74cc70685d59 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 20 Apr 2021 16:23:20 +0530 Subject: [PATCH 073/112] fix: Pass score to ace to let it handle sorting --- .../doctype/server_script/server_script.py | 19 +++++++++---------- frappe/public/js/frappe/form/controls/code.js | 16 ++++++++++------ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index b791997a8b..f80a067cf1 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -140,32 +140,31 @@ class ServerScript(Document): value = obj[key] if isinstance(value, (NamespaceDict, dict)) and value: if key == 'form_dict': - out.append(['form_dict', 3]) + out.append(['form_dict', 7]) continue for subkey, score in get_keys(value): fullkey = f'{key}.{subkey}' out.append([fullkey, score]) else: - if isinstance(value, ModuleType): + if isinstance(value, type) and issubclass(value, Exception): score = 0 + elif isinstance(value, ModuleType): + score = 10 elif isinstance(value, (FunctionType, MethodType)): - score = 1 - elif isinstance(value, type) and issubclass(value, Exception): score = 9 elif isinstance(value, type): - score = 2 + score = 8 elif isinstance(value, dict): - score = 3 + score = 7 else: - score = 4 + score = 6 out.append([key, score]) return out items = frappe.cache().get_value('server_script_autocompletion_items') if not items: - unsorted_items = get_keys(get_safe_globals()) - sorted_items = sorted(unsorted_items, key=lambda k: k[1]) - items = [d[0] for d in sorted_items] + items = get_keys(get_safe_globals()) + items = [{'value': d[0], 'score': d[1]} for d in items] frappe.cache().set_value('server_script_autocompletion_items', items) return items diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 635146563e..33579b3b88 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -66,12 +66,16 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ if (autocompletions.length) { callback( null, - autocompletions.map(a => ({ - name: 'frappe', - value: a, - score: 100, - meta: 'Frappe API' - })) + autocompletions.map(a => { + if (typeof a === 'string') { + a = { value: a }; + } + return { + name: 'frappe', + value: a.value, + score: a.score + } + }) ); } } From c822bd3d770b70d04483f4f0dceb4881cea94d82 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 20 Apr 2021 21:46:20 +0530 Subject: [PATCH 074/112] style: missing semicolon --- frappe/public/js/frappe/form/controls/code.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index 33579b3b88..9600763588 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -74,7 +74,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ name: 'frappe', value: a.value, score: a.score - } + }; }) ); } From 865397c6b5d508f37233c681e00da53417b079fe Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 20 Apr 2021 21:41:39 +0530 Subject: [PATCH 075/112] fix: Resolve value in promise while validating link field --- frappe/public/js/frappe/form/controls/link.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 1a483c5968..1377ecc9ae 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -462,9 +462,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ if(this.frm && this.frm.fetch_dict[df.fieldname]) { fetch = this.frm.fetch_dict[df.fieldname].columns.join(', '); } - // if default and no fetch, no need to validate - if (!fetch && df.__default_value && df.__default_value===value) return value; + if (!fetch && df.__default_value && df.__default_value===value) { + resolve(value); + }; this.fetch_and_validate_link(resolve, df, doctype, docname, value, fetch); }); From 0e3eaa17d12f230f34672fef4e3d95f4575e7992 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 20 Apr 2021 22:03:43 +0530 Subject: [PATCH 076/112] style(sider): Remove unnecessary semicolon --- frappe/public/js/frappe/form/controls/link.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index 1377ecc9ae..c0ff128088 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -465,7 +465,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ // if default and no fetch, no need to validate if (!fetch && df.__default_value && df.__default_value===value) { resolve(value); - }; + } this.fetch_and_validate_link(resolve, df, doctype, docname, value, fetch); }); From ad3407c845ce9773b4e3cdec47525ddd4cde652c Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 6 Apr 2021 14:56:28 +0530 Subject: [PATCH 077/112] fix(backups): ensure delete_temp_backups always respects config --- frappe/utils/backups.py | 30 ++++-------------------------- 1 file changed, 4 insertions(+), 26 deletions(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 77c5761527..3c14cd9d5e 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -15,7 +15,7 @@ import click # imports - module imports import frappe from frappe import _, conf -from frappe.utils import get_file_size, get_url, now, now_datetime +from frappe.utils import get_file_size, get_url, now, now_datetime, cint # backup variable for backwards compatibility verbose = False @@ -474,29 +474,6 @@ download only after 24 hours.""" % { return recipient_list -@frappe.whitelist() -def get_backup(): - """ - This function is executed when the user clicks on - Toos > Download Backup - """ - delete_temp_backups() - odb = BackupGenerator( - frappe.conf.db_name, - frappe.conf.db_name, - frappe.conf.db_password, - db_host=frappe.db.host, - db_type=frappe.conf.db_type, - db_port=frappe.conf.db_port, - ) - odb.get_backup() - recipient_list = odb.send_email() - frappe.msgprint( - _( - "Download link for your backup will be emailed on the following email address: {0}" - ).format(", ".join(recipient_list)) - ) - @frappe.whitelist() def fetch_latest_backups(partial=False): """Fetches paths of the latest backup taken in the last 30 days @@ -570,7 +547,7 @@ def new_backup( force=False, verbose=False, ): - delete_temp_backups(older_than=frappe.conf.keep_backups_for_hours or 24) + delete_temp_backups() odb = BackupGenerator( frappe.conf.db_name, frappe.conf.db_name, @@ -593,10 +570,11 @@ def new_backup( return odb -def delete_temp_backups(older_than=24): +def delete_temp_backups(): """ Cleans up the backup_link_path directory by deleting files older than 24 hours """ + older_than = cint(frappe.conf.keep_backups_for_hours) or 24 backup_path = get_backup_path() if os.path.exists(backup_path): file_list = os.listdir(get_backup_path()) From b59eceb9a6963153f8fb0c43ca185f3d155ff5a0 Mon Sep 17 00:00:00 2001 From: walstanb Date: Fri, 9 Apr 2021 13:06:58 +0530 Subject: [PATCH 078/112] fix: minor changes --- frappe/utils/backups.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 3c14cd9d5e..9a6747a0cf 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -570,11 +570,11 @@ def new_backup( return odb -def delete_temp_backups(): +def delete_temp_backups(older_than=24): """ - Cleans up the backup_link_path directory by deleting files older than 24 hours + Cleans up the backup_link_path directory by deleting older files """ - older_than = cint(frappe.conf.keep_backups_for_hours) or 24 + older_than = cint(frappe.conf.keep_backups_for_hours) or older_than backup_path = get_backup_path() if os.path.exists(backup_path): file_list = os.listdir(get_backup_path()) From c28a7db70cd6f4e3df6d08b7908071ff39c0694f Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 21 Apr 2021 11:42:01 +0530 Subject: [PATCH 079/112] fix: Handle error while session start (#12933) - The occurs randomly at the time of boot --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index cab9b0da76..55cafa917a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -975,7 +975,7 @@ def get_pymodule_path(modulename, *joins): :param *joins: Join additional path elements using `os.path.join`.""" if not "public" in joins: joins = [scrub(part) for part in joins] - return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__), *joins) + return os.path.join(os.path.dirname(get_module(scrub(modulename)).__file__ or ''), *joins) def get_module_list(app_name): """Get list of modules for given all via `app/modules.txt`.""" From c7418b08d3f42c0395d009daa672a9600cd8ee6b Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 21 Apr 2021 12:15:54 +0530 Subject: [PATCH 080/112] fix: Ignore non utf-8 files for translation scan (#12935) --- frappe/translate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index a65a1c28c1..3565bbc32c 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -518,8 +518,13 @@ def get_messages_from_file(path): apps_path = get_bench_dir() if os.path.exists(path): with open(path, 'r') as sourcefile: + try: + file_contents = sourcefile.read() + except Exception: + print("Could not scan file for translation: {0}".format(path)) + return [] data = [(os.path.relpath(path, apps_path), message, context, line) \ - for line, message, context in extract_messages_from_code(sourcefile.read())] + for line, message, context in extract_messages_from_code(file_contents)] return data else: # print "Translate: {0} missing".format(os.path.abspath(path)) From d39622cd1643f9dce1bfe2901a7fe4d2a931b1ee Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 15 Apr 2021 10:20:59 +0530 Subject: [PATCH 081/112] fix: Load server translations in boot (backport #12848) (#12852) (cherry picked from commit a373c00abd1db85a4a8304fecc299890c74e163e) Co-authored-by: thebachy1 From 99df903328ce8e82f3876278abf3e48ae29fffc9 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 21 Apr 2021 11:43:34 +0530 Subject: [PATCH 082/112] chore: Add release notes for v13.1.0 (#12932) --- frappe/change_log/v13/v13_1_0.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 frappe/change_log/v13/v13_1_0.md diff --git a/frappe/change_log/v13/v13_1_0.md b/frappe/change_log/v13/v13_1_0.md new file mode 100644 index 0000000000..87c3bd0906 --- /dev/null +++ b/frappe/change_log/v13/v13_1_0.md @@ -0,0 +1,22 @@ +# Version 13.1.0 Release Notes + +### Features & Enhancements + +- Automated mail notifications will be shown in timeline ([#12693](https://github.com/frappe/frappe/pull/12693)) +- Introduced Client Script for List views ([#12590](https://github.com/frappe/frappe/pull/12590)) +- Introduced language switcher for guest users on website navbar ([#12813](https://github.com/frappe/frappe/pull/12813)) +- Option to give submit permission while sharing a document ([#12799](https://github.com/frappe/frappe/pull/12799)) +- Added option to set `autoname` in Customize Form ([#12413](https://github.com/frappe/frappe/pull/12413)) +- Virtual DocType ([#12121](https://github.com/frappe/frappe/pull/12121)) + +### Fixes + +- Workspace fixes ([#12650](https://github.com/frappe/frappe/pull/12650)) ([#12655](https://github.com/frappe/frappe/pull/12655)) ([#12869](https://github.com/frappe/frappe/pull/12869)) +- Fixed an issue where select options were not getting updated in Grid ([#12839](https://github.com/frappe/frappe/pull/12839)) +- Webform Fixes ([#12630](https://github.com/frappe/frappe/pull/12630)) ([#12756](https://github.com/frappe/frappe/pull/12756)) ([#12819](https://github.com/frappe/frappe/pull/12819)) +- Fixed timespan filter for next and last timespans ([#12509](https://github.com/frappe/frappe/pull/12509)) +- System Notification fixes ([#12719](https://github.com/frappe/frappe/pull/12719)) +- Design Fixes ([#12669](https://github.com/frappe/frappe/pull/12669)) ([#12591](https://github.com/frappe/frappe/pull/12591)) ([#12557](https://github.com/frappe/frappe/pull/12557)) ([#12751](https://github.com/frappe/frappe/pull/12751)) ([#12864](https://github.com/frappe/frappe/pull/12864)) +- Fixed Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861)) +- Fixed grid validation ([#12744](https://github.com/frappe/frappe/pull/12744)) +- Fixed currency value formatting in dashboard chart ([#12613](https://github.com/frappe/frappe/pull/12613)) From 470232cfa4baa84712c41f2e447cbdaba157e697 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 21 Apr 2021 12:53:59 +0550 Subject: [PATCH 083/112] bumped to version 13.1.0 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 55cafa917a..1bcf47e986 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -34,7 +34,7 @@ if PY2: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '13.0.0-dev' +__version__ = '13.1.0' __title__ = "Frappe Framework" From 3631dfdf49b54ada316b670cf1d8b931f7b06dd1 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Wed, 21 Apr 2021 15:22:41 +0530 Subject: [PATCH 084/112] ci: Fix coveralls (#12926) --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 08a2823dca..363191fd05 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -151,7 +151,7 @@ jobs: cd ${GITHUB_WORKSPACE} pip install coveralls==3.0.1 pip install coverage==5.5 - coveralls --service=github + coveralls --service=github-actions env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} From b1a14156946b9d4d2a40462d00b8f2b6b71ca21a Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Wed, 21 Apr 2021 14:44:26 +0530 Subject: [PATCH 085/112] fix: Use grid docfield list while creating row docfield copy Previously, it was using doctype level docfield list which did not had the updated docfields for a grid. --- frappe/public/js/frappe/form/grid_row.js | 1 + frappe/public/js/frappe/model/meta.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index e0fe1b3b54..4afa251c27 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -6,6 +6,7 @@ export default class GridRow { this.on_grid_fields = []; $.extend(this, opts); if (this.doc && this.parent_df.options) { + frappe.meta.make_docfield_copy_for(this.parent_df.options, this.doc.name, this.docfields); this.docfields = frappe.meta.get_docfields(this.parent_df.options, this.doc.name); } this.columns = {}; diff --git a/frappe/public/js/frappe/model/meta.js b/frappe/public/js/frappe/model/meta.js index c2fd6b1ae6..6ee9084adc 100644 --- a/frappe/public/js/frappe/model/meta.js +++ b/frappe/public/js/frappe/model/meta.js @@ -38,14 +38,14 @@ $.extend(frappe.meta, { frappe.meta.docfield_list[df.parent].push(df); }, - make_docfield_copy_for: function(doctype, docname) { + make_docfield_copy_for: function(doctype, docname, docfield_list=null) { var c = frappe.meta.docfield_copy; if(!c[doctype]) c[doctype] = {}; if(!c[doctype][docname]) c[doctype][docname] = {}; - var docfield_list = frappe.meta.docfield_list[doctype] || []; + docfield_list = docfield_list || frappe.meta.docfield_list[doctype] || []; for(var i=0, j=docfield_list.length; i Date: Thu, 22 Apr 2021 00:24:22 +0530 Subject: [PATCH 086/112] fix: Form Dashboard reference link (#12945) --- frappe/public/js/frappe/form/dashboard.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index 9b6d15c1fc..c1c95d94cf 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -290,7 +290,7 @@ frappe.ui.form.Dashboard = class FormDashboard { // bind links transactions_area_body.find(".badge-link").on('click', function() { - me.open_document_list($(this).parent()); + me.open_document_list($(this).closest('.document-link')); }); // bind reports From bb4c01fe9a4ba75d58eeac39d6bfbe78bff9ee29 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 22 Apr 2021 00:41:13 +0530 Subject: [PATCH 087/112] fix(query): Use single quotes for string constant (#12948) --- frappe/translate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 3565bbc32c..5be41f3568 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -115,7 +115,7 @@ def get_dict(fortype, name=None): messages.extend(get_server_messages(app)) messages = deduplicate_messages(messages) - messages += frappe.db.sql("""select "navbar", item_label from `tabNavbar Item` where item_label is not null""") + messages += frappe.db.sql("""select 'navbar', item_label from `tabNavbar Item` where item_label is not null""") messages = get_messages_from_include_files() messages += frappe.db.sql("select 'Print Format:', name from `tabPrint Format`") messages += frappe.db.sql("select 'DocType:', name from tabDocType") From 1f91cbda2611fec87d5889fc269214634ec75e21 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Thu, 22 Apr 2021 01:12:38 +0530 Subject: [PATCH 088/112] fix: build-message-files command (#12950) --- frappe/translate.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/frappe/translate.py b/frappe/translate.py index 5be41f3568..4baf4bdd89 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -606,11 +606,23 @@ def write_csv_file(path, app_messages, lang_dict): from csv import writer with open(path, 'w', newline='') as msgfile: w = writer(msgfile, lineterminator='\n') - for p, m in app_messages: - t = lang_dict.get(m, '') + + for app_message in app_messages: + context = None + if len(app_message) == 2: + path, message = app_message + elif len(app_message) == 3: + path, message, lineno = app_message + elif len(app_message) == 4: + path, message, context, lineno = app_message + else: + continue + + t = lang_dict.get(message, '') # strip whitespaces - t = re.sub('{\s?([0-9]+)\s?}', "{\g<1>}", t) - w.writerow([p if p else '', m, t]) + translated_string = re.sub('{\s?([0-9]+)\s?}', "{\g<1>}", t) + if translated_string: + w.writerow([message, translated_string, context]) def get_untranslated(lang, untranslated_file, get_all=False): """Returns all untranslated strings for a language and writes in a file From d4e5c884190bc9e214e0ba1612c13709e33dcbcf Mon Sep 17 00:00:00 2001 From: Anand Chitipothu Date: Thu, 22 Apr 2021 09:01:59 +0530 Subject: [PATCH 089/112] fix: Invalid HTML generated by the base template (#12953) Closes #12952 --- frappe/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/templates/base.html b/frappe/templates/base.html index 78aa573c99..d59c4b0f2b 100644 --- a/frappe/templates/base.html +++ b/frappe/templates/base.html @@ -60,7 +60,7 @@ window.is_chat_enabled = {{ chat_enable }}; - + {% include "public/icons/timeless/symbol-defs.svg" %} {%- block banner -%} {% include "templates/includes/banner_extension.html" ignore missing %} From d37d0007ea75c7c1b76928a4ef97deea55167a3c Mon Sep 17 00:00:00 2001 From: gavin Date: Thu, 22 Apr 2021 12:41:31 +0530 Subject: [PATCH 090/112] fix(cli): Trigger Scheduler Event (#12955) * Triggers events via Scheduled Job Type's execute method * Exits with code 1 if no event with that name found or process termination * Added feedback if event not found --- frappe/commands/scheduler.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/frappe/commands/scheduler.py b/frappe/commands/scheduler.py index bd9c9d2cb0..e9638800cd 100755 --- a/frappe/commands/scheduler.py +++ b/frappe/commands/scheduler.py @@ -18,22 +18,33 @@ def _is_scheduler_enabled(): return enable_scheduler -@click.command('trigger-scheduler-event') -@click.argument('event') + +@click.command("trigger-scheduler-event", help="Trigger a scheduler event") +@click.argument("event") @pass_context def trigger_scheduler_event(context, event): - "Trigger a scheduler event" import frappe.utils.scheduler + + exit_code = 0 + for site in context.sites: try: frappe.init(site=site) frappe.connect() - frappe.utils.scheduler.trigger(site, event, now=True) + try: + frappe.get_doc("Scheduled Job Type", {"method": event}).execute() + except frappe.DoesNotExistError: + click.secho(f"Event {event} does not exist!", fg="red") + exit_code = 1 finally: frappe.destroy() + if not context.sites: raise SiteNotSpecifiedError + sys.exit(exit_code) + + @click.command('enable-scheduler') @pass_context def enable_scheduler(context): From dc452d042941bf26ae74b136880c348f6eb42eb8 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 22 Apr 2021 12:24:12 +0530 Subject: [PATCH 091/112] perf: low priority for backup processes --- frappe/utils/__init__.py | 13 +++++++++++-- frappe/utils/backups.py | 14 ++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index efa69d4453..251a095343 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -307,14 +307,23 @@ def unesc(s, esc_chars): s = s.replace(esc_str, c) return s -def execute_in_shell(cmd, verbose=0): +def execute_in_shell(cmd, verbose=0, low_priority=False): # using Popen instead of os.system - as recommended by python docs import tempfile from subprocess import Popen with tempfile.TemporaryFile() as stdout: with tempfile.TemporaryFile() as stderr: - p = Popen(cmd, shell=True, stdout=stdout, stderr=stderr) + kwargs = { + "shell": True, + "stdout": stdout, + "stderr": stderr + } + + if low_priority: + kwargs["preexec_fn"] = lambda: os.nice(10) + + p = Popen(cmd, **kwargs) p.wait() stdout.seek(0) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 9a6747a0cf..90a6b94ff0 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -315,8 +315,6 @@ class BackupGenerator: print(template.format(_type.title(), info["path"], info["size"])) def backup_files(self): - import subprocess - for folder in ("public", "private"): files_path = frappe.get_site_path(folder, "files") backup_path = ( @@ -327,12 +325,12 @@ class BackupGenerator: cmd_string = "tar cf - {1} | gzip > {0}" else: cmd_string = "tar -cf {0} {1}" - output = subprocess.check_output( - cmd_string.format(backup_path, files_path), shell=True - ) - if self.verbose and output: - print(output.decode("utf8")) + frappe.utils.execute_in_shell( + cmd_string.format(backup_path, files_path), + verbose=self.verbose, + low_priority=True + ) def copy_site_config(self): site_config_backup_path = self.backup_path_conf @@ -436,7 +434,7 @@ class BackupGenerator: if self.verbose: print(command + "\n") - err, out = frappe.utils.execute_in_shell(command) + err, out = frappe.utils.execute_in_shell(command, low_priority=True) def send_email(self): """ From c899bb5cbd1db653dfb148557ac0ecc41066bbc8 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 22 Apr 2021 12:56:21 +0530 Subject: [PATCH 092/112] fix: remove unsused variables --- frappe/utils/backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 90a6b94ff0..b21efc5e89 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -434,7 +434,7 @@ class BackupGenerator: if self.verbose: print(command + "\n") - err, out = frappe.utils.execute_in_shell(command, low_priority=True) + frappe.utils.execute_in_shell(command, low_priority=True) def send_email(self): """ From 2db5e2242da491d4572ad0377388845ec2dc7ef1 Mon Sep 17 00:00:00 2001 From: Mohammad Hasnain Mohsin Rajan Date: Thu, 22 Apr 2021 15:53:52 +0530 Subject: [PATCH 093/112] ci: Set COVERALLS_SERVICE_NAME as github (#12961) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- .github/workflows/ci-tests.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 363191fd05..d2a00ab05f 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -151,7 +151,8 @@ jobs: cd ${GITHUB_WORKSPACE} pip install coveralls==3.0.1 pip install coverage==5.5 - coveralls --service=github-actions + coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }} + COVERALLS_SERVICE_NAME: github From e268e527e08fbf9fdb830e98482253664dd7a905 Mon Sep 17 00:00:00 2001 From: Richard Case Date: Thu, 11 Mar 2021 01:18:16 +0000 Subject: [PATCH 094/112] fix: build priority on computers with low memory fixes:frappe/bench#1135 --- frappe/commands/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index b9ae02e112..61ee62d352 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -62,11 +62,24 @@ def popen(command, *args, **kwargs): if env: env = dict(environ, **env) + def set_low_prio(): + import psutil + if psutil.LINUX: + psutil.Process().nice(19) + psutil.Process().ionice(psutil.IOPRIO_CLASS_IDLE) + elif psutil.WINDOWS: + psutil.Process().nice(psutil.IDLE_PRIORITY_CLASS) + psutil.Process().ionice(psutil.IOPRIO_VERYLOW) + else: + psutil.Process().nice(19) + # ionice not supported + proc = subprocess.Popen(command, stdout=None if output else subprocess.PIPE, stderr=None if output else subprocess.PIPE, shell=shell, cwd=cwd, + preexec_fn=set_low_prio, env=env ) From d4aad16c0ebcf107c70ddf560629d94222674077 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 22 Apr 2021 14:21:05 +0530 Subject: [PATCH 095/112] fix(control): Check if same value is set to avoid unnecessary change trigger --- .eslintrc | 1 + frappe/public/js/frappe/form/controls/base_control.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index d123023a68..8a509f0df4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -143,6 +143,7 @@ "Cypress": true, "cy": true, "it": true, + "describe": true, "expect": true, "context": true, "before": true, diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 9981398b84..b17ce973ec 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -159,9 +159,10 @@ frappe.ui.form.Control = Class.extend({ }, validate_and_set_in_model: function(value, e) { var me = this; - if(this.inside_change_event) { + if (this.inside_change_event || this.get_model_value() === value) { return Promise.resolve(); } + this.inside_change_event = true; var set = function(value) { me.inside_change_event = false; From 745e1d891e4ed05a3ac5b1fcca46576e3f0f5468 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 22 Apr 2021 15:59:33 +0530 Subject: [PATCH 096/112] fix: Override get_model_value for table multiselect --- frappe/public/js/frappe/form/controls/table_multiselect.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/table_multiselect.js b/frappe/public/js/frappe/form/controls/table_multiselect.js index c306146f90..eb3f1bce6e 100644 --- a/frappe/public/js/frappe/form/controls/table_multiselect.js +++ b/frappe/public/js/frappe/form/controls/table_multiselect.js @@ -66,6 +66,10 @@ frappe.ui.form.ControlTableMultiSelect = frappe.ui.form.ControlLink.extend({ this._rows_list = this.rows.map(row => row[link_field.fieldname]); return this.rows; }, + get_model_value() { + let value = this._super(); + return value ? value.filter(d => !d.__islocal) : value; + }, validate(value) { const rows = (value || []).slice(); From 3e1b195db0da862541f9198db2f9ad3738e2ba5b Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Thu, 22 Apr 2021 23:31:51 +0530 Subject: [PATCH 097/112] fix: Use node.string to extract style and script - node.text stopped working in beautifulsoup 4.9.x --- frappe/website/doctype/web_page/web_page.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/website/doctype/web_page/web_page.py b/frappe/website/doctype/web_page/web_page.py index 86774c79c4..cce00564ff 100644 --- a/frappe/website/doctype/web_page/web_page.py +++ b/frappe/website/doctype/web_page/web_page.py @@ -242,11 +242,11 @@ def extract_script_and_style_tags(html): styles = [] for script in soup.find_all('script'): - scripts.append(script.text) + scripts.append(script.string) script.extract() for style in soup.find_all('style'): - styles.append(style.text) + styles.append(style.string) style.extract() return str(soup), scripts, styles From 02df4a783a76d84a4e60e5c1f6895368156d64f1 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Fri, 23 Apr 2021 09:04:34 +0530 Subject: [PATCH 098/112] ci(semgrep): add more rules, r/python.correctness (#12876) * ci(semgrep): add more rules, r/python.correctness - Added file for defining rules as per frappe data model: frappe_correctness.yml - Add rule for SQLi, with WARNING only for now - Add rule file for UX - WARNING | INFO do not fail the build now * ci(semgrep): on_cancel, on_submit correctness rule * ci(semgrep): split workflow in steps * ci(semgrep): catch line breaks in _() * chore: fix sider issue --- .../semgrep_rules/frappe_correctness.py | 28 ++++ .../semgrep_rules/frappe_correctness.yml | 135 ++++++++++++++++++ .github/helper/semgrep_rules/security.yml | 15 ++ .github/helper/semgrep_rules/translate.yml | 3 +- .github/helper/semgrep_rules/ux.py | 31 ++++ .github/helper/semgrep_rules/ux.yml | 15 ++ .github/workflows/semgrep.yml | 14 +- 7 files changed, 238 insertions(+), 3 deletions(-) create mode 100644 .github/helper/semgrep_rules/frappe_correctness.py create mode 100644 .github/helper/semgrep_rules/frappe_correctness.yml create mode 100644 .github/helper/semgrep_rules/ux.py create mode 100644 .github/helper/semgrep_rules/ux.yml diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py new file mode 100644 index 0000000000..37889fbbb1 --- /dev/null +++ b/.github/helper/semgrep_rules/frappe_correctness.py @@ -0,0 +1,28 @@ +import frappe +from frappe import _, flt + +from frappe.model.document import Document + + +def on_submit(self): + if self.value_of_goods == 0: + frappe.throw(_('Value of goods cannot be 0')) + # ruleid: frappe-modifying-after-submit + self.status = 'Submitted' + +def on_submit(self): # noqa + if flt(self.per_billed) < 100: + self.update_billing_status() + else: + # todook: frappe-modifying-after-submit + self.status = "Completed" + self.db_set("status", "Completed") + +class TestDoc(Document): + pass + + def validate(self): + #ruleid: frappe-modifying-child-tables-while-iterating + for item in self.child_table: + if item.value < 0: + self.remove(item) diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml new file mode 100644 index 0000000000..faab3344a6 --- /dev/null +++ b/.github/helper/semgrep_rules/frappe_correctness.yml @@ -0,0 +1,135 @@ +# This file specifies rules for correctness according to how frappe doctype data model works. + +rules: +- id: frappe-modifying-but-not-comitting + patterns: + - pattern: | + def $METHOD(self, ...): + ... + self.$ATTR = ... + - pattern-not: | + def $METHOD(self, ...): + ... + self.$ATTR = ... + ... + self.db_set(..., self.$ATTR, ...) + - pattern-not: | + def $METHOD(self, ...): + ... + self.$ATTR = $SOME_VAR + ... + self.db_set(..., $SOME_VAR, ...) + - pattern-not: | + def $METHOD(self, ...): + ... + self.$ATTR = $SOME_VAR + ... + self.save() + - metavariable-regex: + metavariable: '$ATTR' + # this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me) + regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$' + - metavariable-regex: + metavariable: "$METHOD" + regex: "(on_submit|on_cancel)" + message: | + DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database. + languages: [python] + severity: ERROR + +- id: frappe-modifying-but-not-comitting-other-method + patterns: + - pattern: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = ... + - pattern-not: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = ... + ... + self.db_set(..., self.$ATTR, ...) + - pattern-not: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = $SOME_VAR + ... + self.db_set(..., $SOME_VAR, ...) + - pattern-not: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + self.save() + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = ... + - metavariable-regex: + metavariable: "$METHOD" + regex: "(on_submit|on_cancel)" + message: | + self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database. + languages: [python] + severity: ERROR + +- id: frappe-print-function-in-doctypes + pattern: print(...) + message: | + Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement. + languages: [python] + severity: WARNING + paths: + exclude: + - test_*.py + include: + - "*/**/doctype/*" + +- id: frappe-modifying-child-tables-while-iterating + pattern-either: + - pattern: | + for $ROW in self.$TABLE: + ... + self.remove(...) + - pattern: | + for $ROW in self.$TABLE: + ... + self.append(...) + message: | + Child table being modified while iterating on it. + languages: [python] + severity: ERROR + paths: + include: + - "*/**/doctype/*" + +- id: frappe-same-key-assigned-twice + pattern-either: + - pattern: | + {..., $X: $A, ..., $X: $B, ...} + - pattern: | + dict(..., ($X, $A), ..., ($X, $B), ...) + - pattern: | + _dict(..., ($X, $A), ..., ($X, $B), ...) + message: | + key `$X` is uselessly assigned twice. This could be a potential bug. + languages: [python] + severity: ERROR diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml index 1937fc0e52..b2cc4b16fc 100644 --- a/.github/helper/semgrep_rules/security.yml +++ b/.github/helper/semgrep_rules/security.yml @@ -12,3 +12,18 @@ rules: exclude: - frappe/__init__.py - frappe/commands/utils.py + +- id: frappe-sqli-format-strings + patterns: + - pattern-inside: | + @frappe.whitelist() + def $FUNC(...): + ... + - pattern-either: + - pattern: frappe.db.sql("..." % ...) + - pattern: frappe.db.sql(f"...", ...) + - pattern: frappe.db.sql("...".format(...), ...) + message: | + Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines + languages: [python] + severity: WARNING diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml index 3737da5a7e..df55089b9f 100644 --- a/.github/helper/semgrep_rules/translate.yml +++ b/.github/helper/semgrep_rules/translate.yml @@ -44,7 +44,8 @@ rules: pattern-either: - pattern: _(...) + ... + _(...) - pattern: _("..." + "...") - - pattern-regex: '_\([^\)]*\\\s*' + - pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\` + - pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( ) message: | Do not split strings inside translate function. Do not concatenate using translate functions. Please refer: https://frappeframework.com/docs/user/en/translations diff --git a/.github/helper/semgrep_rules/ux.py b/.github/helper/semgrep_rules/ux.py new file mode 100644 index 0000000000..4a74457435 --- /dev/null +++ b/.github/helper/semgrep_rules/ux.py @@ -0,0 +1,31 @@ +import frappe +from frappe import msgprint, throw, _ + + +# ruleid: frappe-missing-translate-function +throw("Error Occured") + +# ruleid: frappe-missing-translate-function +frappe.throw("Error Occured") + +# ruleid: frappe-missing-translate-function +frappe.msgprint("Useful message") + +# ruleid: frappe-missing-translate-function +msgprint("Useful message") + + +# ok: frappe-missing-translate-function +translatedmessage = _("Hello") + +# ok: frappe-missing-translate-function +throw(translatedmessage) + +# ok: frappe-missing-translate-function +msgprint(translatedmessage) + +# ok: frappe-missing-translate-function +msgprint(_("Helpful message")) + +# ok: frappe-missing-translate-function +frappe.throw(_("Error occured")) diff --git a/.github/helper/semgrep_rules/ux.yml b/.github/helper/semgrep_rules/ux.yml new file mode 100644 index 0000000000..ed06a6a80c --- /dev/null +++ b/.github/helper/semgrep_rules/ux.yml @@ -0,0 +1,15 @@ +rules: +- id: frappe-missing-translate-function + pattern-either: + - patterns: + - pattern: frappe.msgprint("...", ...) + - pattern-not: frappe.msgprint(_("..."), ...) + - pattern-not: frappe.msgprint(__("..."), ...) + - patterns: + - pattern: frappe.throw("...", ...) + - pattern-not: frappe.throw(_("..."), ...) + - pattern-not: frappe.throw(__("..."), ...) + message: | + All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations + languages: [python, javascript, json] + severity: ERROR diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml index 1d5694f521..5092bf4705 100644 --- a/.github/workflows/semgrep.yml +++ b/.github/workflows/semgrep.yml @@ -14,9 +14,19 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.8 - - name: Run semgrep + + - name: Setup semgrep run: | python -m pip install -q semgrep git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q + + - name: Semgrep errors + run: | files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) - [[ -d .github/helper/semgrep_rules ]] && semgrep --config=.github/helper/semgrep_rules --quiet --error $files + [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files + semgrep --config="r/python.lang.correctness" --quiet --error $files + + - name: Semgrep warnings + run: | + files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) + [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files From f3a6b32f8da318a55e45f439f3f4cbe103cc4431 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Thu, 22 Apr 2021 12:43:30 +0530 Subject: [PATCH 099/112] fix: Hide grid Add Row & Add Multiple buttons when document grid is not editable --- frappe/public/js/frappe/form/grid.js | 2 ++ frappe/public/js/frappe/form/grid_row.js | 4 ++-- frappe/public/js/frappe/form/grid_row_form.js | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 86feefed7a..30a3597ec7 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -387,6 +387,8 @@ export default class Grid { this.wrapper.find('.grid-footer').toggle(false); } + this.wrapper.find('.grid-add-row, .grid-add-multiple-rows').toggle(this.is_editable()) + } truncate_rows() { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 4afa251c27..08267112de 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -558,10 +558,10 @@ export default class GridRow { // this.form_panel.toggle(true); if (this.grid.cannot_add_rows || (this.grid.df && this.grid.df.cannot_add_rows)) { - this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row') + this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row') .addClass('hidden'); } else { - this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row') + this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row') .removeClass('hidden'); } diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js index 68e4178ae7..f5a4af206f 100644 --- a/frappe/public/js/frappe/form/grid_row_form.js +++ b/frappe/public/js/frappe/form/grid_row_form.js @@ -119,7 +119,7 @@ export default class GridRowForm { }); } toggle_add_delete_button_display($parent) { - $parent.find(".row-actions") + $parent.find(".row-actions, .grid-append-row") .toggle(this.row.grid.is_editable()); } refresh_field(fieldname) { From e3887cef1886b48376d619f6077d465fee3765da Mon Sep 17 00:00:00 2001 From: shariquerik Date: Thu, 22 Apr 2021 13:34:45 +0530 Subject: [PATCH 100/112] refactor: Using toggle instead of addClass-removeClass --- frappe/public/js/frappe/form/grid.js | 2 +- frappe/public/js/frappe/form/grid_row.js | 11 ++++------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 30a3597ec7..4d381c9be7 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -387,7 +387,7 @@ export default class Grid { this.wrapper.find('.grid-footer').toggle(false); } - this.wrapper.find('.grid-add-row, .grid-add-multiple-rows').toggle(this.is_editable()) + this.wrapper.find('.grid-add-row, .grid-add-multiple-rows').toggle(this.is_editable()); } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 08267112de..9a689fabf4 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -557,13 +557,10 @@ export default class GridRow { this.row.toggle(false); // this.form_panel.toggle(true); - if (this.grid.cannot_add_rows || (this.grid.df && this.grid.df.cannot_add_rows)) { - this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row') - .addClass('hidden'); - } else { - this.wrapper.find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row') - .removeClass('hidden'); - } + let cannot_add_rows = this.grid.cannot_add_rows || (this.grid.df && this.grid.df.cannot_add_rows); + this.wrapper + .find('.grid-insert-row-below, .grid-insert-row, .grid-duplicate-row, .grid-append-row') + .toggle(!cannot_add_rows); frappe.dom.freeze("", "dark"); if (cur_frm) cur_frm.cur_grid = this; From 46ddad3509e7d20a4801658fc9e69347831ea8f1 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sun, 18 Apr 2021 18:56:45 +0530 Subject: [PATCH 101/112] feat(hooks): auth hooks hook for request authentication --- frappe/api.py | 33 +++++++++++++++------------------ frappe/utils/boilerplate.py | 7 +++++++ 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 6a09b795b0..3a1be2593e 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -149,24 +149,17 @@ def get_request_form_data(): return frappe.parse_json(data) def validate_auth(): - if frappe.get_request_header("Authorization") is None: - return - VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token'] VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title() authorization_header = frappe.get_request_header("Authorization", str()).split(" ") authorization_type = authorization_header[0].lower() - if len(authorization_header) == 1: - frappe.throw(_('Invalid Authorization headers, add a token with a prefix from one of the following: {0}.').format(VALID_AUTH_PREFIX_STRING), frappe.InvalidAuthorizationHeader) - - if authorization_type == "bearer": + if len(authorization_header) == 2: validate_oauth(authorization_header) - elif authorization_type in VALID_AUTH_PREFIX_TYPES: validate_auth_via_api_keys(authorization_header) - else: - frappe.throw(_('Invalid Authorization Type {0}, must be one of {1}.').format(authorization_type, VALID_AUTH_PREFIX_STRING), frappe.InvalidAuthorizationPrefix) + + validate_auth_via_hooks() def validate_oauth(authorization_header): @@ -192,14 +185,13 @@ def validate_oauth(authorization_header): try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict except AttributeError: - frappe.throw(_("Invalid Bearer token, please provide a valid access token with prefix 'Bearer'."), frappe.InvalidAuthorizationToken) - - valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict + pass def validate_auth_via_api_keys(authorization_header): @@ -222,7 +214,7 @@ def validate_auth_via_api_keys(authorization_header): except binascii.Error: frappe.throw(_("Failed to decode token, please provide a valid base64-encoded token."), frappe.InvalidAuthorizationToken) except (AttributeError, TypeError, ValueError): - frappe.throw(_("Invalid token, please provide a valid token with prefix 'Basic' or 'Token'."), frappe.InvalidAuthorizationToken) + pass @@ -248,3 +240,8 @@ def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=Non if frappe.local.login_manager.user in ('', 'Guest'): frappe.set_user(user) frappe.local.form_dict = form_dict + + +def validate_auth_via_hooks(): + for auth_hook in frappe.get_hooks('auth_hooks', []): + frappe.get_attr(auth_hook)() diff --git a/frappe/utils/boilerplate.py b/frappe/utils/boilerplate.py index e59f579f75..ffcb64cff2 100755 --- a/frappe/utils/boilerplate.py +++ b/frappe/utils/boilerplate.py @@ -303,6 +303,13 @@ user_data_fields = [ }} ] +# Authentication and authorization +# -------------------------------- + +# auth_hooks = [ +# "{app_name}.auth.validate" +# ] + """ desktop_template = """# -*- coding: utf-8 -*- From 72972520b95d2c8fbb26d3678a095e3ac0f51279 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Mon, 19 Apr 2021 13:39:39 +0530 Subject: [PATCH 102/112] fix: remove unused variables --- frappe/api.py | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 3a1be2593e..59f14b54c8 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -1,12 +1,10 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt -from __future__ import unicode_literals import base64 import binascii import json - -from six.moves.urllib.parse import urlencode, urlparse +from urllib.parse import urlencode, urlparse import frappe import frappe.client @@ -14,6 +12,7 @@ import frappe.handler from frappe import _ from frappe.utils.response import build_response + def handle(): """ Handler for `/api` methods @@ -38,7 +37,6 @@ def handle(): `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method """ - validate_auth() parts = frappe.request.path[1:].split("/",3) @@ -116,7 +114,7 @@ def handle(): frappe.local.form_dict['fields'] = json.loads(frappe.local.form_dict['fields']) frappe.local.form_dict.setdefault('limit_page_length', 20) frappe.local.response.update({ - "data": frappe.call( + "data": frappe.call( frappe.client.get_list, doctype, **frappe.local.form_dict @@ -140,6 +138,7 @@ def handle(): return build_response("json") + def get_request_form_data(): if frappe.local.form_dict.data is None: data = frappe.safe_decode(frappe.local.request.get_data()) @@ -148,12 +147,9 @@ def get_request_form_data(): return frappe.parse_json(data) -def validate_auth(): - VALID_AUTH_PREFIX_TYPES = ['basic', 'bearer', 'token'] - VALID_AUTH_PREFIX_STRING = ", ".join(VALID_AUTH_PREFIX_TYPES).title() +def validate_auth(): authorization_header = frappe.get_request_header("Authorization", str()).split(" ") - authorization_type = authorization_header[0].lower() if len(authorization_header) == 2: validate_oauth(authorization_header) @@ -170,8 +166,8 @@ def validate_oauth(authorization_header): authorization_header (list of str): The 'Authorization' header containing the prefix and token """ - from frappe.oauth import get_url_delimiter from frappe.integrations.oauth2 import get_oauth_server + from frappe.oauth import get_url_delimiter form_dict = frappe.local.form_dict token = authorization_header[1] @@ -185,14 +181,14 @@ def validate_oauth(authorization_header): try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) - valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict except AttributeError: pass + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict + def validate_auth_via_api_keys(authorization_header): """ @@ -217,7 +213,6 @@ def validate_auth_via_api_keys(authorization_header): pass - def validate_api_key_secret(api_key, api_secret, frappe_authorization_source=None): """frappe_authorization_source to provide api key and secret for a doctype apart from User""" doctype = frappe_authorization_source or 'User' From 2adadd58290d029ba9b759fc72f28dde5c53454c Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 19 Apr 2021 15:18:15 +0530 Subject: [PATCH 103/112] fix: duplicate validate_auth calls --- frappe/api.py | 8 ++++---- frappe/handler.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 59f14b54c8..4117c49333 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -181,13 +181,13 @@ def validate_oauth(authorization_header): try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) + valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) + if valid: + frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) + frappe.local.form_dict = form_dict except AttributeError: pass - valid, oauthlib_request = get_oauth_server().verify_request(uri, http_method, body, headers, required_scopes) - if valid: - frappe.set_user(frappe.db.get_value("OAuth Bearer Token", token, "user")) - frappe.local.form_dict = form_dict def validate_auth_via_api_keys(authorization_header): diff --git a/frappe/handler.py b/frappe/handler.py index 82c1ea65c6..1897abe019 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -24,7 +24,7 @@ ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/ def handle(): """handle request""" - validate_auth() + cmd = frappe.local.form_dict.cmd data = None From 37cd622628c22f0fe494645d5bce71abf7cdb17e Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Mon, 19 Apr 2021 15:54:16 +0530 Subject: [PATCH 104/112] fix: remove unused imports --- frappe/api.py | 5 +++-- frappe/app.py | 1 + frappe/handler.py | 1 - 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/api.py b/frappe/api.py index 4117c49333..4a120f228a 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -37,8 +37,6 @@ def handle(): `/api/resource/{doctype}/{name}?run_method={method}` will run a whitelisted controller method """ - validate_auth() - parts = frappe.request.path[1:].split("/",3) call = doctype = name = None @@ -149,6 +147,9 @@ def get_request_form_data(): def validate_auth(): + """ + Authenticate and sets user for the request. + """ authorization_header = frappe.get_request_header("Authorization", str()).split(" ") if len(authorization_header) == 2: diff --git a/frappe/app.py b/frappe/app.py index 607479ad52..c9e993a853 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -56,6 +56,7 @@ def application(request): frappe.recorder.record() frappe.monitor.start() frappe.rate_limiter.apply() + frappe.api.validate_auth() if request.method == "OPTIONS": response = Response() diff --git a/frappe/handler.py b/frappe/handler.py index 1897abe019..a38feb90fa 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -9,7 +9,6 @@ import frappe import frappe.utils import frappe.sessions from frappe.utils import cint -from frappe.api import validate_auth from frappe import _, is_whitelisted from frappe.utils.response import build_response from frappe.utils.csvutils import build_csv_response From a5ca01f4e2984c33fef6d069c3760655524573d4 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Fri, 23 Apr 2021 14:40:47 +0530 Subject: [PATCH 105/112] docs: add docker repo link in README --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b00d291b96..e00bea7857 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,8 @@ Full-stack web application framework that uses Python and MariaDB on the server ### Installation -[Install via Frappe Bench](https://github.com/frappe/bench) +* [Install via Docker](https://github.com/frappe/frappe_docker) +* [Install via Frappe Bench](https://github.com/frappe/bench) ## Contributing From e603263d26f57dfe9ff07fa0bd6d638187553d67 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 23 Apr 2021 20:03:41 +0530 Subject: [PATCH 106/112] fix: Default values were not triggering change event (#12975) --- frappe/public/js/frappe/form/controls/base_control.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index b17ce973ec..8c2c5c4338 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -159,7 +159,10 @@ frappe.ui.form.Control = Class.extend({ }, validate_and_set_in_model: function(value, e) { var me = this; - if (this.inside_change_event || this.get_model_value() === value) { + let force_value_set = (this.doc && this.doc.__run_link_triggers); + let is_value_same = (this.get_model_value() === value); + + if (this.inside_change_event || (!force_value_set && is_value_same)) { return Promise.resolve(); } From 67149a61e46e904b66671f08158e5d7f6b36c50c Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 23 Apr 2021 20:35:55 +0530 Subject: [PATCH 107/112] fix: Currency labels in grids (#12974) --- frappe/public/js/frappe/form/form.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index de9331a726..2b7562f836 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1203,8 +1203,7 @@ frappe.ui.form.Form = class FrappeForm { $.each(grid_field_label_map, function(fname, label) { fname = fname.split("-"); - var df = frappe.meta.get_docfield(fname[0], fname[1], me.doc.name); - if(df) df.label = label; + me.fields_dict[parentfield].grid.update_docfield_property(fname[1], 'label', label); }); } From 583abc959aed29788628242fea9ad44fac0d784b Mon Sep 17 00:00:00 2001 From: walstanb Date: Sat, 24 Apr 2021 13:48:40 +0530 Subject: [PATCH 108/112] chore: frappe.whitelist for doc methods --- frappe/email/doctype/newsletter/newsletter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index c792347c09..6412338e96 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -24,6 +24,7 @@ class Newsletter(WebsiteGenerator): if self.send_from: validate_email_address(self.send_from, True) + @frappe.whitelist() def test_send(self, doctype="Lead"): self.recipients = frappe.utils.split_emails(self.test_email_id) self.queue_all(test_email=True) From d7102b510f8ee63761b400fa5be08d31e6e58e8f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 25 Apr 2021 11:00:52 +0530 Subject: [PATCH 109/112] refactor: Remove events to redraw charts (#12973) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/public/js/frappe/widgets/chart_widget.js | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/frappe/public/js/frappe/widgets/chart_widget.js b/frappe/public/js/frappe/widgets/chart_widget.js index 01314b436f..0c36f013ec 100644 --- a/frappe/public/js/frappe/widgets/chart_widget.js +++ b/frappe/public/js/frappe/widgets/chart_widget.js @@ -25,7 +25,6 @@ export default class ChartWidget extends Widget { delete this.dashboard_chart; this.set_body(); this.make_chart(); - this.setup_events(); } set_chart_title() { @@ -747,18 +746,4 @@ export default class ChartWidget extends Widget { } }); } - - setup_events() { - $(document.body).on('toggleSidebar', () => { - this.dashboard_chart && this.dashboard_chart.draw(true); - }); - - $(document.body).on('toggleListSidebar', () => { - this.dashboard_chart && this.dashboard_chart.draw(true); - }); - - $(document.body).on('toggleFullWidth', () => { - this.dashboard_chart && this.dashboard_chart.draw(true); - }); - } } From 8e0228b64c11e10a29e2c9589fa3cc94a8209de5 Mon Sep 17 00:00:00 2001 From: Fahim Ali Zain Date: Fri, 23 Apr 2021 01:33:32 +0530 Subject: [PATCH 110/112] fix: multipart/form-data breaks with OAuth tokens --- frappe/api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/api.py b/frappe/api.py index 4a120f228a..9039ae0e5f 100644 --- a/frappe/api.py +++ b/frappe/api.py @@ -177,8 +177,10 @@ def validate_oauth(authorization_header): access_token = {"access_token": token} uri = parsed_url.scheme + "://" + parsed_url.netloc + parsed_url.path + "?" + urlencode(access_token) http_method = req.method - body = req.get_data() headers = req.headers + body = req.get_data() + if req.content_type and "multipart/form-data" in req.content_type: + body = None try: required_scopes = frappe.db.get_value("OAuth Bearer Token", token, "scopes").split(get_url_delimiter()) From febb26e935c40e59e3b6e8172c874dd8fab77b5c Mon Sep 17 00:00:00 2001 From: "hasnain2808@gmail.com" Date: Wed, 14 Apr 2021 13:52:43 +0530 Subject: [PATCH 111/112] feat: allow button of different sizes in df --- frappe/public/js/frappe/form/controls/button.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/button.js b/frappe/public/js/frappe/form/controls/button.js index b44c9d9dcd..d09e9c3a95 100644 --- a/frappe/public/js/frappe/form/controls/button.js +++ b/frappe/public/js/frappe/form/controls/button.js @@ -6,7 +6,10 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ make_input: function() { var me = this; const btn_type = this.df.primary ? 'btn-primary': 'btn-default'; - this.$input = $(`