From 82b98330fd0f0cbfcbd62b6775fc3955796e4d01 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 14 Apr 2021 19:50:08 +0530 Subject: [PATCH 001/213] feat: Add URL option for data type fields --- frappe/model/__init__.py | 3 ++- frappe/model/base_document.py | 3 +++ frappe/public/js/frappe/form/controls/data.js | 3 +++ frappe/public/js/frappe/utils/datatype.js | 4 ++++ frappe/utils/__init__.py | 15 ++++++++++++++- 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index af06696621..205b451336 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -71,7 +71,8 @@ numeric_fieldtypes = ( data_field_options = ( 'Email', 'Name', - 'Phone' + 'Phone', + 'URL' ) default_fields = ( diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 983511f7a4..cf63aa98b6 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -666,6 +666,9 @@ class BaseDocument(object): if data_field_options == "Phone": frappe.utils.validate_phone_number(data, throw=True) + if data_field_options == "URL": + frappe.utils.validate_url(data, throw=True) + def _validate_constants(self): if frappe.flags.in_import or self.is_new() or self.flags.ignore_validate_constants: return diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index f381d1b4a2..b4d24d9a8f 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -126,6 +126,9 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.df.invalid = email_invalid; return v; } + } else if (this.df.options == 'URL') { + this.df.invalid = !validate_url(v); + return v; } else { return v; } diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js index 1b9206f434..ad0fd4324c 100644 --- a/frappe/public/js/frappe/utils/datatype.js +++ b/frappe/public/js/frappe/utils/datatype.js @@ -52,6 +52,10 @@ window.validate_name = function(txt) { return frappe.utils.validate_type(txt, "name"); }; +window.validate_url = function(txt) { + return frappe.utils.validate_type(txt, "url"); +} + window.nth = function(number) { number = cint(number); var s = 'th'; diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index efa69d4453..3e397afee6 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -19,7 +19,7 @@ from gzip import GzipFile from typing import Generator, Iterable from six import string_types, text_type -from six.moves.urllib.parse import quote +from six.moves.urllib.parse import quote, urlparse from werkzeug.test import Client import frappe @@ -161,6 +161,19 @@ def split_emails(txt): return email_list +def validate_url(txt, throw=False): + try: + url = urlparse(txt).netloc + if not url: + raise frappe.ValidationError + except Exception as e: + if throw: + frappe.throw( + frappe._("'{0}' is not a valid URL").format(txt) + ) + + return False + def random_string(length): """generate a random string""" import string From ce2dabed78b5da6e4b428d600c208ec8fc6e1a14 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Thu, 15 Apr 2021 06:02:29 +0530 Subject: [PATCH 002/213] fix: Call to translate function --- frappe/utils/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 3e397afee6..5992fdb6db 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -169,7 +169,9 @@ def validate_url(txt, throw=False): except Exception as e: if throw: frappe.throw( - frappe._("'{0}' is not a valid URL").format(txt) + frappe._( + "'{0}' is not a valid URL" + ).format('' + +'') ) return False From 4d91f318f94781a74e2b0c6361460966e79ab154 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 19 Apr 2021 12:32:19 +0530 Subject: [PATCH 003/213] test: UI with form validation --- .../fixtures/data_field_validation_doctype.js | 57 +++++++++++++++++++ .../integration/data_field_form_validation.js | 39 +++++++++++++ 2 files changed, 96 insertions(+) create mode 100644 cypress/fixtures/data_field_validation_doctype.js create mode 100644 cypress/integration/data_field_form_validation.js diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js new file mode 100644 index 0000000000..75fa88554e --- /dev/null +++ b/cypress/fixtures/data_field_validation_doctype.js @@ -0,0 +1,57 @@ +export default { + name: 'Validation Test', + custom: 1, + actions: [], + creation: '2019-03-15 06:29:07.215072', + doctype: 'DocType', + editable_grid: 1, + engine: 'InnoDB', + fields: [ + { + fieldname: 'email', + fieldtype: 'Data', + label: 'Email', + options: 'Email' + }, + { + fieldname: 'URL', + fieldtype: 'Data', + label: 'URL', + options: 'URL' + }, + { + fieldname: 'Phone', + fieldtype: 'Data', + label: 'Phone', + options: 'Phone' + }, + { + fieldname: 'person_name', + fieldtype: 'Data', + label: 'Person Name', + options: 'Name' + } + ], + issingle: 1, + links: [], + modified: '2021-04-19 14:40:53.127615', + modified_by: 'Administrator', + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1 + } + ], + quick_entry: 1, + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js new file mode 100644 index 0000000000..17a7f8e154 --- /dev/null +++ b/cypress/integration/data_field_form_validation.js @@ -0,0 +1,39 @@ +import data_field_validation_doctype from '../fixtures/data_field_validation_doctype'; +const doctype_name = data_field_validation_doctype.name; + + +context('Data Field Input Validation in New Form', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + return cy.insert_doc('DocType', data_field_validation_doctype, true); + }); + + function validateField(fieldname, invalid_value, valid_value) { + // Invalid, should have has-error class + cy.get_field(fieldname).type(invalid_value).blur(); + cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('have.class', 'has-error'); + // Valid value, should not have has-error class + cy.get_field(fieldname).clear().type(valid_value); + cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error'); + } + + describe('Data Field Options', () => { + it('should validate email address', () => { + cy.new_form(doctype_name); + validateField('email', 'captian', 'hello@test.com'); + }); + + it('should validate URL', () => { + validateField('url', 'jkl', 'https://frappe.io'); + }); + + it('should validate phone number', () => { + validateField('phone', 'america', '89787878'); + }); + + it('should validate name', () => { + validateField('person_name', ' 777Hello', 'James Bond'); + }); + }); +}); \ No newline at end of file From ea38895f1a45cd1c163254d10cfac1d5c3284cce Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Mon, 19 Apr 2021 13:17:20 +0530 Subject: [PATCH 004/213] fix: Sider Issues --- .eslintrc | 2 ++ cypress/fixtures/data_field_validation_doctype.js | 4 ++-- cypress/integration/data_field_form_validation.js | 4 ++-- frappe/public/js/frappe/utils/datatype.js | 2 +- frappe/utils/__init__.py | 4 ++-- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.eslintrc b/.eslintrc index d123023a68..2d17d7937b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -80,6 +80,7 @@ "validate_email": true, "validate_name": true, "validate_phone": true, + "validate_url": true, "get_number_format": true, "format_number": true, "format_currency": true, @@ -144,6 +145,7 @@ "cy": true, "it": true, "expect": true, + "describe": true, "context": true, "before": true, "beforeEach": true, diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js index 75fa88554e..469ff8ca24 100644 --- a/cypress/fixtures/data_field_validation_doctype.js +++ b/cypress/fixtures/data_field_validation_doctype.js @@ -11,13 +11,13 @@ export default { fieldname: 'email', fieldtype: 'Data', label: 'Email', - options: 'Email' + options: 'Email' }, { fieldname: 'URL', fieldtype: 'Data', label: 'URL', - options: 'URL' + options: 'URL' }, { fieldname: 'Phone', diff --git a/cypress/integration/data_field_form_validation.js b/cypress/integration/data_field_form_validation.js index 17a7f8e154..e6f6f13df6 100644 --- a/cypress/integration/data_field_form_validation.js +++ b/cypress/integration/data_field_form_validation.js @@ -18,7 +18,7 @@ context('Data Field Input Validation in New Form', () => { cy.get(`.frappe-control[data-fieldname="${fieldname}"]`).should('not.have.class', 'has-error'); } - describe('Data Field Options', () => { + describe('Data Field Options', () => { it('should validate email address', () => { cy.new_form(doctype_name); validateField('email', 'captian', 'hello@test.com'); @@ -35,5 +35,5 @@ context('Data Field Input Validation in New Form', () => { it('should validate name', () => { validateField('person_name', ' 777Hello', 'James Bond'); }); - }); + }); }); \ No newline at end of file diff --git a/frappe/public/js/frappe/utils/datatype.js b/frappe/public/js/frappe/utils/datatype.js index ad0fd4324c..944d3fca1a 100644 --- a/frappe/public/js/frappe/utils/datatype.js +++ b/frappe/public/js/frappe/utils/datatype.js @@ -54,7 +54,7 @@ window.validate_name = function(txt) { window.validate_url = function(txt) { return frappe.utils.validate_type(txt, "url"); -} +}; window.nth = function(number) { number = cint(number); diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 5992fdb6db..2fdaa05404 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -166,12 +166,12 @@ def validate_url(txt, throw=False): url = urlparse(txt).netloc if not url: raise frappe.ValidationError - except Exception as e: + except Exception: if throw: frappe.throw( frappe._( "'{0}' is not a valid URL" - ).format('' + +'') + ).format('' + txt +'') ) return False From f84aee8abe858dd61210c21aa5b6d1642dba6ec1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Apr 2021 18:16:59 +0200 Subject: [PATCH 005/213] fix: translate report column labels --- frappe/translate.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frappe/translate.py b/frappe/translate.py index 3565bbc32c..5989ff44aa 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -443,8 +443,12 @@ def get_messages_from_report(name): messages = _get_messages_from_page_or_report("Report", name, frappe.db.get_value("DocType", report.ref_doctype, "module")) + if report.columns: + messages.extend([(None, column.label) for column in report.columns]) + if report.query: messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)]) + messages.append((None,report.report_name)) return messages From bf4a73c3d4890a42016d49a34d455aad18307068 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Apr 2021 18:28:02 +0200 Subject: [PATCH 006/213] fix: translate report filter labels --- frappe/translate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/translate.py b/frappe/translate.py index 5989ff44aa..a4128794ac 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -444,7 +444,10 @@ def get_messages_from_report(name): frappe.db.get_value("DocType", report.ref_doctype, "module")) if report.columns: - messages.extend([(None, column.label) for column in report.columns]) + messages.extend([(None, report_column.label) for report_column in report.columns]) + + if report.filters: + messages.extend([(None, report_filter.label) for report_filter in report.filters]) if report.query: messages.extend([(None, message) for message in re.findall('"([^:,^"]*):', report.query) if is_translatable(message)]) From 6250c4ac9d62b943936e92cef15627534a8a1480 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 21 Apr 2021 19:46:33 +0200 Subject: [PATCH 007/213] fix: add context to filter columns --- frappe/public/js/frappe/views/reports/query_report.js | 2 +- frappe/translate.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/views/reports/query_report.js b/frappe/public/js/frappe/views/reports/query_report.js index 834946b437..4e50210d0a 100644 --- a/frappe/public/js/frappe/views/reports/query_report.js +++ b/frappe/public/js/frappe/views/reports/query_report.js @@ -1094,7 +1094,7 @@ frappe.views.QueryReport = class QueryReport extends frappe.views.BaseList { return Object.assign(column, { id: column.fieldname, - name: __(column.label), + name: __(column.label, null, `Column of report '${this.report_name}'`), // context has to match context in get_messages_from_report in translate.py width: parseInt(column.width) || null, editable: false, compareValue: compareFn, diff --git a/frappe/translate.py b/frappe/translate.py index a4128794ac..49e4a0855c 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -444,7 +444,8 @@ def get_messages_from_report(name): frappe.db.get_value("DocType", report.ref_doctype, "module")) if report.columns: - messages.extend([(None, report_column.label) for report_column in report.columns]) + context = "Column of report '%s'" % report.name # context has to match context in `prepare_columns` in query_report.js + messages.extend([(None, report_column.label, context) for report_column in report.columns]) if report.filters: messages.extend([(None, report_filter.label) for report_filter in report.filters]) From 9dc4cac49571144b90ce34a27ebc87b34fd7653b Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 00:30:00 +0530 Subject: [PATCH 008/213] fix: Use grid docfield list while creating grid_row docfield copy (backport #12940) (#12946) Previously, it was using doctype level docfield list which did not had the updated docfields for a grid. (cherry picked from commit acfa1c1cca561c8e4880b0eaf8e554158f1c5656) Co-authored-by: Suraj Shetty --- 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:40:46 +0530 Subject: [PATCH 009/213] fix: Form Dashboard reference link (backport #12945) (#12947) (cherry picked from commit 4d552c241f7b9a2e6b9a5dfa9bd6d430a6f2cbac) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- 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 ed3ad5ea09..55c965db62 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 9025fce1c0308e3248190a7c375d1ed2d36ca6cf Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 01:03:13 +0530 Subject: [PATCH 010/213] fix(query): Use single quotes for string constant (backport #12948) (#12949) (cherry picked from commit 6225f9b35eaa760e817793c2f42f40a1038d720e) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- 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 92366d59aa8de6c18bf0a8d0b4728d5c78e913d3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Thu, 22 Apr 2021 09:06:37 +0530 Subject: [PATCH 011/213] fix: Invalid HTML generated by the base template (backport #12953) (#12954) Closes #12952 (cherry picked from commit 1a30e11b5f54cb7f13309ae3851c3e7f3394d7ab) Co-authored-by: Anand Chitipothu --- 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 c8feddeaab1289eddf8e4b40ab70e8c378fbe347 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 22 Apr 2021 14:21:05 +0530 Subject: [PATCH 012/213] fix(control): Check if same value is set to avoid unnecessary change trigger (cherry picked from commit 162f191b7727bc2b8359aff2fcf5efe849d35e4a) --- .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 10deafbdecd44c44be88a5ef6ba1722d3c2f6f3c Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 22 Apr 2021 15:59:33 +0530 Subject: [PATCH 013/213] fix: Override get_model_value for table multiselect (cherry picked from commit 1ef4a58aa8b4720592124b991051363f1a98a2a2) --- 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 cf42986d8873e61238827e84bef7c74df12d5a3e Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 22 Apr 2021 16:44:59 +0530 Subject: [PATCH 014/213] fix(UI): dont disable dialog scroll on focusing a Link/Autocomplete field --- .../public/js/frappe/form/controls/autocomplete.js | 6 ------ frappe/public/js/frappe/form/controls/link.js | 6 ------ frappe/public/js/frappe/ui/filters/field_select.js | 12 ------------ 3 files changed, 24 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/autocomplete.js b/frappe/public/js/frappe/form/controls/autocomplete.js index 30eb277f08..7b5680f394 100644 --- a/frappe/public/js/frappe/form/controls/autocomplete.js +++ b/frappe/public/js/frappe/form/controls/autocomplete.js @@ -90,16 +90,10 @@ frappe.ui.form.ControlAutocomplete = frappe.ui.form.ControlData.extend({ }); this.$input.on("awesomplete-open", () => { - this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable'); - this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable'); - this.autocomplete_open = true; }); this.$input.on("awesomplete-close", () => { - this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true); - this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true); - this.autocomplete_open = false; }); diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index c0ff128088..c32c99f0ed 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -241,16 +241,10 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ }); this.$input.on("awesomplete-open", () => { - this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable'); - this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable'); - this.autocomplete_open = true; }); this.$input.on("awesomplete-close", () => { - this.toggle_container_scroll('.modal-dialog', 'modal-dialog-scrollable', true); - this.toggle_container_scroll('.grid-form-body .form-area', 'scrollable', true); - this.autocomplete_open = false; }); diff --git a/frappe/public/js/frappe/ui/filters/field_select.js b/frappe/public/js/frappe/ui/filters/field_select.js index c362214ce2..ed271a73aa 100644 --- a/frappe/public/js/frappe/ui/filters/field_select.js +++ b/frappe/public/js/frappe/ui/filters/field_select.js @@ -36,18 +36,6 @@ frappe.ui.FieldSelect = Class.extend({ var item = me.awesomplete.get_item(value); me.$input.val(item.label); }); - this.$input.on("awesomplete-open", () => { - let modal = this.$input.parents('.modal-dialog')[0]; - if (modal) { - $(modal).removeClass("modal-dialog-scrollable"); - } - }); - this.$input.on("awesomplete-close", () => { - let modal = this.$input.parents('.modal-dialog')[0]; - if (modal) { - $(modal).addClass("modal-dialog-scrollable"); - } - }); if(this.filter_fields) { for(var i in this.filter_fields) From ef5a7ef10afaafb4320c38533481b14367083412 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 22 Apr 2021 16:47:51 +0530 Subject: [PATCH 015/213] chore: Bump version to v13.1.1 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index d323068d65..771b93feee 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -33,7 +33,7 @@ if PY2: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '13.1.0' +__version__ = '13.1.1' __title__ = "Frappe Framework" From a4a32fc4ed93dae32d1cf6c8cf20692a21f01548 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 22 Apr 2021 17:58:21 +0530 Subject: [PATCH 016/213] feat(cli): Format Option for list-apps --- frappe/commands/site.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 0102d3ac40..6e334c59a8 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -202,10 +202,13 @@ def install_app(context, apps): @click.command("list-apps") +@click.option("--format", "-f", type=click.Choice(["text", "json"]), default="text") @pass_context -def list_apps(context): +def list_apps(context, format): "List apps in site" + summary_dict = {} + def fix_whitespaces(text): if site == context.sites[-1]: text = text.rstrip() @@ -234,18 +237,25 @@ def list_apps(context): ] applications_summary = "\n".join(installed_applications) summary = f"{site_title}\n{applications_summary}\n" + summary_dict[site] = [app.app_name for app in apps] else: - applications_summary = "\n".join(frappe.get_installed_apps()) + installed_applications = frappe.get_installed_apps() + applications_summary = "\n".join(installed_applications) summary = f"{site_title}\n{applications_summary}\n" + summary_dict[site] = installed_applications summary = fix_whitespaces(summary) - if applications_summary and summary: + if format == "text" and applications_summary and summary: print(summary) frappe.destroy() + if format == "json": + import json + + print(json.dumps(summary_dict)) @click.command('add-system-manager') @click.argument('email') From f2079f6e68f3cbd03745dd809650de03e567d2b5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 23 Apr 2021 11:40:22 +0530 Subject: [PATCH 017/213] feat: Get form link with query params --- frappe/public/js/frappe/utils/utils.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 7ce30a525c..bffff38c62 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -831,10 +831,13 @@ Object.assign(frappe.utils, { if (callNow) func.apply(context, args); }; }, - get_form_link: function(doctype, name, html = false, display_text = null) { + get_form_link: function(doctype, name, html=false, display_text=null, query_params_obj=null) { display_text = display_text || name; name = encodeURIComponent(name); - const route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`; + let route = `/app/${encodeURIComponent(doctype.toLowerCase().replace(/ /g, '-'))}/${name}`; + if (query_params_obj) { + route += frappe.utils.make_query_string(query_params_obj); + } if (html) { return `${display_text}`; } From f2e6478daffa06326ac6c419594ac7699b9afddc Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 23 Apr 2021 11:42:03 +0530 Subject: [PATCH 018/213] feat: Add utility function to get clipboard data --- frappe/public/js/frappe/utils/utils.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index bffff38c62..ee1f356819 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1322,5 +1322,11 @@ Object.assign(frappe.utils, { frappe.msgprint(__('Please enable pop-ups')); return; } + }, + + get_clipboard_data(clipboard_paste_event) { + let e = clipboard_paste_event; + let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; + return clipboard_data.getData('Text'); } }); From 94c75411b39e13d31605fc74280bdf009ce16ab5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 23 Apr 2021 11:44:09 +0530 Subject: [PATCH 019/213] refactor: Remove redundant code - Use utility function instead --- frappe/public/js/frappe/desk.js | 4 ++-- frappe/public/js/frappe/form/controls/table.js | 3 +-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 216ec967a4..ffb7a0d144 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -611,8 +611,7 @@ frappe.Application = Class.extend({ setup_copy_doc_listener() { $('body').on('paste', (e) => { try { - let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; - let pasted_data = clipboard_data.getData('Text'); + let pasted_data = frappe.utils.get_clipboard_data(e); let doc = JSON.parse(pasted_data); if (doc.doctype) { e.preventDefault(); @@ -627,6 +626,7 @@ frappe.Application = Class.extend({ let res = frappe.model.with_doctype(doc.doctype, () => { let newdoc = frappe.model.copy_doc(doc); newdoc.__newname = doc.name; + delete doc.name; newdoc.idx = null; newdoc.__run_link_triggers = false; frappe.set_route('Form', newdoc.doctype, newdoc.name); diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index c40f471939..95284d86bf 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -26,8 +26,7 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ const row_docname = $(e.target).closest('.grid-row').data('name'); const in_grid_form = $(e.target).closest('.form-in-grid').length; - let clipboard_data = e.clipboardData || window.clipboardData || e.originalEvent.clipboardData; - let pasted_data = clipboard_data.getData('Text'); + let pasted_data = frappe.utils.get_clipboard_data(e); if (!pasted_data || in_grid_form) return; From af95a2452c4d438cffa29b0bfdb872fe0ba099cf Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 23 Apr 2021 12:11:25 +0530 Subject: [PATCH 020/213] feat: Show alert if the data gets clipped while pasting data --- frappe/public/js/frappe/form/controls/data.js | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index b4d24d9a8f..26e35943a0 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -18,6 +18,41 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.$input.attr("maxlength", this.df.length || 140); } + this.$input.on('paste', (e) => { + let pasted_data = frappe.utils.get_clipboard_data(e); + let maxlength = this.$input.attr('maxlength'); + if (maxlength && pasted_data.length > maxlength) { + let warning_message = __('The value you pasted was {0} characters long. Max allowed characters is {1}.', [ + cstr(pasted_data.length).bold(), + cstr(maxlength).bold() + ]); + + // Only show edit link to users who can update the doctype + if (this.frm && frappe.model.can_write(this.frm.doctype)) { + let doctype_edit_link = null + if (this.frm.meta.custom) { + doctype_edit_link = frappe.utils.get_form_link('DocType', this.frm.doctype, true, + __('this form')) + } else { + doctype_edit_link = frappe.utils.get_form_link('Customize Form', 'Customize Form', true, null, { + doc_type: this.frm.doctype + }) + } + let edit_note = __('{0}: You can increase the limit for the field if required via {1}', [ + __('Note').bold(), + doctype_edit_link + ]); + warning_message += `

${edit_note}`; + } + + frappe.msgprint({ + message: warning_message, + indicator: 'orange', + title: __('Data Clipped') + }); + } + }); + this.set_input_attributes(); this.input = this.$input.get(0); this.has_input = true; From ec8bf035ea9c3d5b862c43a23d96c64b72001b1d Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 20:37:05 +0530 Subject: [PATCH 021/213] fix: Default values were not triggering change event (backport #12975) (#12976) (cherry picked from commit 669fead7991bb57a6c7996276850e62f4d56d7fe) Co-authored-by: rohitwaghchaure --- 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 8c2a5fc0c1260b34a1397c781d6d7c4b9e61cd3f Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Fri, 23 Apr 2021 20:49:58 +0530 Subject: [PATCH 022/213] fix: Currency labels in grids (backport #12974) (#12979) (cherry picked from commit 2c7136761eefc976eb280b45d386ecd1d80f0dfc) Co-authored-by: Saqib --- 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 66b57365bb6fde984f3c1139077627a73e88d6a9 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 23 Apr 2021 21:01:25 +0530 Subject: [PATCH 023/213] chore: Bump to vv13.1.2 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 771b93feee..218010a014 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -33,7 +33,7 @@ if PY2: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '13.1.1' +__version__ = 'v13.1.2' __title__ = "Frappe Framework" From 5582da424edb38226991639fc249207da42dd156 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Fri, 23 Apr 2021 21:03:48 +0530 Subject: [PATCH 024/213] fix: Typo --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 218010a014..8e03c43373 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -33,7 +33,7 @@ if PY2: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = 'v13.1.2' +__version__ = '13.1.2' __title__ = "Frappe Framework" From d8d86f7498a6f20652d037541be64935755799e0 Mon Sep 17 00:00:00 2001 From: leela Date: Mon, 26 Apr 2021 06:24:03 +0530 Subject: [PATCH 025/213] refactor: enable profiler from env variable --- frappe/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/app.py b/frappe/app.py index 607479ad52..f17f1494b2 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -286,7 +286,7 @@ def serve(port=8000, profile=False, no_reload=False, no_threading=False, site=No from werkzeug.serving import run_simple - if profile: + if profile or os.environ.get('USE_PROFILER'): application = ProfilerMiddleware(application, sort_by=('cumtime', 'calls')) if not os.environ.get('NO_STATICS'): From 794308cb4a5147ff1d9c6f768b579debd228c060 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 27 Apr 2021 06:27:07 +0530 Subject: [PATCH 026/213] feat: Open Link from read-only URL field - Also, remove one depracated $ method --- frappe/public/js/frappe/form/formatters.js | 7 +++++-- frappe/utils/__init__.py | 4 +--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index f792d5b173..075c03da71 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -15,7 +15,10 @@ frappe.form.formatters = { return "
" + value + "
"; } }, - Data: function(value) { + Data: function(value, df) { + if (df && df.options == "URL") { + return `${value}`; + } return value==null ? "" : value; }, Select: function(value) { @@ -159,7 +162,7 @@ frappe.form.formatters = { return value || ""; }, DateRange: function(value) { - if($.isArray(value)) { + if(Array.isArray(value)) { return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]); } else { return value || ""; diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 274e6ca3fb..7c8ac9ff48 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -169,9 +169,7 @@ def validate_url(txt, throw=False): except Exception: if throw: frappe.throw( - frappe._( - "'{0}' is not a valid URL" - ).format('' + txt +'') + frappe._("'{0}' is not a valid URL").format(frappe.bold(txt)) ) return False From 472b21c4b76a84b45f401147a1d5acb3f94011aa Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 27 Apr 2021 12:03:07 +0530 Subject: [PATCH 027/213] fix: Remove duplicated key --- .eslintrc | 1 - 1 file changed, 1 deletion(-) diff --git a/.eslintrc b/.eslintrc index 2b4afb03e7..d87b0d8c07 100644 --- a/.eslintrc +++ b/.eslintrc @@ -146,7 +146,6 @@ "it": true, "describe": true, "expect": true, - "describe": true, "context": true, "before": true, "beforeEach": true, From 6975e895bcebe4d39ef06a36a371d59d633e8b9d Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Tue, 27 Apr 2021 13:30:30 +0530 Subject: [PATCH 028/213] fix: Sider Issues --- frappe/public/js/frappe/form/controls/data.js | 11 +++++++---- frappe/public/js/frappe/form/formatters.js | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 26e35943a0..98ca6995a2 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -29,14 +29,17 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ // Only show edit link to users who can update the doctype if (this.frm && frappe.model.can_write(this.frm.doctype)) { - let doctype_edit_link = null + let doctype_edit_link = null; if (this.frm.meta.custom) { - doctype_edit_link = frappe.utils.get_form_link('DocType', this.frm.doctype, true, - __('this form')) + doctype_edit_link = frappe.utils.get_form_link( + 'DocType', + this.frm.doctype, true, + __('this form') + ); } else { doctype_edit_link = frappe.utils.get_form_link('Customize Form', 'Customize Form', true, null, { doc_type: this.frm.doctype - }) + }); } let edit_note = __('{0}: You can increase the limit for the field if required via {1}', [ __('Note').bold(), diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 075c03da71..c4372b83f9 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -162,7 +162,7 @@ frappe.form.formatters = { return value || ""; }, DateRange: function(value) { - if(Array.isArray(value)) { + if (Array.isArray(value)) { return __("{0} to {1}", [frappe.datetime.str_to_user(value[0]), frappe.datetime.str_to_user(value[1])]); } else { return value || ""; 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 029/213] 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 030/213] 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 031/213] 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 032/213] 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 033/213] 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 034/213] 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 035/213] 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 036/213] 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 037/213] 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 038/213] 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 039/213] 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 040/213] 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 041/213] 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 042/213] 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 043/213] 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 044/213] 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 045/213] 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 046/213] 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 047/213] 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 048/213] 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 049/213] 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 050/213] 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 051/213] 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 052/213] 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 053/213] 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 054/213] 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 055/213] 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 056/213] 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 057/213] 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 058/213] 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 059/213] 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 060/213] 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 061/213] 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 062/213] 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 063/213] 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 064/213] 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 065/213] 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 066/213] 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 067/213] 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 068/213] 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 069/213] 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 070/213] 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 071/213] 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 072/213] 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 073/213] 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 074/213] 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 075/213] 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 076/213] 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 077/213] 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 078/213] 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 079/213] 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 080/213] 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 081/213] 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 082/213] 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 083/213] 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 084/213] 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 085/213] 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 086/213] 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 087/213] 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 088/213] 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 089/213] 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 090/213] 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 091/213] 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 092/213] 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 093/213] 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 094/213] 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 095/213] 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 096/213] 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 097/213] 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 098/213] 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 099/213] 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 100/213] 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 101/213] 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 102/213] 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 103/213] 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 104/213] 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 105/213] 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 106/213] 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 107/213] 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 108/213] 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 = $(`" + "" + ""; - allCheckbox = actions.lastChild.firstChild; - commit = actions.firstChild; - reset = commit.nextSibling; - addEvent(commit, "click", applyUrlParams); - - dropDownList.id = "qunit-modulefilter-dropdown-list"; - dropDownList.innerHTML = moduleListHtml(); - - dropDown.id = "qunit-modulefilter-dropdown"; - dropDown.style.display = "none"; - dropDown.appendChild(actions); - dropDown.appendChild(dropDownList); - addEvent(dropDown, "change", selectionChange); - selectionChange(); - - moduleFilter.id = "qunit-modulefilter"; - moduleFilter.appendChild(label); - moduleFilter.appendChild(dropDown); - addEvent(moduleFilter, "submit", interceptNavigation); - addEvent(moduleFilter, "reset", function () { - - // Let the reset happen, then update styles - window.setTimeout(selectionChange); - }); - - // Enables show/hide for the dropdown - function searchFocus() { - if (dropDown.style.display !== "none") { - return; - } - - dropDown.style.display = "block"; - addEvent(document$$1, "click", hideHandler); - addEvent(document$$1, "keydown", hideHandler); - - // Hide on Escape keydown or outside-container click - function hideHandler(e) { - var inContainer = moduleFilter.contains(e.target); - - if (e.keyCode === 27 || !inContainer) { - if (e.keyCode === 27 && inContainer) { - moduleSearch.focus(); - } - dropDown.style.display = "none"; - removeEvent(document$$1, "click", hideHandler); - removeEvent(document$$1, "keydown", hideHandler); - moduleSearch.value = ""; - searchInput(); - } - } - } - - // Processes module search box input - function searchInput() { - var i, - item, - searchText = moduleSearch.value.toLowerCase(), - listItems = dropDownList.children; - - for (i = 0; i < listItems.length; i++) { - item = listItems[i]; - if (!searchText || item.textContent.toLowerCase().indexOf(searchText) > -1) { - item.style.display = ""; - } else { - item.style.display = "none"; - } - } - } - - // Processes selection changes - function selectionChange(evt) { - var i, - item, - checkbox = evt && evt.target || allCheckbox, - modulesList = dropDownList.getElementsByTagName("input"), - selectedNames = []; - - toggleClass(checkbox.parentNode, "checked", checkbox.checked); - - dirty = false; - if (checkbox.checked && checkbox !== allCheckbox) { - allCheckbox.checked = false; - removeClass(allCheckbox.parentNode, "checked"); - } - for (i = 0; i < modulesList.length; i++) { - item = modulesList[i]; - if (!evt) { - toggleClass(item.parentNode, "checked", item.checked); - } else if (checkbox === allCheckbox && checkbox.checked) { - item.checked = false; - removeClass(item.parentNode, "checked"); - } - dirty = dirty || item.checked !== item.defaultChecked; - if (item.checked) { - selectedNames.push(item.parentNode.textContent); - } - } - - commit.style.display = reset.style.display = dirty ? "" : "none"; - moduleSearch.placeholder = selectedNames.join(", ") || allCheckbox.parentNode.textContent; - moduleSearch.title = "Type to filter list. Current selection:\n" + (selectedNames.join("\n") || allCheckbox.parentNode.textContent); - } - - return moduleFilter; - } - - function appendToolbar() { - var toolbar = id("qunit-testrunner-toolbar"); - - if (toolbar) { - toolbar.appendChild(toolbarUrlConfigContainer()); - toolbar.appendChild(toolbarModuleFilter()); - toolbar.appendChild(toolbarLooseFilter()); - toolbar.appendChild(document$$1.createElement("div")).className = "clearfix"; - } - } - - function appendHeader() { - var header = id("qunit-header"); - - if (header) { - header.innerHTML = "" + header.innerHTML + " "; - } - } - - function appendBanner() { - var banner = id("qunit-banner"); - - if (banner) { - banner.className = ""; - } - } - - function appendTestResults() { - var tests = id("qunit-tests"), - result = id("qunit-testresult"), - controls; - - if (result) { - result.parentNode.removeChild(result); - } - - if (tests) { - tests.innerHTML = ""; - result = document$$1.createElement("p"); - result.id = "qunit-testresult"; - result.className = "result"; - tests.parentNode.insertBefore(result, tests); - result.innerHTML = "
Running...
 
" + "
" + "
"; - controls = id("qunit-testresult-controls"); - } - - if (controls) { - controls.appendChild(abortTestsButton()); - } - } - - function appendFilteredTest() { - var testId = QUnit.config.testId; - if (!testId || testId.length <= 0) { - return ""; - } - return "
Rerunning selected tests: " + escapeText(testId.join(", ")) + " Run all tests
"; - } - - function appendUserAgent() { - var userAgent = id("qunit-userAgent"); - - if (userAgent) { - userAgent.innerHTML = ""; - userAgent.appendChild(document$$1.createTextNode("QUnit " + QUnit.version + "; " + navigator.userAgent)); - } - } - - function appendInterface() { - var qunit = id("qunit"); - - if (qunit) { - qunit.innerHTML = "

" + escapeText(document$$1.title) + "

" + "

" + "
" + appendFilteredTest() + "

" + "
    "; - } - - appendHeader(); - appendBanner(); - appendTestResults(); - appendUserAgent(); - appendToolbar(); - } - - function appendTestsList(modules) { - var i, l, x, z, test, moduleObj; - - for (i = 0, l = modules.length; i < l; i++) { - moduleObj = modules[i]; - - for (x = 0, z = moduleObj.tests.length; x < z; x++) { - test = moduleObj.tests[x]; - - appendTest(test.name, test.testId, moduleObj.name); - } - } - } - - function appendTest(name, testId, moduleName) { - var title, - rerunTrigger, - testBlock, - assertList, - tests = id("qunit-tests"); - - if (!tests) { - return; - } - - title = document$$1.createElement("strong"); - title.innerHTML = getNameHtml(name, moduleName); - - rerunTrigger = document$$1.createElement("a"); - rerunTrigger.innerHTML = "Rerun"; - rerunTrigger.href = setUrl({ testId: testId }); - - testBlock = document$$1.createElement("li"); - testBlock.appendChild(title); - testBlock.appendChild(rerunTrigger); - testBlock.id = "qunit-test-output-" + testId; - - assertList = document$$1.createElement("ol"); - assertList.className = "qunit-assert-list"; - - testBlock.appendChild(assertList); - - tests.appendChild(testBlock); - } - - // HTML Reporter initialization and load - QUnit.begin(function (details) { - var i, moduleObj, tests; - - // Sort modules by name for the picker - for (i = 0; i < details.modules.length; i++) { - moduleObj = details.modules[i]; - if (moduleObj.name) { - modulesList.push(moduleObj.name); - } - } - modulesList.sort(function (a, b) { - return a.localeCompare(b); - }); - - // Initialize QUnit elements - appendInterface(); - appendTestsList(details.modules); - tests = id("qunit-tests"); - if (tests && config.hidepassed) { - addClass(tests, "hidepass"); - } - }); - - QUnit.done(function (details) { - var banner = id("qunit-banner"), - tests = id("qunit-tests"), - abortButton = id("qunit-abort-tests-button"), - totalTests = stats.passedTests + stats.skippedTests + stats.todoTests + stats.failedTests, - html = [totalTests, " tests completed in ", details.runtime, " milliseconds, with ", stats.failedTests, " failed, ", stats.skippedTests, " skipped, and ", stats.todoTests, " todo.
    ", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed."].join(""), - test, - assertLi, - assertList; - - // Update remaing tests to aborted - if (abortButton && abortButton.disabled) { - html = "Tests aborted after " + details.runtime + " milliseconds."; - - for (var i = 0; i < tests.children.length; i++) { - test = tests.children[i]; - if (test.className === "" || test.className === "running") { - test.className = "aborted"; - assertList = test.getElementsByTagName("ol")[0]; - assertLi = document$$1.createElement("li"); - assertLi.className = "fail"; - assertLi.innerHTML = "Test aborted."; - assertList.appendChild(assertLi); - } - } - } - - if (banner && (!abortButton || abortButton.disabled === false)) { - banner.className = stats.failedTests ? "qunit-fail" : "qunit-pass"; - } - - if (abortButton) { - abortButton.parentNode.removeChild(abortButton); - } - - if (tests) { - id("qunit-testresult-display").innerHTML = html; - } - - if (config.altertitle && document$$1.title) { - - // Show ✖ for good, ✔ for bad suite result in title - // use escape sequences in case file gets loaded with non-utf-8-charset - document$$1.title = [stats.failedTests ? "\u2716" : "\u2714", document$$1.title.replace(/^[\u2714\u2716] /i, "")].join(" "); - } - - // Scroll back to top to show results - if (config.scrolltop && window.scrollTo) { - window.scrollTo(0, 0); - } - }); - - function getNameHtml(name, module) { - var nameHtml = ""; - - if (module) { - nameHtml = "" + escapeText(module) + ": "; - } - - nameHtml += "" + escapeText(name) + ""; - - return nameHtml; - } - - QUnit.testStart(function (details) { - var running, testBlock, bad; - - testBlock = id("qunit-test-output-" + details.testId); - if (testBlock) { - testBlock.className = "running"; - } else { - - // Report later registered tests - appendTest(details.name, details.testId, details.module); - } - - running = id("qunit-testresult-display"); - if (running) { - bad = QUnit.config.reorder && details.previousFailure; - - running.innerHTML = (bad ? "Rerunning previously failed test:
    " : "Running:
    ") + getNameHtml(details.name, details.module); - } - }); - - function stripHtml(string) { - - // Strip tags, html entity and whitespaces - return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); - } - - QUnit.log(function (details) { - var assertList, - assertLi, - message, - expected, - actual, - diff, - showDiff = false, - testItem = id("qunit-test-output-" + details.testId); - - if (!testItem) { - return; - } - - message = escapeText(details.message) || (details.result ? "okay" : "failed"); - message = "" + message + ""; - message += "@ " + details.runtime + " ms"; - - // The pushFailure doesn't provide details.expected - // when it calls, it's implicit to also not show expected and diff stuff - // Also, we need to check details.expected existence, as it can exist and be undefined - if (!details.result && hasOwn.call(details, "expected")) { - if (details.negative) { - expected = "NOT " + QUnit.dump.parse(details.expected); - } else { - expected = QUnit.dump.parse(details.expected); - } - - actual = QUnit.dump.parse(details.actual); - message += ""; - - if (actual !== expected) { - - message += ""; - - if (typeof details.actual === "number" && typeof details.expected === "number") { - if (!isNaN(details.actual) && !isNaN(details.expected)) { - showDiff = true; - diff = details.actual - details.expected; - diff = (diff > 0 ? "+" : "") + diff; - } - } else if (typeof details.actual !== "boolean" && typeof details.expected !== "boolean") { - diff = QUnit.diff(expected, actual); - - // don't show diff if there is zero overlap - showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length; - } - - if (showDiff) { - message += ""; - } - } else if (expected.indexOf("[object Array]") !== -1 || expected.indexOf("[object Object]") !== -1) { - message += ""; - } else { - message += ""; - } - - if (details.source) { - message += ""; - } - - message += "
    Expected:
    " + escapeText(expected) + "
    Result:
    " + escapeText(actual) + "
    Diff:
    " + diff + "
    Message: " + "Diff suppressed as the depth of object is more than current max depth (" + QUnit.config.maxDepth + ").

    Hint: Use QUnit.dump.maxDepth to " + " run with a higher max depth or " + "Rerun without max depth.

    Message: " + "Diff suppressed as the expected and actual results have an equivalent" + " serialization
    Source:
    " + escapeText(details.source) + "
    "; - - // This occurs when pushFailure is set and we have an extracted stack trace - } else if (!details.result && details.source) { - message += "" + "" + "
    Source:
    " + escapeText(details.source) + "
    "; - } - - assertList = testItem.getElementsByTagName("ol")[0]; - - assertLi = document$$1.createElement("li"); - assertLi.className = details.result ? "pass" : "fail"; - assertLi.innerHTML = message; - assertList.appendChild(assertLi); - }); - - QUnit.testDone(function (details) { - var testTitle, - time, - testItem, - assertList, - good, - bad, - testCounts, - skipped, - sourceName, - tests = id("qunit-tests"); - - if (!tests) { - return; - } - - testItem = id("qunit-test-output-" + details.testId); - - assertList = testItem.getElementsByTagName("ol")[0]; - - good = details.passed; - bad = details.failed; - - // This test passed if it has no unexpected failed assertions - var testPassed = details.failed > 0 ? details.todo : !details.todo; - - if (testPassed) { - - // Collapse the passing tests - addClass(assertList, "qunit-collapsed"); - } else if (config.collapse) { - if (!collapseNext) { - - // Skip collapsing the first failing test - collapseNext = true; - } else { - - // Collapse remaining tests - addClass(assertList, "qunit-collapsed"); - } - } - - // The testItem.firstChild is the test name - testTitle = testItem.firstChild; - - testCounts = bad ? "" + bad + ", " + "" + good + ", " : ""; - - testTitle.innerHTML += " (" + testCounts + details.assertions.length + ")"; - - if (details.skipped) { - stats.skippedTests++; - - testItem.className = "skipped"; - skipped = document$$1.createElement("em"); - skipped.className = "qunit-skipped-label"; - skipped.innerHTML = "skipped"; - testItem.insertBefore(skipped, testTitle); - } else { - addEvent(testTitle, "click", function () { - toggleClass(assertList, "qunit-collapsed"); - }); - - testItem.className = testPassed ? "pass" : "fail"; - - if (details.todo) { - var todoLabel = document$$1.createElement("em"); - todoLabel.className = "qunit-todo-label"; - todoLabel.innerHTML = "todo"; - testItem.className += " todo"; - testItem.insertBefore(todoLabel, testTitle); - } - - time = document$$1.createElement("span"); - time.className = "runtime"; - time.innerHTML = details.runtime + " ms"; - testItem.insertBefore(time, assertList); - - if (!testPassed) { - stats.failedTests++; - } else if (details.todo) { - stats.todoTests++; - } else { - stats.passedTests++; - } - } - - // Show the source of the test when showing assertions - if (details.source) { - sourceName = document$$1.createElement("p"); - sourceName.innerHTML = "Source: " + details.source; - addClass(sourceName, "qunit-source"); - if (testPassed) { - addClass(sourceName, "qunit-collapsed"); - } - addEvent(testTitle, "click", function () { - toggleClass(sourceName, "qunit-collapsed"); - }); - testItem.appendChild(sourceName); - } - }); - - // Avoid readyState issue with phantomjs - // Ref: #818 - var notPhantom = function (p) { - return !(p && p.version && p.version.major > 0); - }(window.phantom); - - if (notPhantom && document$$1.readyState === "complete") { - QUnit.load(); - } else { - addEvent(window, "load", QUnit.load); - } - - // Wrap window.onerror. We will call the original window.onerror to see if - // the existing handler fully handles the error; if not, we will call the - // QUnit.onError function. - var originalWindowOnError = window.onerror; - - // Cover uncaught exceptions - // Returning true will suppress the default browser handler, - // returning false will let it run. - window.onerror = function (message, fileName, lineNumber) { - var ret = false; - if (originalWindowOnError) { - for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { - args[_key - 3] = arguments[_key]; - } - - ret = originalWindowOnError.call.apply(originalWindowOnError, [this, message, fileName, lineNumber].concat(args)); - } - - // Treat return value as window.onerror itself does, - // Only do our handling if not suppressed. - if (ret !== true) { - var error = { - message: message, - fileName: fileName, - lineNumber: lineNumber - }; - - ret = QUnit.onError(error); - } - - return ret; - }; - })(); - - /* - * This file is a modified version of google-diff-match-patch's JavaScript implementation - * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), - * modifications are licensed as more fully set forth in LICENSE.txt. - * - * The original source of google-diff-match-patch is attributable and licensed as follows: - * - * Copyright 2006 Google Inc. - * https://code.google.com/p/google-diff-match-patch/ - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * More Info: - * https://code.google.com/p/google-diff-match-patch/ - * - * Usage: QUnit.diff(expected, actual) - * - */ - QUnit.diff = function () { - function DiffMatchPatch() {} - - // DIFF FUNCTIONS - - /** - * The data structure representing a diff is an array of tuples: - * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] - * which means: delete 'Hello', add 'Goodbye' and keep ' world.' - */ - var DIFF_DELETE = -1, - DIFF_INSERT = 1, - DIFF_EQUAL = 0; - - /** - * Find the differences between two texts. Simplifies the problem by stripping - * any common prefix or suffix off the texts before diffing. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean=} optChecklines Optional speedup flag. If present and false, - * then don't run a line-level diff first to identify the changed areas. - * Defaults to true, which does a faster, slightly less optimal diff. - * @return {!Array.} Array of diff tuples. - */ - DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) { - var deadline, checklines, commonlength, commonprefix, commonsuffix, diffs; - - // The diff must be complete in up to 1 second. - deadline = new Date().getTime() + 1000; - - // Check for null inputs. - if (text1 === null || text2 === null) { - throw new Error("Null input. (DiffMain)"); - } - - // Check for equality (speedup). - if (text1 === text2) { - if (text1) { - return [[DIFF_EQUAL, text1]]; - } - return []; - } - - if (typeof optChecklines === "undefined") { - optChecklines = true; - } - - checklines = optChecklines; - - // Trim off common prefix (speedup). - commonlength = this.diffCommonPrefix(text1, text2); - commonprefix = text1.substring(0, commonlength); - text1 = text1.substring(commonlength); - text2 = text2.substring(commonlength); - - // Trim off common suffix (speedup). - commonlength = this.diffCommonSuffix(text1, text2); - commonsuffix = text1.substring(text1.length - commonlength); - text1 = text1.substring(0, text1.length - commonlength); - text2 = text2.substring(0, text2.length - commonlength); - - // Compute the diff on the middle block. - diffs = this.diffCompute(text1, text2, checklines, deadline); - - // Restore the prefix and suffix. - if (commonprefix) { - diffs.unshift([DIFF_EQUAL, commonprefix]); - } - if (commonsuffix) { - diffs.push([DIFF_EQUAL, commonsuffix]); - } - this.diffCleanupMerge(diffs); - return diffs; - }; - - /** - * Reduce the number of edits by eliminating operationally trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) { - var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - - // Is there an insertion operation before the last equality. - preIns = false; - - // Is there a deletion operation before the last equality. - preDel = false; - - // Is there an insertion operation after the last equality. - postIns = false; - - // Is there a deletion operation after the last equality. - postDel = false; - while (pointer < diffs.length) { - - // Equality found. - if (diffs[pointer][0] === DIFF_EQUAL) { - if (diffs[pointer][1].length < 4 && (postIns || postDel)) { - - // Candidate found. - equalities[equalitiesLength++] = pointer; - preIns = postIns; - preDel = postDel; - lastequality = diffs[pointer][1]; - } else { - - // Not a candidate, and can never become one. - equalitiesLength = 0; - lastequality = null; - } - postIns = postDel = false; - - // An insertion or deletion. - } else { - - if (diffs[pointer][0] === DIFF_DELETE) { - postDel = true; - } else { - postIns = true; - } - - /* - * Five types to be split: - * ABXYCD - * AXCD - * ABXC - * AXCD - * ABXC - */ - if (lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)) { - - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - equalitiesLength--; // Throw away the equality we just deleted; - lastequality = null; - if (preIns && preDel) { - - // No changes made which could affect previous entry, keep going. - postIns = postDel = true; - equalitiesLength = 0; - } else { - equalitiesLength--; // Throw away the previous equality. - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - postIns = postDel = false; - } - changes = true; - } - } - pointer++; - } - - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - /** - * Convert a diff array into a pretty HTML report. - * @param {!Array.} diffs Array of diff tuples. - * @param {integer} string to be beautified. - * @return {string} HTML representation. - */ - DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) { - var op, - data, - x, - html = []; - for (x = 0; x < diffs.length; x++) { - op = diffs[x][0]; // Operation (insert, delete, equal) - data = diffs[x][1]; // Text of change. - switch (op) { - case DIFF_INSERT: - html[x] = "" + escapeText(data) + ""; - break; - case DIFF_DELETE: - html[x] = "" + escapeText(data) + ""; - break; - case DIFF_EQUAL: - html[x] = "" + escapeText(data) + ""; - break; - } - } - return html.join(""); - }; - - /** - * Determine the common prefix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the start of each - * string. - */ - DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) { - var pointermid, pointermax, pointermin, pointerstart; - - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { - return 0; - } - - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerstart = 0; - while (pointermin < pointermid) { - if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { - pointermin = pointermid; - pointerstart = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; - }; - - /** - * Determine the common suffix of two strings. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of each string. - */ - DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) { - var pointermid, pointermax, pointermin, pointerend; - - // Quick check for common null cases. - if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { - return 0; - } - - // Binary search. - // Performance analysis: https://neil.fraser.name/news/2007/10/09/ - pointermin = 0; - pointermax = Math.min(text1.length, text2.length); - pointermid = pointermax; - pointerend = 0; - while (pointermin < pointermid) { - if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { - pointermin = pointermid; - pointerend = pointermin; - } else { - pointermax = pointermid; - } - pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); - } - return pointermid; - }; - - /** - * Find the differences between two texts. Assumes that the texts do not - * have any common prefix or suffix. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {boolean} checklines Speedup flag. If false, then don't run a - * line-level diff first to identify the changed areas. - * If true, then run a faster, slightly less optimal diff. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) { - var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; - - if (!text1) { - - // Just add some text (speedup). - return [[DIFF_INSERT, text2]]; - } - - if (!text2) { - - // Just delete some text (speedup). - return [[DIFF_DELETE, text1]]; - } - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - i = longtext.indexOf(shorttext); - if (i !== -1) { - - // Shorter text is inside the longer text (speedup). - diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; - - // Swap insertions for deletions if diff is reversed. - if (text1.length > text2.length) { - diffs[0][0] = diffs[2][0] = DIFF_DELETE; - } - return diffs; - } - - if (shorttext.length === 1) { - - // Single character string. - // After the previous speedup, the character can't be an equality. - return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; - } - - // Check to see if the problem can be split in two. - hm = this.diffHalfMatch(text1, text2); - if (hm) { - - // A half-match was found, sort out the return data. - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - midCommon = hm[4]; - - // Send both pairs off for separate processing. - diffsA = this.DiffMain(text1A, text2A, checklines, deadline); - diffsB = this.DiffMain(text1B, text2B, checklines, deadline); - - // Merge the results. - return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB); - } - - if (checklines && text1.length > 100 && text2.length > 100) { - return this.diffLineMode(text1, text2, deadline); - } - - return this.diffBisect(text1, text2, deadline); - }; - - /** - * Do the two texts share a substring which is at least half the length of the - * longer text? - * This speedup can produce non-minimal diffs. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {Array.} Five element Array, containing the prefix of - * text1, the suffix of text1, the prefix of text2, the suffix of - * text2 and the common middle. Or null if there was no match. - * @private - */ - DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) { - var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; - - longtext = text1.length > text2.length ? text1 : text2; - shorttext = text1.length > text2.length ? text2 : text1; - if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { - return null; // Pointless. - } - dmp = this; // 'this' becomes 'window' in a closure. - - /** - * Does a substring of shorttext exist within longtext such that the substring - * is at least half the length of longtext? - * Closure, but does not reference any external variables. - * @param {string} longtext Longer string. - * @param {string} shorttext Shorter string. - * @param {number} i Start index of quarter length substring within longtext. - * @return {Array.} Five element Array, containing the prefix of - * longtext, the suffix of longtext, the prefix of shorttext, the suffix - * of shorttext and the common middle. Or null if there was no match. - * @private - */ - function diffHalfMatchI(longtext, shorttext, i) { - var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; - - // Start with a 1/4 length substring at position i as a seed. - seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); - j = -1; - bestCommon = ""; - while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { - prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j)); - suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); - if (bestCommon.length < suffixLength + prefixLength) { - bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); - bestLongtextA = longtext.substring(0, i - suffixLength); - bestLongtextB = longtext.substring(i + prefixLength); - bestShorttextA = shorttext.substring(0, j - suffixLength); - bestShorttextB = shorttext.substring(j + prefixLength); - } - } - if (bestCommon.length * 2 >= longtext.length) { - return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon]; - } else { - return null; - } - } - - // First check if the second quarter is the seed for a half-match. - hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); - - // Check again based on the third quarter. - hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); - if (!hm1 && !hm2) { - return null; - } else if (!hm2) { - hm = hm1; - } else if (!hm1) { - hm = hm2; - } else { - - // Both matched. Select the longest. - hm = hm1[4].length > hm2[4].length ? hm1 : hm2; - } - - // A half-match was found, sort out the return data. - if (text1.length > text2.length) { - text1A = hm[0]; - text1B = hm[1]; - text2A = hm[2]; - text2B = hm[3]; - } else { - text2A = hm[0]; - text2B = hm[1]; - text1A = hm[2]; - text1B = hm[3]; - } - midCommon = hm[4]; - return [text1A, text1B, text2A, text2B, midCommon]; - }; - - /** - * Do a quick line-level diff on both strings, then rediff the parts for - * greater accuracy. - * This speedup can produce non-minimal diffs. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time when the diff should be complete by. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) { - var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; - - // Scan the text on a line-by-line basis first. - a = this.diffLinesToChars(text1, text2); - text1 = a.chars1; - text2 = a.chars2; - linearray = a.lineArray; - - diffs = this.DiffMain(text1, text2, false, deadline); - - // Convert the diff back to original text. - this.diffCharsToLines(diffs, linearray); - - // Eliminate freak matches (e.g. blank lines) - this.diffCleanupSemantic(diffs); - - // Rediff any replacement blocks, this time character-by-character. - // Add a dummy entry at the end. - diffs.push([DIFF_EQUAL, ""]); - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - break; - case DIFF_EQUAL: - - // Upon reaching an equality, check for prior redundancies. - if (countDelete >= 1 && countInsert >= 1) { - - // Delete the offending records and add the merged ones. - diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert); - pointer = pointer - countDelete - countInsert; - a = this.DiffMain(textDelete, textInsert, false, deadline); - for (j = a.length - 1; j >= 0; j--) { - diffs.splice(pointer, 0, a[j]); - } - pointer = pointer + a.length; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - pointer++; - } - diffs.pop(); // Remove the dummy entry at the end. - - return diffs; - }; - - /** - * Find the 'middle snake' of a diff, split the problem in two - * and return the recursively constructed diff. - * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) { - var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; - - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - maxD = Math.ceil((text1Length + text2Length) / 2); - vOffset = maxD; - vLength = 2 * maxD; - v1 = new Array(vLength); - v2 = new Array(vLength); - - // Setting all elements to -1 is faster in Chrome & Firefox than mixing - // integers and undefined. - for (x = 0; x < vLength; x++) { - v1[x] = -1; - v2[x] = -1; - } - v1[vOffset + 1] = 0; - v2[vOffset + 1] = 0; - delta = text1Length - text2Length; - - // If the total number of characters is odd, then the front path will collide - // with the reverse path. - front = delta % 2 !== 0; - - // Offsets for start and end of k loop. - // Prevents mapping of space beyond the grid. - k1start = 0; - k1end = 0; - k2start = 0; - k2end = 0; - for (d = 0; d < maxD; d++) { - - // Bail out if deadline is reached. - if (new Date().getTime() > deadline) { - break; - } - - // Walk the front path one step. - for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { - k1Offset = vOffset + k1; - if (k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]) { - x1 = v1[k1Offset + 1]; - } else { - x1 = v1[k1Offset - 1] + 1; - } - y1 = x1 - k1; - while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) { - x1++; - y1++; - } - v1[k1Offset] = x1; - if (x1 > text1Length) { - - // Ran off the right of the graph. - k1end += 2; - } else if (y1 > text2Length) { - - // Ran off the bottom of the graph. - k1start += 2; - } else if (front) { - k2Offset = vOffset + delta - k1; - if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { - - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - v2[k2Offset]; - if (x1 >= x2) { - - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - - // Walk the reverse path one step. - for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { - k2Offset = vOffset + k2; - if (k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]) { - x2 = v2[k2Offset + 1]; - } else { - x2 = v2[k2Offset - 1] + 1; - } - y2 = x2 - k2; - while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) { - x2++; - y2++; - } - v2[k2Offset] = x2; - if (x2 > text1Length) { - - // Ran off the left of the graph. - k2end += 2; - } else if (y2 > text2Length) { - - // Ran off the top of the graph. - k2start += 2; - } else if (!front) { - k1Offset = vOffset + delta - k2; - if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { - x1 = v1[k1Offset]; - y1 = vOffset + x1 - k1Offset; - - // Mirror x2 onto top-left coordinate system. - x2 = text1Length - x2; - if (x1 >= x2) { - - // Overlap detected. - return this.diffBisectSplit(text1, text2, x1, y1, deadline); - } - } - } - } - } - - // Diff took too long and hit the deadline or - // number of diffs equals number of characters, no commonality at all. - return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; - }; - - /** - * Given the location of the 'middle snake', split the diff in two parts - * and recurse. - * @param {string} text1 Old string to be diffed. - * @param {string} text2 New string to be diffed. - * @param {number} x Index of split point in text1. - * @param {number} y Index of split point in text2. - * @param {number} deadline Time at which to bail if not yet complete. - * @return {!Array.} Array of diff tuples. - * @private - */ - DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) { - var text1a, text1b, text2a, text2b, diffs, diffsb; - text1a = text1.substring(0, x); - text2a = text2.substring(0, y); - text1b = text1.substring(x); - text2b = text2.substring(y); - - // Compute both diffs serially. - diffs = this.DiffMain(text1a, text2a, false, deadline); - diffsb = this.DiffMain(text1b, text2b, false, deadline); - - return diffs.concat(diffsb); - }; - - /** - * Reduce the number of edits by eliminating semantically trivial equalities. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) { - var changes, equalities, equalitiesLength, lastequality, pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; - changes = false; - equalities = []; // Stack of indices where equalities are found. - equalitiesLength = 0; // Keeping our own length var is faster in JS. - /** @type {?string} */ - lastequality = null; - - // Always equal to diffs[equalities[equalitiesLength - 1]][1] - pointer = 0; // Index of current position. - - // Number of characters that changed prior to the equality. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - - // Number of characters that changed after the equality. - lengthInsertions2 = 0; - lengthDeletions2 = 0; - while (pointer < diffs.length) { - if (diffs[pointer][0] === DIFF_EQUAL) { - // Equality found. - equalities[equalitiesLength++] = pointer; - lengthInsertions1 = lengthInsertions2; - lengthDeletions1 = lengthDeletions2; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = diffs[pointer][1]; - } else { - // An insertion or deletion. - if (diffs[pointer][0] === DIFF_INSERT) { - lengthInsertions2 += diffs[pointer][1].length; - } else { - lengthDeletions2 += diffs[pointer][1].length; - } - - // Eliminate an equality that is smaller or equal to the edits on both - // sides of it. - if (lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)) { - - // Duplicate record. - diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); - - // Change second copy to insert. - diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; - - // Throw away the equality we just deleted. - equalitiesLength--; - - // Throw away the previous equality (it needs to be reevaluated). - equalitiesLength--; - pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; - - // Reset the counters. - lengthInsertions1 = 0; - lengthDeletions1 = 0; - lengthInsertions2 = 0; - lengthDeletions2 = 0; - lastequality = null; - changes = true; - } - } - pointer++; - } - - // Normalize the diff. - if (changes) { - this.diffCleanupMerge(diffs); - } - - // Find any overlaps between deletions and insertions. - // e.g: abcxxxxxxdef - // -> abcxxxdef - // e.g: xxxabcdefxxx - // -> defxxxabc - // Only extract an overlap if it is as big as the edit ahead or behind it. - pointer = 1; - while (pointer < diffs.length) { - if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { - deletion = diffs[pointer - 1][1]; - insertion = diffs[pointer][1]; - overlapLength1 = this.diffCommonOverlap(deletion, insertion); - overlapLength2 = this.diffCommonOverlap(insertion, deletion); - if (overlapLength1 >= overlapLength2) { - if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) { - - // Overlap found. Insert an equality and trim the surrounding edits. - diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)]); - diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1); - diffs[pointer + 1][1] = insertion.substring(overlapLength1); - pointer++; - } - } else { - if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) { - - // Reverse overlap found. - // Insert an equality and swap and trim the surrounding edits. - diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)]); - - diffs[pointer - 1][0] = DIFF_INSERT; - diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2); - diffs[pointer + 1][0] = DIFF_DELETE; - diffs[pointer + 1][1] = deletion.substring(overlapLength2); - pointer++; - } - } - pointer++; - } - pointer++; - } - }; - - /** - * Determine if the suffix of one string is the prefix of another. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {number} The number of characters common to the end of the first - * string and the start of the second string. - * @private - */ - DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) { - var text1Length, text2Length, textLength, best, length, pattern, found; - - // Cache the text lengths to prevent multiple calls. - text1Length = text1.length; - text2Length = text2.length; - - // Eliminate the null case. - if (text1Length === 0 || text2Length === 0) { - return 0; - } - - // Truncate the longer string. - if (text1Length > text2Length) { - text1 = text1.substring(text1Length - text2Length); - } else if (text1Length < text2Length) { - text2 = text2.substring(0, text1Length); - } - textLength = Math.min(text1Length, text2Length); - - // Quick check for the worst case. - if (text1 === text2) { - return textLength; - } - - // Start by looking for a single character match - // and increase length until no match is found. - // Performance analysis: https://neil.fraser.name/news/2010/11/04/ - best = 0; - length = 1; - while (true) { - pattern = text1.substring(textLength - length); - found = text2.indexOf(pattern); - if (found === -1) { - return best; - } - length += found; - if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) { - best = length; - length++; - } - } - }; - - /** - * Split two texts into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * @param {string} text1 First string. - * @param {string} text2 Second string. - * @return {{chars1: string, chars2: string, lineArray: !Array.}} - * An object containing the encoded text1, the encoded text2 and - * the array of unique strings. - * The zeroth element of the array of unique strings is intentionally blank. - * @private - */ - DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) { - var lineArray, lineHash, chars1, chars2; - lineArray = []; // E.g. lineArray[4] === 'Hello\n' - lineHash = {}; // E.g. lineHash['Hello\n'] === 4 - - // '\x00' is a valid character, but various debuggers don't like it. - // So we'll insert a junk entry to avoid generating a null character. - lineArray[0] = ""; - - /** - * Split a text into an array of strings. Reduce the texts to a string of - * hashes where each Unicode character represents one line. - * Modifies linearray and linehash through being a closure. - * @param {string} text String to encode. - * @return {string} Encoded string. - * @private - */ - function diffLinesToCharsMunge(text) { - var chars, lineStart, lineEnd, lineArrayLength, line; - chars = ""; - - // Walk the text, pulling out a substring for each line. - // text.split('\n') would would temporarily double our memory footprint. - // Modifying text would create many large strings to garbage collect. - lineStart = 0; - lineEnd = -1; - - // Keeping our own length variable is faster than looking it up. - lineArrayLength = lineArray.length; - while (lineEnd < text.length - 1) { - lineEnd = text.indexOf("\n", lineStart); - if (lineEnd === -1) { - lineEnd = text.length - 1; - } - line = text.substring(lineStart, lineEnd + 1); - lineStart = lineEnd + 1; - - if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : lineHash[line] !== undefined) { - chars += String.fromCharCode(lineHash[line]); - } else { - chars += String.fromCharCode(lineArrayLength); - lineHash[line] = lineArrayLength; - lineArray[lineArrayLength++] = line; - } - } - return chars; - } - - chars1 = diffLinesToCharsMunge(text1); - chars2 = diffLinesToCharsMunge(text2); - return { - chars1: chars1, - chars2: chars2, - lineArray: lineArray - }; - }; - - /** - * Rehydrate the text in a diff from a string of line hashes to real lines of - * text. - * @param {!Array.} diffs Array of diff tuples. - * @param {!Array.} lineArray Array of unique strings. - * @private - */ - DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) { - var x, chars, text, y; - for (x = 0; x < diffs.length; x++) { - chars = diffs[x][1]; - text = []; - for (y = 0; y < chars.length; y++) { - text[y] = lineArray[chars.charCodeAt(y)]; - } - diffs[x][1] = text.join(""); - } - }; - - /** - * Reorder and merge like edit sections. Merge equalities. - * Any edit section can move as long as it doesn't cross an equality. - * @param {!Array.} diffs Array of diff tuples. - */ - DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) { - var pointer, countDelete, countInsert, textInsert, textDelete, commonlength, changes, diffPointer, position; - diffs.push([DIFF_EQUAL, ""]); // Add a dummy entry at the end. - pointer = 0; - countDelete = 0; - countInsert = 0; - textDelete = ""; - textInsert = ""; - - while (pointer < diffs.length) { - switch (diffs[pointer][0]) { - case DIFF_INSERT: - countInsert++; - textInsert += diffs[pointer][1]; - pointer++; - break; - case DIFF_DELETE: - countDelete++; - textDelete += diffs[pointer][1]; - pointer++; - break; - case DIFF_EQUAL: - - // Upon reaching an equality, check for prior redundancies. - if (countDelete + countInsert > 1) { - if (countDelete !== 0 && countInsert !== 0) { - - // Factor out any common prefixes. - commonlength = this.diffCommonPrefix(textInsert, textDelete); - if (commonlength !== 0) { - if (pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) { - diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength); - } else { - diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)]); - pointer++; - } - textInsert = textInsert.substring(commonlength); - textDelete = textDelete.substring(commonlength); - } - - // Factor out any common suffixies. - commonlength = this.diffCommonSuffix(textInsert, textDelete); - if (commonlength !== 0) { - diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]; - textInsert = textInsert.substring(0, textInsert.length - commonlength); - textDelete = textDelete.substring(0, textDelete.length - commonlength); - } - } - - // Delete the offending records and add the merged ones. - if (countDelete === 0) { - diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]); - } else if (countInsert === 0) { - diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]); - } else { - diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert]); - } - pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; - } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { - - // Merge this equality with the previous one. - diffs[pointer - 1][1] += diffs[pointer][1]; - diffs.splice(pointer, 1); - } else { - pointer++; - } - countInsert = 0; - countDelete = 0; - textDelete = ""; - textInsert = ""; - break; - } - } - if (diffs[diffs.length - 1][1] === "") { - diffs.pop(); // Remove the dummy entry at the end. - } - - // Second pass: look for single edits surrounded on both sides by equalities - // which can be shifted sideways to eliminate an equality. - // e.g: ABAC -> ABAC - changes = false; - pointer = 1; - - // Intentionally ignore the first and last element (don't need checking). - while (pointer < diffs.length - 1) { - if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { - - diffPointer = diffs[pointer][1]; - position = diffPointer.substring(diffPointer.length - diffs[pointer - 1][1].length); - - // This is a single edit surrounded by equalities. - if (position === diffs[pointer - 1][1]) { - - // Shift the edit over the previous equality. - diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); - diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; - diffs.splice(pointer - 1, 1); - changes = true; - } else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { - - // Shift the edit over the next equality. - diffs[pointer - 1][1] += diffs[pointer + 1][1]; - diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; - diffs.splice(pointer + 1, 1); - changes = true; - } - } - pointer++; - } - - // If shifts were made, the diff needs reordering and another shift sweep. - if (changes) { - this.diffCleanupMerge(diffs); - } - }; - - return function (o, n) { - var diff, output, text; - diff = new DiffMatchPatch(); - output = diff.DiffMain(o, n); - diff.diffCleanupEfficiency(output); - text = diff.diffPrettyHtml(output); - - return text; - }; - }(); - -}((function() { return this; }()))); \ No newline at end of file diff --git a/frappe/website/doctype/contact_us_settings/test_contact_us_settings.js b/frappe/website/doctype/contact_us_settings/test_contact_us_settings.js deleted file mode 100644 index ab53ac56c1..0000000000 --- a/frappe/website/doctype/contact_us_settings/test_contact_us_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Contact Us Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Contact Us Settings - () => frappe.tests.make('Contact Us Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/website/doctype/web_form/test_web_form.js b/frappe/website/doctype/web_form/test_web_form.js deleted file mode 100644 index c6960aa6d5..0000000000 --- a/frappe/website/doctype/web_form/test_web_form.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Web Form", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Web Form - () => frappe.tests.make('Web Form', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/website/doctype/web_page/test_web_page.js b/frappe/website/doctype/web_page/test_web_page.js deleted file mode 100644 index 0ad6bd58b6..0000000000 --- a/frappe/website/doctype/web_page/test_web_page.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Web Page", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Web Page - () => frappe.tests.make('Web Page', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/frappe/workflow/doctype/workflow/tests/test_workflow_create.js b/frappe/workflow/doctype/workflow/tests/test_workflow_create.js deleted file mode 100644 index 6af4bbff67..0000000000 --- a/frappe/workflow/doctype/workflow/tests/test_workflow_create.js +++ /dev/null @@ -1,59 +0,0 @@ -QUnit.module('setup'); - -QUnit.test("Test Workflow", function(assert) { - assert.expect(1); - let done = assert.async(); - - frappe.run_serially([ - () => { - return frappe.tests.make('Workflow', [ - {workflow_name: "Test User Workflow"}, - {document_type: "User"}, - {is_active: 1}, - {override_status: 1}, - {states: [ - [ - {state: 'Pending'}, - {doc_status: 0}, - {allow_edit: 'Administrator'} - ], - [ - {state: 'Approved'}, - {doc_status: 1}, - {allow_edit: 'Administrator'} - ], - [ - {state: 'Rejected'}, - {doc_status: 2}, - {allow_edit: 'Administrator'} - ] - ]}, - {transitions: [ - [ - {state: 'Pending'}, - {action: 'Review'}, - {next_state: 'Pending'}, - {allowed: 'Administrator'} - ], - [ - {state: 'Pending'}, - {action: 'Approve'}, - {next_state: 'Approved'}, - {allowed: 'Administrator'} - ], - [ - {state: 'Approved'}, - {action: 'Reject'}, - {next_state: 'Rejected'}, - {allowed: 'Administrator'} - ], - ]}, - {workflow_state_field: 'workflow_state'} - ]); - }, - () => frappe.timeout(1), - () => {assert.equal($('.msgprint').text(), "Created Custom Field workflow_state in User", "Workflow created");}, - () => frappe.tests.click_button('Close'), - () => done() - ]); -}); \ No newline at end of file diff --git a/frappe/workflow/doctype/workflow/tests/test_workflow_test.js b/frappe/workflow/doctype/workflow/tests/test_workflow_test.js deleted file mode 100644 index c92358f71f..0000000000 --- a/frappe/workflow/doctype/workflow/tests/test_workflow_test.js +++ /dev/null @@ -1,51 +0,0 @@ -QUnit.module('setup'); - -QUnit.test("Test Workflow", function(assert) { - assert.expect(5); - let done = assert.async(); - - frappe.run_serially([ - () => frappe.set_route('Form', 'User', 'New User 1'), - () => frappe.timeout(1), - () => { - cur_frm.set_value('email', 'test1@testmail.com'); - cur_frm.set_value('first_name', 'Test Name'); - cur_frm.set_value('send_welcome_email', 0); - return cur_frm.save(); - }, - () => frappe.tests.click_button('Actions'), - () => frappe.timeout(0.5), - () => { - let review = $(`.dropdown-menu li:contains("Review"):visible`).size(); - let approve = $(`.dropdown-menu li:contains("Approve"):visible`).size(); - assert.equal(review, 1, "Review Action exists"); - assert.equal(approve, 1, "Approve Action exists"); - }, - () => frappe.tests.click_dropdown_item('Approve'), - () => frappe.timeout(1), - () => frappe.tests.click_button('Yes'), - () => frappe.timeout(1), - () => { - assert.equal($('.msgprint').text(), "Did not saveInsufficient Permission for User", "Approve action working"); - frappe.tests.click_button('Close'); - }, - () => frappe.timeout(1), - () => { - cur_frm.set_value('role_profile_name', 'Test 2'); - return cur_frm.save(); - }, - () => frappe.tests.click_button('Actions'), - () => frappe.timeout(1), - () => { - let reject = $(`.dropdown-menu li:contains("Reject"):visible`).size(); - assert.equal(reject, 1, "Reject Action exists"); - }, - () => frappe.tests.click_dropdown_item('Reject'), - () => frappe.timeout(1), - () => { - if(frappe.tests.click_button('Close')) - assert.equal(1, 1, "Reject action works"); - }, - () => done() - ]); -}); \ No newline at end of file From 68f8315dc8da29a48732bceb192d815e3715a59a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 27 Apr 2021 17:16:25 +0000 Subject: [PATCH 114/213] chore(deps): bump redis from 2.8.0 to 3.1.1 Bumps [redis](https://github.com/NodeRedis/node-redis) from 2.8.0 to 3.1.1. - [Release notes](https://github.com/NodeRedis/node-redis/releases) - [Changelog](https://github.com/NodeRedis/node-redis/blob/master/CHANGELOG.md) - [Commits](https://github.com/NodeRedis/node-redis/compare/v.2.8.0...v3.1.1) Signed-off-by: dependabot[bot] (cherry picked from commit 43285fd2d841e22903d4702aae4747f8b81fae2d) --- package.json | 2 +- yarn.lock | 48 ++++++++++++++++++++++++++++-------------------- 2 files changed, 29 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 6e82890617..928456578c 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "quill": "2.0.0-dev.4", "quill-image-resize": "^3.0.9", "qz-tray": "^2.0.8", - "redis": "^2.8.0", + "redis": "^3.1.1", "showdown": "^1.9.1", "snyk": "^1.518.0", "socket.io": "^2.4.0", diff --git a/yarn.lock b/yarn.lock index 8ac348011d..fbd567debf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2021,6 +2021,11 @@ delegates@^1.0.0: resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= +denque@^1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.0.tgz#773de0686ff2d8ec2ff92914316a47b73b1c73de" + integrity sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -2110,11 +2115,6 @@ dotnet-deps-parser@5.0.0: tslib "^1.10.0" xml2js "0.4.23" -double-ended-queue@^2.1.0-0: - version "2.1.0-0" - resolved "https://registry.yarnpkg.com/double-ended-queue/-/double-ended-queue-2.1.0-0.tgz#103d3527fd31528f40188130c841efdd78264e5c" - integrity sha1-ED01J/0xUo9AGIEwyEHv3XgmTlw= - driver.js@^0.9.8: version "0.9.8" resolved "https://registry.yarnpkg.com/driver.js/-/driver.js-0.9.8.tgz#4b327f4537b1c9b9fb19419de86174be821ae32a" @@ -6169,24 +6169,32 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" -redis-commands@^1.2.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.4.0.tgz#52f9cf99153efcce56a8f86af986bd04e988602f" - integrity sha512-cu8EF+MtkwI4DLIT0x9P8qNTLFhQD4jLfxLR0cCNkeGzs87FN6879JOJwNQR/1zD7aSYNbU0hgsV9zGY71Itvw== +redis-commands@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.7.0.tgz#15a6fea2d58281e27b1cd1acfb4b293e278c3a89" + integrity sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ== -redis-parser@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" - integrity sha1-Uu0J2srBCPGmMcB+m2mUHnoZUEs= +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= -redis@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" - integrity sha512-M1OkonEQwtRmZv4tEWF2VgpG0JWJ8Fv1PhlgT5+B+uNq2cA3Rt1Yt/ryoR+vQNOQcIEgdCdfH0jr3bDpihAw1A== +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= dependencies: - double-ended-queue "^2.1.0-0" - redis-commands "^1.2.0" - redis-parser "^2.6.0" + redis-errors "^1.0.0" + +redis@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/redis/-/redis-3.1.1.tgz#a44bee7c072dcf685e139048d6a1a4d3b00f5d01" + integrity sha512-QhkKhOuzhogR1NDJfBD34TQJz2ZJwDhhIC6ZmvpftlmfYShHHQXjjNspAJ+Z2HH5NwSBVYBVganbiZ8bgFMHjg== + dependencies: + denque "^1.5.0" + redis-commands "^1.7.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" regenerate-unicode-properties@^7.0.0: version "7.0.0" From ddc4dc83e9fb9c59a5043f82e3044ff390a3e4bb Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 28 Apr 2021 12:13:35 +0530 Subject: [PATCH 115/213] fix: Increase link icon size --- frappe/public/js/frappe/form/controls/data.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 854992f7f1..eab1eacb76 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -70,7 +70,7 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.$wrapper.find('.control-input').append( ` - ${frappe.utils.icon('link-url', 'xs')} + ${frappe.utils.icon('link-url', 'sm')} ` ); From ee2e4e2bf3d75656c47ffcb2a07f3be735db38cd Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 28 Apr 2021 14:12:48 +0530 Subject: [PATCH 116/213] test: URL Data field Integrations --- .../fixtures/data_field_validation_doctype.js | 8 ++++ cypress/integration/url_data_field.js | 43 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 cypress/integration/url_data_field.js diff --git a/cypress/fixtures/data_field_validation_doctype.js b/cypress/fixtures/data_field_validation_doctype.js index 469ff8ca24..da091af7e5 100644 --- a/cypress/fixtures/data_field_validation_doctype.js +++ b/cypress/fixtures/data_field_validation_doctype.js @@ -30,6 +30,14 @@ export default { fieldtype: 'Data', label: 'Person Name', options: 'Name' + }, + { + fieldname: 'read_only_url', + fieldtype: 'Data', + label: 'Read Only URL', + options: 'URL', + read_only: '1', + default: 'https://frappe.io' } ], issingle: 1, diff --git a/cypress/integration/url_data_field.js b/cypress/integration/url_data_field.js new file mode 100644 index 0000000000..cf22c62363 --- /dev/null +++ b/cypress/integration/url_data_field.js @@ -0,0 +1,43 @@ +import data_field_validation_doctype from '../fixtures/data_field_validation_doctype'; + +const doctype_name = data_field_validation_doctype.name; + +context('URL Data Field Input', () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + return cy.insert_doc('DocType', data_field_validation_doctype, true); + }); + + + describe('URL Data Field Input ', () => { + it('should not show URL link button without focus', () => { + cy.new_form(doctype_name); + cy.get_field('url').clear().type('https://frappe.io'); + cy.get_field('url').blur().wait(500); + cy.get('.link-btn').should('not.be.visible'); + }); + + it('should show URL link button on focus', () => { + cy.get_field('url').focus().wait(500); + cy.get('.link-btn').should('be.visible'); + }); + + it('should not show URL link button for invalid URL', () => { + cy.get_field('url').clear().type('fuzzbuzz'); + cy.get('.link-btn').should('not.be.visible'); + }); + + it('should have valid URL link with target _blank', () => { + cy.get_field('url').clear().type('https://frappe.io'); + cy.get('.link-btn .btn-open').should('have.attr', 'href', 'https://frappe.io'); + cy.get('.link-btn .btn-open').should('have.attr', 'target', '_blank'); + }); + + it('should inject anchor tag in read-only URL data field', () => { + cy.get('[data-fieldname="read_only_url"]') + .find('a') + .should('have.attr', 'target', '_blank'); + }); + }); +}); \ No newline at end of file From 81047bb1d7670595cd2b30d4a75b900152099bf7 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Wed, 28 Apr 2021 14:21:50 +0530 Subject: [PATCH 117/213] fix: Sider Issues --- frappe/public/js/frappe/form/controls/data.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index eab1eacb76..991f93f30b 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -82,7 +82,7 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ setTimeout(() => { let inputValue = this.get_input_value(); - if(inputValue && validate_url(inputValue)) { + if (inputValue && validate_url(inputValue)) { this.$link.toggle(true); this.$link_open.attr('href', this.get_input_value()); } @@ -93,7 +93,7 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.$input.bind("input", () => { let inputValue = this.get_input_value(); - if(inputValue && validate_url(inputValue)) { + if (inputValue && validate_url(inputValue)) { this.$link.toggle(true); this.$link_open.attr('href', this.get_input_value()); } else { From 68b27ab8f78fc2be8ac9a921b97bf5aed42d2bde Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 28 Apr 2021 13:34:11 +0530 Subject: [PATCH 118/213] fix: disabled checkbox should be disabled (cherry picked from commit c7591d51c4726fb4e43ea77c7458cd21d1587dbf) --- frappe/public/js/frappe/form/formatters.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index f792d5b173..0b396ad35e 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -97,11 +97,8 @@ frappe.form.formatters = { } }, Check: function(value) { - if (value) { - return ``; - } else { - return ``; - } + return ``; }, Link: function(value, docfield, options, doc) { var doctype = docfield._options || docfield.options; From fabe6d72abfbd10c7befbba011db9d20a4fd9a80 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 26 Apr 2021 22:16:18 +0530 Subject: [PATCH 119/213] feat(DX): sourceURL for injected javascript Adds sourceURL to injected javascript code, this helps in debugging injected javascript using client script or doctype specific javascript. `no-docs` (cherry picked from commit 8dd925743c59f735567d7f73395648e7c4fbb742) --- frappe/desk/form/meta.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/frappe/desk/form/meta.py b/frappe/desk/form/meta.py index e637f4969a..087cc54d9d 100644 --- a/frappe/desk/form/meta.py +++ b/frappe/desk/form/meta.py @@ -109,8 +109,9 @@ class FormMeta(Meta): def _add_code(self, path, fieldname): js = get_js(path) if js: - self.set(fieldname, (self.get(fieldname) or "") - + "\n\n/* Adding {0} */\n\n".format(path) + js) + comment = f"\n\n/* Adding {path} */\n\n" + sourceURL = f"\n\n//# sourceURL={scrub(self.name) + fieldname}" + self.set(fieldname, (self.get(fieldname) or "") + comment + js + sourceURL) def add_html_templates(self, path): if self.custom: @@ -145,6 +146,10 @@ class FormMeta(Meta): if script.view == 'Form': form_script += script.script + file = scrub(self.name) + form_script += f"\n\n//# sourceURL={file}__custom_js" + list_script += f"\n\n//# sourceURL={file}__custom_list_js" + self.set("__custom_js", form_script) self.set("__custom_list_js", list_script) From 8651d873122646f8348db3b883c7e85560971de1 Mon Sep 17 00:00:00 2001 From: hasnain2808 Date: Wed, 28 Apr 2021 18:40:31 +0530 Subject: [PATCH 120/213] fix: decode uri before importing file via weblink (cherry picked from commit 9a8d1fb43f4e5ab4f552315c2bbdf2ca86675626) --- frappe/public/js/frappe/file_uploader/FileUploader.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/file_uploader/FileUploader.vue b/frappe/public/js/frappe/file_uploader/FileUploader.vue index d0b09c7593..5199d98a1d 100644 --- a/frappe/public/js/frappe/file_uploader/FileUploader.vue +++ b/frappe/public/js/frappe/file_uploader/FileUploader.vue @@ -318,7 +318,7 @@ export default { frappe.msgprint(__('Invalid URL')); return Promise.reject(); } - + file_url = decodeURI(file_url) return this.upload_file({ file_url }); From 7953896ab5b386b4b3cd580748a22da3ea952af2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Mon, 26 Apr 2021 17:59:02 +0530 Subject: [PATCH 121/213] feat: Deletion Steps in Data Deletion Tool * Track deletion steps in PDDR * Option to commit via pddr._anonymize_data --- .../personal_data_deletion_request.json | 19 +++++- .../personal_data_deletion_request.py | 64 ++++++++++++++++-- .../personal_data_deletion_step/__init__.py | 0 .../personal_data_deletion_step.json | 66 +++++++++++++++++++ .../personal_data_deletion_step.py | 10 +++ 5 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 frappe/website/doctype/personal_data_deletion_step/__init__.py create mode 100644 frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.json create mode 100644 frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.py diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json index e1439fd2dc..0cb11068f5 100644 --- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json +++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.json @@ -6,7 +6,9 @@ "engine": "InnoDB", "field_order": [ "email", - "status" + "status", + "anonymization_matrix", + "deletion_steps" ], "fields": [ { @@ -27,10 +29,23 @@ "label": "Status", "options": "Pending Verification\nPending Approval\nDeleted", "read_only": 1 + }, + { + "fieldname": "anonymization_matrix", + "fieldtype": "Code", + "label": "Anonymization Matrix", + "options": "JSON", + "read_only": 1 + }, + { + "fieldname": "deletion_steps", + "fieldtype": "Table", + "label": "Deletion Steps ", + "options": "Personal Data Deletion Step" } ], "links": [], - "modified": "2021-02-28 12:36:08.219719", + "modified": "2021-04-23 13:25:53.629308", "modified_by": "Administrator", "module": "Website", "name": "Personal Data Deletion Request", diff --git a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py index 23857a5e66..481012753a 100644 --- a/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py +++ b/frappe/website/doctype/personal_data_deletion_request/personal_data_deletion_request.py @@ -10,6 +10,8 @@ from frappe.model.document import Document from frappe.utils import get_fullname from frappe.utils.user import get_system_managers from frappe.utils.verified_command import get_signed_params, verify_request +import json +from frappe.core.utils import find class PersonalDataDeletionRequest(Document): @@ -118,6 +120,24 @@ class PersonalDataDeletionRequest(Document): now=frappe.flags.in_test, ) + def add_deletion_steps(self): + if self.deletion_steps: + return + + for step in self.full_match_privacy_docs + self.partial_privacy_docs: + row_data = { + "status": "Pending", + "document_type": step.get("doctype"), + "partial": step.get("partial") or False, + "fields": json.dumps(step.get("redact_fields", [])), + "filtered_by": step.get("filtered_by") or "", + } + self.append("deletion_steps", row_data) + + self.anonymization_matrix = json.dumps(self.anonymization_value_map, indent=4) + self.save() + self.reload() + def redact_partial_match_data(self, doctype): self.__redact_partial_match_data(doctype) self.rename_documents(doctype) @@ -207,21 +227,57 @@ class PersonalDataDeletionRequest(Document): ref["doctype"], doc["name"], self.anon, force=True, show_alert=False ) - def _anonymize_data(self, email=None, anon=None, set_data=True): + def _anonymize_data(self, email=None, anon=None, set_data=True, commit=False): email = email or self.email anon = anon or self.name if set_data: self.__set_anonymization_data(email, anon) - for doctype in self.full_match_privacy_docs: - self.redact_full_match_data(doctype, email) + self.add_deletion_steps() - for doctype in self.partial_privacy_docs: + self.full_match_doctypes = ( + x + for x in self.full_match_privacy_docs + if filter( + lambda x: x.document_type == x and x.status == "Pending", self.deletion_steps + ) + ) + + self.partial_match_doctypes = ( + x + for x in self.partial_privacy_docs + if filter( + lambda x: x.document_type == x and x.status == "Pending", self.deletion_steps + ) + ) + + for doctype in self.full_match_doctypes: + self.redact_full_match_data(doctype, email) + self.set_step_status(doctype["doctype"]) + if commit: + frappe.db.commit() + + for doctype in self.partial_match_doctypes: self.redact_partial_match_data(doctype) + self.set_step_status(doctype["doctype"]) + if commit: + frappe.db.commit() frappe.rename_doc("User", email, anon, force=True, show_alert=False) self.db_set("status", "Deleted") + if commit: + frappe.db.commit() + + def set_step_status(self, step, status="Deleted"): + del_step = find(self.deletion_steps, lambda x: x.document_type == step and x.status != status) + + if not del_step: + del_step = find(self.deletion_steps, lambda x: x.document_type == step) + + del_step.status = status + self.save() + self.reload() def __set_anonymization_data(self, email, anon): self.anon = anon or self.name diff --git a/frappe/website/doctype/personal_data_deletion_step/__init__.py b/frappe/website/doctype/personal_data_deletion_step/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.json b/frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.json new file mode 100644 index 0000000000..5d38ea6222 --- /dev/null +++ b/frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.json @@ -0,0 +1,66 @@ +{ + "actions": [], + "creation": "2021-04-23 13:25:26.162797", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "document_type", + "status", + "partial", + "fields", + "filtered_by" + ], + "fields": [ + { + "allow_in_quick_entry": 1, + "fieldname": "document_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Document Type", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_preview": 1, + "label": "Status", + "options": "Pending\nDeleted", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "partial", + "fieldtype": "Check", + "in_preview": 1, + "label": "Partial", + "read_only": 1 + }, + { + "fieldname": "fields", + "fieldtype": "Small Text", + "label": "Fields", + "read_only": 1 + }, + { + "fieldname": "filtered_by", + "fieldtype": "Data", + "label": "Filtered By", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-04-23 13:48:59.658681", + "modified_by": "Administrator", + "module": "Website", + "name": "Personal Data Deletion Step", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.py b/frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.py new file mode 100644 index 0000000000..2a7451473d --- /dev/null +++ b/frappe/website/doctype/personal_data_deletion_step/personal_data_deletion_step.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 PersonalDataDeletionStep(Document): + pass From 4ed6dbc7159b3d990238f0c236b95a757e1d0af4 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Thu, 29 Apr 2021 14:10:33 +0530 Subject: [PATCH 122/213] fix: Grid row color picker field not working (cherry picked from commit 67671c98bbee1e2ecec7fa317ecd50ce26368143) --- .../public/js/frappe/form/controls/color.js | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index bf04581abd..814a716d2b 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -48,7 +48,24 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ $(window).off('hashchange.color-popover'); }); - this.$wrapper.find('.control-input').on('click', (e) => { + this.picker.on_change = (color) => { + this.set_value(color); + }; + + if (!this.selected_color) { + this.selected_color = $(`
    `); + this.selected_color.insertAfter(this.$input); + } + + if (!this.$wrapper.find('.control-input').get(0)) { + this.$wrapper.find('.selected-color') + .css({ + "top": "calc(23% + 1px)", + "z-index": "2" + }); + } + + this.$wrapper.find('.selected-color').parent().on('click', (e) => { this.$wrapper.popover('toggle'); if (!this.get_color()) { this.$input.val(''); @@ -63,15 +80,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ this.$wrapper.popover('hide'); }); }); - - this.picker.on_change = (color) => { - this.set_value(color); - }; - - if (!this.selected_color) { - this.selected_color = $(`
    `); - this.selected_color.insertAfter(this.$input); - } }, refresh() { this._super(); From 5ea207b9bab5a7d6f292fc63a6bc812ce40156ac Mon Sep 17 00:00:00 2001 From: shariquerik Date: Thu, 29 Apr 2021 15:26:52 +0530 Subject: [PATCH 123/213] refactor: Conditional CSS instead of code (cherry picked from commit f148bf36c345ccf1a66bdf9de86ea9643357319a) --- frappe/public/js/frappe/form/controls/color.js | 8 -------- frappe/public/scss/common/color_picker.scss | 7 +++++++ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 814a716d2b..25e8598d49 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -57,14 +57,6 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ this.selected_color.insertAfter(this.$input); } - if (!this.$wrapper.find('.control-input').get(0)) { - this.$wrapper.find('.selected-color') - .css({ - "top": "calc(23% + 1px)", - "z-index": "2" - }); - } - this.$wrapper.find('.selected-color').parent().on('click', (e) => { this.$wrapper.popover('toggle'); if (!this.get_color()) { diff --git a/frappe/public/scss/common/color_picker.scss b/frappe/public/scss/common/color_picker.scss index 7ab9b0c504..84755beb18 100644 --- a/frappe/public/scss/common/color_picker.scss +++ b/frappe/public/scss/common/color_picker.scss @@ -121,3 +121,10 @@ } } } + +.data-row.row { + .selected-color { + top: calc(50% - 11px); + z-index: 2; + } +} From 7cb306e9835dbb2df75808abf5e7b3bffbe34e73 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 29 Apr 2021 17:41:48 +0530 Subject: [PATCH 124/213] fix: Use placeholer attr to set color placeholder instead of value (cherry picked from commit b1d6e7ad16f1263a0d01ac24b47eb9fa95dd6b48) --- frappe/public/js/frappe/form/controls/color.js | 4 ++-- frappe/public/js/frappe/form/grid_row.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 25e8598d49..9fb827d223 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -6,6 +6,7 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ this.make_color_input(); }, make_color_input: function () { + this.df.placeholder = __('Choose a color'); let picker_wrapper = $('
    '); this.picker = new Picker({ parent: picker_wrapper[0], @@ -83,8 +84,7 @@ frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ }, set_formatted_input: function(value) { this._super(value); - - this.$input.val(value || __('Choose a color')); + this.$input.val(value); this.selected_color.css({ "background-color": value || 'transparent', }); diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 9a689fabf4..f6da88df57 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -422,7 +422,7 @@ export default class GridRow { field.$input .addClass('input-sm') .attr('data-col-idx', column.column_index) - .attr('placeholder', __(df.label)); + .attr('placeholder', __(df.placeholder || df.label)); // flag list input if (this.columns_list && this.columns_list.slice(-1)[0]===column) { field.$input.attr('data-last-input', 1); From 77860e3178a78dc79ccba6d382e8a30de4d277f8 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 29 Apr 2021 17:42:57 +0530 Subject: [PATCH 125/213] fix: Show nothing for empty value instead of undefined (cherry picked from commit 7a84a3f3eab62709819e3e2a73847baff364b5f4) --- frappe/public/js/frappe/form/formatters.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 0b396ad35e..15e491baae 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -292,12 +292,12 @@ frappe.form.formatters = { return formatted_values.join(', '); }, Color: (value) => { - return `
    + return value ? `
    ${value} -
    `; +
    ` : ''; } -} +}; frappe.form.get_formatter = function(fieldtype) { if(!fieldtype) From ba5c8abadbeb29614b1a0bea0609d9eb797c6436 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 29 Apr 2021 17:55:19 +0530 Subject: [PATCH 126/213] fix: HTML Editor overflow in grid (cherry picked from commit 28196bda54e6a0fd83efd219c0147267baded004) --- frappe/public/scss/common/grid.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index c4e30eec08..3cc5139d9e 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -196,7 +196,7 @@ margin-left: 1rem; } - .grid-static-col[data-fieldtype="Code"] { + .grid-static-col[data-fieldtype="Code"], .grid-static-col[data-fieldtype="HTML Editor"] { overflow: hidden; .static-area { From 9669a90ed6641e64ffd1ee9975459a8b0b15c96d Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 29 Apr 2021 18:13:18 +0530 Subject: [PATCH 127/213] fix: Set placeholder value before creating input (cherry picked from commit ca9cafb999cb81718f39a705b2dc760e9b31883c) --- frappe/public/js/frappe/form/controls/color.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/controls/color.js b/frappe/public/js/frappe/form/controls/color.js index 9fb827d223..8a60f3e3da 100644 --- a/frappe/public/js/frappe/form/controls/color.js +++ b/frappe/public/js/frappe/form/controls/color.js @@ -2,11 +2,11 @@ import Picker from '../../color_picker/color_picker'; frappe.ui.form.ControlColor = frappe.ui.form.ControlData.extend({ make_input: function () { + this.df.placeholder = this.df.placeholder || __('Choose a color'); this._super(); this.make_color_input(); }, make_color_input: function () { - this.df.placeholder = __('Choose a color'); let picker_wrapper = $('
    '); this.picker = new Picker({ parent: picker_wrapper[0], From 852cc7c5969d88507e36eafa62698532ec4d03d0 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 28 Apr 2021 15:11:02 +0530 Subject: [PATCH 128/213] fix(UI): consistent checkboxes on all browsers (cherry picked from commit 47630b8ea094aed4da0885076006420e63c6e686) --- frappe/public/scss/common/global.scss | 78 +++++++++------------- frappe/public/scss/desk/css_variables.scss | 7 +- frappe/public/scss/desk/list.scss | 3 +- 3 files changed, 38 insertions(+), 50 deletions(-) diff --git a/frappe/public/scss/common/global.scss b/frappe/public/scss/common/global.scss index 20778176d4..e6f14f6b17 100644 --- a/frappe/public/scss/common/global.scss +++ b/frappe/public/scss/common/global.scss @@ -16,69 +16,57 @@ } } +$check-icon: url("data:image/svg+xml, "); + input[type="checkbox"] { position: relative; - width: 0 !important; - height: var(--custom-checkbox-size); - margin-right: calc(var(--custom-checkbox-size) + var(--checkbox-right-margin)) !important; - font-size: calc(var(--custom-checkbox-size) - 1px); + width: var(--checkbox-size) !important; + height: var(--checkbox-size); + margin-right: var(--checkbox-right-margin) !important; + background-repeat: no-repeat; + background-position: center; + border: 1px solid var(--gray-400); + box-sizing: border-box; + box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); + border-radius: 4px; - &:before { - width: var(--custom-checkbox-size); - height: var(--custom-checkbox-size); - position: absolute; - top: 0; - display: inline-block; - line-height: 1; - text-align: center; - content: ' '; - border: 1px solid var(--gray-400); - box-sizing: border-box; - box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); - border-radius: 4px; + // Reset Browser Behavior + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + + -webkit-print-color-adjust: exact; + color-adjust: exact; + + .grid-static-col & { + margin-right: 0 !important; } - &:checked:before { - content: url("data: image/svg+xml;utf8, "); - background: linear-gradient(180deg, #4AC3F8 -124.51%, #2490EF 100%); + &:checked { + background-color: #2490EF; + background-image: $check-icon, linear-gradient(180deg, #4AC3F8 -124.51%, #2490EF 100%); + background-size: 57%, 100%; box-shadow: none; border: none; } - - &.disabled-deselected:before, &:disabled:not([checked])::before { - background: var(--disabled-control-bg); - border: 0.5px solid var(--gray-300); - box-sizing: border-box; + &.disabled-deselected, &:disabled { + background-color: var(--disabled-control-bg); box-shadow: inset 0px 1px 7px rgba(0, 0, 0, 0.1); - border-radius: 4px; + border: 0.5px solid var(--gray-300); pointer-events: none; } - &.disabled-selected:before, &:disabled:checked::before { - content: url("data: image/svg+xml;utf8, "); - background: var(--gray-500); - box-sizing: border-box; + &.disabled-selected, &:disabled:checked { + background-color: var(--gray-500); + background-image: $check-icon; + background-size: 57%; box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.1); - border-radius: 4px; - line-height: 10px; + border: none; pointer-events: none; } } -// Firefox doesn't support -// pseudo elements on checkbox -html.firefox, html.safari { - :root { - --custom-checkbox-size: 0px; - } - input[type="checkbox"] { - width: var(--base-checkbox-size) !important; - height: var(--base-checkbox-size); - margin-right: var(--checkbox-right-margin) !important; - } -} - .frappe-card { @include card(); } diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss index 5aca23a0b0..ee9ccd7ba5 100644 --- a/frappe/public/scss/desk/css_variables.scss +++ b/frappe/public/scss/desk/css_variables.scss @@ -31,9 +31,6 @@ $input-height: 28px !default; --modal-bg: white; --toast-bg: var(--modal-bg); --popover-bg: white; - --checkbox-right-margin: var(--margin-xs); - --base-checkbox-size: 14px; - --custom-checkbox-size: 14px; --appreciation-color: var(--dark-green-600); --appreciation-bg: var(--dark-green-100); @@ -52,6 +49,10 @@ $input-height: 28px !default; --input-height: #{$input-height}; --input-disabled-bg: var(--gray-200); + // checkbox + --checkbox-right-margin: var(--margin-xs); + --checkbox-size: 14px; + // timeline --timeline-item-icon-size: 34px; --timeline-item-left-margin: var(--margin-xl); diff --git a/frappe/public/scss/desk/list.scss b/frappe/public/scss/desk/list.scss index 0230138e8f..72fdf2c0f2 100644 --- a/frappe/public/scss/desk/list.scss +++ b/frappe/public/scss/desk/list.scss @@ -197,8 +197,7 @@ $level-margin-right: 8px; input.list-check-all, input.list-row-checkbox { margin-top: 0px; - margin-left: calc(var(--custom-checkbox-size) / 2); - --checkbox-right-margin: #{$level-margin-right}; + --checkbox-right-margin: calc(var(--checkbox-size) / 2 + #{$level-margin-right}); } .filterable { From 2ae8adb08a79a2dc5a57b79d6130300762736f1e Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Wed, 28 Apr 2021 16:48:50 +0530 Subject: [PATCH 129/213] fix: add box-shadow on :focus for tab key navigation (cherry picked from commit 0742a754b893383096773e1e31345da62387f70e) --- frappe/public/scss/common/global.scss | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/frappe/public/scss/common/global.scss b/frappe/public/scss/common/global.scss index e6f14f6b17..6696c2eb4b 100644 --- a/frappe/public/scss/common/global.scss +++ b/frappe/public/scss/common/global.scss @@ -50,6 +50,15 @@ input[type="checkbox"] { border: none; } + &:focus { + outline: none; // Prevent browser behavior + box-shadow: 0 0 0 2px var(--gray-300); + + [data-theme="dark"] & { + box-shadow: 0 0 0 2px var(--gray-600); + } + } + &.disabled-deselected, &:disabled { background-color: var(--disabled-control-bg); box-shadow: inset 0px 1px 7px rgba(0, 0, 0, 0.1); From 84e6a99743bb52ecba79ae20e9ea27432a09f68d Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Thu, 29 Apr 2021 15:17:17 +0530 Subject: [PATCH 130/213] fix: use css variable for themeability (cherry picked from commit 2fefa244628d62f4f35024821edcc94f0284a934) --- frappe/public/scss/common/global.scss | 8 ++------ frappe/public/scss/desk/css_variables.scss | 1 + frappe/public/scss/desk/dark.scss | 3 +++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/public/scss/common/global.scss b/frappe/public/scss/common/global.scss index 6696c2eb4b..726b3a98a6 100644 --- a/frappe/public/scss/common/global.scss +++ b/frappe/public/scss/common/global.scss @@ -30,7 +30,7 @@ input[type="checkbox"] { box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1); border-radius: 4px; - // Reset Browser Behavior + // Reset browser behavior -webkit-appearance: none; -moz-appearance: none; appearance: none; @@ -52,11 +52,7 @@ input[type="checkbox"] { &:focus { outline: none; // Prevent browser behavior - box-shadow: 0 0 0 2px var(--gray-300); - - [data-theme="dark"] & { - box-shadow: 0 0 0 2px var(--gray-600); - } + box-shadow: var(--checkbox-focus-shadow); } &.disabled-deselected, &:disabled { diff --git a/frappe/public/scss/desk/css_variables.scss b/frappe/public/scss/desk/css_variables.scss index ee9ccd7ba5..135bb7a9f5 100644 --- a/frappe/public/scss/desk/css_variables.scss +++ b/frappe/public/scss/desk/css_variables.scss @@ -52,6 +52,7 @@ $input-height: 28px !default; // checkbox --checkbox-right-margin: var(--margin-xs); --checkbox-size: 14px; + --checkbox-focus-shadow: 0 0 0 2px var(--gray-300); // timeline --timeline-item-icon-size: 34px; diff --git a/frappe/public/scss/desk/dark.scss b/frappe/public/scss/desk/dark.scss index 4e83f4db47..76dcf90bc3 100644 --- a/frappe/public/scss/desk/dark.scss +++ b/frappe/public/scss/desk/dark.scss @@ -78,6 +78,9 @@ // input --input-disabled-bg: none; + // checkbox + --checkbox-focus-shadow: 0 0 0 2px var(--gray-600); + color-scheme: dark; .frappe-card { From 518eeee035295d5ea1d2e30595cd75fd0d9d7ffa Mon Sep 17 00:00:00 2001 From: Anupam Date: Thu, 29 Apr 2021 21:25:10 +0530 Subject: [PATCH 131/213] fix: naming section display --- frappe/custom/doctype/customize_form/customize_form.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json index 442b8dbb31..2b5b5daa14 100644 --- a/frappe/custom/doctype/customize_form/customize_form.json +++ b/frappe/custom/doctype/customize_form/customize_form.json @@ -278,6 +278,7 @@ }, { "collapsible": 1, + "depends_on": "doc_type", "fieldname": "naming_section", "fieldtype": "Section Break", "label": "Naming" @@ -295,7 +296,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-03-22 12:27:15.462727", + "modified": "2021-04-29 21:21:06.476372", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form", @@ -316,4 +317,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file From 1af59ce16cea79767438efaacc02a9f675c88362 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 10:42:19 +0530 Subject: [PATCH 132/213] refactor: Move mention list generation logic to server-side - Moved mention list generation logic to server-side to get latest mention list everytime - To indicate group option, added a users icon. --- frappe/desk/search.py | 34 ++++++++++++++++++ .../public/js/frappe/form/controls/comment.js | 35 +++++++------------ frappe/public/js/frappe/form/footer/footer.js | 2 +- .../js/frappe/form/footer/form_timeline.js | 2 +- 4 files changed, 49 insertions(+), 24 deletions(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 6181261fc2..b3af1ea6f5 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -221,3 +221,37 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs): return [] return fn(**kwargs) + + +@frappe.whitelist() +def get_names_for_mentions(search_term): + users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions) + user_groups = frappe.cache().get_value('users_groups', get_user_groups) + + filtered_mentions = [] + for mention_data in users_for_mentions + user_groups: + if search_term.lower() not in mention_data.value.lower(): + continue + + mention_data['link'] = frappe.utils.get_url_to_form( + 'User Group' if mention_data.get('is_group') else 'User', + mention_data['id'] + ) + + filtered_mentions.append(mention_data) + + return sorted(filtered_mentions, key=lambda d: d['value']) + +def get_users_for_mentions(): + return frappe.get_all('User', + fields=['name as id', 'full_name as value'], + filters={ + 'name': ['not in', ('Administrator', 'Guest')], + 'allowed_in_mentions': True, + 'user_type': 'System User', + }) + +def get_user_groups(): + return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={ + 'is_group': True + }) diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index 59b53bf59e..f20d496b11 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -78,35 +78,26 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ }, get_mention_options() { - if (!(this.mentions && this.mentions.length)) { + if (!this.enable_mentions) { return null; } - - const at_values = this.mentions.slice(); - + let me = this; return { allowedChars: /^[A-Za-z0-9_]*$/, mentionDenotationChars: ["@"], isolateCharacter: true, - source: function (searchTerm, renderList, mentionChar) { - let values; + source: frappe.utils.debounce(async function(search_term, renderList) { + let method = me.mention_search_method || 'frappe.desk.search.get_names_for_mentions'; + let values = await frappe.xcall(method, { + search_term + }); + renderList(values, search_term); + }, 300), + renderItem(item) { + let value = item.value; + return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`; - if (mentionChar === "@") { - values = at_values; - } - - if (searchTerm.length === 0) { - renderList(values, searchTerm); - } else { - const matches = []; - for (let i = 0; i < values.length; i++) { - if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase())) { - matches.push(values[i]); - } - } - renderList(matches, searchTerm); - } - }, + } }; }, diff --git a/frappe/public/js/frappe/form/footer/footer.js b/frappe/public/js/frappe/form/footer/footer.js index a1dabedff0..63d8b0b57d 100644 --- a/frappe/public/js/frappe/form/footer/footer.js +++ b/frappe/public/js/frappe/form/footer/footer.js @@ -24,7 +24,7 @@ frappe.ui.form.Footer = Class.extend({ parent: this.wrapper.find(".comment-box"), render_input: true, only_input: true, - mentions: frappe.utils.get_names_for_mentions(), + enable_mentions: true, df: { fieldtype: 'Comment', fieldname: 'comment' diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index bd64c504ca..ab83ed2f71 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -492,7 +492,7 @@ class FormTimeline extends BaseTimeline { fieldname: 'comment', label: 'Comment' }, - mentions: frappe.utils.get_names_for_mentions(), + enable_mentions: true, render_input: true, only_input: true, no_wrapper: true From d8c777e98cc12d159b7f000885d630cbf759d7e5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 10:43:07 +0530 Subject: [PATCH 133/213] refactor: Remove unnecessary code --- frappe/boot.py | 2 -- frappe/public/js/frappe/desk.js | 11 ----------- frappe/public/js/frappe/utils/utils.js | 25 ------------------------- 3 files changed, 38 deletions(-) diff --git a/frappe/boot.py b/frappe/boot.py index 65a07b15e5..0dfcb8d1b4 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -42,8 +42,6 @@ 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/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index c093a73689..2331766710 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -114,8 +114,6 @@ frappe.Application = Class.extend({ dialog.get_close_btn().toggle(false); }); - this.setup_user_group_listeners(); - // listen to build errors this.setup_build_error_listener(); @@ -593,15 +591,6 @@ frappe.Application = Class.extend({ } }, - 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); - }); - }, - setup_energy_point_listeners() { frappe.realtime.on('energy_point_alert', (message) => { frappe.show_alert(message); diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 7ce30a525c..8e6a458c3e 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1272,31 +1272,6 @@ Object.assign(frappe.utils, {
    `); }, - get_names_for_mentions() { - let names_for_mentions = Object.keys(frappe.boot.user_info || []) - .filter(user => { - return !["Administrator", "Guest"].includes(user) - && frappe.boot.user_info[user].allowed_in_mentions - && frappe.boot.user_info[user].user_type === 'System User'; - }) - .map(user => { - return { - id: frappe.boot.user_info[user].name, - 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, - link: frappe.utils.get_form_link('User Group', group) - }); - }); - - return names_for_mentions; - }, print(doctype, docname, print_format, letterhead, lang_code) { let w = window.open( frappe.urllib.get_full_url( From 959b27ef0ebb55da72499cac56985423e06339ed Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 10:44:09 +0530 Subject: [PATCH 134/213] fix: Bust user / user group cache on change --- frappe/core/doctype/user/user.py | 7 +++++++ frappe/core/doctype/user_group/user_group.py | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 0462de8643..a4d13a57e0 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -56,6 +56,7 @@ class User(Document): def after_insert(self): create_notification_settings(self.name) + frappe.cache().delete_key('users_for_mentions') def validate(self): self.check_demo() @@ -129,6 +130,9 @@ class User(Document): if self.time_zone: frappe.defaults.set_default("time_zone", self.time_zone, self.name) + if self.has_value_changed('allow_in_mentions') or self.has_value_changed('user_type'): + frappe.cache().delete_key('users_for_mentions') + def has_website_permission(self, ptype, user, verbose=False): """Returns true if current user is the session user""" return self.name == frappe.session.user @@ -389,6 +393,9 @@ class User(Document): # delete notification settings frappe.delete_doc("Notification Settings", self.name, ignore_permissions=True) + if self.get('allow_in_mentions'): + frappe.cache().delete_key('users_for_mentions') + def before_rename(self, old_name, new_name, merge=False): self.check_demo() diff --git a/frappe/core/doctype/user_group/user_group.py b/frappe/core/doctype/user_group/user_group.py index 64bffa06d0..b1d0fede4c 100644 --- a/frappe/core/doctype/user_group/user_group.py +++ b/frappe/core/doctype/user_group/user_group.py @@ -9,7 +9,7 @@ import frappe class UserGroup(Document): def after_insert(self): - frappe.publish_realtime('user_group_added', self.name) + frappe.cache().delete_key('user_groups') def on_trash(self): - frappe.publish_realtime('user_group_deleted', self.name) + frappe.cache().delete_key('user_groups') From 22a2803f7b36608eb085d3fdb94771d82e498cd5 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 10:44:46 +0530 Subject: [PATCH 135/213] fix: Show pointer on hovering on mention list --- frappe/public/scss/common/quill.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index d15ca7e036..6dd0853635 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -105,6 +105,7 @@ padding: 10px 12px; height: initial; line-height: initial; + cursor: pointer; &.selected { background-color: var(--control-bg); From 183af96465a44cbf2526a22764603c1f0bdb8a01 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 10:53:20 +0530 Subject: [PATCH 136/213] fix: Link user mention to User Profile --- frappe/desk/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index b3af1ea6f5..588120588c 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -234,7 +234,7 @@ def get_names_for_mentions(search_term): continue mention_data['link'] = frappe.utils.get_url_to_form( - 'User Group' if mention_data.get('is_group') else 'User', + 'User Group' if mention_data.get('is_group') else 'User Profile', mention_data['id'] ) From 395eed225ff0a03babcc9f6fe38350cd6b85f717 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 11:03:18 +0530 Subject: [PATCH 137/213] style: Fix formatting --- frappe/desk/search.py | 8 ++++---- frappe/public/js/frappe/form/controls/comment.js | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 588120588c..065dc5c3ed 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -246,10 +246,10 @@ def get_users_for_mentions(): return frappe.get_all('User', fields=['name as id', 'full_name as value'], filters={ - 'name': ['not in', ('Administrator', 'Guest')], - 'allowed_in_mentions': True, - 'user_type': 'System User', - }) + 'name': ['not in', ('Administrator', 'Guest')], + 'allowed_in_mentions': True, + 'user_type': 'System User', + }) def get_user_groups(): return frappe.get_all('User Group', fields=['name as id', 'name as value'], update={ diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index f20d496b11..7efc60b61d 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -96,7 +96,6 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ renderItem(item) { let value = item.value; return `${value} ${item.is_group ? frappe.utils.icon('users') : ''}`; - } }; }, From 43a4f1861e30f1a762e4ea1a74921b05412c3e00 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 11:09:13 +0530 Subject: [PATCH 138/213] fix: Typo --- frappe/desk/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 065dc5c3ed..3c9109eca9 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -226,7 +226,7 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs): @frappe.whitelist() def get_names_for_mentions(search_term): users_for_mentions = frappe.cache().get_value('users_for_mentions', get_users_for_mentions) - user_groups = frappe.cache().get_value('users_groups', get_user_groups) + user_groups = frappe.cache().get_value('user_groups', get_user_groups) filtered_mentions = [] for mention_data in users_for_mentions + user_groups: From 127f5223e405b029a2e6e0712288378bc48e8b05 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 29 Apr 2021 11:01:57 +0530 Subject: [PATCH 139/213] fix: Respond to /api requests as JSON by default If header 'Accept: application/json' isn't set, the failure responses to /api endpoints is HTML. Success responses are of type JSON. (cherry picked from commit fcf63622bc0b557fdbbf4b91c30c638a7d106891) --- frappe/app.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frappe/app.py b/frappe/app.py index c9e993a853..794d0f18af 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -201,12 +201,20 @@ def handle_exception(e): response = None http_status_code = getattr(e, "http_status_code", 500) return_as_message = False + accept_header = frappe.get_request_header("Accept") or "" + respond_as_json = ( + frappe.get_request_header('Accept') + and (frappe.local.is_ajax or 'application/json' in accept_header) + or ( + frappe.local.request.path.startswith("/api/") and not accept_header.startswith("text") + ) + ) if frappe.conf.get('developer_mode'): # don't fail silently print(frappe.get_traceback()) - if frappe.get_request_header('Accept') and (frappe.local.is_ajax or 'application/json' in frappe.get_request_header('Accept')): + if respond_as_json: # handle ajax responses first # if the request is ajax, send back the trace or error message response = frappe.utils.response.report_error(http_status_code) From 509d4825413ea017641475e7b59ad800cb42d5c5 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Fri, 30 Apr 2021 12:12:23 +0530 Subject: [PATCH 140/213] fix: Changed shorcut widgets color picker to dropdown --- .../public/js/frappe/widgets/shortcut_widget.js | 3 +-- frappe/public/js/frappe/widgets/widget_dialog.js | 12 +++++++++++- frappe/public/scss/common/global.scss | 16 ++++++++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/widgets/shortcut_widget.js b/frappe/public/js/frappe/widgets/shortcut_widget.js index 689c57f361..a60903f7dc 100644 --- a/frappe/public/js/frappe/widgets/shortcut_widget.js +++ b/frappe/public/js/frappe/widgets/shortcut_widget.js @@ -2,7 +2,6 @@ import Widget from "./base_widget.js"; frappe.provide("frappe.utils"); -const INDICATOR_COLORS = ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan", "Teal"]; export default class ShortcutWidget extends Widget { constructor(opts) { opts.shadow = true; @@ -79,7 +78,7 @@ export default class ShortcutWidget extends Widget { this.action_area.empty(); const label = get_label(); - let color = INDICATOR_COLORS.includes(this.color) && count ? this.color.toLowerCase() : 'gray'; + let color = this.color && count ? this.color.toLowerCase() : 'gray'; $(`
    ${label}
    `).appendTo(this.action_area); } } diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index fce2a35539..14b09e78bc 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -237,9 +237,19 @@ class ShortcutDialog extends WidgetDialog { hidden: 1, }, { - fieldtype: "Color", + fieldtype: "Select", fieldname: "color", label: __("Color"), + options: ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan", "Teal"], + default: "Grey", + onchange: () => { + let color = this.dialog.fields_dict.color.value.toLowerCase(); + let $select = this.dialog.fields_dict.color.$input; + if (!$select.parent().find('.color-box').get(0)) { + $(`
    `).insertBefore($select.get(0)); + } + $select.parent().find('.color-box').get(0).style.backgroundColor = color; + } }, { fieldtype: "Column Break", diff --git a/frappe/public/scss/common/global.scss b/frappe/public/scss/common/global.scss index 726b3a98a6..e9646b30e5 100644 --- a/frappe/public/scss/common/global.scss +++ b/frappe/public/scss/common/global.scss @@ -76,6 +76,22 @@ input[type="checkbox"] { @include card(); } +.frappe-control[data-fieldtype="Select"].frappe-control[data-fieldname="color"] { + select { + padding-left: 40px; + } + + .color-box { + position: absolute; + top: calc(50% - 11px); + left: 8px; + width: 22px; + height: 22px; + border-radius: 5px; + z-index: 1; + } +} + .frappe-control[data-fieldtype="Select"] .control-input, .frappe-control[data-fieldtype="Select"].form-group { position: relative; From 2f6e72293455b7a0d04d09f8450ce9aa1083bb05 Mon Sep 17 00:00:00 2001 From: pateljannat Date: Fri, 30 Apr 2021 12:29:02 +0530 Subject: [PATCH 141/213] fix: added password field in web form --- frappe/website/doctype/web_form_field/web_form_field.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/website/doctype/web_form_field/web_form_field.json b/frappe/website/doctype/web_form_field/web_form_field.json index 72fcccf555..2770f03e80 100644 --- a/frappe/website/doctype/web_form_field/web_form_field.json +++ b/frappe/website/doctype/web_form_field/web_form_field.json @@ -39,7 +39,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Fieldtype", - "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break" + "options": "Attach\nAttach Image\nCheck\nCurrency\nData\nDate\nDatetime\nDuration\nFloat\nHTML\nInt\nLink\nPassword\nRating\nSelect\nSmall Text\nText\nText Editor\nTable\nSection Break\nColumn Break" }, { "fieldname": "label", @@ -146,7 +146,7 @@ ], "istable": 1, "links": [], - "modified": "2020-11-10 23:20:44.354862", + "modified": "2021-04-30 12:02:25.422345", "modified_by": "Administrator", "module": "Website", "name": "Web Form Field", From 9bb0ea0301bc6b28ffaacfa9638cc87102bcfcb2 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 30 Apr 2021 12:43:55 +0530 Subject: [PATCH 142/213] fix: Show icon instead of a different color for group --- .../js/frappe/form/controls/quill-mention/blots/mention.js | 1 + frappe/public/scss/common/quill.scss | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) 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 d6907158f9..4a3c2d2eba 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 @@ -12,6 +12,7 @@ class MentionBlot extends Embed { denotationChar.innerHTML = data.denotationChar; node.appendChild(denotationChar); node.innerHTML += data.value; + node.innerHTML += `${data.isGroup === 'true' ? frappe.utils.icon('users') : ''}`; node.dataset.id = data.id; node.dataset.value = data.value; node.dataset.denotationChar = data.denotationChar; diff --git a/frappe/public/scss/common/quill.scss b/frappe/public/scss/common/quill.scss index 6dd0853635..24e8293cf3 100644 --- a/frappe/public/scss/common/quill.scss +++ b/frappe/public/scss/common/quill.scss @@ -197,5 +197,8 @@ } .mention[data-is-group="true"] { - background-color: var(--group-mention-bg-color); + .icon { + margin-top: -2px; + margin-left: 4px; + } } From 9d4ee238d76811ad4eccb079fed0c6b2df92ec8f Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 30 Apr 2021 14:52:16 +0530 Subject: [PATCH 143/213] fix: Remove duplicate validation function --- .../doctype/event_producer/event_producer.py | 3 +-- frappe/utils/__init__.py | 14 ++++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index 3d97583549..a6068960ac 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -55,8 +55,7 @@ class EventProducer(Document): self.reload() def check_url(self): - if not frappe.utils.validate_url(self.producer_url): - frappe.throw(_('Invalid URL')) + frappe.utils.validate_url(self.producer_url, throw=True) # remove '/' from the end of the url like http://test_site.com/ # to prevent mismatch in get_url() results diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 6942bcb5c5..051b896c56 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -156,10 +156,16 @@ def split_emails(txt): return email_list def validate_url(txt, throw=False): + if not url: + return True + try: url = urlparse(txt).netloc if not url: raise frappe.ValidationError + else: + return True + except Exception: if throw: frappe.throw( @@ -820,11 +826,3 @@ def groupby_metric(iterable: typing.Dict[str, list], key: str): for item in items: records.setdefault(item[key], {}).setdefault(category, []).append(item) return records - - -def validate_url(url_string): - try: - result = urlparse(url_string) - return result.scheme and result.scheme in ["http", "https", "ftp", "ftps"] - except Exception: - return False From b965cebbd43c0e898e0fc71edd059ef31ef06905 Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 30 Apr 2021 14:55:32 +0530 Subject: [PATCH 144/213] fix: Sider issues --- frappe/utils/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 051b896c56..30b3a3584e 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -156,7 +156,7 @@ def split_emails(txt): return email_list def validate_url(txt, throw=False): - if not url: + if not txt: return True try: From 3e229e931ae2218f2734ac90cd365db8f017f76c Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Fri, 30 Apr 2021 15:37:55 +0530 Subject: [PATCH 145/213] test: Email and URL validate functions --- frappe/model/base_document.py | 3 ++ frappe/tests/test_utils.py | 53 +++++++++++++++++++++++++++++++---- frappe/utils/__init__.py | 3 -- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index cf63aa98b6..05435482bd 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -667,6 +667,9 @@ class BaseDocument(object): frappe.utils.validate_phone_number(data, throw=True) if data_field_options == "URL": + if not data: + continue + frappe.utils.validate_url(data, throw=True) def _validate_constants(self): diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index ebba60b8e8..20d2bf127c 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -3,8 +3,10 @@ from __future__ import unicode_literals import unittest +import frappe from frappe.utils import evaluate_filters, money_in_words, scrub_urls, get_url +from frappe.utils import validate_url, validate_email_address from frappe.utils import ceil, floor from PIL import Image @@ -54,14 +56,15 @@ class TestMoney(unittest.TestCase): for num in nums_bhd: self.assertEqual( - money_in_words(num[0], "BHD"), num[1], "{0} is not the same as {1}". - format(money_in_words(num[0], "BHD"), num[1]) + money_in_words(num[0], "BHD"), + num[1], + "{0} is not the same as {1}".format(money_in_words(num[0], "BHD"), num[1]) ) for num in nums_ngn: self.assertEqual( - money_in_words(num[0], "NGN"), num[1], "{0} is not the same as {1}". - format(money_in_words(num[0], "NGN"), num[1]) + money_in_words(num[0], "NGN"), num[1], + "{0} is not the same as {1}".format(money_in_words(num[0], "NGN"), num[1]) ) class TestDataManipulation(unittest.TestCase): @@ -93,7 +96,7 @@ class TestDataManipulation(unittest.TestCase): class TestMathUtils(unittest.TestCase): def test_floor(self): from decimal import Decimal - self.assertEqual(floor(2), 2 ) + self.assertEqual(floor(2), 2) self.assertEqual(floor(12.32904), 12) self.assertEqual(floor(22.7330), 22) self.assertEqual(floor('24.7'), 24) @@ -102,7 +105,7 @@ class TestMathUtils(unittest.TestCase): def test_ceil(self): from decimal import Decimal - self.assertEqual(ceil(2), 2 ) + self.assertEqual(ceil(2), 2) self.assertEqual(ceil(12.32904), 13) self.assertEqual(ceil(22.7330), 23) self.assertEqual(ceil('24.7'), 25) @@ -127,6 +130,44 @@ class TestHTMLUtils(unittest.TestCase): self.assertTrue('

    Hello

    ' in clean) self.assertTrue('text' in clean) +class TestValidationUtils(unittest.TestCase): + def test_valid_url(self): + # Edge cases + self.assertFalse(validate_url('')) + self.assertFalse(validate_url(None)) + + # Valid URLs + self.assertTrue(validate_url('https://google.com')) + self.assertTrue(validate_url('https://frappe.io', throw=True)) + + # Invalid URLs without throw + self.assertFalse(validate_url('google.io')) + self.assertFalse(validate_url('google.io')) + + # Invalid URL with throw + self.assertRaises(frappe.ValidationError, validate_url, 'frappe', throw=True) + + def test_valid_email(self): + # Edge cases + self.assertFalse(validate_email_address('')) + self.assertFalse(validate_email_address(None)) + + # Valid addresses + self.assertTrue(validate_email_address('someone@frappe.com')) + self.assertTrue(validate_email_address('someone@frappe.com, anyone@frappe.io')) + + # Invalid address + self.assertFalse(validate_email_address('someone')) + self.assertFalse(validate_email_address('someone@----.com')) + + # Invalid with throw + self.assertRaises( + frappe.InvalidEmailAddressError, + validate_email_address, + 'someone.com', + throw=True + ) + class TestImage(unittest.TestCase): def test_strip_exif_data(self): original_image = Image.open("../apps/frappe/frappe/tests/data/exif_sample_image.jpg") diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 30b3a3584e..fb778834de 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -156,9 +156,6 @@ def split_emails(txt): return email_list def validate_url(txt, throw=False): - if not txt: - return True - try: url = urlparse(txt).netloc if not url: From 80e009f8069c32ecd5420e8a3279c11bddf455cc Mon Sep 17 00:00:00 2001 From: Steffen Date: Fri, 30 Apr 2021 16:05:28 +0200 Subject: [PATCH 146/213] fix: error for bench drop-site. Added missing import. --- frappe/commands/site.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 658bd21a45..14a300ecc2 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1,6 +1,7 @@ # imports - standard imports import os import sys +import shutil # imports - third party imports import click From 5b9fe286af3e68f7bcc2d903367ec550f18cd012 Mon Sep 17 00:00:00 2001 From: Steffen Date: Fri, 30 Apr 2021 16:05:28 +0200 Subject: [PATCH 147/213] fix: error for bench drop-site. Added missing import. (cherry picked from commit 80e009f8069c32ecd5420e8a3279c11bddf455cc) --- frappe/commands/site.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 0102d3ac40..022fe5f22d 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -1,6 +1,7 @@ # imports - standard imports import os import sys +import shutil # imports - third party imports import click From 024e759a70283478434dc37b37cdfae5609f5cac Mon Sep 17 00:00:00 2001 From: Hussain Nagaria Date: Sat, 1 May 2021 00:34:19 +0530 Subject: [PATCH 148/213] refactor: Add optional URL scheme validation --- .../doctype/event_producer/event_producer.py | 3 +- frappe/oauth.py | 3 +- frappe/tests/test_utils.py | 14 ++++++- frappe/utils/__init__.py | 39 ++++++++++++------- 4 files changed, 42 insertions(+), 17 deletions(-) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index a6068960ac..8785ee9989 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -55,7 +55,8 @@ class EventProducer(Document): self.reload() def check_url(self): - frappe.utils.validate_url(self.producer_url, throw=True) + valid_url_schemes = ("http", "https", "ftp", "ftps") + frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes) # remove '/' from the end of the url like http://test_site.com/ # to prevent mismatch in get_url() results diff --git a/frappe/oauth.py b/frappe/oauth.py index 3287bf7520..35f047a2b6 100644 --- a/frappe/oauth.py +++ b/frappe/oauth.py @@ -599,9 +599,10 @@ def get_client_scopes(client_id): def get_userinfo(user): picture = None frappe_server_url = get_server_url() + valid_url_schemes = ("http", "https", "ftp", "ftps") if user.user_image: - if frappe.utils.validate_url(user.user_image): + if frappe.utils.validate_url(user.user_image, valid_schemes=valid_url_schemes): picture = user.user_image else: picture = frappe_server_url + "/" + user.user_image diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 20d2bf127c..998afb86c1 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -138,7 +138,7 @@ class TestValidationUtils(unittest.TestCase): # Valid URLs self.assertTrue(validate_url('https://google.com')) - self.assertTrue(validate_url('https://frappe.io', throw=True)) + self.assertTrue(validate_url('http://frappe.io', throw=True)) # Invalid URLs without throw self.assertFalse(validate_url('google.io')) @@ -147,6 +147,18 @@ class TestValidationUtils(unittest.TestCase): # Invalid URL with throw self.assertRaises(frappe.ValidationError, validate_url, 'frappe', throw=True) + # Scheme validation + self.assertFalse(validate_url('https://google.com', valid_schemes='http')) + self.assertTrue(validate_url('ftp://frappe.cloud', valid_schemes=['https', 'ftp'])) + self.assertFalse(validate_url('bolo://frappe.io', valid_schemes=("http", "https", "ftp", "ftps"))) + self.assertRaises( + frappe.ValidationError, + validate_url, + 'bitcoin://joker.edu', + valid_schemes='https', + throw=True + ) + def test_valid_email(self): # Edge cases self.assertFalse(validate_email_address('')) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index fb778834de..41d9a07542 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -155,21 +155,32 @@ def split_emails(txt): return email_list -def validate_url(txt, throw=False): - try: - url = urlparse(txt).netloc - if not url: - raise frappe.ValidationError - else: - return True +def validate_url(txt, throw=False, valid_schemes=None): + """ + Tests wether the `txt` is a valid URL - except Exception: - if throw: - frappe.throw( - frappe._("'{0}' is not a valid URL").format(frappe.bold(txt)) - ) - - return False + Parameters: + throw (`bool`): throws a validationError if URL is not valid + valid_schemes (`str` or `list`): if provided checks the given URL's scheme against this + + Returns: + bool: if `txt` represents a valid URL + """ + url = urlparse(txt) + is_valid = bool(url.netloc) + + # Handle scheme validation + if isinstance(valid_schemes, str): + is_valid = is_valid and (url.scheme == valid_schemes) + elif isinstance(valid_schemes, (list, tuple, set)): + is_valid = is_valid and (url.scheme in valid_schemes) + + if not is_valid and throw: + frappe.throw( + frappe._("'{0}' is not a valid URL").format(frappe.bold(txt)) + ) + + return is_valid def random_string(length): """generate a random string""" From b4726175ca87c7e40152839209f2a674bba4b632 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 1 May 2021 14:10:27 +0530 Subject: [PATCH 149/213] chore: Add release note for v13.2.0 --- frappe/change_log/v13/v13_2_0.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 frappe/change_log/v13/v13_2_0.md diff --git a/frappe/change_log/v13/v13_2_0.md b/frappe/change_log/v13/v13_2_0.md new file mode 100644 index 0000000000..6fc3eec5e3 --- /dev/null +++ b/frappe/change_log/v13/v13_2_0.md @@ -0,0 +1,32 @@ +# Version 13.2.0 Release Notes + +### Features & Enhancements + +- Add option to mention a group of users ([#12844](https://github.com/frappe/frappe/pull/12844)) +- Copy DocType / documents across sites ([#12872](https://github.com/frappe/frappe/pull/12872)) +- Scheduler log in notifications ([#1135](https://github.com/frappe/frappe/pull/1135)) +- Add Enable/Disable Webhook via Check Field ([#12842](https://github.com/frappe/frappe/pull/12842)) +- Allow query/custom reports to save custom data in the json field ([#12534](https://github.com/frappe/frappe/pull/12534)) + +### Fixes + +- Load server translations in boot (backport #12848) ([#12852](https://github.com/frappe/frappe/pull/12852)) +- Allow to override dashboard chart properties type/color ([#12846](https://github.com/frappe/frappe/pull/12846)) +- Multi-column paste in grid ([#12861](https://github.com/frappe/frappe/pull/12861)) +- Add log_error and FrappeClient to restricted python ([#12857](https://github.com/frappe/frappe/pull/12857)) +- Redirect Web Form user directly to success URL, if no amount is due ([#12661](https://github.com/frappe/frappe/pull/12661)) +- Attachment pill lock icon redirects to File ([#12864](https://github.com/frappe/frappe/pull/12864)) +- Redirect Web Form user directly to success URL, if no amount is due (backport #12661) ([#12856](https://github.com/frappe/frappe/pull/12856)) +- Remove events to redraw charts ([#12973](https://github.com/frappe/frappe/pull/12973)) +- Don't allow user to remove/change data source file in data import ([#12827](https://github.com/frappe/frappe/pull/12827)) +- Load server translations in boot ([#12848](https://github.com/frappe/frappe/pull/12848)) +- Newly created Workspace not being accessible unless a shortcut u… ([#12866](https://github.com/frappe/frappe/pull/12866)) +- Currency labels in grids ([#12974](https://github.com/frappe/frappe/pull/12974)) +- Handle error while session start ([#12933](https://github.com/frappe/frappe/pull/12933)) +- Add field type check in custom field validation ([#12858](https://github.com/frappe/frappe/pull/12858)) +- Make language select optional and fix breakpoint issues ([#12860](https://github.com/frappe/frappe/pull/12860)) +- Form Dashboard reference link ([#12945](https://github.com/frappe/frappe/pull/12945)) +- Invalid HTML generated by the base template ([#12953](https://github.com/frappe/frappe/pull/12953)) +- Default values were not triggering change event ([#12975](https://github.com/frappe/frappe/pull/12975)) +- Make strings translatable ([#12877](https://github.com/frappe/frappe/pull/12877)) +- Added build-message-files command ([#12950](https://github.com/frappe/frappe/pull/12950)) \ No newline at end of file From 8c7661b6820f21b0fd818a8155508f1a98d9d915 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 1 May 2021 15:04:35 +0550 Subject: [PATCH 150/213] bumped to version 13.2.0 --- frappe/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 3e9c0bfedf..7a7881947f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -34,7 +34,7 @@ if PY2: reload(sys) sys.setdefaultencoding("utf-8") -__version__ = '13.1.2' +__version__ = '13.2.0' __title__ = "Frappe Framework" From 9fd6491631648a0434e4e54fb40ecbaa2e8bbb9e Mon Sep 17 00:00:00 2001 From: shariquerik Date: Sat, 1 May 2021 15:08:31 +0530 Subject: [PATCH 151/213] refactor: Used css variables also added missing colors --- .../public/js/frappe/widgets/widget_dialog.js | 4 ++-- frappe/public/scss/common/css_variables.scss | 15 ++++++++++++++ frappe/public/scss/common/indicator.scss | 20 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/widgets/widget_dialog.js b/frappe/public/js/frappe/widgets/widget_dialog.js index 14b09e78bc..eefb78c29a 100644 --- a/frappe/public/js/frappe/widgets/widget_dialog.js +++ b/frappe/public/js/frappe/widgets/widget_dialog.js @@ -240,7 +240,7 @@ class ShortcutDialog extends WidgetDialog { fieldtype: "Select", fieldname: "color", label: __("Color"), - options: ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan", "Teal"], + options: ["Grey", "Green", "Red", "Orange", "Pink", "Yellow", "Blue", "Cyan"], default: "Grey", onchange: () => { let color = this.dialog.fields_dict.color.value.toLowerCase(); @@ -248,7 +248,7 @@ class ShortcutDialog extends WidgetDialog { if (!$select.parent().find('.color-box').get(0)) { $(`
    `).insertBefore($select.get(0)); } - $select.parent().find('.color-box').get(0).style.backgroundColor = color; + $select.parent().find('.color-box').get(0).style.backgroundColor = `var(--text-on-${color})`; } }, { diff --git a/frappe/public/scss/common/css_variables.scss b/frappe/public/scss/common/css_variables.scss index 8f4af36389..7058fdc42c 100644 --- a/frappe/public/scss/common/css_variables.scss +++ b/frappe/public/scss/common/css_variables.scss @@ -24,6 +24,17 @@ --blue-100: #D3E9FC; --blue-50 : #F0F8FE; + --cyan-900: #006464; + --cyan-800: #007272; + --cyan-700: #008b8b; + --cyan-600: #02c5c5; + --cyan-500: #00ffff; + --cyan-400: #2ef8f8; + --cyan-300: #6efcfc; + --cyan-200: #a0f8f8; + --cyan-100: #c7fcfc; + --cyan-50 : #dafafa; + --green-900: #2D401D; --green-800: #44622A; --green-700: #518B21; @@ -151,6 +162,8 @@ --bg-gray: var(--gray-200); --bg-light-gray: var(--gray-100); --bg-purple: var(--purple-100); + --bg-pink: var(--pink-50); + --bg-cyan: var(--cyan-50); --text-on-blue: var(--blue-600); --text-on-light-blue: var(--blue-500); @@ -163,6 +176,8 @@ --text-on-gray: var(--gray-600); --text-on-light-gray: var(--gray-800); --text-on-purple: var(--purple-500); + --text-on-pink: var(--pink-500); + --text-on-cyan: var(--cyan-600); --awesomplete-hover-bg: var(--control-bg); diff --git a/frappe/public/scss/common/indicator.scss b/frappe/public/scss/common/indicator.scss index 75063edc83..62d7cacc9d 100644 --- a/frappe/public/scss/common/indicator.scss +++ b/frappe/public/scss/common/indicator.scss @@ -77,6 +77,16 @@ @include indicator-pill-color('green'); } +.indicator.cyan { + @include indicator-color('cyan'); +} + +.indicator-pill.cyan, +.indicator-pill-right.cyan, +.indicator-pill-round.cyan { + @include indicator-pill-color('cyan'); +} + .indicator.blue { @include indicator-color('blue'); } @@ -131,6 +141,16 @@ @include indicator-pill-color('red'); } +.indicator.pink { + @include indicator-color('pink'); +} + +.indicator-pill.pink, +.indicator-pill-right.pink, +.indicator-pill-round.pink { + @include indicator-pill-color('pink'); +} + .indicator-pill.darkgrey, .indicator-pill-right.darkgrey, .indicator-pill-round.darkgrey { From 835daa18edf3596001f3b91e74e2ffeed8e61043 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Sat, 1 May 2021 17:22:29 +0530 Subject: [PATCH 152/213] feat: switch theme with left/right keys --- frappe/public/js/frappe/ui/theme_switcher.js | 28 ++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/frappe/public/js/frappe/ui/theme_switcher.js b/frappe/public/js/frappe/ui/theme_switcher.js index 317198bca5..4524472415 100644 --- a/frappe/public/js/frappe/ui/theme_switcher.js +++ b/frappe/public/js/frappe/ui/theme_switcher.js @@ -11,6 +11,34 @@ frappe.ui.ThemeSwitcher = class ThemeSwitcher { title: __("Switch Theme") }); this.body = $(`
    `).appendTo(this.dialog.$body); + this.bind_events(); + } + + bind_events() { + this.dialog.$wrapper.on('keydown', (e) => { + if (!this.themes) return; + + const key = frappe.ui.keys.get_key(e); + let increment_by; + + if (key === "right") { + increment_by = 1; + } else if (key === "left") { + increment_by = -1; + } else { + return; + } + + const current_index = this.themes.findIndex(theme => { + return theme.name === this.current_theme; + }); + + const new_theme = this.themes[current_index + increment_by]; + if (!new_theme) return; + + new_theme.$html.click(); + return false; + }); } refresh() { From 9c5ac3a1a4b177f440168201f142d5075d21c307 Mon Sep 17 00:00:00 2001 From: codescientist703 Date: Sat, 1 May 2021 19:43:38 +0530 Subject: [PATCH 153/213] fix: Added conditional rendering for content field in split section with image template --- .../split_section_with_image/split_section_with_image.html | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/website/web_template/split_section_with_image/split_section_with_image.html b/frappe/website/web_template/split_section_with_image/split_section_with_image.html index cae2482910..5ebeef3912 100644 --- a/frappe/website/web_template/split_section_with_image/split_section_with_image.html +++ b/frappe/website/web_template/split_section_with_image/split_section_with_image.html @@ -16,7 +16,9 @@ {%- endif -%}

    {{ title }}

    + {%- if content -%}

    {{ content }}

    + {%- endif -%} {%- if link_label and link_url -%} {{ link_label }} From f4e4fc98f92b04d2878413dcea008d5d2bf6e642 Mon Sep 17 00:00:00 2001 From: Abhishek Balam Date: Sun, 2 May 2021 00:32:01 +0530 Subject: [PATCH 154/213] fix: remove unnecessary schemes and minor changes --- frappe/event_streaming/doctype/event_producer/event_producer.py | 2 +- frappe/tests/test_utils.py | 2 +- frappe/utils/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index 8785ee9989..4836276734 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -55,7 +55,7 @@ class EventProducer(Document): self.reload() def check_url(self): - valid_url_schemes = ("http", "https", "ftp", "ftps") + valid_url_schemes = ("http", "https") frappe.utils.validate_url(self.producer_url, throw=True, valid_schemes=valid_url_schemes) # remove '/' from the end of the url like http://test_site.com/ diff --git a/frappe/tests/test_utils.py b/frappe/tests/test_utils.py index 998afb86c1..74ceec8287 100644 --- a/frappe/tests/test_utils.py +++ b/frappe/tests/test_utils.py @@ -154,7 +154,7 @@ class TestValidationUtils(unittest.TestCase): self.assertRaises( frappe.ValidationError, validate_url, - 'bitcoin://joker.edu', + 'gopher://frappe.io', valid_schemes='https', throw=True ) diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 41d9a07542..1da4cd3bae 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -157,7 +157,7 @@ def split_emails(txt): def validate_url(txt, throw=False, valid_schemes=None): """ - Tests wether the `txt` is a valid URL + Checks whether `txt` has a valid URL string Parameters: throw (`bool`): throws a validationError if URL is not valid From a8f74d9471c5116017d7469e21190135e66d8494 Mon Sep 17 00:00:00 2001 From: leela Date: Tue, 27 Apr 2021 09:15:03 +0530 Subject: [PATCH 155/213] refactor: Move finding email accounts code to EmailAccount doctype --- frappe/app.py | 2 +- frappe/core/doctype/communication/email.py | 21 +-- .../doctype/email_account/email_account.py | 155 +++++++++++++++++- .../doctype/email_domain/test_records.json | 6 +- frappe/email/email_body.py | 11 +- frappe/email/queue.py | 9 +- frappe/email/smtp.py | 132 +-------------- frappe/email/test_smtp.py | 10 +- frappe/utils/error.py | 50 +++++- 9 files changed, 231 insertions(+), 165 deletions(-) diff --git a/frappe/app.py b/frappe/app.py index f17f1494b2..5dbebb061d 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -184,7 +184,7 @@ def make_form_dict(request): args = request.form or request.args if not isinstance(args, dict): - frappe.throw("Invalid request arguments") + frappe.throw(_("Invalid request arguments")) try: frappe.local.form_dict = frappe._dict({ k:v[0] if isinstance(v, (list, tuple)) else v \ diff --git a/frappe/core/doctype/communication/email.py b/frappe/core/doctype/communication/email.py index 731cb85d7c..d3017055cf 100755 --- a/frappe/core/doctype/communication/email.py +++ b/frappe/core/doctype/communication/email.py @@ -272,22 +272,13 @@ def prepare_to_notify(doc, print_html=None, print_format=None, attachments=None) doc.attachments.append(a) def set_incoming_outgoing_accounts(doc): - doc.incoming_email_account = doc.outgoing_email_account = None + from frappe.email.doctype.email_account.email_account import EmailAccount + incoming_email_account = EmailAccount.find_incoming( + match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) + doc.incoming_email_account = incoming_email_account.email_id if incoming_email_account else None - if not doc.incoming_email_account and doc.sender: - doc.incoming_email_account = frappe.db.get_value("Email Account", - {"email_id": doc.sender, "enable_incoming": 1}, "email_id") - - if not doc.incoming_email_account and doc.reference_doctype: - doc.incoming_email_account = frappe.db.get_value("Email Account", - {"append_to": doc.reference_doctype, }, "email_id") - - if not doc.incoming_email_account: - doc.incoming_email_account = frappe.db.get_value("Email Account", - {"default_incoming": 1, "enable_incoming": 1}, "email_id") - - doc.outgoing_email_account = frappe.email.smtp.get_outgoing_email_account(raise_exception_not_set=False, - append_to=doc.doctype, sender=doc.sender) + doc.outgoing_email_account = EmailAccount.find_outgoing( + match_by_email=doc.sender, match_by_doctype=doc.reference_doctype) if doc.sent_or_received == "Sent": doc.db_set("email_account", doc.outgoing_email_account.name) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 4869c5a9bf..3aa7c10ea5 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -8,9 +8,14 @@ import re import json import socket import time -from frappe import _ +import functools + +import email.utils + +from frappe import _, are_emails_muted from frappe.model.document import Document -from frappe.utils import validate_email_address, cint, cstr, get_datetime, DATE_FORMAT, strip, comma_or, sanitize_html, add_days +from frappe.utils import (validate_email_address, cint, cstr, get_datetime, + DATE_FORMAT, strip, comma_or, sanitize_html, add_days, parse_addr) from frappe.utils.user import is_system_user from frappe.utils.jinja import render_template from frappe.email.smtp import SMTPServer @@ -21,17 +26,40 @@ from datetime import datetime, timedelta from frappe.desk.form import assign_to from frappe.utils.user import get_system_managers from frappe.utils.background_jobs import enqueue, get_jobs -from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts from frappe.utils.html_utils import clean_email_html +from frappe.utils.error import raise_error_on_no_output from frappe.email.utils import get_port +OUTGOING_EMAIL_ACCOUNT_MISSING = _("Please setup default Email Account from Setup > Email > Email Account") + class SentEmailInInbox(Exception): pass class InvalidEmailCredentials(frappe.ValidationError): pass +def cache_email_account(cache_name): + def decorator_cache_email_account(func): + @functools.wraps(func) + def wrapper_cache_email_account(*args, **kwargs): + if not hasattr(frappe.local, cache_name): + setattr(frappe.local, cache_name, {}) + + cached_accounts = getattr(frappe.local, cache_name) + match_by = list(kwargs.values()) + ['default'] + matched_accounts = list(filter(None, [cached_accounts.get(key) for key in match_by])) + if matched_accounts: + return matched_accounts[0] + + matched_accounts = func(*args, **kwargs) + cached_accounts.update(matched_accounts or {}) + return matched_accounts and list(matched_accounts.values())[0] + return wrapper_cache_email_account + return decorator_cache_email_account + class EmailAccount(Document): + DOCTYPE = 'Email Account' + def autoname(self): """Set name as `email_account_name` or make title from Email Address.""" if not self.email_account_name: @@ -249,6 +277,15 @@ class EmailAccount(Document): else: raise + @property + def _password(self): + raise_exception = not self.no_smtp_authentication + return self.get_password(raise_exception=raise_exception) + + @property + def default_sender(self): + return email.utils.formataddr((self.name, self.get("email_id"))) + @classmethod def throw_invalid_credentials_exception(cls): frappe.throw( @@ -257,6 +294,114 @@ class EmailAccount(Document): title=_("Invalid Credentials") ) + @classmethod + def from_record(cls, record): + email_account = frappe.new_doc(cls.DOCTYPE) + email_account.update(record) + return email_account + + @classmethod + def find(cls, name): + return frappe.get_doc(cls.DOCTYPE, name) + + @classmethod + def find_one_by_filters(cls, **kwargs): + name = frappe.db.get_value(cls.DOCTYPE, kwargs) + return cls.find(name) if name else None + + @classmethod + def find_from_config(cls): + config = cls.get_account_details_from_site_config() + return cls.from_record(config) if config else None + + @classmethod + def create_dummy(cls): + return cls.from_record({"sender": "notifications@example.com"}) + + @classmethod + @raise_error_on_no_output( + keep_quiet = lambda: not cint(frappe.get_system_settings('setup_complete')), + error_message = OUTGOING_EMAIL_ACCOUNT_MISSING, error_type = frappe.OutgoingEmailError) # noqa + @cache_email_account('outgoing_email_account') + def find_outgoing(cls, match_by_email=None, match_by_doctype=None, _raise_error=False): + """Find the outgoing Email account to use. + + :param match_by_email: Find account using emailID + :param match_by_doctype: Find account by matching `Append To` doctype + :param _raise_error: This is used by raise_error_on_no_output decorator to raise error. + """ + if match_by_email: + match_by_email = parse_addr(match_by_email)[1] + doc = cls.find_one_by_filters(enable_outgoing=1, email_id=match_by_email) + if doc: + return {match_by_email: doc} + + if match_by_doctype: + doc = cls.find_one_by_filters(enable_outgoing=1, enable_incoming=1, append_to=match_by_doctype) + if doc: + return {match_by_doctype: doc} + + doc = cls.find_default_outgoing() + if doc: + return {'default': doc} + + @classmethod + def find_default_outgoing(cls): + """ Find default outgoing account. + """ + doc = cls.find_one_by_filters(enable_outgoing=1, default_outgoing=1) + doc = doc or cls.find_from_config() + return doc or (are_emails_muted() and cls.create_dummy()) + + @classmethod + def find_incoming(cls, match_by_email=None, match_by_doctype=None): + """Find the incoming Email account to use. + :param match_by_email: Find account using emailID + :param match_by_doctype: Find account by matching `Append To` doctype + """ + doc = cls.find_one_by_filters(enable_incoming=1, email_id=match_by_email) + if doc: + return doc + + doc = cls.find_one_by_filters(enable_incoming=1, append_to=match_by_doctype) + if doc: + return doc + + doc = cls.find_default_incoming() + return doc + + @classmethod + def find_default_incoming(cls): + doc = cls.find_one_by_filters(enable_incoming=1, default_incoming=1) + return doc + + @classmethod + def get_account_details_from_site_config(cls): + if not frappe.conf.get("mail_server"): + return {} + + field_to_conf_name_map = { + 'smtp_server': {'conf_names': ('mail_server',)}, + 'smtp_port': {'conf_names': ('mail_port',)}, + 'use_tls': {'conf_names': ('use_tls', 'mail_login')}, + 'login_id': {'conf_names': ('mail_login',)}, + 'email_id': {'conf_names': ('auto_email_id', 'mail_login'), 'default': 'notifications@example.com'}, + 'password': {'conf_names': ('mail_password',)}, + 'always_use_account_email_id_as_sender': + {'conf_names': ('always_use_account_email_id_as_sender',), 'default': 0}, + 'always_use_account_name_as_sender_name': + {'conf_names': ('always_use_account_name_as_sender_name',), 'default': 0}, + 'name': {'conf_names': ('email_sender_name',), 'default': 'Frappe'}, + 'from_site_config': {'default': True} + } + + account_details = {} + for doc_field_name, d in field_to_conf_name_map.items(): + conf_names, default = d.get('conf_names') or [], d.get('default') + value = [frappe.conf.get(k) for k in conf_names if frappe.conf.get(k)] + account_details[doc_field_name] = (value and value[0]) or default + return account_details + def handle_incoming_connect_error(self, description): if test_internet(): if self.get_failed_attempts_count() > 2: @@ -642,6 +787,8 @@ class EmailAccount(Document): def send_auto_reply(self, communication, email): """Send auto reply if set.""" + from frappe.core.doctype.communication.email import set_incoming_outgoing_accounts + if self.enable_auto_reply: set_incoming_outgoing_accounts(communication) @@ -653,7 +800,7 @@ class EmailAccount(Document): frappe.sendmail(recipients = [email.from_email], sender = self.email_id, reply_to = communication.incoming_email_account, - subject = _("Re: ") + communication.subject, + subject = " ".join([_("Re:"), communication.subject]), content = render_template(self.auto_reply_message or "", communication.as_dict()) or \ frappe.get_template("templates/emails/auto_reply.html").render(communication.as_dict()), reference_doctype = communication.reference_doctype, diff --git a/frappe/email/doctype/email_domain/test_records.json b/frappe/email/doctype/email_domain/test_records.json index 32bc66e150..a6ccc99f06 100644 --- a/frappe/email/doctype/email_domain/test_records.json +++ b/frappe/email/doctype/email_domain/test_records.json @@ -10,7 +10,8 @@ "incoming_port": "993", "attachment_limit": "1", "smtp_server": "smtp.test.com", - "smtp_port": "587" + "smtp_port": "587", + "password": "password" }, { "doctype": "Email Account", @@ -25,6 +26,7 @@ "incoming_port": "143", "attachment_limit": "1", "smtp_server": "smtp.test.com", - "smtp_port": "587" + "smtp_port": "587", + "password": "password" } ] diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 3dcdf00a8e..45888119ea 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, re, os from frappe.utils.pdf import get_pdf -from frappe.email.smtp import get_outgoing_email_account +from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.utils import (get_url, scrub_urls, strip, expand_relative_urls, cint, split_emails, to_markdown, markdown, random_string, parse_addr) import email.utils @@ -75,7 +75,8 @@ class EMail: self.bcc = bcc or [] self.html_set = False - self.email_account = email_account or get_outgoing_email_account(sender=sender) + self.email_account = email_account or \ + EmailAccount.find_outgoing(match_by_email=sender, _raise_error=True) def set_html(self, message, text_content = None, footer=None, print_html=None, formatted=None, inline_images=None, header=None): @@ -249,8 +250,8 @@ class EMail: def get_formatted_html(subject, message, footer=None, print_html=None, email_account=None, header=None, unsubscribe_link=None, sender=None, with_container=False): - if not email_account: - email_account = get_outgoing_email_account(False, sender=sender) + + email_account = email_account or EmailAccount.find_outgoing(match_by_email=sender) signature = None if "" not in message: @@ -480,4 +481,4 @@ def sanitize_email_header(str): return str.replace('\r', '').replace('\n', '') def get_brand_logo(email_account): - return email_account.get('brand_logo') \ No newline at end of file + return email_account.get('brand_logo') diff --git a/frappe/email/queue.py b/frappe/email/queue.py index 2aff04edc9..cd984e9bf9 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -7,7 +7,8 @@ import sys from six.moves import html_parser as HTMLParser import smtplib, quopri, json from frappe import msgprint, _, safe_decode, safe_encode, enqueue -from frappe.email.smtp import SMTPServer, get_outgoing_email_account +from frappe.email.smtp import SMTPServer +from frappe.email.doctype.email_account.email_account import EmailAccount from frappe.email.email_body import get_email, get_formatted_html, add_attachment from frappe.utils.verified_command import get_signed_params, verify_request from html2text import html2text @@ -73,7 +74,9 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= if isinstance(send_after, int): send_after = add_days(nowdate(), send_after) - email_account = get_outgoing_email_account(True, append_to=reference_doctype, sender=sender) + email_account = EmailAccount.find_outgoing( + match_by_doctype=reference_doctype, match_by_email=sender, _raise_error=True) + if not sender or sender == "Administrator": sender = email_account.default_sender @@ -516,7 +519,7 @@ def prepare_message(email, recipient, recipients_list): return "" # Parse "Email Account" from "Email Sender" - email_account = get_outgoing_email_account(raise_exception_not_set=False, sender=email.sender) + email_account = EmailAccount.find_outgoing(match_by_email=email.sender) if frappe.conf.use_ssl and email_account.track_email_status: # Using SSL => Publically available domain => Email Read Reciept Possible message = message.replace("", quopri.encodestring(''.format(frappe.local.site, email.communication).encode()).decode()) diff --git a/frappe/email/smtp.py b/frappe/email/smtp.py index 9ba81fa146..ca69e621cc 100644 --- a/frappe/email/smtp.py +++ b/frappe/email/smtp.py @@ -34,126 +34,6 @@ def send(email, append_to=None, retry=1): _send(retry) -def get_outgoing_email_account(raise_exception_not_set=True, append_to=None, sender=None): - """Returns outgoing email account based on `append_to` or the default - outgoing account. If default outgoing account is not found, it will - try getting settings from `site_config.json`.""" - - sender_email_id = None - _email_account = None - - if sender: - sender_email_id = parse_addr(sender)[1] - - if not getattr(frappe.local, "outgoing_email_account", None): - frappe.local.outgoing_email_account = {} - - if not (frappe.local.outgoing_email_account.get(append_to) - or frappe.local.outgoing_email_account.get(sender_email_id) - or frappe.local.outgoing_email_account.get("default")): - email_account = None - - if sender_email_id: - # check if the sender has an email account with enable_outgoing - email_account = _get_email_account({"enable_outgoing": 1, - "email_id": sender_email_id}) - - if not email_account and append_to: - # append_to is only valid when enable_incoming is checked - email_accounts = frappe.db.get_values("Email Account", { - "enable_outgoing": 1, - "enable_incoming": 1, - "append_to": append_to, - }, cache=True) - - if email_accounts: - _email_account = email_accounts[0] - - else: - email_account = _get_email_account({ - "enable_outgoing": 1, - "enable_incoming": 1, - "append_to": append_to - }) - - if not email_account: - # sender don't have the outging email account - sender_email_id = None - email_account = get_default_outgoing_email_account(raise_exception_not_set=raise_exception_not_set) - - if not email_account and _email_account: - # if default email account is not configured then setup first email account based on append to - email_account = _email_account - - if not email_account and raise_exception_not_set and cint(frappe.db.get_single_value('System Settings', 'setup_complete')): - frappe.throw(_("Please setup default Email Account from Setup > Email > Email Account"), - frappe.OutgoingEmailError) - - if email_account: - if email_account.enable_outgoing and not getattr(email_account, 'from_site_config', False): - raise_exception = True - if email_account.smtp_server in ['localhost','127.0.0.1'] or email_account.no_smtp_authentication: - raise_exception = False - email_account.password = email_account.get_password(raise_exception=raise_exception) - email_account.default_sender = email.utils.formataddr((email_account.name, email_account.get("email_id"))) - - frappe.local.outgoing_email_account[append_to or sender_email_id or "default"] = email_account - - return frappe.local.outgoing_email_account.get(append_to) \ - or frappe.local.outgoing_email_account.get(sender_email_id) \ - or frappe.local.outgoing_email_account.get("default") - -def get_default_outgoing_email_account(raise_exception_not_set=True): - '''conf should be like: - { - "mail_server": "smtp.example.com", - "mail_port": 587, - "use_tls": 1, - "mail_login": "emails@example.com", - "mail_password": "Super.Secret.Password", - "auto_email_id": "emails@example.com", - "email_sender_name": "Example Notifications", - "always_use_account_email_id_as_sender": 0, - "always_use_account_name_as_sender_name": 0 - } - ''' - email_account = _get_email_account({"enable_outgoing": 1, "default_outgoing": 1}) - if email_account: - email_account.password = email_account.get_password(raise_exception=False) - - if not email_account and frappe.conf.get("mail_server"): - # from site_config.json - email_account = frappe.new_doc("Email Account") - email_account.update({ - "smtp_server": frappe.conf.get("mail_server"), - "smtp_port": frappe.conf.get("mail_port"), - - # legacy: use_ssl was used in site_config instead of use_tls, but meant the same thing - "use_tls": cint(frappe.conf.get("use_tls") or 0) or cint(frappe.conf.get("use_ssl") or 0), - "login_id": frappe.conf.get("mail_login"), - "email_id": frappe.conf.get("auto_email_id") or frappe.conf.get("mail_login") or 'notifications@example.com', - "password": frappe.conf.get("mail_password"), - "always_use_account_email_id_as_sender": frappe.conf.get("always_use_account_email_id_as_sender", 0), - "always_use_account_name_as_sender_name": frappe.conf.get("always_use_account_name_as_sender_name", 0) - }) - email_account.from_site_config = True - email_account.name = frappe.conf.get("email_sender_name") or "Frappe" - - if not email_account and not raise_exception_not_set: - return None - - if frappe.are_emails_muted(): - # create a stub - email_account = frappe.new_doc("Email Account") - email_account.update({ - "email_id": "notifications@example.com" - }) - - return email_account - -def _get_email_account(filters): - name = frappe.db.get_value("Email Account", filters) - return frappe.get_doc("Email Account", name) if name else None class SMTPServer: def __init__(self, login=None, password=None, server=None, port=None, use_tls=None, use_ssl=None, append_to=None): @@ -176,17 +56,15 @@ class SMTPServer: self.setup_email_account(append_to) def setup_email_account(self, append_to=None, sender=None): - self.email_account = get_outgoing_email_account(raise_exception_not_set=False, append_to=append_to, sender=sender) + from frappe.email.doctype.email_account.email_account import EmailAccount + self.email_account = EmailAccount.find_outgoing(match_by_doctype=append_to, match_by_email=sender) if self.email_account: self.server = self.email_account.smtp_server self.login = (getattr(self.email_account, "login_id", None) or self.email_account.email_id) - if not self.email_account.no_smtp_authentication: - if self.email_account.ascii_encode_password: - self.password = frappe.safe_encode(self.email_account.password, 'ascii') - else: - self.password = self.email_account.password - else: + if self.email_account.no_smtp_authentication or frappe.local.flags.in_test: self.password = None + else: + self.password = self.email_account._password self.port = self.email_account.smtp_port self.use_tls = self.email_account.use_tls self.sender = self.email_account.email_id diff --git a/frappe/email/test_smtp.py b/frappe/email/test_smtp.py index 0b11c559a2..e170617383 100644 --- a/frappe/email/test_smtp.py +++ b/frappe/email/test_smtp.py @@ -4,7 +4,7 @@ import unittest import frappe from frappe.email.smtp import SMTPServer -from frappe.email.smtp import get_outgoing_email_account +from frappe.email.doctype.email_account.email_account import EmailAccount class TestSMTP(unittest.TestCase): def test_smtp_ssl_session(self): @@ -33,13 +33,13 @@ class TestSMTP(unittest.TestCase): 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") + create_email_account(email_id="default_outgoing_enabled@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1) + self.assertEqual(EmailAccount.find_outgoing().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") + create_email_account(email_id="append_to@gmail.com", password="password", enable_outgoing = 1, default_outgoing=1, append_to="Blog Post") + self.assertEqual(EmailAccount.find_outgoing(match_by_doctype="Blog Post").email_id, "append_to@gmail.com") # add back the mail_server frappe.conf['mail_server'] = mail_server diff --git a/frappe/utils/error.py b/frappe/utils/error.py index d0e21a4188..2d8d6491a5 100644 --- a/frappe/utils/error.py +++ b/frappe/utils/error.py @@ -4,12 +4,14 @@ from __future__ import unicode_literals -import frappe -from frappe.utils import cstr, encode import os import sys -import inspect import traceback +import functools + +import frappe +from frappe.utils import cstr, encode +import inspect import linecache import pydoc import cgitb @@ -190,3 +192,45 @@ def clear_old_snapshots(): def get_error_snapshot_path(): return frappe.get_site_path('error-snapshots') + +def get_default_args(func): + """Get default arguments of a function from its signature. + """ + signature = inspect.signature(func) + return {k: v.default + for k, v in signature.parameters.items() if v.default is not inspect.Parameter.empty} + +def raise_error_on_no_output(error_message, error_type=None, keep_quiet=None): + """Decorate any function to throw error incase of missing output. + + TODO: Remove keep_quiet flag after testing and fixing sendmail flow. + + :param error_message: error message to raise + :param error_type: type of error to raise + :param keep_quiet: control error raising with external factor. + :type error_message: str + :type error_type: Exception Class + :type keep_quiet: function + + >>> @raise_error_on_no_output("Ingradients missing") + ... def get_indradients(_raise_error=1): return + ... + >>> get_indradients() + `Exception Name`: Ingradients missing + """ + def decorator_raise_error_on_no_output(func): + @functools.wraps(func) + def wrapper_raise_error_on_no_output(*args, **kwargs): + response = func(*args, **kwargs) + if callable(keep_quiet) and keep_quiet(): + return response + + default_kwargs = get_default_args(func) + default_raise_error = default_kwargs.get('_raise_error') + raise_error = kwargs.get('_raise_error') if '_raise_error' in kwargs else default_raise_error + + if (not response) and raise_error: + frappe.throw(error_message, error_type or Exception) + return response + return wrapper_raise_error_on_no_output + return decorator_raise_error_on_no_output From 0a1902e65063772640f90d77e999a055726c10e5 Mon Sep 17 00:00:00 2001 From: leela Date: Mon, 3 May 2021 06:25:57 +0530 Subject: [PATCH 156/213] fix: semgrep's split translation regex --- .github/helper/semgrep_rules/translate.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml index 3737da5a7e..732c3daca5 100644 --- a/.github/helper/semgrep_rules/translate.yml +++ b/.github/helper/semgrep_rules/translate.yml @@ -42,7 +42,7 @@ rules: - id: frappe-translation-python-splitting pattern-either: - - pattern: _(...) + ... + _(...) + - pattern: _(...) + _(...) - pattern: _("..." + "...") - pattern-regex: '_\([^\)]*\\\s*' message: | From b0af8886d54ed940111d4eeddca44564db237f56 Mon Sep 17 00:00:00 2001 From: Ernesto Ruiz Date: Sun, 2 May 2021 22:04:16 -0600 Subject: [PATCH 157/213] fix: Make strings translatable (#13046) Co-authored-by: Mohammad Hasnain Mohsin Rajan Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/core/page/recorder/recorder.js | 2 +- frappe/desk/page/backups/backups.js | 2 +- .../page/translation_tool/translation_tool.js | 2 +- .../desk/page/user_profile/user_profile.html | 8 +-- frappe/public/js/frappe/form/toolbar.js | 4 +- .../public/js/frappe/list/list_view_select.js | 6 +- .../js/frappe/recorder/RecorderDetail.vue | 36 ++++++------ .../js/frappe/recorder/RequestDetail.vue | 56 +++++++++---------- .../frappe/ui/notifications/notifications.js | 4 +- .../frappe/views/dashboard/dashboard_view.js | 4 +- .../js/frappe/views/reports/query_report.js | 10 ++-- 11 files changed, 67 insertions(+), 67 deletions(-) diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js index b75ea6a41c..fdca93e8b9 100644 --- a/frappe/core/page/recorder/recorder.js +++ b/frappe/core/page/recorder/recorder.js @@ -1,7 +1,7 @@ frappe.pages['recorder'].on_page_load = function(wrapper) { frappe.ui.make_app_page({ parent: wrapper, - title: 'Recorder', + title: __('Recorder'), single_column: true, card_layout: true }); diff --git a/frappe/desk/page/backups/backups.js b/frappe/desk/page/backups/backups.js index c82407c6bd..337ad33f43 100644 --- a/frappe/desk/page/backups/backups.js +++ b/frappe/desk/page/backups/backups.js @@ -1,7 +1,7 @@ frappe.pages['backups'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: 'Download Backups', + title: __('Download Backups'), single_column: true }); diff --git a/frappe/desk/page/translation_tool/translation_tool.js b/frappe/desk/page/translation_tool/translation_tool.js index b3f0c032e3..13f68e647a 100644 --- a/frappe/desk/page/translation_tool/translation_tool.js +++ b/frappe/desk/page/translation_tool/translation_tool.js @@ -1,7 +1,7 @@ frappe.pages['translation-tool'].on_page_load = function(wrapper) { var page = frappe.ui.make_app_page({ parent: wrapper, - title: 'Translation Tool', + title: __('Translation Tool'), single_column: true, card_layout: true, }); diff --git a/frappe/desk/page/user_profile/user_profile.html b/frappe/desk/page/user_profile/user_profile.html index 911ccc702d..f134441b74 100644 --- a/frappe/desk/page/user_profile/user_profile.html +++ b/frappe/desk/page/user_profile/user_profile.html @@ -8,7 +8,7 @@
    - No Data to Show + {%=__("No Data to Show") %}
    @@ -19,7 +19,7 @@
    - No Data to Show + {%=__("No Data to Show") %}
    @@ -30,7 +30,7 @@
    - No Data to Show + {%=__("No Data to Show") %}
    @@ -41,4 +41,4 @@ - \ No newline at end of file + diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 22787b70c1..c93466e024 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -32,7 +32,7 @@ frappe.ui.form.Toolbar = class Toolbar { } set_title() { if (this.frm.is_new()) { - var title = __('New {0}', [this.frm.meta.name]); + var title = __('New {0}', [__(this.frm.meta.name)]); } else if (this.frm.meta.title_field) { let title_field = (this.frm.doc[this.frm.meta.title_field] || "").toString().trim(); var title = strip_html(title_field || this.frm.docname); @@ -551,7 +551,7 @@ frappe.ui.form.Toolbar = class Toolbar { let fields = this.frm.fields .filter(visible_fields_filter) - .map(f => ({ label: f.df.label, value: f.df.fieldname })); + .map(f => ({ label: __(f.df.label), value: f.df.fieldname })); let dialog = new frappe.ui.Dialog({ title: __('Jump to field'), diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js index 9607be6e90..c89815d200 100644 --- a/frappe/public/js/frappe/list/list_view_select.js +++ b/frappe/public/js/frappe/list/list_view_select.js @@ -150,13 +150,13 @@ frappe.views.ListViewSelect = class ListViewSelect { const views_wrapper = this.sidebar.sidebar.find(".views-section"); views_wrapper.find(".sidebar-label").html(`${__(view)}`); const $dropdown = views_wrapper.find(".views-dropdown"); - - let placeholder = `Select ${view}`; + + let placeholder = `${__("Select {0}", [__(view)])}`; let html = ``; if (!items || !items.length) { html = `
    - ${__("No {} Found", [view])} + ${__("No {0} Found", [__(view)])}
    `; } else { const page_name = this.get_page_name(); diff --git a/frappe/public/js/frappe/recorder/RecorderDetail.vue b/frappe/public/js/frappe/recorder/RecorderDetail.vue index 57e63a0233..5d934d7e1e 100644 --- a/frappe/public/js/frappe/recorder/RecorderDetail.vue +++ b/frappe/public/js/frappe/recorder/RecorderDetail.vue @@ -5,7 +5,7 @@
    @@ -71,12 +71,12 @@
    -

    Recorder is Inactive

    -

    +

    {{ __("Recorder is Inactive") }}

    +

    -

    No Requests found

    -

    Go make some noise

    +

    {{ __("No Requests found") }}

    +

    {{ __("Go make some noise") }}

    @@ -108,12 +108,12 @@ export default { return { requests: [], columns: [ - {label: "Path", slug: "path"}, - {label: "Duration (ms)", slug: "duration", sortable: true, number: true}, - {label: "Time in Queries (ms)", slug: "time_queries", sortable: true, number: true}, - {label: "Queries", slug: "queries", sortable: true, number: true}, - {label: "Method", slug: "method"}, - {label: "Time", slug: "time", sortable: true}, + {label: __("Path"), slug: "path"}, + {label: __("Duration (ms)"), slug: "duration", sortable: true, number: true}, + {label: __("Time in Queries (ms)"), slug: "time_queries", sortable: true, number: true}, + {label: __("Queries"), slug: "queries", sortable: true, number: true}, + {label: __("Method"), slug: "method"}, + {label: __("Time"), slug: "time", sortable: true}, ], query: { sort: "duration", @@ -140,7 +140,7 @@ export default { mounted() { this.fetch_status(); this.refresh(); - this.$root.page.set_secondary_action("Clear", () => { + this.$root.page.set_secondary_action(__("Clear"), () => { frappe.set_route("recorder"); this.clear(); }); @@ -151,11 +151,11 @@ export default { const current_page = this.query.pagination.page; const total_pages = this.query.pagination.total; return [{ - label: "First", + label: __("First"), number: 1, status: (current_page == 1) ? "disabled" : "", },{ - label: "Previous", + label: __("Previous)", number: Math.max(current_page - 1, 1), status: (current_page == 1) ? "disabled" : "", }, { @@ -163,11 +163,11 @@ export default { number: current_page, status: "btn-info", }, { - label: "Next", + label: __("Next"), number: Math.min(current_page + 1, total_pages), status: (current_page == total_pages) ? "disabled" : "", }, { - label: "Last", + label: __("Last"), number: total_pages, status: (current_page == total_pages) ? "disabled" : "", }]; @@ -230,11 +230,11 @@ export default { }, update_buttons: function() { if(this.status.status == "Active") { - this.$root.page.set_primary_action("Stop", () => { + this.$root.page.set_primary_action(__("Stop"), () => { this.stop(); }); } else { - this.$root.page.set_primary_action("Start", () => { + this.$root.page.set_primary_action(__("Start"), () => { this.start(); }); } diff --git a/frappe/public/js/frappe/recorder/RequestDetail.vue b/frappe/public/js/frappe/recorder/RequestDetail.vue index cc056686d5..2e995bca39 100644 --- a/frappe/public/js/frappe/recorder/RequestDetail.vue +++ b/frappe/public/js/frappe/recorder/RequestDetail.vue @@ -16,7 +16,7 @@
    -
    SQL Queries
    +
    {{ __("SQL Queries") }}
    @@ -37,7 +37,7 @@
    @@ -48,15 +48,15 @@
    - Index
    + {{ __("Index") }}
    -
    Query
    +
    {{ __("Query") }}
    -
    Duration (ms)
    +
    {{ __("Duration (ms)") }}
    -
    Exact Copies
    +
    {{ __("Exact Copies") }}
    @@ -82,7 +82,7 @@
    - SQL Query #{{ call.index }} + {{ __("SQL Query") }} #{{ call.index }}
    @@ -98,25 +98,25 @@
    -
    +
    -
    +
    -
    +
    -
    +