From 2ea74dee36f1bbd094a7fa967fc50be26ba5e09d Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Tue, 26 May 2020 18:47:17 +0530 Subject: [PATCH 01/45] fix: check for whitelist before calling from search search widget takes query as an input, but does not check whether the query function that is called is whitelisted, basically allowing anyone logged-in to call any function regardless of the whitelist. Signed-off-by: Chinmay D. Pai --- frappe/desk/search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/frappe/desk/search.py b/frappe/desk/search.py index c70b650945..1da2a3d0b5 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe.utils import cstr, unique, cint from frappe.permissions import has_permission +from frappe.handler import is_whitelisted from frappe import _ from six import string_types import re @@ -74,6 +75,7 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, if query and query.split()[0].lower()!="select": # by method + is_whitelisted(query) frappe.response["values"] = frappe.call(query, doctype, txt, searchfield, start, page_length, filters, as_dict=as_dict) elif not query and doctype in standard_queries: From c4d4fc3574dcb0f716fc8ad5edb162f171a990fe Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Tue, 26 May 2020 18:49:12 +0530 Subject: [PATCH 02/45] chore: add standard queries hooks to whitelist standard queries are used within the search widget, and now require to be whitelisted before they can be executed through the search widget. Signed-off-by: Chinmay D. Pai --- frappe/core/doctype/user/user.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 0c5ebc3ede..f571240454 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -811,6 +811,7 @@ def reset_password(user): frappe.clear_messages() return 'not found' +@frappe.whitelist() def user_query(doctype, txt, searchfield, start, page_len, filters): from frappe.desk.reportview import get_match_cond From 67e1198d064ec6e0b838f2d242cb119e2b906c8e Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 12 Jun 2020 16:24:41 +0530 Subject: [PATCH 03/45] feat: prompt in case of site downgrades on restore --- frappe/commands/site.py | 12 +++++++++--- frappe/installer.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 28e61282eb..c327d85af0 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -108,12 +108,14 @@ def _new_site(db_name, site, mariadb_root_username=None, mariadb_root_password=N @click.option('--install-app', multiple=True, help='Install app after installation') @click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file') @click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file') +@click.option('--force', is_flag=True, default=False, help='Use a bit of force to get the job done') @pass_context def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None): "Restore site database from an sql file" - from frappe.installer import extract_sql_gzip, extract_tar_files - # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file + from frappe.installer import extract_sql_gzip, extract_tar_files, is_downgrade + force = context.force or force + # Extract the gzip file if user has passed *.sql.gz file instead of *.sql file if not os.path.exists(sql_file_path): base_path = '..' sql_file_path = os.path.join(base_path, sql_file_path) @@ -125,7 +127,6 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas else: base_path = '.' - if sql_file_path.endswith('sql.gz'): decompressed_file_name = extract_sql_gzip(os.path.abspath(sql_file_path)) else: @@ -133,6 +134,11 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas site = get_site(context) frappe.init(site=site) + + # dont allow downgrading to older versions of frappe without force + if not force and is_downgrade(decompressed_file_name): + click.confirm("Downgrading sites may lead to a broken site. Do you wish to continue?", abort=True) + _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name, diff --git a/frappe/installer.py b/frappe/installer.py index 4fc19b282a..38a8aadc3b 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -326,3 +326,30 @@ def extract_tar_files(site_name, file_path, folder_name): frappe.destroy() return tar_path + +def is_downgrade(sql_file_path): + """checks if input db backup will get downgraded on current bench""" + from semantic_version import Version + from frappe.utils.change_log import get_app_branch + head = "INSERT INTO `tabInstalled Application` VALUES" + + with open(sql_file_path) as f: + for line in f: + if head in line: + # ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'press','0.0.1','master') + line = line.strip().lstrip(head).rstrip(";").strip() + # [('frappe', '12.x.x-develop ()', 'develop'), ('press', '0.0.1', 'master')] + all_apps = [ x[-3:] for x in frappe.safe_eval(line) ] + + for app in all_apps: + app_name = app[0] + app_version = app[1].split(" ")[0] + + if app_name == "frappe": + try: + current_version = Version(frappe.__version__) + backup_version = Version(app_version[1:] if app_version[0] is "v" else app_version) + except ValueError: + return False + + return backup_version > current_version From 13ca526be8bc35a27a7a2d4ac1971ecf0d2391af Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 12 Jun 2020 16:36:50 +0530 Subject: [PATCH 04/45] chore: Sider --- frappe/installer.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/installer.py b/frappe/installer.py index 38a8aadc3b..962a9e40e5 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -330,7 +330,6 @@ def extract_tar_files(site_name, file_path, folder_name): def is_downgrade(sql_file_path): """checks if input db backup will get downgraded on current bench""" from semantic_version import Version - from frappe.utils.change_log import get_app_branch head = "INSERT INTO `tabInstalled Application` VALUES" with open(sql_file_path) as f: @@ -348,7 +347,7 @@ def is_downgrade(sql_file_path): if app_name == "frappe": try: current_version = Version(frappe.__version__) - backup_version = Version(app_version[1:] if app_version[0] is "v" else app_version) + backup_version = Version(app_version[1:] if app_version[0] == "v" else app_version) except ValueError: return False From 2afe0cdaa48c88de5ad884f180eba9b3ae2d5215 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 22 Jun 2020 09:01:37 +0530 Subject: [PATCH 05/45] fix: Column mapping --- .../core/doctype/data_import/data_import.js | 4 +- frappe/core/doctype/data_import/importer.py | 6 ++- .../js/frappe/data_import/import_preview.js | 37 ++++++++++++++++--- 3 files changed, 39 insertions(+), 8 deletions(-) diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 81a7bc9705..9ccf43436b 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -301,8 +301,8 @@ frappe.ui.form.on('Data Import', { events: { remap_column(changed_map) { let template_options = JSON.parse(frm.doc.template_options || '{}'); - template_options.remap_column = template_options.remap_column || {}; - Object.assign(template_options.remap_column, changed_map); + template_options.column_to_field_map = template_options.column_to_field_map || {}; + Object.assign(template_options.column_to_field_map, changed_map); frm.set_value('template_options', JSON.stringify(template_options)); frm.save().then(() => frm.trigger('import_file')); } diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 4761652c70..09849dd5b9 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -766,8 +766,9 @@ class Header(Row): for j, header in enumerate(row): column_values = [get_item_at_index(r, j) for r in raw_data] + map_to_field = column_to_field_map.get(str(j)) column = Column( - j, header, self.doctype, column_values, column_to_field_map.get(header), self.seen + j, header, self.doctype, column_values, map_to_field, self.seen ) self.seen.append(header) self.columns.append(column) @@ -944,6 +945,9 @@ class Column: d.map_to_field = self.map_to_field d.date_format = self.date_format d.df = self.df + if hasattr(self.df, 'is_child_table_field'): + d.is_child_table_field = self.df.is_child_table_field + d.child_table_df = self.df.child_table_df d.skip_import = self.skip_import d.warnings = self.warnings return d diff --git a/frappe/public/js/frappe/data_import/import_preview.js b/frappe/public/js/frappe/data_import/import_preview.js index 7cf8431456..4edcb87aeb 100644 --- a/frappe/public/js/frappe/data_import/import_preview.js +++ b/frappe/public/js/frappe/data_import/import_preview.js @@ -245,11 +245,12 @@ frappe.data_import.ImportPreview = class ImportPreview { let fieldname; if (!df) { fieldname = null; + } else if (col.map_to_field) { + fieldname = col.map_to_field; + } else if (col.is_child_table_field) { + fieldname = `${col.child_table_df.fieldname}.${df.fieldname}`; } else { - fieldname = - df.parent === this.doctype - ? df.fieldname - : `${df.parent}:${df.fieldname}`; + fieldname = df.fieldname; } return [ { @@ -272,7 +273,7 @@ frappe.data_import.ImportPreview = class ImportPreview { label: __("Don't Import"), value: "Don't Import" } - ].concat(column_picker_fields.get_fields_as_options()), + ].concat(get_fields_as_options(this.doctype, column_picker_fields)), default: fieldname || "Don't Import", change() { changed.push(i); @@ -328,3 +329,29 @@ frappe.data_import.ImportPreview = class ImportPreview { }); } }; + +function get_fields_as_options(doctype, column_map) { + let keys = [doctype]; + frappe.meta.get_table_fields(doctype).forEach(df => { + keys.push(df.fieldname); + }); + // flatten array + return [].concat( + ...keys.map(key => { + return column_map[key].map(df => { + let label = df.label; + let value = df.fieldname; + if (doctype !== key) { + let table_field = frappe.meta.get_docfield(doctype, key); + label = `${df.label} (${table_field.label})`; + value = `${table_field.fieldname}.${df.fieldname}`; + } + return { + label, + value, + description: value + }; + }); + }) + ); +} \ No newline at end of file From b502bfff07da834745bde3018e4a2ebc316d3a4f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 22 Jun 2020 09:02:07 +0530 Subject: [PATCH 06/45] fix: Serialize df manually --- frappe/core/doctype/data_import/importer.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 09849dd5b9..13684aaad3 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -607,7 +607,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": msg, } ) @@ -622,7 +622,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": msg, } ) @@ -635,7 +635,7 @@ class Row: { "row": self.row_number, "col": col.column_number, - "field": df.as_dict(convert_dates_to_str=True), + "field": df_as_json(df), "message": _("Value {0} must in {1} format").format( frappe.bold(value), frappe.bold(get_user_format(col.date_format)) ), @@ -646,7 +646,7 @@ class Row: return value def link_exists(self, value, df): - key = df.options + "::" + value + key = df.options + "::" + cstr(value) if Row.link_values_exist_map.get(key) is None: Row.link_values_exist_map[key] = frappe.db.exists(df.options, value) return Row.link_values_exist_map.get(key) @@ -1117,3 +1117,13 @@ def get_user_format(date_format): .replace("%m", "mm") .replace("%d", "dd") ) + +def df_as_json(df): + return { + 'fieldname': df.fieldname, + 'fieldtype': df.fieldtype, + 'label': df.label, + 'options': df.options, + 'parent': df.parent, + 'default': df.default + } From bbdc5e0db897f7fe7e5ce09b4a917142fe7e56be Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Mon, 22 Jun 2020 09:03:00 +0530 Subject: [PATCH 07/45] fix(exporter): Count for child table filters - Extract count method from list_view.js for reuse --- .../js/frappe/data_import/data_exporter.js | 8 +++--- frappe/public/js/frappe/db.js | 26 ++++++++++++++----- frappe/public/js/frappe/list/list_view.js | 24 +++-------------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js index 735237189d..6ee4e907fc 100644 --- a/frappe/public/js/frappe/data_import/data_exporter.js +++ b/frappe/public/js/frappe/data_import/data_exporter.js @@ -285,11 +285,9 @@ frappe.data_import.DataExporter = class DataExporter { } get_filters() { - return this.filter_group.get_filters().reduce((acc, filter) => { - return Object.assign(acc, { - [filter[1]]: [filter[2], filter[3]] - }); - }, {}); + return this.filter_group.get_filters().map(filter => { + return filter.slice(0, 4) + }); } get_multicheck_options(doctype, child_fieldname = null) { diff --git a/frappe/public/js/frappe/db.js b/frappe/public/js/frappe/db.js index 1b6fb0e438..cf716c67e5 100644 --- a/frappe/public/js/frappe/db.js +++ b/frappe/public/js/frappe/db.js @@ -91,12 +91,26 @@ frappe.db = { }); }, count: function(doctype, args={}) { - return new Promise(resolve => { - frappe.call({ - method: 'frappe.client.get_count', - type: 'GET', - args: Object.assign(args, { doctype }) - }).then(r => resolve(r.message)); + let filters = args.filters || {}; + const with_child_table_filter = Array.isArray(filters) && filters.some(filter => { + return filter[0] !== doctype; + }); + + const fields = [ + // cannot break this line as it adds extra \n's and \t's which breaks the query + `count(${with_child_table_filter ? 'distinct': ''} ${frappe.model.get_full_column_name('name', doctype)}) AS total_count` + ]; + + return frappe.call({ + type: 'GET', + method: 'frappe.desk.reportview.get', + args: { + doctype, + filters, + fields, + } + }).then(r => { + return r.message.values[0][0]; }); }, get_link_options(doctype, txt = '', filters={}) { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 9e1ba1b9bd..8da73b0dec 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -760,26 +760,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { let current_count = this.data.length; let count_without_children = this.data.uniqBy(d => d.name).length; - const filters = this.get_filters_for_args(); - const with_child_table_filter = filters.some(filter => { - return filter[0] !== this.doctype; - }); - - const fields = [ - // cannot break this line as it adds extra \n's and \t's which breaks the query - `count(${with_child_table_filter ? 'distinct': ''}${frappe.model.get_full_column_name('name', this.doctype)}) AS total_count` - ]; - - return frappe.call({ - type: 'GET', - method: this.method, - args: { - doctype: this.doctype, - filters, - fields, - } - }).then(r => { - this.total_count = r.message.values[0][0] || current_count; + return frappe.db.count(this.doctype, { + filters: this.get_filters_for_args() + }).then(total_count => { + this.total_count = total_count || current_count; let str = __('{0} of {1}', [current_count, this.total_count]); if (count_without_children !== current_count) { str = __('{0} of {1} ({2} rows with children)', [count_without_children, this.total_count, current_count]); From 711f8231bc0642120d7e01f650fae050fa6c8da1 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 13:36:15 +0530 Subject: [PATCH 08/45] fix(patch): Drop DI Legacy if exists - Ignore patch if already run --- frappe/patches.txt | 2 +- frappe/patches/v13_0/replace_old_data_import.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/frappe/patches.txt b/frappe/patches.txt index a03d31918b..f883d2a7bb 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -289,4 +289,4 @@ execute:frappe.delete_doc("DocType", "Onboarding Slide Field") execute:frappe.delete_doc("DocType", "Onboarding Slide Help Link") frappe.patches.v13_0.update_date_filters_in_user_settings frappe.patches.v13_0.update_duration_options -frappe.patches.v13_0.replace_old_data_import +frappe.patches.v13_0.replace_old_data_import # 2020-06-24 diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py index 1c00ae5f34..469703e172 100644 --- a/frappe/patches/v13_0/replace_old_data_import.py +++ b/frappe/patches/v13_0/replace_old_data_import.py @@ -6,6 +6,10 @@ import frappe def execute(): + if not frappe.db.exists("DocType", "Data Import Beta"): + return + + frappe.db.sql("DROP TABLE IF EXISTS `tabData Import Legacy`") frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy') frappe.db.commit() frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`") From 5e31a6886ce55d5ae3ca4e8933685316028eef70 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 15:06:25 +0530 Subject: [PATCH 09/45] fix: Data Exporter - Set options as mandatory only when inserting new records - Hide Select Mandatory button during update --- .../core/doctype/data_import/data_import.js | 28 +++++------ .../js/frappe/data_import/data_exporter.js | 49 ++++--------------- 2 files changed, 22 insertions(+), 55 deletions(-) diff --git a/frappe/core/doctype/data_import/data_import.js b/frappe/core/doctype/data_import/data_import.js index 9ccf43436b..6a922618cb 100644 --- a/frappe/core/doctype/data_import/data_import.js +++ b/frappe/core/doctype/data_import/data_import.js @@ -102,6 +102,10 @@ frappe.ui.form.on('Data Import', { }, update_primary_action(frm) { + if (frm.is_dirty()) { + frm.enable_save(); + return; + } frm.disable_save(); if (frm.doc.status !== 'Success') { if (!frm.is_new() && (frm.has_import_file())) { @@ -199,20 +203,12 @@ frappe.ui.form.on('Data Import', { }, download_template(frm) { - if ( - frm.data_exporter && - frm.data_exporter.doctype === frm.doc.reference_doctype - ) { - frm.data_exporter.exporting_for = frm.doc.import_type; - frm.data_exporter.dialog.show(); - } else { - frappe.require('/assets/js/data_import_tools.min.js', () => { - frm.data_exporter = new frappe.data_import.DataExporter( - frm.doc.reference_doctype, - frm.doc.import_type - ); - }); - } + frappe.require('/assets/js/data_import_tools.min.js', () => { + frm.data_exporter = new frappe.data_import.DataExporter( + frm.doc.reference_doctype, + frm.doc.import_type + ); + }); }, reference_doctype(frm) { @@ -435,10 +431,10 @@ frappe.ui.form.on('Data Import', { .join(''); let id = frappe.dom.get_unique_id(); html = `${messages} - -
+
${log.exception}
diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js index 6ee4e907fc..238e916c34 100644 --- a/frappe/public/js/frappe/data_import/data_exporter.js +++ b/frappe/public/js/frappe/data_import/data_exporter.js @@ -13,36 +13,6 @@ frappe.data_import.DataExporter = class DataExporter { this.dialog = new frappe.ui.Dialog({ title: __('Export Data'), fields: [ - { - fieldtype: 'Select', - fieldname: 'exporting_for', - label: __('Exporting For'), - options: [ - { - label: __('Insert New Records'), - value: 'Insert New Records' - }, - { - label: __('Update Existing Records'), - value: 'Update Existing Records' - } - ], - change: () => { - let exporting_for = this.dialog.get_value('exporting_for'); - this.dialog.set_value( - 'export_records', - exporting_for === 'Insert New Records' ? 'blank_template' : 'all' - ); - - // Force ID field to be exported when updating existing records - let id_field = this.dialog.get_field(this.doctype).options[0]; - if (id_field.value === 'name' && id_field.$checkbox) { - id_field.$checkbox - .find('input') - .prop('disabled', exporting_for === 'Update Existing Records'); - } - } - }, { fieldtype: 'Select', fieldname: 'export_records', @@ -65,7 +35,7 @@ frappe.data_import.DataExporter = class DataExporter { value: 'blank_template' } ], - default: 'blank_template', + default: this.exporting_for === 'Insert New Records' ? 'blank_template' : 'all', change: () => { this.update_record_count_message(); } @@ -119,10 +89,6 @@ frappe.data_import.DataExporter = class DataExporter { on_page_show: () => this.select_mandatory() }); - if (this.exporting_for) { - this.dialog.set_value('exporting_for', this.exporting_for); - } - this.make_filter_area(); this.make_select_all_buttons(); this.update_record_count_message(); @@ -172,15 +138,17 @@ frappe.data_import.DataExporter = class DataExporter { } make_select_all_buttons() { + let for_insert = this.exporting_for === 'Insert New Records'; + let section_title = for_insert ? __('Select Fields To Insert') : __('Select Fields To Update'); let $select_all_buttons = $(`
-
${__('Select fields to export')}
+
${section_title}
- + `: ''} @@ -306,6 +274,9 @@ frappe.data_import.DataExporter = class DataExporter { ? this.column_map[child_fieldname] : this.column_map[doctype]; + let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname) + || (df.reqd && this.exporting_for == 'Insert New Records') + return fields .filter(df => { if (autoname_field && df.fieldname === autoname_field.fieldname) { @@ -321,7 +292,7 @@ frappe.data_import.DataExporter = class DataExporter { return { label, value: df.fieldname, - danger: df.reqd, + danger: is_field_mandatory(df), checked: false, description: `${df.fieldname} ${df.reqd ? __('(Mandatory)') : ''}` }; From 8d8c9a348b93226c94b12124660ae116689baf4b Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 15:07:23 +0530 Subject: [PATCH 10/45] fix: Data Import form - Hide Download Template button for new doc - Hide Refresh Google Sheet button if doc is dirty --- frappe/core/doctype/data_import/data_import.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/core/doctype/data_import/data_import.json b/frappe/core/doctype/data_import/data_import.json index 177252ea22..8b1b6c4e07 100644 --- a/frappe/core/doctype/data_import/data_import.json +++ b/frappe/core/doctype/data_import/data_import.json @@ -119,7 +119,7 @@ { "fieldname": "import_warnings_section", "fieldtype": "Section Break", - "label": "Warnings" + "label": "Import File Errors and Warnings" }, { "fieldname": "import_warnings", @@ -127,7 +127,7 @@ "label": "Import Warnings" }, { - "depends_on": "reference_doctype", + "depends_on": "eval:!doc.__islocal", "fieldname": "download_template", "fieldtype": "Button", "label": "Download Template" @@ -159,7 +159,7 @@ "label": "Import from Google Sheets" }, { - "depends_on": "eval:doc.google_sheets_url", + "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved", "fieldname": "refresh_google_sheet", "fieldtype": "Button", "label": "Refresh Google Sheet" @@ -167,7 +167,7 @@ ], "hide_toolbar": 1, "links": [], - "modified": "2020-06-18 16:05:54.211034", + "modified": "2020-06-24 14:33:03.173876", "modified_by": "Administrator", "module": "Core", "name": "Data Import", From ebc3c26f7d52d76066012737799561f80e32c636 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 15:09:54 +0530 Subject: [PATCH 11/45] fix: Data Import for Update - Update doc only if changed else throw error - Check for mandatory only for inserting --- frappe/core/doctype/data_import/importer.py | 84 +++++++++++++-------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 13684aaad3..11228b834b 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -16,6 +16,7 @@ from frappe.utils.xlsxutils import ( read_xls_file_from_attached_file, ) from frappe.model import no_value_fields, table_fields as table_fieldtypes +from frappe.core.doctype.version.version import get_diff INVALID_VALUES = ("", None) MAX_ROWS_IN_PREVIEW = 10 @@ -216,14 +217,22 @@ class Importer: def update_record(self, doc): id_field = get_id_field(self.doctype) existing_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname)) - existing_doc.flags.updater_reference = { - "doctype": self.data_import.doctype, - "docname": self.data_import.name, - "label": _("via Data Import"), - } - existing_doc.update(doc) - existing_doc.save() - return existing_doc + + updated_doc = frappe.get_doc(self.doctype, doc.get(id_field.fieldname)) + updated_doc.update(doc) + + if get_diff(existing_doc, updated_doc): + # update doc if there are changes + updated_doc.flags.updater_reference = { + "doctype": self.data_import.doctype, + "docname": self.data_import.name, + "label": _("via Data Import"), + } + updated_doc.save() + return updated_doc + else: + # throw if no changes + frappe.throw('No changes to update') def get_eta(self, current, total, processing_time): self.last_eta = getattr(self, "last_eta", 0) @@ -306,8 +315,9 @@ class ImportFile: ) self.column_to_field_map = self.template_options.column_to_field_map self.import_type = import_type + self.warnings = [] - self.file_doc = self.file_path = None + self.file_doc = self.file_path = self.google_sheets_url = None if isinstance(file, frappe.string_types): if frappe.db.exists("File", {"file_url": file}): self.file_doc = frappe.get_doc("File", {"file_url": file}) @@ -462,38 +472,46 @@ class ImportFile: parent_doc[table_df.fieldname].append(child_doc) doc = parent_doc - # check if there is atleast one row for mandatory table fields - meta = frappe.get_meta(self.doctype) - mandatory_table_fields = [ - df - for df in meta.fields - if df.fieldtype in table_fieldtypes - and df.reqd - and len(doc.get(df.fieldname, [])) == 0 - ] - if len(mandatory_table_fields) == 1: - self.warnings.append( - { - "row": first_row.row_number, - "message": _("There should be atleast one row for {0} table").format( - mandatory_table_fields[0].label - ), - } - ) - elif mandatory_table_fields: - fields_string = ", ".join([df.label for df in mandatory_table_fields]) - message = _("There should be atleast one row for the following tables: {0}").format( - fields_string - ) - self.warnings.append({"row": first_row.row_number, "message": message}) + + if self.import_type == INSERT: + # check if there is atleast one row for mandatory table fields + meta = frappe.get_meta(self.doctype) + mandatory_table_fields = [ + df + for df in meta.fields + if df.fieldtype in table_fieldtypes + and df.reqd + and len(doc.get(df.fieldname, [])) == 0 + ] + if len(mandatory_table_fields) == 1: + self.warnings.append( + { + "row": first_row.row_number, + "message": _("There should be atleast one row for {0} table").format( + frappe.bold(mandatory_table_fields[0].label) + ), + } + ) + elif mandatory_table_fields: + fields_string = ", ".join([df.label for df in mandatory_table_fields]) + message = _("There should be atleast one row for the following tables: {0}").format( + fields_string + ) + self.warnings.append({"row": first_row.row_number, "message": message}) return doc, rows, data[len(rows) :] def get_warnings(self): warnings = [] + + # ImportFile warnings + warnings += self.warnings + + # Column warnings for col in self.header.columns: warnings += col.warnings + # Row warnings for row in self.data: warnings += row.warnings From f3943ed663d9849ef87bf358948aa505e9174f46 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 15:10:21 +0530 Subject: [PATCH 12/45] fix: Column - Check if values exists in Link Column --- frappe/core/doctype/data_import/importer.py | 26 +++++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 11228b834b..6e530f93db 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -843,7 +843,7 @@ class Column: self.meta = frappe.get_meta(doctype) self.parse() - self.parse_date_format() + self.validate_values() def parse(self): header_title = self.header_title @@ -916,10 +916,6 @@ class Column: self.df = df self.skip_import = skip_import - def parse_date_format(self): - if self.df and self.df.fieldtype in ("Date", "Time", "Datetime"): - self.date_format = self.guess_date_format_for_column() - def guess_date_format_for_column(self): """ Guesses date format for a column by parsing all the values in the column, getting the date format and then returning the one which has the maximum frequency @@ -954,6 +950,26 @@ class Column: return max_occurred_date_format + def validate_values(self): + if not self.df: + return + + if self.df.fieldtype == 'Link': + # find all values that dont exist + values = list(set([v for v in self.column_values[1:] if v])) + exists = [d.name for d in frappe.db.get_all(self.df.options, filters={'name': ('in', values)})] + not_exists = list(set(values) - set(exists)) + if not_exists: + missing_values = ', '.join(not_exists) + self.warnings.append({ + 'col': self.column_number, + 'message': "The following values do not exist for {}: {}".format(self.df.options, missing_values), + 'type': 'warning' + }) + elif self.df.fieldtype in ("Date", "Time", "Datetime"): + # guess date format + self.date_format = self.guess_date_format_for_column() + def as_dict(self): d = frappe._dict() d.index = self.index From 68b4591f10ed970f3cfe8215d046ff1c4d17e1f0 Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 15:10:29 +0530 Subject: [PATCH 13/45] fix: Update yarn.lock --- yarn.lock | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index d858972218..251aa893b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -252,18 +252,6 @@ "@types/events" "*" "@types/node" "*" -"@types/bunyan@*": - version "1.8.6" - resolved "https://registry.yarnpkg.com/@types/bunyan/-/bunyan-1.8.6.tgz#6527641cca30bedec5feb9ab527b7803b8000582" - integrity sha512-YiozPOOsS6bIuz31ilYqR5SlLif4TBWsousN2aCWLi5233nZSX19tFbcQUPdR7xJ8ypPyxkCGNxg0CIV5n9qxQ== - dependencies: - "@types/node" "*" - -"@types/color-name@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0" - integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ== - "@types/debug@^4.1.4": version "4.1.5" resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.5.tgz#b14efa8852b7768d898906613c23f688713e02cd" @@ -3529,7 +3517,7 @@ lodash.uniq@^4.5.0: resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= -lodash@4.17.15, lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: +lodash@4.17.15, lodash@^4.0.0, lodash@^4.17.10, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.10: version "4.17.15" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== @@ -4803,11 +4791,6 @@ prettier@^1.19.1: resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== -pretty-hrtime@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" - integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= - process-nextick-args@~1.0.6: version "1.0.7" resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" From 8343105093af811351c09d7283b4a4ab819044ac Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Wed, 24 Jun 2020 15:18:22 +0530 Subject: [PATCH 14/45] fix: Handle empty column_to_field_map --- frappe/core/doctype/data_import/importer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 6e530f93db..ec3cccc1b1 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -773,11 +773,12 @@ class Row: class Header(Row): - def __init__(self, index, row, doctype, raw_data, column_to_field_map): + def __init__(self, index, row, doctype, raw_data, column_to_field_map=None): self.index = index self.row_number = index + 1 self.data = row self.doctype = doctype + column_to_field_map = column_to_field_map or frappe._dict() self.seen = [] self.columns = [] From 04d0ce54af6873aed342c193be9080c9f486c11e Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 25 Jun 2020 23:12:25 +0530 Subject: [PATCH 15/45] fix: include start date in chart result --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 703db16a48..c06020f175 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -259,7 +259,7 @@ def get_aggregate_function(chart_type): def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [] + result = [[start_date, 0.0]] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) From 1379a722f5bc0a5e8c1cdd1aa9a3a89b86575d47 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 26 Jun 2020 00:03:35 +0530 Subject: [PATCH 16/45] fix: append start date only if timegrain is daily --- frappe/desk/doctype/dashboard_chart/dashboard_chart.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index c06020f175..a5c5504db2 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -259,7 +259,10 @@ def get_aggregate_function(chart_type): def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [[start_date, 0.0]] + + result = [] + if timegrain == 'Daily': + result.append([start_date, 0.0]) while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) From ad206edb2014667a1a1d3697dde2c1f197dd108d Mon Sep 17 00:00:00 2001 From: Saurabh Date: Fri, 26 Jun 2020 16:33:55 +0530 Subject: [PATCH 17/45] feat: do not send notification for existing emails --- frappe/email/doctype/email_account/email_account.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index ff09024f69..2065f5558a 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -274,6 +274,8 @@ class EmailAccount(Document): for idx, msg in enumerate(incoming_mails): uid = None if not uid_list else uid_list[idx] + self.flags.notify = True + try: args = { "uid": uid, @@ -294,7 +296,11 @@ class EmailAccount(Document): else: frappe.db.commit() - if communication: + if communication and self.flags.notify: + + # If email already exists in the system + # then do not send notifications for the same email. + attachments = [] if hasattr(communication, '_attachments'): @@ -363,6 +369,9 @@ class EmailAccount(Document): name = names[0].get("name") # email is already available update communication uid instead frappe.db.set_value("Communication", name, "uid", uid, update_modified=False) + + self.flags.notify = False + return frappe.get_doc("Communication", name) if email.content_type == 'text/html': From b482b2ce5c0821683d72d08f6bd1a83105cb85b8 Mon Sep 17 00:00:00 2001 From: prssanna Date: Fri, 26 Jun 2020 17:47:59 +0530 Subject: [PATCH 18/45] fix: test for daily charts --- .../dashboard_chart/test_dashboard_chart.py | 90 ++++++++++++------- 1 file changed, 60 insertions(+), 30 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index dfc6edbf58..1a300e471a 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -35,9 +35,6 @@ class TestDashboardChart(unittest.TestCase): self.assertEqual(get_period_ending('2019-10-01', 'Quarterly'), getdate('2019-12-31')) - self.assertEqual(get_period_ending('2019-10-01', 'Yearly'), - getdate('2019-12-31')) - def test_dashboard_chart(self): if frappe.db.exists('Dashboard Chart', 'Test Dashboard Chart'): frappe.delete_doc('Dashboard Chart', 'Test Dashboard Chart') @@ -50,7 +47,7 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() @@ -82,7 +79,7 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() @@ -114,7 +111,7 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() @@ -132,6 +129,60 @@ class TestDashboardChart(unittest.TestCase): frappe.db.rollback() + def test_group_by_chart_type(self): + if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart') + + frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert() + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Group By Dashboard Chart', + chart_type = 'Group By', + document_type = 'ToDo', + group_by_based_on = 'status', + filters_json = '{}', + )).insert() + + result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) + todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]}) + + self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count) + + frappe.db.rollback() + + def test_daily_dashboard_chart(self): + insert_test_records() + + if frappe.db.exists('Dashboard Chart', 'Test Daily Dashboard Chart'): + frappe.delete_doc('Dashboard Chart', 'Test Daily Dashboard Chart') + + frappe.get_doc(dict( + doctype = 'Dashboard Chart', + chart_name = 'Test Daily Dashboard Chart', + chart_type = 'Sum', + document_type = 'Communication', + based_on = 'communication_date', + value_based_on = 'rating', + timespan = 'Select Date Range', + time_interval = 'Daily', + from_date = datetime(2019, 1, 6), + to_date = datetime(2019, 1, 11), + filters_json = '{}', + timeseries = 1 + )).insert() + + result = get(chart_name ='Test Daily Dashboard Chart', refresh = 1) + + self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 300.0, 0.0, 100.0, 0.0]) + self.assertEqual( + result.get('labels'), + [formatdate('2019-01-06'), formatdate('2019-01-07'), formatdate('2019-01-08'),\ + formatdate('2019-01-09'), formatdate('2019-01-10'), formatdate('2019-01-11')] + ) + + frappe.db.rollback() + def test_weekly_dashboard_chart(self): insert_test_records() @@ -149,42 +200,21 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Weekly', from_date = datetime(2018, 12, 30), to_date = datetime(2019, 1, 15), - filters_json = '[]', + filters_json = '{}', timeseries = 1 )).insert() result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 400.0, 0.0]) + self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0]) self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) frappe.db.rollback() - def test_group_by_chart_type(self): - if frappe.db.exists('Dashboard Chart', 'Test Group By Dashboard Chart'): - frappe.delete_doc('Dashboard Chart', 'Test Group By Dashboard Chart') - - frappe.get_doc({"doctype":"ToDo", "description": "test"}).insert() - - frappe.get_doc(dict( - doctype = 'Dashboard Chart', - chart_name = 'Test Group By Dashboard Chart', - chart_type = 'Group By', - document_type = 'ToDo', - group_by_based_on = 'status', - filters_json = '[]', - )).insert() - - result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) - todo_status_count = frappe.db.count('ToDo', {'status': result.get('labels')[0]}) - - self.assertEqual(result.get('datasets')[0].get('values')[0], todo_status_count) - - frappe.db.rollback() - def insert_test_records(): create_new_communication(datetime(2019, 1, 10), 100) create_new_communication(datetime(2019, 1, 6), 200) + create_new_communication(datetime(2019, 1, 7), 400) create_new_communication(datetime(2019, 1, 8), 300) def create_new_communication(date, rating): From 346937aed738fdb1f23e896153ea16ddb8096f1a Mon Sep 17 00:00:00 2001 From: Afshan Date: Fri, 26 Jun 2020 19:28:48 +0530 Subject: [PATCH 19/45] fix: handle condition that "Setters" could be "Array" or "Object" --- .../js/frappe/form/multi_select_dialog.js | 62 ++++++++++++------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 41b87e0207..bb157efc38 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -101,19 +101,25 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { columns[1] = []; columns[2] = []; - Object.keys(this.setters).forEach((setter, index) => { - let df_prop = frappe.meta.docfield_map[this.doctype][setter]; + if($.isArray(this.setters)) { + for (const df of this.setters) { + columns[1].push(df, {fieldtype: "Column Break"}); + } + } else { + Object.keys(this.setters).forEach((setter, index) => { + let df_prop = frappe.meta.docfield_map[this.doctype][setter]; - // Index + 1 to start filling from index 1 - // Since Search is a standrd field already pushed - columns[(index + 1) % 3].push({ - fieldtype: df_prop.fieldtype, - label: df_prop.label, - fieldname: setter, - options: df_prop.options, - default: this.setters[setter] + // Index + 1 to start filling from index 1 + // Since Search is a standrd field already pushed + columns[(index + 1) % 3].push({ + fieldtype: df_prop.fieldtype, + label: df_prop.label, + fieldname: setter, + options: df_prop.options, + default: this.setters[setter] + }); }); - }); + } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal if (Object.seal) { @@ -217,7 +223,13 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let contents = ``; let columns = ["name"]; - columns = columns.concat(Object.keys(this.setters)); + if($.isArray(this.setters)) { + for (let df of this.setters) { + columns.push(df.fieldname); + } + } else { + columns = columns.concat(Object.keys(this.setters)); + } columns.forEach(function (column) { contents += `
@@ -290,16 +302,24 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let filters = this.get_query ? this.get_query().filters : {} || {}; let filter_fields = []; - Object.keys(this.setters).forEach(function (setter) { - var value = me.dialog.fields_dict[setter].get_value(); - if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) { - filters[setter] = ["like", "%" + value + "%"]; - } else { - filters[setter] = value || undefined; - me.args[setter] = filters[setter]; - filter_fields.push(setter); + if($.isArray(this.setters)) { + for (let df of this.setters) { + filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; + me.args[df.fieldname] = filters[df.fieldname]; + filter_fields.push(df.fieldname); } - }); + } else { + Object.keys(this.setters).forEach(function (setter) { + var value = me.dialog.fields_dict[setter].get_value(); + if (me.dialog.fields_dict[setter].df.fieldtype == "Data" && value) { + filters[setter] = ["like", "%" + value + "%"]; + } else { + filters[setter] = value || undefined; + me.args[setter] = filters[setter]; + filter_fields.push(setter); + } + }); + } let filter_group = this.get_custom_filters(); Object.assign(filters, filter_group); From 4ce29c312029ad4f92e39c7ee89cf1350c6ac8ef Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Fri, 26 Jun 2020 22:41:29 +0530 Subject: [PATCH 20/45] fix: Notificaions on data import --- frappe/model/document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/document.py b/frappe/model/document.py index 24450f0cc6..e316bfff70 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -825,7 +825,7 @@ class Document(BaseDocument): def run_notifications(self, method): """Run notifications for this method""" - if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: + if (frappe.flags.in_import and frappe.flags.mute_emails) or frappe.flags.in_patch or frappe.flags.in_install: return if self.flags.notifications_executed==None: From 1bfa56cd0cac9af5e58d9f1dea8e59cd73e9b8f6 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 27 Jun 2020 16:01:30 +0530 Subject: [PATCH 21/45] feat: backup and restore postgres site --- frappe/commands/site.py | 2 +- frappe/database/postgres/setup_db.py | 8 +++-- frappe/utils/backups.py | 51 +++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 13 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index 55ac05bd71..a975d29a15 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -136,7 +136,7 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name, - force=True) + force=True, db_type=frappe.conf.db_type) # Extract public and/or private files to the restored site, if user has given the path if with_public_files: diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index 01a97178f9..1dc1ea4c97 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -1,7 +1,7 @@ import frappe, subprocess, os from six.moves import input -def setup_database(force, source_sql, verbose): +def setup_database(force, source_sql=None, verbose=False): root_conn = get_root_connection() root_conn.commit() root_conn.sql("DROP DATABASE IF EXISTS `{0}`".format(frappe.conf.db_name)) @@ -16,10 +16,12 @@ def setup_database(force, source_sql, verbose): subprocess_env = os.environ.copy() subprocess_env['PGPASSWORD'] = str(frappe.conf.db_password) # bootstrap db + if not source_sql: + source_sql = os.path.join(os.path.dirname(__file__), 'framework_postgres.sql') + subprocess.check_output([ 'psql', frappe.conf.db_name, '-h', frappe.conf.db_host or 'localhost', '-U', - frappe.conf.db_name, '-f', - os.path.join(os.path.dirname(__file__), 'framework_postgres.sql') + frappe.conf.db_name, '-f', source_sql ], env=subprocess_env) frappe.connect() diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 7bb17d644b..dc73c2f84d 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -26,17 +26,24 @@ class BackupGenerator: If specifying db_file_name, also append ".sql.gz" """ def __init__(self, db_name, user, password, backup_path_db=None, backup_path_files=None, - backup_path_private_files=None, db_host="localhost", db_port=3306, verbose=False): + backup_path_private_files=None, db_host="localhost", db_port=None, verbose=False, + db_type='mariadb'): global _verbose self.db_host = db_host - self.db_port = db_port or 3306 + self.db_port = db_port self.db_name = db_name + self.db_type = db_type self.user = user self.password = password self.backup_path_files = backup_path_files self.backup_path_db = backup_path_db self.backup_path_private_files = backup_path_private_files + if not self.db_port and self.db_type == 'mariadb': + self.db_port = 3306 + elif not self.db_port and self.db_type == 'postgres': + self.db_port = 5432 + site = frappe.local.site or frappe.generate_hash(length=8) self.site_slug = site.replace('.', '_') @@ -141,6 +148,17 @@ class BackupGenerator: for item in self.__dict__.copy().items()) cmd_string = """mysqldump --single-transaction --quick --lock-tables=false -u %(user)s -p%(password)s %(db_name)s -h %(db_host)s -P %(db_port)s | gzip > %(backup_path_db)s """ % args + + if self.db_type == 'postgres': + cmd_string = "pg_dump postgres://{user}:{password}@{db_host}:{db_port}/{db_name} | gzip > {backup_path_db}".format( + user=args.get('user'), + password=args.get('password'), + db_host=args.get('db_host'), + db_port=args.get('db_port'), + db_name=args.get('db_name'), + backup_path_db=args.get('backup_path_db') + ) + err, out = frappe.utils.execute_in_shell(cmd_string) def send_email(self): @@ -181,7 +199,8 @@ def get_backup(): """ delete_temp_backups() odb = BackupGenerator(frappe.conf.db_name, frappe.conf.db_name,\ - frappe.conf.db_password, db_host = frappe.db.host) + 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))) @@ -201,6 +220,7 @@ def new_backup(older_than=6, ignore_files=False, backup_path_db=None, backup_pat backup_path_private_files=backup_path_private_files, db_host = frappe.db.host, db_port = frappe.db.port, + db_type = frappe.conf.db_type, verbose=verbose) odb.get_backup(older_than, ignore_files, force=force) return odb @@ -258,25 +278,38 @@ def backup(with_files=False, backup_path_db=None, backup_path_files=None, quiet= if __name__ == "__main__": """ - is_file_old db_name user password db_host - get_backup db_name user password db_host + is_file_old db_name user password db_host db_type db_port + get_backup db_name user password db_host db_type db_port """ import sys cmd = sys.argv[1] + + db_type = 'mariadb' + try: + db_type = sys.argv[6] + except IndexError as error: + pass + + db_port = 3306 + try: + db_port = int(sys.argv[7]) + except IndexError as error: + pass + if cmd == "is_file_old": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) is_file_old(odb.db_file_name) if cmd == "get_backup": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) odb.get_backup() if cmd == "take_dump": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) odb.take_dump() if cmd == "send_email": - odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost") + odb = BackupGenerator(sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5] or "localhost", db_type=db_type, db_port=db_port) odb.send_email("abc.sql.gz") if cmd == "delete_temp_backups": From f146d1ffb251eb22d95daeb62f13711aef2471a1 Mon Sep 17 00:00:00 2001 From: Afshan Date: Sat, 27 Jun 2020 18:01:26 +0530 Subject: [PATCH 22/45] style: format according to sider --- frappe/public/js/frappe/form/multi_select_dialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index bb157efc38..6920870859 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -101,7 +101,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { columns[1] = []; columns[2] = []; - if($.isArray(this.setters)) { + if ($.isArray(this.setters)) { for (const df of this.setters) { columns[1].push(df, {fieldtype: "Column Break"}); } @@ -223,7 +223,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let contents = ``; let columns = ["name"]; - if($.isArray(this.setters)) { + if ($.isArray(this.setters)) { for (let df of this.setters) { columns.push(df.fieldname); } @@ -302,7 +302,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { let filters = this.get_query ? this.get_query().filters : {} || {}; let filter_fields = []; - if($.isArray(this.setters)) { + if ($.isArray(this.setters)) { for (let df of this.setters) { filters[df.fieldname] = me.dialog.fields_dict[df.fieldname].get_value() || undefined; me.args[df.fieldname] = filters[df.fieldname]; From e0ad88594459e22f19bafeeced29fb9248bd2f86 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Sat, 27 Jun 2020 19:20:38 +0530 Subject: [PATCH 23/45] fix: allow users to disable google drive sync --- frappe/integrations/doctype/google_drive/google_drive.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frappe/integrations/doctype/google_drive/google_drive.py b/frappe/integrations/doctype/google_drive/google_drive.py index 60ee173bbf..869f0a4854 100644 --- a/frappe/integrations/doctype/google_drive/google_drive.py +++ b/frappe/integrations/doctype/google_drive/google_drive.py @@ -218,11 +218,13 @@ def upload_system_backup_to_google_drive(): return _("Google Drive Backup Successful.") def daily_backup(): - if frappe.db.get_single_value("Google Drive", "frequency") == "Daily": + drive_settings = frappe.db.get_singles_dict('Google Drive') + if drive_settings.enable and drive_settings.frequency == "Daily": upload_system_backup_to_google_drive() def weekly_backup(): - if frappe.db.get_single_value("Google Drive", "frequency") == "Weekly": + drive_settings = frappe.db.get_singles_dict('Google Drive') + if drive_settings.enable and drive_settings.frequency == "Weekly": upload_system_backup_to_google_drive() def get_absolute_path(filename): From 64c2fa374481f8a8c8a87678cf639712faa818b2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Sat, 27 Jun 2020 19:54:41 +0530 Subject: [PATCH 24/45] fix: update google api client --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index db51826b1c..92a423e495 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,10 +15,10 @@ Faker==2.0.4 future==0.18.2 GitPython==2.1.15 gitdb2==2.0.6;python_version<'3.4' -google-api-python-client==1.7.11 +google-api-python-client==1.9.3 google-auth-httplib2==0.0.3 google-auth-oauthlib==0.4.1 -google-auth==1.17.1 +google-auth==1.18.0 googlemaps==3.1.1 gunicorn==19.10.0 html2text==2016.9.19 From ec881a49770a27e6358ef25e0650777177796428 Mon Sep 17 00:00:00 2001 From: Revant Nandgaonkar Date: Sat, 27 Jun 2020 20:47:42 +0530 Subject: [PATCH 25/45] fix: remove unused variable --- frappe/utils/backups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index dc73c2f84d..2343db8a96 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -287,13 +287,13 @@ if __name__ == "__main__": db_type = 'mariadb' try: db_type = sys.argv[6] - except IndexError as error: + except IndexError: pass db_port = 3306 try: db_port = int(sys.argv[7]) - except IndexError as error: + except IndexError: pass if cmd == "is_file_old": From 620fafeb964407412e34dc3efc69ffb88ba24f2e Mon Sep 17 00:00:00 2001 From: Afshan Date: Sat, 27 Jun 2020 22:19:27 +0530 Subject: [PATCH 26/45] fix: divide filter fields into 3 columns using index. --- frappe/public/js/frappe/form/multi_select_dialog.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 6920870859..a0bb927563 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -102,9 +102,9 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { columns[2] = []; if ($.isArray(this.setters)) { - for (const df of this.setters) { - columns[1].push(df, {fieldtype: "Column Break"}); - } + this.setters.forEach((setter, index) => { + columns[(index + 1) % 3].push(setter); + }); } else { Object.keys(this.setters).forEach((setter, index) => { let df_prop = frappe.meta.docfield_map[this.doctype][setter]; From fc69f77f738f1ae04b513c819dd4858865329d83 Mon Sep 17 00:00:00 2001 From: Anand Narayan Date: Fri, 26 Jun 2020 15:51:21 -0700 Subject: [PATCH 27/45] fix(linked_docs): infinite recursion due to loops --- frappe/desk/form/linked_with.py | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index 72917d0341..a121e71dc8 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module @frappe.whitelist() -def get_submitted_linked_docs(doctype, name, docs=None, linked=None): +def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=None): """ Get all nested submitted linked doctype linkinfo @@ -34,10 +34,18 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None): if not linked: linked = {} + if not visited: + visited = [] + + if name in visited: + return + linkinfo = get_linked_doctypes(doctype) linked_docs = get_linked_docs(doctype, name, linkinfo) link_count = 0 + visited.append(name) + for link_doctype, link_names in linked_docs.items(): if link_doctype not in linked: linked[link_doctype] = [] @@ -61,13 +69,14 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None): if link.name in [doc.get("name") for doc in docs]: continue - links = get_submitted_linked_docs(link_doctype, link.name, docs, linked) - docs.append({ - "doctype": link_doctype, - "name": link.name, - "docstatus": link.docstatus, - "link_count": links.get("count") - }) + links = get_submitted_linked_docs(link_doctype, link.name, docs, linked, visited) + if links: + docs.append({ + "doctype": link_doctype, + "name": link.name, + "docstatus": link.docstatus, + "link_count": links.get("count") + }) # sort linked documents by ascending number of links docs.sort(key=lambda doc: doc.get("link_count")) From 500191f7fb1ab192be7d6c27601046d3eedf2b24 Mon Sep 17 00:00:00 2001 From: michellealva Date: Sun, 28 Jun 2020 11:09:13 +0530 Subject: [PATCH 28/45] fix: merge error in assigment rule --- frappe/automation/doctype/assignment_rule/assignment_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/automation/doctype/assignment_rule/assignment_rule.py b/frappe/automation/doctype/assignment_rule/assignment_rule.py index bf45347c4f..78f05e7fe9 100644 --- a/frappe/automation/doctype/assignment_rule/assignment_rule.py +++ b/frappe/automation/doctype/assignment_rule/assignment_rule.py @@ -21,7 +21,7 @@ class AssignmentRule(Document): def on_update(self): # pylint: disable=no-self-use frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name) - def after_rename(self): # pylint: disable=no-self-use + def after_rename(self, old, new, merge): # pylint: disable=no-self-use frappe.cache_manager.clear_doctype_map('Assignment Rule', self.name) def apply_unassign(self, doc, assignments): From bc9af7256215f8e7aa771b7bc108dbf959a78c56 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sun, 28 Jun 2020 16:25:01 +0530 Subject: [PATCH 29/45] fix: filter condition description doesn't get cleared --- frappe/public/js/frappe/ui/filters/edit_filter.html | 1 + frappe/public/js/frappe/ui/filters/filter.js | 13 ++++++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/edit_filter.html b/frappe/public/js/frappe/ui/filters/edit_filter.html index 3908c63fa1..f6618a2107 100644 --- a/frappe/public/js/frappe/ui/filters/edit_filter.html +++ b/frappe/public/js/frappe/ui/filters/edit_filter.html @@ -10,6 +10,7 @@
+
diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index 37eab50957..a205651fd9 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -110,6 +110,8 @@ frappe.ui.Filter = class { if(["in", "like", "not in", "not like"].includes(condition)) { fieldtype = 'Data'; this.add_condition_help(condition); + } else { + this.filter_edit_area.find('.filter-description').empty(); } if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) { @@ -331,14 +333,11 @@ frappe.ui.Filter = class { } add_condition_help(condition) { - let $desc = this.field.desc_area; - if(!$desc) { - $desc = $('
').appendTo(this.field.wrapper); - } - // set description - $desc.html((in_list(["in", "not in"], condition)==="in" + const description = ["in", "not in"].includes(condition) ? __("values separated by commas") - : __("use % as wildcard"))+'
'); + : __("use % as wildcard"); + + this.filter_edit_area.find('.filter-description').html(description); } hide_invalid_conditions(fieldtype, original_type) { From 62a8774bb10f6d122ffad1775a3f2848b3903607 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Mon, 29 Jun 2020 08:13:23 +0530 Subject: [PATCH 30/45] feat(role permission): if desk access is removed from a role, attempt to remove it from users too (#10838) * feat(minor): if desk access is removed from a role, attempt to remove it from users too * fix(minor): exception for install; * fix(minor): exception for install --- frappe/core/doctype/role/role.py | 24 ++++++++++++++++++------ frappe/core/doctype/role/test_role.py | 25 +++++++++++++++++++++++++ frappe/model/document.py | 5 +++++ frappe/tests/test_document.py | 7 +++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index 7ce2537da3..657340ec24 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -22,16 +22,28 @@ class Role(Document): frappe.db.sql("delete from `tabHas Role` where role = %s", self.name) frappe.clear_cache() + def on_update(self): + '''update system user desk access if this has changed in this update''' + if frappe.flags.in_install: return + if self.has_value_changed('desk_access'): + for user_name in get_users(self.name): + user = frappe.get_doc('User', user_name) + user_type = user.user_type + user.set_system_user() + if user_type != user.user_type: + user.save() + # Get email addresses of all users that have been assigned this role def get_emails_from_role(role): emails = [] - users = frappe.get_list("Has Role", filters={"role": role, "parenttype": "User"}, - fields=["parent"]) - - for user in users: - user_email, enabled = frappe.db.get_value("User", user.parent, ["email", "enabled"]) + for user in get_users(role): + user_email, enabled = frappe.db.get_value("User", user, ["email", "enabled"]) if enabled and user_email not in ["admin@example.com", "guest@example.com"]: emails.append(user_email) - return emails \ No newline at end of file + return emails + +def get_users(role): + return [d.parent for d in frappe.get_all("Has Role", filters={"role": role, "parenttype": "User"}, + fields=["parent"])] diff --git a/frappe/core/doctype/role/test_role.py b/frappe/core/doctype/role/test_role.py index 31efb5d4e8..6459a72c98 100644 --- a/frappe/core/doctype/role/test_role.py +++ b/frappe/core/doctype/role/test_role.py @@ -23,3 +23,28 @@ class TestUser(unittest.TestCase): frappe.get_doc("User", "test@example.com").add_roles("_Test Role 3") self.assertTrue("_Test Role 3" in frappe.get_roles("test@example.com")) + + def test_change_desk_access(self): + '''if we change desk acecss from role, remove from user''' + frappe.delete_doc_if_exists('User', 'test-user-for-desk-access@example.com') + frappe.delete_doc_if_exists('Role', 'desk-access-test') + user = frappe.get_doc(dict( + doctype='User', + email='test-user-for-desk-access@example.com', + first_name='test')).insert() + role = frappe.get_doc(dict( + doctype = 'Role', + role_name = 'desk-access-test', + desk_access = 0 + )).insert() + user.add_roles(role.name) + user.save() + self.assertTrue(user.user_type=='Website User') + role.desk_access = 1 + role.save() + user.reload() + self.assertTrue(user.user_type=='System User') + role.desk_access = 0 + role.save() + user.reload() + self.assertTrue(user.user_type=='Website User') diff --git a/frappe/model/document.py b/frappe/model/document.py index 24450f0cc6..30d3442954 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -396,6 +396,11 @@ class Document(BaseDocument): def get_doc_before_save(self): return getattr(self, '_doc_before_save', None) + def has_value_changed(self, fieldname): + '''Returns true if value is changed before and after saving''' + previous = self.get_doc_before_save() + return previous.get(fieldname)!=self.get(fieldname) if previous else True + def set_new_name(self, force=False, set_name=None, set_child_names=True): """Calls `frappe.naming.set_new_name` for parent and child docs.""" if self.flags.name_set and not force: diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 470ab35fb6..c96076cfba 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -66,6 +66,13 @@ class TestDocument(unittest.TestCase): self.assertEqual(frappe.db.get_value(d.doctype, d.name, "subject"), "subject changed") + def test_value_changed(self): + d = self.test_insert() + d.subject = "subject changed again" + d.save() + self.assertTrue(d.has_value_changed('subject')) + self.assertFalse(d.has_value_changed('event_type')) + def test_mandatory(self): # TODO: recheck if it is OK to force delete frappe.delete_doc_if_exists("User", "test_mandatory@example.com", 1) From 239f35298d3a7a3e72b34a72bddc29275edce5a1 Mon Sep 17 00:00:00 2001 From: prssanna Date: Sun, 28 Jun 2020 16:46:56 +0530 Subject: [PATCH 31/45] style: fix formatting --- frappe/public/js/frappe/ui/filters/filter.js | 315 +++++++++++-------- 1 file changed, 191 insertions(+), 124 deletions(-) diff --git a/frappe/public/js/frappe/ui/filters/filter.js b/frappe/public/js/frappe/ui/filters/filter.js index a205651fd9..5e41ed645e 100644 --- a/frappe/public/js/frappe/ui/filters/filter.js +++ b/frappe/public/js/frappe/ui/filters/filter.js @@ -13,26 +13,26 @@ frappe.ui.Filter = class { set_conditions() { this.conditions = [ - ["=", __("Equals")], - ["!=", __("Not Equals")], - ["like", __("Like")], - ["not like", __("Not Like")], - ["in", __("In")], - ["not in", __("Not In")], - ["is", __("Is")], - [">", ">"], - ["<", "<"], - [">=", ">="], - ["<=", "<="], - ["Between", __("Between")], - ["Timespan", __("Timespan")], + ['=', __('Equals')], + ['!=', __('Not Equals')], + ['like', __('Like')], + ['not like', __('Not Like')], + ['in', __('In')], + ['not in', __('Not In')], + ['is', __('Is')], + ['>', '>'], + ['<', '<'], + ['>=', '>='], + ['<=', '<='], + ['Between', __('Between')], + ['Timespan', __('Timespan')], ]; this.nested_set_conditions = [ - ["descendants of", __("Descendants Of")], - ["not descendants of", __("Not Descendants Of")], - ["ancestors of", __("Ancestors Of")], - ["not ancestors of", __("Not Ancestors Of")], + ['descendants of', __('Descendants Of')], + ['not descendants of', __('Not Descendants Of')], + ['ancestors of', __('Ancestors Of')], + ['not ancestors of', __('Not Ancestors Of')], ]; this.conditions.push(...this.nested_set_conditions); @@ -42,10 +42,10 @@ frappe.ui.Filter = class { Datetime: ['like', 'not like'], Data: ['Between', 'Timespan'], Select: ['like', 'not like', 'Between', 'Timespan'], - Link: ["Between", 'Timespan', '>', '<', '>=', '<='], - Currency: ["Between", 'Timespan'], - Color: ["Between", 'Timespan'], - Check: this.conditions.map(c => c[0]).filter(c => c !== '=') + Link: ['Between', 'Timespan', '>', '<', '>=', '<='], + Currency: ['Between', 'Timespan'], + Color: ['Between', 'Timespan'], + Check: this.conditions.map((c) => c[0]).filter((c) => c !== '='), }; } @@ -65,10 +65,11 @@ frappe.ui.Filter = class { } make() { - this.filter_edit_area = $(frappe.render_template("edit_filter", { - conditions: this.conditions - })) - .appendTo(this.parent.find('.filter-edit-area')); + this.filter_edit_area = $( + frappe.render_template('edit_filter', { + conditions: this.conditions, + }) + ).appendTo(this.parent.find('.filter-edit-area')); this.make_select(); this.set_events(); @@ -82,43 +83,51 @@ frappe.ui.Filter = class { filter_fields: this.filter_fields, select: (doctype, fieldname) => { this.set_field(doctype, fieldname); - } + }, }); - if(this.fieldname) { + if (this.fieldname) { this.fieldselect.set_value(this.doctype, this.fieldname); } } set_events() { - this.filter_edit_area.find("a.remove-filter").on("click", () => { + this.filter_edit_area.find('a.remove-filter').on('click', () => { this.remove(); }); - this.filter_edit_area.find(".set-filter-and-run").on("click", () => { - this.filter_edit_area.removeClass("new-filter"); + this.filter_edit_area.find('.set-filter-and-run').on('click', () => { + this.filter_edit_area.removeClass('new-filter'); this.on_change(); this.update_filter_tag(); }); this.filter_edit_area.find('.condition').change(() => { - if(!this.field) return; + if (!this.field) return; let condition = this.get_condition(); let fieldtype = null; - if(["in", "like", "not in", "not like"].includes(condition)) { + if (['in', 'like', 'not in', 'not like'].includes(condition)) { fieldtype = 'Data'; this.add_condition_help(condition); } else { this.filter_edit_area.find('.filter-description').empty(); } - if (['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && ["in", "not in"].includes(condition)) { + if ( + ['Select', 'MultiSelect'].includes(this.field.df.fieldtype) && + ['in', 'not in'].includes(condition) + ) { fieldtype = 'MultiSelect'; } - this.set_field(this.field.df.parent, this.field.df.fieldname, fieldtype, condition); + this.set_field( + this.field.df.parent, + this.field.df.fieldname, + fieldtype, + condition + ); }); } @@ -131,12 +140,12 @@ frappe.ui.Filter = class { setup_state(is_new) { let promise = Promise.resolve(); if (is_new) { - this.filter_edit_area.addClass("new-filter"); + this.filter_edit_area.addClass('new-filter'); } else { promise = this.update_filter_tag(); } - if(this.hidden) { + if (this.hidden) { promise.then(() => this.$filter_tag.hide()); } } @@ -166,13 +175,13 @@ frappe.ui.Filter = class { set_values(doctype, fieldname, condition, value) { // presents given (could be via tags!) if (this.set_field(doctype, fieldname) === false) { - return + return; } - if(this.field.df.original_type==='Check') { - value = (value==1) ? 'Yes' : 'No'; + if (this.field.df.original_type === 'Check') { + value = value == 1 ? 'Yes' : 'No'; } - if(condition) this.set_condition(condition, true); + if (condition) this.set_condition(condition, true); // set value can be asynchronous, so update_filter_tag should happen after field is set this._filter_value_set = Promise.resolve(); @@ -192,11 +201,13 @@ frappe.ui.Filter = class { set_field(doctype, fieldname, fieldtype, condition) { // set in fieldname (again) let cur = {}; - if(this.field) for(let k in this.field.df) cur[k] = this.field.df[k]; + if (this.field) for (let k in this.field.df) cur[k] = this.field.df[k]; - let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[fieldname]; + let original_docfield = (this.fieldselect.fields_by_name[doctype] || {})[ + fieldname + ]; - if(!original_docfield) { + if (!original_docfield) { console.warn(`Field ${fieldname} is not selectable.`); this.remove(); return false; @@ -216,8 +227,13 @@ frappe.ui.Filter = class { // called when condition is changed, // don't change if all is well - if(this.field && cur.fieldname == fieldname && df.fieldtype == cur.fieldtype && - df.parent == cur.parent && df.options == cur.options) { + if ( + this.field && + cur.fieldname == fieldname && + df.fieldtype == cur.fieldtype && + df.parent == cur.parent && + df.options == cur.options + ) { return; } @@ -225,20 +241,25 @@ frappe.ui.Filter = class { this.fieldselect.selected_doctype = doctype; this.fieldselect.selected_fieldname = fieldname; - if (this.filters_config && this.filters_config[condition] - && this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype)) { + if ( + this.filters_config && + this.filters_config[condition] && + this.filters_config[condition].valid_for_fieldtypes.includes(df.fieldtype) + ) { let args = {}; if (this.filters_config[condition].depends_on) { const field_name = this.filters_config[condition].depends_on; const filter_value = this.base_list.get_filter_value(field_name); args[field_name] = filter_value; } - frappe.xcall(this.filters_config[condition].get_field, args).then(field => { - df.fieldtype = field.fieldtype; - df.options = field.options; - df.fieldname = fieldname; - this.make_field(df, cur.fieldtype); - }); + frappe + .xcall(this.filters_config[condition].get_field, args) + .then(field => { + df.fieldtype = field.fieldtype; + df.options = field.options; + df.fieldname = fieldname; + this.make_field(df, cur.fieldtype); + }); } else { this.make_field(df, cur.fieldtype); } @@ -257,16 +278,18 @@ frappe.ui.Filter = class { f.refresh(); this.field = f; - if(old_text && f.fieldtype===old_fieldtype) { + if (old_text && f.fieldtype === old_fieldtype) { this.field.set_value(old_text); } // run on enter - $(this.field.wrapper).find(':input').keydown(e => { - if(e.which==13 && this.field.df.fieldtype !== 'MultiSelect') { - this.on_change(); - } - }); + $(this.field.wrapper) + .find(':input') + .keydown(e => { + if (e.which == 13 && this.field.df.fieldtype !== 'MultiSelect') { + this.on_change(); + } + }); } get_value() { @@ -275,7 +298,7 @@ frappe.ui.Filter = class { this.field.df.fieldname, this.get_condition(), this.get_selected_value(), - this.hidden + this.hidden, ]; } get_selected_value() { @@ -286,87 +309,101 @@ frappe.ui.Filter = class { return this.filter_edit_area.find('.condition').val(); } - set_condition(condition, trigger_change=false) { + set_condition(condition, trigger_change = false) { let $condition_field = this.filter_edit_area.find('.condition'); $condition_field.val(condition); - if(trigger_change) $condition_field.change(); - + if (trigger_change) $condition_field.change(); } make_tag() { if (!this.field) return; - this.$filter_tag = this.get_filter_tag_element() - .insertAfter(this.parent.find(".active-tag-filters .clear-filters")); + this.$filter_tag = this.get_filter_tag_element().insertAfter( + this.parent.find('.active-tag-filters .clear-filters') + ); this.set_filter_button_text(); this.bind_tag(); } bind_tag() { - this.$filter_tag.find(".remove-filter").on("click", this.remove.bind(this)); + this.$filter_tag.find('.remove-filter').on('click', this.remove.bind(this)); - let filter_button = this.$filter_tag.find(".toggle-filter"); - filter_button.on("click", () => { - filter_button.closest('.tag-filters-area').find('.filter-edit-area').show(); + let filter_button = this.$filter_tag.find('.toggle-filter'); + filter_button.on('click', () => { + filter_button + .closest('.tag-filters-area') + .find('.filter-edit-area') + .show(); this.filter_edit_area.toggle(); }); } set_filter_button_text() { - this.$filter_tag.find(".toggle-filter").html(this.get_filter_button_text()); + this.$filter_tag.find('.toggle-filter').html(this.get_filter_button_text()); } get_filter_button_text() { - let value = this.utils.get_formatted_value(this.field, this.get_selected_value()); - return `${__(this.field.df.label)} ${__(this.get_condition())} ${__(value)}`; + let value = this.utils.get_formatted_value( + this.field, + this.get_selected_value() + ); + return `${__(this.field.df.label)} ${__(this.get_condition())} ${__( + value + )}`; } get_filter_tag_element() { return $(`
`); } add_condition_help(condition) { - const description = ["in", "not in"].includes(condition) - ? __("values separated by commas") - : __("use % as wildcard"); + const description = ['in', 'not in'].includes(condition) + ? __('values separated by commas') + : __('use % as wildcard'); this.filter_edit_area.find('.filter-description').html(description); } hide_invalid_conditions(fieldtype, original_type) { - let invalid_conditions = this.invalid_condition_map[original_type] - || this.invalid_condition_map[fieldtype] || []; + let invalid_conditions = + this.invalid_condition_map[original_type] || + this.invalid_condition_map[fieldtype] || + []; for (let condition of this.conditions) { - this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle( - !invalid_conditions.includes(condition[0]) - ); + this.filter_edit_area + .find(`.condition option[value="${condition[0]}"]`) + .toggle(!invalid_conditions.includes(condition[0])); } } toggle_nested_set_conditions(df) { - let show_condition = df.fieldtype === "Link" && frappe.boot.nested_set_doctypes.includes(df.options); - this.nested_set_conditions.forEach(condition => { - this.filter_edit_area.find(`.condition option[value="${condition[0]}"]`).toggle(show_condition); + let show_condition = + df.fieldtype === 'Link' && + frappe.boot.nested_set_doctypes.includes(df.options); + this.nested_set_conditions.forEach((condition) => { + this.filter_edit_area + .find(`.condition option[value="${condition[0]}"]`) + .toggle(show_condition); }); } }; frappe.ui.filter_utils = { get_formatted_value(field, value) { - if(field.df.fieldname==="docstatus") { - value = {0:"Draft", 1:"Submitted", 2:"Cancelled"}[value] || value; - } else if(field.df.original_type==="Check") { - value = {0:"No", 1:"Yes"}[cint(value)]; + if (field.df.fieldname === 'docstatus') { + value = { 0: 'Draft', 1: 'Submitted', 2: 'Cancelled' }[value] || value; + } else if (field.df.original_type === 'Check') { + value = { 0: 'No', 1: 'Yes' }[cint(value)]; } - return frappe.format(value, field.df, {only_value: 1}); + return frappe.format(value, field.df, { only_value: 1 }); }, get_selected_value(field, condition) { @@ -381,7 +418,7 @@ frappe.ui.filter_utils = { } if (field.df.original_type == 'Check') { - val = (val=='Yes' ? 1 :0); + val = val == 'Yes' ? 1 : 0; } if (condition.indexOf('like', 'not like') !== -1) { @@ -389,12 +426,13 @@ frappe.ui.filter_utils = { if (val && !(val.startsWith('%') || val.endsWith('%'))) { val = '%' + val + '%'; } - } else if (in_list(["in", "not in"], condition)) { + } else if (in_list(['in', 'not in'], condition)) { if (val) { - val = val.split(',').map(v => strip(v)); + val = val.split(',').map((v) => strip(v)); } - } if (val === '%') { - val = ""; + } + if (val === '%') { + val = ''; } return val; @@ -403,7 +441,7 @@ frappe.ui.filter_utils = { get_default_condition(df) { if (df.fieldtype == 'Data') { return 'like'; - } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime'){ + } else if (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') { return 'Between'; } else { return '='; @@ -412,44 +450,73 @@ frappe.ui.filter_utils = { set_fieldtype(df, fieldtype, condition) { // reset - if(df.original_type) - df.fieldtype = df.original_type; - else - df.original_type = df.fieldtype; + if (df.original_type) df.fieldtype = df.original_type; + else df.original_type = df.fieldtype; - df.description = ''; df.reqd = 0; + df.description = ''; + df.reqd = 0; df.ignore_link_validation = true; // given - if(fieldtype) { + if (fieldtype) { df.fieldtype = fieldtype; return; } // scrub - if(df.fieldname=="docstatus") { - df.fieldtype="Select", - df.options=[ - {value:0, label:__("Draft")}, - {value:1, label:__("Submitted")}, - {value:2, label:__("Cancelled")} + if (df.fieldname == 'docstatus') { + df.fieldtype = 'Select', + df.options = [ + { value: 0, label: __('Draft') }, + { value: 1, label: __('Submitted') }, + { value: 2, label: __('Cancelled') }, ]; - } else if(df.fieldtype=='Check') { - df.fieldtype='Select'; - df.options='No\nYes'; - } else if(['Text','Small Text','Text Editor','Code','Tag','Comments', - 'Dynamic Link','Read Only','Assign'].indexOf(df.fieldtype)!=-1) { + } else if (df.fieldtype == 'Check') { + df.fieldtype = 'Select'; + df.options = 'No\nYes'; + } else if ( + [ + 'Text', + 'Small Text', + 'Text Editor', + 'Code', + 'Tag', + 'Comments', + 'Dynamic Link', + 'Read Only', + 'Assign', + ].indexOf(df.fieldtype) != -1 + ) { df.fieldtype = 'Data'; - } else if(df.fieldtype=='Link' && ['=', '!=', 'descendants of', 'ancestors of', 'not descendants of', 'not ancestors of'].indexOf(condition)==-1) { + } else if ( + df.fieldtype == 'Link' && + [ + '=', + '!=', + 'descendants of', + 'ancestors of', + 'not descendants of', + 'not ancestors of', + ].indexOf(condition) == -1 + ) { df.fieldtype = 'Data'; } - if(df.fieldtype==="Data" && (df.options || "").toLowerCase()==="email") { + if ( + df.fieldtype === 'Data' && + (df.options || '').toLowerCase() === 'email' + ) { df.options = null; } - if(condition == "Between" && (df.fieldtype == 'Date' || df.fieldtype == 'Datetime')){ + if ( + condition == 'Between' && + (df.fieldtype == 'Date' || df.fieldtype == 'Datetime') + ) { df.fieldtype = 'DateRange'; } - if (condition == 'Timespan' && ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype)) { + if ( + condition == 'Timespan' && + ['Date', 'Datetime', 'DateRange', 'Select'].includes(df.fieldtype) + ) { df.fieldtype = 'Select'; df.options = this.get_timespan_options(['Last', 'Today', 'This', 'Next']); } @@ -465,15 +532,15 @@ frappe.ui.filter_utils = { get_timespan_options(periods) { const period_map = { - 'Last': ['Week', 'Month', 'Quarter', '6 months', 'Year'], - 'Today': null, - 'This': ['Week', 'Month', 'Quarter', 'Year'], - 'Next': ['Week', 'Month', 'Quarter', '6 months', 'Year'] + Last: ['Week', 'Month', 'Quarter', '6 months', 'Year'], + Today: null, + This: ['Week', 'Month', 'Quarter', 'Year'], + Next: ['Week', 'Month', 'Quarter', '6 months', 'Year'], }; let options = []; - periods.forEach(period => { + periods.forEach((period) => { if (period_map[period]) { - period_map[period].forEach(p => { + period_map[period].forEach((p) => { options.push({ label: __(`{0} {1}`, [period, p]), value: `${period.toLowerCase()} ${p.toLowerCase()}`, @@ -487,5 +554,5 @@ frappe.ui.filter_utils = { } }); return options; - } + }, }; From 1ad23c962cb2a5df11576137cb172cb15fef7ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=BCrker=20Tunal=C4=B1?= Date: Mon, 29 Jun 2020 09:25:39 +0300 Subject: [PATCH 32/45] Turkish translations need this fix (#10555) num2words library has Turkish translation since version 0.5.6. And their last version 0.5.10 seems ok. We need this fix to better support Turkish users. Co-authored-by: Gavin D'souza --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index b478e7abaa..2d38f12faf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,7 +29,7 @@ ldap3==2.7 markdown2==2.3.9 maxminddb-geolite2==2018.703 ndg-httpsclient==0.5.1 -num2words==0.5.5 +num2words==0.5.10 oauthlib==3.1.0 openpyxl==2.6.4 passlib==1.7.2 From 478b87e5be816dc02fa7cb89fa4e41d4265bde11 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Sun, 28 Jun 2020 06:27:18 +0530 Subject: [PATCH 33/45] fix: add verbosity in is_downgrade --- frappe/commands/site.py | 5 +++-- frappe/installer.py | 13 +++++++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index c327d85af0..812d63b2f6 100755 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -136,8 +136,9 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas frappe.init(site=site) # dont allow downgrading to older versions of frappe without force - if not force and is_downgrade(decompressed_file_name): - click.confirm("Downgrading sites may lead to a broken site. Do you wish to continue?", abort=True) + if not force and is_downgrade(decompressed_file_name, verbose=True): + warn_message = "This is not recommended and may lead to unexpected behaviour. Do you want to continue anyway?" + click.confirm(warn_message, abort=True) _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username, mariadb_root_password=mariadb_root_password, admin_password=admin_password, diff --git a/frappe/installer.py b/frappe/installer.py index 962a9e40e5..fa6e25375e 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -327,7 +327,7 @@ def extract_tar_files(site_name, file_path, folder_name): return tar_path -def is_downgrade(sql_file_path): +def is_downgrade(sql_file_path, verbose=False): """checks if input db backup will get downgraded on current bench""" from semantic_version import Version head = "INSERT INTO `tabInstalled Application` VALUES" @@ -335,9 +335,9 @@ def is_downgrade(sql_file_path): with open(sql_file_path) as f: for line in f: if head in line: - # ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'press','0.0.1','master') + # 'line' (str) format: ('2056588823','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',1,'frappe','v10.1.71-74 (3c50d5e) (v10.x.x)','v10.x.x'),('855c640b8e','2020-05-11 18:21:31.488367','2020-06-12 11:49:31.079506','Administrator','Administrator',0,'Installed Applications','installed_applications','Installed Applications',2,'your_custom_app','0.0.1','master') line = line.strip().lstrip(head).rstrip(";").strip() - # [('frappe', '12.x.x-develop ()', 'develop'), ('press', '0.0.1', 'master')] + # 'all_apps' (list) format: [('frappe', '12.x.x-develop ()', 'develop'), ('your_custom_app', '0.0.1', 'master')] all_apps = [ x[-3:] for x in frappe.safe_eval(line) ] for app in all_apps: @@ -351,4 +351,9 @@ def is_downgrade(sql_file_path): except ValueError: return False - return backup_version > current_version + downgrade = backup_version > current_version + + if verbose and downgrade: + print("Your site will be downgraded from Frappe {0} to {1}".format(current_version, backup_version)) + + return downgrade From 94bfc16bfb3db6706431d448c4e94d4f3c231c90 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Mon, 29 Jun 2020 19:30:43 +0530 Subject: [PATCH 34/45] fix: declare site_config_backup_path variable before referencing --- frappe/utils/backups.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 2343db8a96..bb03c85bf9 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -98,6 +98,7 @@ class BackupGenerator: backup_path_files = None backup_path_db = None backup_path_private_files = None + site_config_backup_path = None for this_file in file_list: this_file = cstr(this_file) From 20815aa3491cbca3e5377f9e6fcc30f3e21e8f0f Mon Sep 17 00:00:00 2001 From: Prssanna Desai Date: Tue, 30 Jun 2020 08:03:28 +0530 Subject: [PATCH 35/45] refactor: remove unnecessary code from get_submitted_linked_docs (#10844) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- frappe/desk/form/linked_with.py | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/frappe/desk/form/linked_with.py b/frappe/desk/form/linked_with.py index a121e71dc8..5bae49ea95 100644 --- a/frappe/desk/form/linked_with.py +++ b/frappe/desk/form/linked_with.py @@ -13,7 +13,7 @@ from frappe.modules import load_doctype_module @frappe.whitelist() -def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=None): +def get_submitted_linked_docs(doctype, name, docs=None, visited=None): """ Get all nested submitted linked doctype linkinfo @@ -31,34 +31,27 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non if not docs: docs = [] - if not linked: - linked = {} - if not visited: - visited = [] + visited = {} - if name in visited: + if doctype not in visited: + visited[doctype] = [] + + if name in visited[doctype]: return linkinfo = get_linked_doctypes(doctype) linked_docs = get_linked_docs(doctype, name, linkinfo) link_count = 0 - visited.append(name) - + visited[doctype].append(name) + for link_doctype, link_names in linked_docs.items(): - if link_doctype not in linked: - linked[link_doctype] = [] for link in link_names: if link['name'] == name: continue - if linked and name in linked[link_doctype]: - continue - - linked[link_doctype].append(link['name']) - docinfo = link.update({"doctype": link_doctype}) validated_doc = validate_linked_doc(docinfo) @@ -66,10 +59,8 @@ def get_submitted_linked_docs(doctype, name, docs=None, linked=None, visited=Non continue link_count += 1 - if link.name in [doc.get("name") for doc in docs]: - continue - links = get_submitted_linked_docs(link_doctype, link.name, docs, linked, visited) + links = get_submitted_linked_docs(link_doctype, link.name, docs, visited) if links: docs.append({ "doctype": link_doctype, From 9f769b980c966b70c75030770374277cb524e13c Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 30 Jun 2020 09:26:49 +0530 Subject: [PATCH 36/45] chore: Mergify configuration update (#10862) --- .mergify.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index b145834cc4..582bbc2ee5 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -4,8 +4,7 @@ pull_request_rules: - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - - status-success=security/snyk - package.json (frappe) - - status-success=security/snyk - requirements.txt (frappe) + - status-success=security/snyk (frappe) - label!=don't-merge - label!=squash - "#approved-reviews-by>=1" @@ -17,8 +16,7 @@ pull_request_rules: - status-success=Sider - status-success=Semantic Pull Request - status-success=Travis CI - Pull Request - - status-success=security/snyk - package.json (frappe) - - status-success=security/snyk - requirements.txt (frappe) + - status-success=security/snyk (frappe) - label!=don't-merge - label=squash - "#approved-reviews-by>=1" From c2135f247151315bdac2549cefb2da379cbad90f Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 30 Jun 2020 10:21:23 +0530 Subject: [PATCH 37/45] fix: Missing semicolon --- frappe/public/js/frappe/data_import/data_exporter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/data_import/data_exporter.js b/frappe/public/js/frappe/data_import/data_exporter.js index 238e916c34..f6af338235 100644 --- a/frappe/public/js/frappe/data_import/data_exporter.js +++ b/frappe/public/js/frappe/data_import/data_exporter.js @@ -254,7 +254,7 @@ frappe.data_import.DataExporter = class DataExporter { get_filters() { return this.filter_group.get_filters().map(filter => { - return filter.slice(0, 4) + return filter.slice(0, 4); }); } @@ -275,7 +275,7 @@ frappe.data_import.DataExporter = class DataExporter { : this.column_map[doctype]; let is_field_mandatory = df => (df.fieldname === 'name' && !child_fieldname) - || (df.reqd && this.exporting_for == 'Insert New Records') + || (df.reqd && this.exporting_for == 'Insert New Records'); return fields .filter(df => { From e5e6c7c9fea48539c84b81bb447d852dcd7871cc Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 30 Jun 2020 12:14:48 +0530 Subject: [PATCH 38/45] fix: Extract gid from google sheets url --- frappe/utils/csvutils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frappe/utils/csvutils.py b/frappe/utils/csvutils.py index 1d326bd1c5..f73f80582a 100644 --- a/frappe/utils/csvutils.py +++ b/frappe/utils/csvutils.py @@ -175,12 +175,18 @@ def getlink(doctype, name): return '%(name)s' % locals() def get_csv_content_from_google_sheets(url): + # https://docs.google.com/spreadsheets/d/{sheetid}}/edit#gid={gid} validate_google_sheets_url(url) - + # get gid, defaults to first sheet + if "gid=" in url: + gid = url.rsplit('gid=', 1)[1] + else: + gid = 0 # remove /edit path url = url.rsplit('/edit', 1)[0] - # add /export path, defaults to first sheet - url = url + '/export?format=csv&gid=0' + # add /export path, + url = url + '/export?format=csv&gid={0}'.format(gid) + headers = { 'Accept': 'text/csv' } From 45d097f6b52670140983455fd3c101e691e68abd Mon Sep 17 00:00:00 2001 From: Faris Ansari Date: Tue, 30 Jun 2020 13:13:56 +0530 Subject: [PATCH 39/45] fix(patch): Use rename_doc --- frappe/patches/v13_0/replace_old_data_import.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/patches/v13_0/replace_old_data_import.py b/frappe/patches/v13_0/replace_old_data_import.py index 469703e172..f3eed6253c 100644 --- a/frappe/patches/v13_0/replace_old_data_import.py +++ b/frappe/patches/v13_0/replace_old_data_import.py @@ -13,6 +13,4 @@ def execute(): frappe.rename_doc('DocType', 'Data Import', 'Data Import Legacy') frappe.db.commit() frappe.db.sql("DROP TABLE IF EXISTS `tabData Import`") - frappe.reload_doc("core", "doctype", "data_import") - frappe.get_doc("DocType", "Data Import").on_update() - frappe.delete_doc_if_exists("DocType", "Data Import Beta") + frappe.rename_doc('DocType', 'Data Import Beta', 'Data Import') From 63cdb5f506902e460e509a83eb01f5b45561c234 Mon Sep 17 00:00:00 2001 From: prssanna Date: Tue, 30 Jun 2020 15:07:05 +0530 Subject: [PATCH 40/45] fix: always append start date to result --- .../dashboard_chart/dashboard_chart.py | 11 +--- .../dashboard_chart/test_dashboard_chart.py | 55 ++++++++++++------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index a5c5504db2..4ad6943e0b 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -260,9 +260,7 @@ def get_result(data, timegrain, from_date, to_date): start_date = getdate(from_date) end_date = getdate(to_date) - result = [] - if timegrain == 'Daily': - result.append([start_date, 0.0]) + result = [[start_date, 0.0]] while start_date < end_date: next_date = get_next_expected_date(start_date, timegrain) @@ -280,11 +278,8 @@ def get_result(data, timegrain, from_date, to_date): def get_next_expected_date(date, timegrain): next_date = None - if timegrain=='Daily': - next_date = add_to_date(date, days=1) - else: - # given date is always assumed to be the period ending date - next_date = get_period_ending(add_to_date(date, days=1), timegrain) + # given date is always assumed to be the period ending date + next_date = get_period_ending(add_to_date(date, days=1), timegrain) return getdate(next_date) def get_period_ending(date, timegrain): diff --git a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py index 1a300e471a..5e39998e62 100644 --- a/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/test_dashboard_chart.py @@ -4,13 +4,12 @@ from __future__ import unicode_literals import unittest, frappe -from frappe.utils import getdate, formatdate +from frappe.utils import getdate, formatdate, get_last_day from frappe.desk.doctype.dashboard_chart.dashboard_chart import (get, get_period_ending) from datetime import datetime from dateutil.relativedelta import relativedelta -import calendar class TestDashboardChart(unittest.TestCase): def test_period_ending(self): @@ -53,16 +52,18 @@ class TestDashboardChart(unittest.TestCase): cur_date = datetime.now() - relativedelta(years=1) - result = get(chart_name ='Test Dashboard Chart', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + result = get(chart_name='Test Dashboard Chart', refresh=1) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) - # self.assertEqual(result.get('datasets')[0].get('values')[:-1], - # [44, 28, 8, 11, 2, 6, 18, 6, 4, 5, 15, 13]) - frappe.db.rollback() def test_empty_dashboard_chart(self): @@ -79,15 +80,20 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() cur_date = datetime.now() - relativedelta(years=1) - result = get(chart_name ='Test Empty Dashboard Chart', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + result = get(chart_name ='Test Empty Dashboard Chart', refresh=1) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) @@ -111,15 +117,20 @@ class TestDashboardChart(unittest.TestCase): based_on = 'creation', timespan = 'Last Year', time_interval = 'Monthly', - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() cur_date = datetime.now() - relativedelta(years=1) result = get(chart_name ='Test Empty Dashboard Chart 2', refresh = 1) - for idx in range(13): - month = datetime(int(cur_date.year), int(cur_date.strftime('%m')), int(calendar.monthrange(cur_date.year, cur_date.month)[1])) + self.assertEqual(result.get('labels')[0], formatdate(cur_date.strftime('%Y-%m-%d'))) + + if formatdate(cur_date.strftime('%Y-%m-%d')) == formatdate(get_last_day(cur_date).strftime('%Y-%m-%d')): + cur_date += relativedelta(months=1) + + for idx in range(1, 13): + month = get_last_day(cur_date) month = formatdate(month.strftime('%Y-%m-%d')) self.assertEqual(result.get('labels')[idx], month) cur_date += relativedelta(months=1) @@ -141,7 +152,7 @@ class TestDashboardChart(unittest.TestCase): chart_type = 'Group By', document_type = 'ToDo', group_by_based_on = 'status', - filters_json = '{}', + filters_json = '[]', )).insert() result = get(chart_name ='Test Group By Dashboard Chart', refresh = 1) @@ -168,7 +179,7 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Daily', from_date = datetime(2019, 1, 6), to_date = datetime(2019, 1, 11), - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() @@ -200,22 +211,24 @@ class TestDashboardChart(unittest.TestCase): time_interval = 'Weekly', from_date = datetime(2018, 12, 30), to_date = datetime(2019, 1, 15), - filters_json = '{}', + filters_json = '[]', timeseries = 1 )).insert() result = get(chart_name ='Test Weekly Dashboard Chart', refresh = 1) - self.assertEqual(result.get('datasets')[0].get('values'), [200.0, 800.0, 0.0]) - self.assertEqual(result.get('labels'), [formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) + self.assertEqual(result.get('datasets')[0].get('values'), [50.0, 300.0, 800.0, 0.0]) + self.assertEqual(result.get('labels'), [formatdate('2018-12-30'), formatdate('2019-01-06'), formatdate('2019-01-13'), formatdate('2019-01-20')]) frappe.db.rollback() def insert_test_records(): - create_new_communication(datetime(2019, 1, 10), 100) + create_new_communication(datetime(2018, 12, 30), 50) + create_new_communication(datetime(2019, 1, 4), 100) create_new_communication(datetime(2019, 1, 6), 200) create_new_communication(datetime(2019, 1, 7), 400) create_new_communication(datetime(2019, 1, 8), 300) + create_new_communication(datetime(2019, 1, 10), 100) def create_new_communication(date, rating): communication = { From a6c68f7dc04b357c490a8cc4088dafab3ed5aae4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 30 Jun 2020 17:38:27 +0530 Subject: [PATCH 41/45] fix: Initialize email_sent_to_any_recipient outsite try block (#10866) to avoid UnboundLocalError: local variable 'email_sent_to_any_recipient' referenced before assignment --- frappe/email/queue.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/frappe/email/queue.py b/frappe/email/queue.py index ce512de276..8bffc108b9 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -347,7 +347,7 @@ def flush(from_test=False): if not smtpserver: smtpserver = SMTPServer() smtpserver_dict[email.sender] = smtpserver - + if from_test: send_one(email.name, smtpserver, auto_commit) else: @@ -390,12 +390,12 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): where name=%s for update''', email, as_dict=True) - + if len(email): email = email[0] else: return - + recipients_list = frappe.db.sql('''select name, recipient, status from `tabEmail Queue Recipient` where parent=%s''', email.name, as_dict=1) @@ -417,6 +417,8 @@ def send_one(email, smtpserver=None, auto_commit=True, now=False): if email.communication: frappe.get_doc('Communication', email.communication).set_delivery_status(commit=auto_commit) + email_sent_to_any_recipient = None + try: message = None From 124db4170c04d2dae3d570b8fd5c144c95fb3b45 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Tue, 30 Jun 2020 17:52:57 +0530 Subject: [PATCH 42/45] chore: set develop version as 13.x.x --- frappe/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/hooks.py b/frappe/hooks.py index c207e61de5..1f209f00a2 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -12,7 +12,7 @@ source_link = "https://github.com/frappe/frappe" app_license = "MIT" app_logo_url = '/assets/frappe/images/frappe-framework-logo.png' -develop_version = '12.x.x-develop' +develop_version = '13.x.x-develop' app_email = "info@frappe.io" From adadb26732c106bd37a4e0915babf09e46ef27cd Mon Sep 17 00:00:00 2001 From: "Chinmay D. Pai" Date: Tue, 30 Jun 2020 17:55:25 +0530 Subject: [PATCH 43/45] chore: only validate s3 settings if enabled Signed-off-by: Chinmay D. Pai --- .../doctype/s3_backup_settings/s3_backup_settings.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py index 6cfd3646b2..c8b007ba7b 100755 --- a/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py +++ b/frappe/integrations/doctype/s3_backup_settings/s3_backup_settings.py @@ -19,6 +19,9 @@ from botocore.exceptions import ClientError class S3BackupSettings(Document): def validate(self): + if not self.enabled: + return + if not self.endpoint_url: self.endpoint_url = 'https://s3.amazonaws.com' conn = boto3.client( From dc3402c5461d5941a5b47b6fc7ae2c8d3a42037d Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 1 Jul 2020 10:31:17 +0530 Subject: [PATCH 44/45] feat: Option to add custom timeline contents (#10677) --- frappe/desk/form/load.py | 12 +++++++++ .../public/js/frappe/form/footer/timeline.js | 20 +++++++++++--- .../js/frappe/form/templates/timeline.html | 4 +-- .../frappe/form/templates/timeline_item.html | 13 ++++++--- frappe/public/less/form.less | 27 ++++++++++++++++--- 5 files changed, 62 insertions(+), 14 deletions(-) diff --git a/frappe/desk/form/load.py b/frappe/desk/form/load.py index f24f33df07..cacbd3c633 100644 --- a/frappe/desk/form/load.py +++ b/frappe/desk/form/load.py @@ -100,6 +100,7 @@ def get_docinfo(doc=None, doctype=None, name=None): "shared": frappe.share.get_users(doc.doctype, doc.name), "views": get_view_logs(doc.doctype, doc.name), "energy_point_logs": get_point_logs(doc.doctype, doc.name), + "additional_timeline_content": get_additional_timeline_content(doc.doctype, doc.name), "milestones": get_milestones(doc.doctype, doc.name), "is_document_followed": is_document_followed(doc.doctype, doc.name, frappe.session.user), "tags": get_tags(doc.doctype, doc.name), @@ -277,3 +278,14 @@ def get_document_email(doctype, name): def get_automatic_email_link(): return frappe.db.get_value("Email Account", {"enable_incoming": 1, "enable_automatic_linking": 1}, "email_id") + +def get_additional_timeline_content(doctype, docname): + contents = [] + hooks = frappe.get_hooks().get('additional_timeline_content', {}) + methods_for_all_doctype = hooks.get('*', []) + methods_for_current_doctype = hooks.get(doctype, []) + + for method in methods_for_all_doctype + methods_for_current_doctype: + contents.extend(frappe.get_attr(method)(doctype, docname) or []) + + return contents \ No newline at end of file diff --git a/frappe/public/js/frappe/form/footer/timeline.js b/frappe/public/js/frappe/form/footer/timeline.js index 7821a04c50..84f34d4757 100644 --- a/frappe/public/js/frappe/form/footer/timeline.js +++ b/frappe/public/js/frappe/form/footer/timeline.js @@ -120,9 +120,11 @@ frappe.ui.form.Timeline = class Timeline { display_automatic_link_email() { let docinfo = this.frm.get_docinfo(); - if (docinfo.document_email){ + if (docinfo.document_email) { let link = __("Send an email to {0} to link it here", [`${docinfo.document_email}`]); - $('.timeline-email-import').html(link); + const email_link = $('.timeline-email-import'); + email_link.removeClass('hide'); + email_link.html(link); } } @@ -180,12 +182,15 @@ frappe.ui.form.Timeline = class Timeline { // append energy point logs timeline = timeline.concat(this.get_energy_point_logs()); + // custom contents + timeline = timeline.concat(this.get_additional_timeline_content()); + // append milestones timeline = timeline.concat(this.get_milestones()); // sort timeline - .filter(a => a.content) + .filter(a => a.content || a.template) .sort((b, c) => me.compare_dates(b, c)) .forEach(d => { d.frm = me.frm; @@ -407,7 +412,10 @@ frappe.ui.form.Timeline = class Timeline { c.original_content = c.content; c.content = frappe.utils.toggle_blockquote(c.content); } - if(!frappe.utils.is_html(c.content)) { + + if (c.template) { + c.content_html = frappe.render_template(c.template, c.template_data); + } else if (!frappe.utils.is_html(c.content)) { c.content_html = frappe.markdown(__(c.content)); } else { c.content_html = c.content; @@ -529,6 +537,10 @@ frappe.ui.form.Timeline = class Timeline { return energy_point_logs; } + get_additional_timeline_content() { + return this.frm.get_docinfo().additional_timeline_content || []; + } + get_milestones() { let milestones = this.frm.get_docinfo().milestones; milestones.map(log => { diff --git a/frappe/public/js/frappe/form/templates/timeline.html b/frappe/public/js/frappe/form/templates/timeline.html index 5600f9384f..0a8a631142 100644 --- a/frappe/public/js/frappe/form/templates/timeline.html +++ b/frappe/public/js/frappe/form/templates/timeline.html @@ -17,9 +17,7 @@ {% } %} {% } %}
-
- -
+
diff --git a/frappe/public/js/frappe/form/templates/timeline_item.html b/frappe/public/js/frappe/form/templates/timeline_item.html index 1442374e24..9cb8499771 100755 --- a/frappe/public/js/frappe/form/templates/timeline_item.html +++ b/frappe/public/js/frappe/form/templates/timeline_item.html @@ -1,4 +1,6 @@ -
{% if (data.user_content) { %} - {% } else if (data.comment_type == "Energy Points") { %} + {% } else if (data.comment_type == "Energy Points" || data.template) { %} {{ data.content_html }} {% } else { %} {%= data.fullname %} @@ -200,8 +202,11 @@ {% } %} {% } %} - - – {%= data.comment_on %} + {% if (!data.template) { %} + + – {%= data.comment_on %} + + {% } %}
{% } %}
diff --git a/frappe/public/less/form.less b/frappe/public/less/form.less index cd391c1f10..8d01cd6dd7 100644 --- a/frappe/public/less/form.less +++ b/frappe/public/less/form.less @@ -349,6 +349,9 @@ h6.uppercase, .h6.uppercase { .form-section { padding: 15px 7px; } + .hide-border { + padding-top: 0; + } } .help ol { @@ -573,7 +576,13 @@ h6.uppercase, .h6.uppercase { margin-left: 5px; } - .media-body:after, .media-body:before { + .media-body { + .left-arrow; + } +} + +.left-arrow { + &::after, &::before { right: 100%; top: 15px; border: solid transparent; @@ -584,13 +593,13 @@ h6.uppercase, .h6.uppercase { pointer-events: none; } - .media-body:after { + &::after { border-color: rgba(136, 183, 213, 0); border-right-color: #fafbfc; border-width: 6px; margin-top: -6px; } - .media-body:before { + &::before { border-color: rgba(194, 225, 245, 0); border-right-color: @border-color; border-width: 7px; @@ -638,6 +647,18 @@ h6.uppercase, .h6.uppercase { top: 5px; } +.timeline-item.user-content.show-indicator { + position: relative; + .media-body { + margin-left: 50px; + } + &::before { + .timeline-indicator(); + left: 13px; + top: 13px; + } +} + .timeline-item.notification-content::before { .timeline-indicator(); } From 8096c9e3710d35449a941ee6cc671fed79e689b4 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Wed, 1 Jul 2020 12:08:15 +0530 Subject: [PATCH 45/45] fix(permission): Show allowed rights on document for passed user (#10864) --- .../permitted_documents_for_user.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py index 95a04360be..8b2d1e01fa 100644 --- a/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py +++ b/frappe/core/report/permitted_documents_for_user/permitted_documents_for_user.py @@ -5,23 +5,23 @@ from __future__ import unicode_literals import frappe from frappe import _, throw import frappe.utils.user -from frappe.permissions import check_admin_or_system_manager +from frappe.permissions import check_admin_or_system_manager, rights from frappe.model import data_fieldtypes def execute(filters=None): user, doctype, show_permissions = filters.get("user"), filters.get("doctype"), filters.get("show_permissions") + if not validate(user, doctype): return [], [] columns, fields = get_columns_and_fields(doctype) data = frappe.get_list(doctype, fields=fields, as_list=True, user=user) if show_permissions: - columns = columns + ["Read", "Write", "Create", "Delete", "Submit", "Cancel", "Amend", "Print", "Email", - "Report", "Import", "Export", "Share"] + columns = columns + [frappe.unscrub(right) + ':Check:80' for right in rights] data = list(data) - for i,item in enumerate(data): - temp = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, item[0]), False,user) - data[i] = item+(temp.get("read"),temp.get("write"),temp.get("create"),temp.get("delete"),temp.get("submit"),temp.get("cancel"),temp.get("amend"),temp.get("print"),temp.get("email"),temp.get("report"),temp.get("import"),temp.get("export"),temp.get("share"),) + for i, doc in enumerate(data): + permission = frappe.permissions.get_doc_permissions(frappe.get_doc(doctype, doc[0]), user) + data[i] = doc + tuple(permission.get(right) for right in rights) return columns, data