From 9bfc97a823328163d7c632072528c163e66c9e23 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 23 Feb 2021 18:53:24 +0530 Subject: [PATCH 001/139] fix: 'Not Saved' even after saving/submitting a doctype --- frappe/core/doctype/user_permission/user_permission.js | 2 +- frappe/public/js/frappe/form/form.js | 4 ++-- frappe/public/js/frappe/model/model.js | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js index 4c3f5b4eb8..6c6b74c5df 100644 --- a/frappe/core/doctype/user_permission/user_permission.js +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -45,7 +45,7 @@ frappe.ui.form.on('User Permission', { set_applicable_for_constraint: frm => { frm.toggle_reqd('applicable_for', !frm.doc.apply_to_all_doctypes); if (frm.doc.apply_to_all_doctypes) { - frm.set_value('applicable_for', null); + frm.set_value('applicable_for', null, null, true); } }, diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 8d96054d16..a0f546b42c 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -1320,7 +1320,7 @@ frappe.ui.form.Form = class FrappeForm { return doc; } - set_value(field, value, if_missing) { + set_value(field, value, if_missing, avoid_dirty=false) { var me = this; var _set = function(f, v) { var fieldobj = me.fields_dict[f]; @@ -1340,7 +1340,7 @@ frappe.ui.form.Form = class FrappeForm { me.refresh_field(f); return Promise.resolve(); } else { - return frappe.model.set_value(me.doctype, me.doc.name, f, v); + return frappe.model.set_value(me.doctype, me.doc.name, f, v, me.fieldtype, avoid_dirty); } } } else { diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 9ec7b0e931..f93f712740 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -401,7 +401,7 @@ $.extend(frappe.model, { } }, - set_value: function(doctype, docname, fieldname, value, fieldtype) { + set_value: function(doctype, docname, fieldname, value, fieldtype, avoid_dirty=false) { /* help: Set a value locally (if changed) and execute triggers */ var doc; @@ -427,7 +427,7 @@ $.extend(frappe.model, { } doc[key] = value; - tasks.push(() => frappe.model.trigger(key, value, doc)); + if (!avoid_dirty) tasks.push(() => frappe.model.trigger(key, value, doc)); } else { // execute link triggers (want to reselect to execute triggers) if(in_list(["Link", "Dynamic Link"], fieldtype) && doc) { From cf024adbb210dde634df4b5e372af042a509b72f Mon Sep 17 00:00:00 2001 From: Bhavesh Maheshwari <34086262+bhavesh95863@users.noreply.github.com> Date: Mon, 21 Feb 2022 13:27:04 +0530 Subject: [PATCH 002/139] fix: ignore link validate for sort workspace --- frappe/desk/doctype/workspace/workspace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index b40f517350..a4357c7b87 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -277,6 +277,7 @@ def sort_page(workspace_pages, pages): doc = frappe.get_doc('Workspace', page.name) doc.sequence_id = seq + 1 doc.parent_page = d.get('parent_page') or "" + doc.flags.ignore_links = True doc.save(ignore_permissions=True) break From 97387b3dbc1a15cbfe8d6c07a14b5299e2f63055 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 22 Feb 2022 13:06:21 +0530 Subject: [PATCH 003/139] feat: grid search --- frappe/public/js/frappe/form/grid.js | 129 +++++++++++++++++--- frappe/public/js/frappe/form/grid_row.js | 144 +++++++++++++++++++++-- frappe/public/js/frappe/utils/utils.js | 12 +- frappe/public/scss/common/grid.scss | 24 ++++ 4 files changed, 285 insertions(+), 24 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 8b615f3c59..e806e46ebc 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -35,7 +35,7 @@ export default class Grid { && this.frm.meta.__form_grid_templates[this.df.fieldname]) { this.template = this.frm.meta.__form_grid_templates[this.df.fieldname]; } - + this.filter = {}; this.is_grid = true; this.debounced_refresh = this.refresh.bind(this); this.debounced_refresh = frappe.utils.debounce(this.debounced_refresh, 100); @@ -274,6 +274,8 @@ export default class Grid { } make_head() { + if (this.prevent_build) return; + // labels if (this.header_row) { $(this.parent).find(".grid-heading-row .grid-row").remove(); @@ -286,12 +288,42 @@ export default class Grid { grid: this, configure_columns: true }); + + this.header_search = new GridRow({ + parent: $(this.parent).find(".grid-heading-row"), + parent_df: this.df, + docfields: this.docfields, + frm: this.frm, + grid: this, + show_search: true + }); + + Object.keys(this.filter).length !== 0 && + this.update_search_columns(); } - refresh(force) { + update_search_columns() { + for (const field in this.filter) { + if (this.filter[field] && !this.header_search.search_columns[field]) { + delete this.filter[field]; + this.data = this.get_data(Object.keys(this.filter).length !== 0); + break; + } + + if (this.filter[field] && this.filter[field].value) { + let $input = this.header_search.row_index.find('input'); + if (field && field !== 'row-index') { + $input = this.header_search.search_columns[field].find('input'); + } + $input.val(this.filter[field].value); + } + }; + } + + refresh() { if (this.frm && this.frm.setting_dependency) return; - this.data = this.get_data(); + this.data = this.get_data(Object.keys(this.filter).length !== 0); !this.wrapper && this.make(); let $rows = $(this.parent).find('.rows'); @@ -453,7 +485,7 @@ export default class Grid { } make_sortable($rows) { - new Sortable($rows.get(0), { + this.grid_sortable = new Sortable($rows.get(0), { group: { name: this.df.fieldname }, handle: '.sortable-handle', draggable: '.grid-row', @@ -484,14 +516,74 @@ export default class Grid { $(this.frm.wrapper).trigger("grid-make-sortable", [this.frm]); } - get_data() { - var data = this.frm ? - this.frm.doc[this.df.fieldname] || [] - : this.df.data || this.get_modal_data(); - // data.sort(function(a, b) { return a.idx - b.idx}); + get_data(filter_field) { + let data = []; + if (filter_field) { + data = this.get_filtered_data(); + } else { + data = this.frm ? + this.frm.doc[this.df.fieldname] || [] + : this.df.data || this.get_modal_data(); + } return data; } + get_filtered_data() { + if (!this.frm) return; + + let all_data = this.frm.doc[this.df.fieldname]; + + for (const field in this.filter) { + all_data = all_data.filter(data => { + let {df, value} = this.filter[field]; + + if (["Check"].includes(df.fieldtype)) { + return (data[df.fieldname] === parseInt(value || 0)) && data; + } else if (df.fieldtype === "Sr No" && data.idx.toString().indexOf(value) > -1) { + return data; + } else if (["Currency", "Float", "Int", "Percent", "Rating"].includes(df.fieldtype)) { + let num = data[df.fieldname] || 0; + + if (df.fieldtype === "Rating") { + let out_of_rating = parseInt(df.options) || 5; + num = data[df.fieldname] * out_of_rating; + } + + if (num.toString().indexOf(value) > -1) { + return data; + } + } else if (["Datetime", "Date"].includes(df.fieldtype) && data[df.fieldname]) { + let user_formatted_date = frappe.datetime.str_to_user(data[df.fieldname]); + + if (user_formatted_date.includes(value)) { + return data; + } + } else if (df.fieldtype === "Duration" && data[df.fieldname]) { + let formatted_duration = frappe.utils.get_formatted_duration(data[df.fieldname]); + + if (formatted_duration.includes(value.toLowerCase())) { + return data; + } + } else if (df.fieldtype === "Barcode" && data[df.fieldname]) { + let svg = data[df.fieldname]; + + if (svg.startsWith(' { if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) { @@ -701,7 +793,7 @@ export default class Grid { if (this.visible_columns && this.visible_columns.length > 0) return; this.user_defined_columns = []; - this.setup_user_defined_columns(); + this.setup_user_settings(); var total_colsize = 1, fields = (this.user_defined_columns && this.user_defined_columns.length > 0) ? this.user_defined_columns : this.editable_fields || this.docfields; @@ -775,12 +867,16 @@ export default class Grid { df.colsize = colsize; } - setup_user_defined_columns() { - if (this.frm) { - let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); - if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) { - this.user_defined_columns = user_settings[this.doctype].map(row => { + setup_user_settings() { + if (!this.frm) return; + + let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); + + if (user_settings && user_settings[this.doctype] && user_settings[this.doctype]) { + if (user_settings[this.doctype]['columns'] && user_settings[this.doctype]['columns'].length) { + this.user_defined_columns = user_settings[this.doctype]['columns'].map(row => { let column = frappe.meta.get_docfield(this.doctype, row.fieldname); + if (column) { column.in_list_view = 1; column.columns = row.columns; @@ -788,6 +884,9 @@ export default class Grid { } }); } + + this.show_search = this.frm.doc[this.df.fieldname].length >= + (user_settings[this.doctype]['enable_search_count'] || 15); } } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index a40f428969..97263d2529 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -12,7 +12,7 @@ export default class GridRow { } this.columns = {}; this.columns_list = []; - this.row_check_html = ''; + this.row_check_html = ''; this.make(); } make() { @@ -192,23 +192,67 @@ export default class GridRow { this.set_row_index(); // index (1, 2, 3 etc) - if(!this.row_index) { + if(!this.row_index && !this.show_search) { // REDESIGN-TODO: Make translation contextual, this No is Number var txt = (this.doc ? this.doc.idx : __("No.")); - this.row_index = $( - `
+ + this.row_check = $( + `
${this.row_check_html} -
`) +
`) + .appendTo(this.row); + + this.row_index = $( + ``) .appendTo(this.row) .on('click', function(e) { if(!$(e.target).hasClass('grid-row-check')) { me.toggle_view(); } }); + } else if (this.show_search) { + let timer = null; + this.row_check = $( + `` + ).appendTo(this.row); + + this.row_index = $( + `` + ).appendTo(this.row); + + this.row_index.find('input').on('keyup', (e) => { + clearTimeout(timer); + timer = setTimeout(() => { + let df = { + fieldtype: "Sr No" + }; + + this.grid.filter['row-index'] = { + df: df, + value: e.target.value + } + + if(e.target.value == "") { + delete this.grid.filter['row-index']; + } + + this.grid.grid_sortable + .option('disabled', Object.keys(this.grid.filter).length !== 0); + + this.grid.prevent_build = true; + me.grid.refresh(); + this.grid.prevent_build = false; + }, 500); + }); + frappe.utils.only_allow_num_decimal(this.row_index.find('input')); } else { this.row_index.find('span').html(txt); } - + this.show_search && this.show_search_columns(); this.setup_columns(); this.add_open_form_button(); this.add_column_configure_button(); @@ -266,14 +310,26 @@ export default class GridRow { } configure_dialog_for_columns_selector() { + let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); + let enable_search_count = user_settings[this.grid.doctype] && + user_settings[this.grid.doctype]["enable_search_count"] || 15; + this.grid_settings_dialog = new frappe.ui.Dialog({ title: __("Configure Columns"), fields: [{ 'fieldtype': 'HTML', 'fieldname': 'fields_html' + }, + { + 'label': 'Enable Grid Search Count', + 'fieldtype': 'Data', + 'fieldname': 'enable_search', + 'default': enable_search_count, + 'description': __("Enable grid search if the grid row's are greater than or equal to the entered number") }] }); + this.enable_search_count = this.grid_settings_dialog.fields_dict.enable_search; this.grid.setup_visible_columns(); this.setup_columns_for_dialog(); this.prepare_wrapper_for_columns(); @@ -512,7 +568,10 @@ export default class GridRow { } let value = {}; - value[this.grid.doctype] = this.selected_columns_for_grid; + value[this.grid.doctype] = {}; + value[this.grid.doctype]['columns'] = this.selected_columns_for_grid; + value[this.grid.doctype]['enable_search_count'] = this.enable_search_count.get_value(); + frappe.model.user_settings.save(this.frm.doctype, 'GridView', value) .then((r) => { frappe.model.user_settings[this.frm.doctype] = r.message || r; @@ -530,6 +589,7 @@ export default class GridRow { setup_columns() { this.focus_set = false; + this.search_columns = {}; this.grid.setup_visible_columns(); this.grid.visible_columns.forEach((col, ci) => { @@ -545,8 +605,10 @@ export default class GridRow { txt = __(txt); } let column; - if (!this.columns[df.fieldname]) { + if (!this.columns[df.fieldname] && !this.show_search) { column = this.make_column(df, colsize, txt, ci); + } else if (!this.columns[df.fieldname] && this.show_search) { + column = this.make_search_column(df, colsize); } else { column = this.columns[df.fieldname]; this.refresh_field(df.fieldname, txt); @@ -564,6 +626,72 @@ export default class GridRow { } } }); + + if (this.show_search) { + // last empty column + $(`
`) + .appendTo(this.row) + } + } + + show_search_columns() { + // show or remove search columns based on Grid Search Count + this.grid.setup_user_settings(); + !this.grid.show_search && this.wrapper.remove(); + } + + make_search_column(df, colsize) { + let timer = null; + let title = ""; + let input_class = ""; + let is_disabled = ""; + + if (["Text", "Small Text"].includes(df.fieldtype)) { + input_class = "grid-overflow-no-ellipsis"; + } else if (["Int", "Currency", "Float", "Percent"].includes(df.fieldtype)) { + input_class = "text-right"; + } else if (df.fieldtype === "Check") { + title = __("1 = True & 0 = False"); + input_class = "text-center"; + } else if (df.fieldtype === 'Password') { + is_disabled = 'disabled' + title = __('Password cannot be filtered') + } + + let $col = $('') + .appendTo(this.row); + + let $search_input = $(` + + `).appendTo($col); + + this.search_columns[df.fieldname] = $col; + + $search_input.on('keyup', (e) => { + clearTimeout(timer); + timer = setTimeout(() => { + this.grid.filter[df.fieldname] = { + df: df, + value: e.target.value + } + + if(e.target.value == '') { + delete this.grid.filter[df.fieldname]; + } + + this.grid.grid_sortable + .option('disabled', Object.keys(this.grid.filter).length !== 0); + + this.grid.prevent_build = true; + this.grid.refresh(); + this.grid.prevent_build = false; + }, 500); + }); + + ["Currency", "Float", "Int", "Percent", "Rating", "Check"].includes(df.fieldtype) && + frappe.utils.only_allow_num_decimal($search_input); + + return $col; } make_column(df, colsize, txt, ci) { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index dc75239ed5..ae63f79e82 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1093,7 +1093,7 @@ Object.assign(frappe.utils, { seconds: round(seconds % 60) }; - if (duration_options.hide_days) { + if (duration_options && duration_options.hide_days) { total_duration.hours = round(seconds / 3600); total_duration.days = 0; } @@ -1453,5 +1453,15 @@ Object.assign(frappe.utils, { console.log(error); // eslint-disable-line return Promise.resolve(name); } + }, + + only_allow_num_decimal(input) { + input.on('input', (e) => { + let self = $(e.target); + self.val(self.val().replace(/[^0-9\.]/g, '')); + if ((e.which != 46 || self.val().indexOf('.') != -1) && (e.which < 48 || e.which > 57)) { + e.preventDefault(); + } + }); } }); diff --git a/frappe/public/scss/common/grid.scss b/frappe/public/scss/common/grid.scss index 1903413fbb..d5c9ae8d6b 100644 --- a/frappe/public/scss/common/grid.scss +++ b/frappe/public/scss/common/grid.scss @@ -82,6 +82,29 @@ height: 34px; padding: 8px; max-height: 200px; + + &.search { + padding: 7px !important; + + input { + height: -webkit-fill-available; + padding: 3px 7px; + } + } +} + +.row-check { + height: 34px; + padding: 8px 3px !important; + text-align: center; + + input { + margin-right: 0 !important; + } + + &.search { + padding: 0 !important; + } } .grid-row-check { @@ -409,6 +432,7 @@ } .page-number { + background-color: var(--fg-color); padding: 0 3px; } From cf4f35e8deb7a5e576e2b45ae86482ee46a960de Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 22 Feb 2022 15:51:54 +0530 Subject: [PATCH 004/139] fix(sider): missing semicolons --- frappe/public/js/frappe/form/grid.js | 2 +- frappe/public/js/frappe/form/grid_row.js | 16 ++++++++-------- frappe/public/js/frappe/utils/utils.js | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index e806e46ebc..adbaa5bcad 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -317,7 +317,7 @@ export default class Grid { } $input.val(this.filter[field].value); } - }; + } } refresh() { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 97263d2529..789114572b 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -192,7 +192,7 @@ export default class GridRow { this.set_row_index(); // index (1, 2, 3 etc) - if(!this.row_index && !this.show_search) { + if (!this.row_index && !this.show_search) { // REDESIGN-TODO: Make translation contextual, this No is Number var txt = (this.doc ? this.doc.idx : __("No.")); @@ -234,9 +234,9 @@ export default class GridRow { this.grid.filter['row-index'] = { df: df, value: e.target.value - } + }; - if(e.target.value == "") { + if (e.target.value == "") { delete this.grid.filter['row-index']; } @@ -630,7 +630,7 @@ export default class GridRow { if (this.show_search) { // last empty column $(`
`) - .appendTo(this.row) + .appendTo(this.row); } } @@ -654,8 +654,8 @@ export default class GridRow { title = __("1 = True & 0 = False"); input_class = "text-center"; } else if (df.fieldtype === 'Password') { - is_disabled = 'disabled' - title = __('Password cannot be filtered') + is_disabled = 'disabled'; + title = __('Password cannot be filtered'); } let $col = $('') @@ -673,9 +673,9 @@ export default class GridRow { this.grid.filter[df.fieldname] = { df: df, value: e.target.value - } + }; - if(e.target.value == '') { + if (e.target.value == '') { delete this.grid.filter[df.fieldname]; } diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index ae63f79e82..759b7b5499 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1458,7 +1458,7 @@ Object.assign(frappe.utils, { only_allow_num_decimal(input) { input.on('input', (e) => { let self = $(e.target); - self.val(self.val().replace(/[^0-9\.]/g, '')); + self.val(self.val().replace(/[^0-9.]/g, '')); if ((e.which != 46 || self.val().indexOf('.') != -1) && (e.which < 48 || e.which > 57)) { e.preventDefault(); } From 058d89312b63cc39535eded7f638e2d061e68b01 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 24 Feb 2022 12:15:02 +0530 Subject: [PATCH 005/139] revert: search row visiblitity is customizable (made it hard coded) --- frappe/public/js/frappe/form/grid.js | 18 +++++-------- frappe/public/js/frappe/form/grid_row.js | 34 ++++++++---------------- 2 files changed, 17 insertions(+), 35 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index adbaa5bcad..61a5309b39 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -793,7 +793,7 @@ export default class Grid { if (this.visible_columns && this.visible_columns.length > 0) return; this.user_defined_columns = []; - this.setup_user_settings(); + this.setup_user_defined_columns(); var total_colsize = 1, fields = (this.user_defined_columns && this.user_defined_columns.length > 0) ? this.user_defined_columns : this.editable_fields || this.docfields; @@ -867,14 +867,11 @@ export default class Grid { df.colsize = colsize; } - setup_user_settings() { - if (!this.frm) return; - - let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); - - if (user_settings && user_settings[this.doctype] && user_settings[this.doctype]) { - if (user_settings[this.doctype]['columns'] && user_settings[this.doctype]['columns'].length) { - this.user_defined_columns = user_settings[this.doctype]['columns'].map(row => { + setup_user_defined_columns() { + if (this.frm) { + let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); + if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) { + this.user_defined_columns = user_settings[this.doctype].map(row => { let column = frappe.meta.get_docfield(this.doctype, row.fieldname); if (column) { @@ -884,9 +881,6 @@ export default class Grid { } }); } - - this.show_search = this.frm.doc[this.df.fieldname].length >= - (user_settings[this.doctype]['enable_search_count'] || 15); } } diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 789114572b..554af0c436 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -188,7 +188,9 @@ export default class GridRow { })); } render_row(refresh) { - var me = this; + if (this.show_search && !this.show_search_row()) return; + + let me = this; this.set_row_index(); // index (1, 2, 3 etc) @@ -252,7 +254,7 @@ export default class GridRow { } else { this.row_index.find('span').html(txt); } - this.show_search && this.show_search_columns(); + this.setup_columns(); this.add_open_form_button(); this.add_column_configure_button(); @@ -310,26 +312,14 @@ export default class GridRow { } configure_dialog_for_columns_selector() { - let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); - let enable_search_count = user_settings[this.grid.doctype] && - user_settings[this.grid.doctype]["enable_search_count"] || 15; - this.grid_settings_dialog = new frappe.ui.Dialog({ title: __("Configure Columns"), fields: [{ 'fieldtype': 'HTML', 'fieldname': 'fields_html' - }, - { - 'label': 'Enable Grid Search Count', - 'fieldtype': 'Data', - 'fieldname': 'enable_search', - 'default': enable_search_count, - 'description': __("Enable grid search if the grid row's are greater than or equal to the entered number") }] }); - this.enable_search_count = this.grid_settings_dialog.fields_dict.enable_search; this.grid.setup_visible_columns(); this.setup_columns_for_dialog(); this.prepare_wrapper_for_columns(); @@ -568,10 +558,7 @@ export default class GridRow { } let value = {}; - value[this.grid.doctype] = {}; - value[this.grid.doctype]['columns'] = this.selected_columns_for_grid; - value[this.grid.doctype]['enable_search_count'] = this.enable_search_count.get_value(); - + value[this.grid.doctype] = this.selected_columns_for_grid; frappe.model.user_settings.save(this.frm.doctype, 'GridView', value) .then((r) => { frappe.model.user_settings[this.frm.doctype] = r.message || r; @@ -634,10 +621,11 @@ export default class GridRow { } } - show_search_columns() { - // show or remove search columns based on Grid Search Count - this.grid.setup_user_settings(); - !this.grid.show_search && this.wrapper.remove(); + show_search_row() { + // show or remove search columns based on grid rows + this.show_search = this.frm.doc[this.grid.df.fieldname].length >= 15; + !this.show_search && this.wrapper.remove(); + return this.show_search; } make_search_column(df, colsize) { @@ -668,7 +656,7 @@ export default class GridRow { this.search_columns[df.fieldname] = $col; $search_input.on('keyup', (e) => { - clearTimeout(timer); + clearTimeout(timer); timer = setTimeout(() => { this.grid.filter[df.fieldname] = { df: df, From 3dda9aeb8354ac81e6c89f1c5066da794ee8c402 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 24 Feb 2022 15:43:43 +0530 Subject: [PATCH 006/139] chore: created function for code readability --- frappe/public/js/frappe/form/grid.js | 114 ++++++++++++++------------- 1 file changed, 60 insertions(+), 54 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 61a5309b39..0a6211767e 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -536,54 +536,60 @@ export default class Grid { for (const field in this.filter) { all_data = all_data.filter(data => { let {df, value} = this.filter[field]; - - if (["Check"].includes(df.fieldtype)) { - return (data[df.fieldname] === parseInt(value || 0)) && data; - } else if (df.fieldtype === "Sr No" && data.idx.toString().indexOf(value) > -1) { - return data; - } else if (["Currency", "Float", "Int", "Percent", "Rating"].includes(df.fieldtype)) { - let num = data[df.fieldname] || 0; - - if (df.fieldtype === "Rating") { - let out_of_rating = parseInt(df.options) || 5; - num = data[df.fieldname] * out_of_rating; - } - - if (num.toString().indexOf(value) > -1) { - return data; - } - } else if (["Datetime", "Date"].includes(df.fieldtype) && data[df.fieldname]) { - let user_formatted_date = frappe.datetime.str_to_user(data[df.fieldname]); - - if (user_formatted_date.includes(value)) { - return data; - } - } else if (df.fieldtype === "Duration" && data[df.fieldname]) { - let formatted_duration = frappe.utils.get_formatted_duration(data[df.fieldname]); - - if (formatted_duration.includes(value.toLowerCase())) { - return data; - } - } else if (df.fieldtype === "Barcode" && data[df.fieldname]) { - let svg = data[df.fieldname]; - - if (svg.startsWith(' -1) { + return data; + } + } else if (fieldvalue && fieldvalue.toLowerCase().includes(value)) { + return data; + } + } + get_modal_data() { return this.df.get_data ? this.df.get_data().filter(data => { if (!this.deleted_docs || !in_list(this.deleted_docs, data.name)) { @@ -868,19 +874,19 @@ export default class Grid { } setup_user_defined_columns() { - if (this.frm) { - let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); - if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) { - this.user_defined_columns = user_settings[this.doctype].map(row => { - let column = frappe.meta.get_docfield(this.doctype, row.fieldname); + if (!this.frm) return; - if (column) { - column.in_list_view = 1; - column.columns = row.columns; - return column; - } - }); - } + let user_settings = frappe.get_user_settings(this.frm.doctype, 'GridView'); + if (user_settings && user_settings[this.doctype] && user_settings[this.doctype].length) { + this.user_defined_columns = user_settings[this.doctype].map(row => { + let column = frappe.meta.get_docfield(this.doctype, row.fieldname); + + if (column) { + column.in_list_view = 1; + column.columns = row.columns; + return column; + } + }); } } From e1ada33cc56b0243c7266580e0573a75a3e306f7 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 24 Feb 2022 16:40:26 +0530 Subject: [PATCH 007/139] fix: show grid search row if rows are >= 20 --- frappe/public/js/frappe/form/grid_row.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 9b270fb8b0..e40fc3ba64 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -640,7 +640,7 @@ export default class GridRow { show_search_row() { // show or remove search columns based on grid rows - this.show_search = this.frm.doc[this.grid.df.fieldname].length >= 15; + this.show_search = this.frm.doc[this.grid.df.fieldname].length >= 20; !this.show_search && this.wrapper.remove(); return this.show_search; } From d800495810f813462172559a232a8ff37c9c8ddc Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 24 Feb 2022 19:36:59 +0530 Subject: [PATCH 008/139] fix: added fieldtype attribute on search field --- frappe/public/js/frappe/form/grid_row.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index e40fc3ba64..509875d8eb 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -667,7 +667,13 @@ export default class GridRow { .appendTo(this.row); let $search_input = $(` - + `).appendTo($col); this.search_columns[df.fieldname] = $col; From 1e68cca66330c9ace32102382c42f04a97d985de Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 24 Feb 2022 19:37:52 +0530 Subject: [PATCH 009/139] fix: allow both svg and text in barcode --- frappe/public/js/frappe/form/grid.js | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 385bd15cdf..99d9bbe0fc 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -559,14 +559,11 @@ export default class Grid { return data; } } else if (fieldtype === "Barcode" && fieldvalue) { - let svg = fieldvalue; + let barcode = fieldvalue.startsWith(' Date: Thu, 24 Feb 2022 19:44:19 +0530 Subject: [PATCH 010/139] test: UI test for grid search --- cypress/fixtures/child_table_doctype_1.js | 59 +++++++++++ cypress/fixtures/doctype_with_child_table.js | 6 ++ cypress/integration/grid_search.js | 104 +++++++++++++++++++ frappe/tests/ui_test_helpers.py | 45 +++++++- 4 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 cypress/fixtures/child_table_doctype_1.js create mode 100644 cypress/integration/grid_search.js diff --git a/cypress/fixtures/child_table_doctype_1.js b/cypress/fixtures/child_table_doctype_1.js new file mode 100644 index 0000000000..4657d63e2e --- /dev/null +++ b/cypress/fixtures/child_table_doctype_1.js @@ -0,0 +1,59 @@ +export default { + name: "Child Table Doctype 1", + actions: [], + custom: 1, + autoname: "format: Test-{####}", + creation: "2022-02-09 20:15:21.242213", + doctype: "DocType", + editable_grid: 1, + engine: "InnoDB", + fields: [ + { + fieldname: "data", + fieldtype: "Data", + in_list_view: 1, + label: "Data" + }, + { + fieldname: "barcode", + fieldtype: "Barcode", + in_list_view: 1, + label: "Barcode" + }, + { + fieldname: "check", + fieldtype: "Check", + in_list_view: 1, + label: "Check" + }, + { + fieldname: "rating", + fieldtype: "Rating", + in_list_view: 1, + label: "Rating" + }, + { + fieldname: "duration", + fieldtype: "Duration", + in_list_view: 1, + label: "Duration" + }, + { + fieldname: "date", + fieldtype: "Date", + in_list_view: 1, + label: "Date" + } + ], + links: [], + istable: 1, + modified: "2022-02-10 12:03:12.603763", + modified_by: "Administrator", + module: "Custom", + naming_rule: "By fieldname", + owner: "Administrator", + permissions: [], + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; \ No newline at end of file diff --git a/cypress/fixtures/doctype_with_child_table.js b/cypress/fixtures/doctype_with_child_table.js index bbb2127448..014074b0b5 100644 --- a/cypress/fixtures/doctype_with_child_table.js +++ b/cypress/fixtures/doctype_with_child_table.js @@ -20,6 +20,12 @@ export default { label: "Child Table", options: "Child Table Doctype", reqd: 1 + }, + { + fieldname: "child_table_1", + fieldtype: "Table", + label: "Child Table 1", + options: "Child Table Doctype 1" } ], links: [], diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js new file mode 100644 index 0000000000..6ccf3e37f0 --- /dev/null +++ b/cypress/integration/grid_search.js @@ -0,0 +1,104 @@ +import doctype_with_child_table from '../fixtures/doctype_with_child_table'; +import child_table_doctype from '../fixtures/child_table_doctype'; +import child_table_doctype_1 from '../fixtures/child_table_doctype_1'; +const doctype_with_child_table_name = doctype_with_child_table.name; + +context('Grid Search', () => { + before(() => { + cy.visit('/login'); + cy.login(); + cy.insert_doc('DocType', child_table_doctype, true); + cy.insert_doc('DocType', child_table_doctype_1, true); + cy.insert_doc('DocType', doctype_with_child_table, true); + return cy.window().its('frappe').then(frappe => { + frappe.model.user_settings.save('Doctype With Child Table', 'GridView', { + 'Child Table Doctype 1': [ + {'fieldname': 'data', 'columns': 2}, + {'fieldname': 'barcode', 'columns': 1}, + {'fieldname': 'check', 'columns': 1}, + {'fieldname': 'rating', 'columns': 2}, + {'fieldname': 'duration', 'columns': 2}, + {'fieldname': 'date', 'columns': 2} + ] + }); + + return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", { + name: doctype_with_child_table_name + }); + }); + }); + + it('Test search row visibility', () => { + cy.visit(`/app/doctype-with-child-table/Test Grid Search`); + + cy.get('[title="child_table_1"]').as('table'); + cy.get('@table').find('.grid-row-check:last').click(); + cy.get('@table').find('.grid-footer').contains('Delete').click(); + cy.get('.grid-heading-row .grid-row .search').should('not.exist'); + }); + + it('test search field for different fieldtypes', () => { + cy.visit(`/app/doctype-with-child-table/Test Grid Search`); + + cy.get('[title="child_table_1"]').as('table'); + + // Index Column + cy.get('@table').find('.grid-heading-row .row-index.search input').type('3'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); + cy.get('@table').find('.grid-heading-row .row-index.search input').clear(); + + // Data Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('Data'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 1); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').clear(); + + // Barcode Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('092'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').clear(); + + // Check Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('1'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 9); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); + + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').type('0'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 11); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Check"]').clear(); + + // Rating Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').type('3'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Rating"]').clear(); + + // Duration Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('3d'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 3); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').clear(); + + // Date Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('2022'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 4); + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').clear(); + }); + + it('test with multiple filter', () => { + cy.get('[title="child_table_1"]').as('table'); + + // Data Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Data"]').type('a'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 10); + + // Barcode Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Barcode"]').type('0'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 8); + + // Duration Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Duration"]').type('d'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 5); + + // Date Column + cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('-02'); + cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); + }) +}); \ No newline at end of file diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 26c20f3d18..ca41615ca1 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -268,4 +268,47 @@ def update_child_table(name): 'options': 'Doctype to Link' }) - doc.save() \ No newline at end of file + doc.save() + + +@frappe.whitelist() +def insert_doctype_with_child_table_record(name): + if frappe.db.get_all(name, {'title': 'Test Grid Search'}): + return + + def insert_child(doc, data, barcode, check, rating, duration, date): + doc.append('child_table_1', { + 'data': data, + 'barcode': barcode, + 'check': check, + 'rating': rating, + 'duration': duration, + 'date': date, + }) + + doc = frappe.new_doc(name) + doc.title = 'Test Grid Search' + doc.append('child_table', {'title': 'Test Grid Search'}) + + insert_child(doc, 'Data', '09709KJKKH2432', 1, 0.5, 266851, "2022-02-21") + insert_child(doc, 'Test', '09209KJHKH2432', 1, 0.8, 547877, "2021-05-27") + insert_child(doc, 'New', '09709KJHYH1132', 0, 0.1, 3, "2019-03-02") + insert_child(doc, 'Old', '09701KJHKH8750', 0, 0, 127455, "2022-01-11") + insert_child(doc, 'Alpha', '09204KJHKH2432', 0, 0.6, 364, "2019-12-31") + insert_child(doc, 'Delta', '09709KSPIO2432', 1, 0.9, 1242000, "2020-04-21") + insert_child(doc, 'Update', '76989KJLVA2432', 0, 1, 183845, "2022-02-10") + insert_child(doc, 'Delete', '29189KLHVA1432', 0, 0, 365647, "2021-05-07") + insert_child(doc, 'Make', '09689KJHAA2431', 0, 0.3, 24, "2020-11-11") + insert_child(doc, 'Create', '09709KLKKH2432', 1, 0.3, 264851, "2021-02-21") + insert_child(doc, 'Group', '09209KJLKH2432', 1, 0.8, 537877, "2020-03-15") + insert_child(doc, 'Slide', '01909KJHYH1132', 0, 0.5, 9, "2018-03-02") + insert_child(doc, 'Drop', '09701KJHKH8750', 1, 0, 127255, "2018-01-01") + insert_child(doc, 'Beta', '09204QJHKN2432', 0, 0.6, 354, "2017-12-30") + insert_child(doc, 'Flag', '09709KXPIP2432', 1, 0, 1241000, "2021-04-21") + insert_child(doc, 'Upgrade', '75989ZJLVA2432', 0.8, 1, 183645, "2020-08-13") + insert_child(doc, 'Down', '28189KLHRA1432', 1, 0, 362647, "2020-06-17") + insert_child(doc, 'Note', '09689DJHAA2431', 0, 0.1, 29, "2021-09-11") + insert_child(doc, 'Click', '08189DJHAA2431', 1, 0.3, 209, "2020-07-04") + insert_child(doc, 'Drag', '08189DIHAA2981', 0, 0.7, 342628, "2022-05-04") + + doc.insert() From daa2b7921c2dfff5996e7607565e031d1888000e Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 24 Feb 2022 19:48:10 +0530 Subject: [PATCH 011/139] fix(sider): missing semicolon --- cypress/integration/grid_search.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js index 6ccf3e37f0..dfb153f67b 100644 --- a/cypress/integration/grid_search.js +++ b/cypress/integration/grid_search.js @@ -100,5 +100,5 @@ context('Grid Search', () => { // Date Column cy.get('@table').find('.grid-heading-row .search input[data-fieldtype="Date"]').type('-02'); cy.get('@table').find('.grid-body .rows .grid-row').should('have.length', 2); - }) + }); }); \ No newline at end of file From 839f488c9185fb7a963102abcc9d36b5c873d0be Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 25 Feb 2022 11:50:55 +0530 Subject: [PATCH 012/139] fix: failing grid search UI test --- cypress/integration/grid_search.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cypress/integration/grid_search.js b/cypress/integration/grid_search.js index dfb153f67b..10444b2d2a 100644 --- a/cypress/integration/grid_search.js +++ b/cypress/integration/grid_search.js @@ -7,10 +7,19 @@ context('Grid Search', () => { before(() => { cy.visit('/login'); cy.login(); + cy.visit('/app/website'); cy.insert_doc('DocType', child_table_doctype, true); cy.insert_doc('DocType', child_table_doctype_1, true); cy.insert_doc('DocType', doctype_with_child_table, true); return cy.window().its('frappe').then(frappe => { + return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", { + name: doctype_with_child_table_name + }); + }); + }); + + it('Test search row visibility', () => { + cy.window().its('frappe').then(frappe => { frappe.model.user_settings.save('Doctype With Child Table', 'GridView', { 'Child Table Doctype 1': [ {'fieldname': 'data', 'columns': 2}, @@ -21,14 +30,8 @@ context('Grid Search', () => { {'fieldname': 'date', 'columns': 2} ] }); - - return frappe.xcall("frappe.tests.ui_test_helpers.insert_doctype_with_child_table_record", { - name: doctype_with_child_table_name - }); }); - }); - it('Test search row visibility', () => { cy.visit(`/app/doctype-with-child-table/Test Grid Search`); cy.get('[title="child_table_1"]').as('table'); From 3768572500c938ad4ca5eb4f853609762ebb4f20 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Fri, 25 Feb 2022 17:14:27 +0530 Subject: [PATCH 013/139] fix: print for tree view is broken --- frappe/public/css/tree.css | 2 +- .../js/frappe/views/reports/print_tree.html | 185 ++++++++++-------- 2 files changed, 101 insertions(+), 86 deletions(-) diff --git a/frappe/public/css/tree.css b/frappe/public/css/tree.css index 2aa411bc11..8b216bc321 100644 --- a/frappe/public/css/tree.css +++ b/frappe/public/css/tree.css @@ -24,7 +24,7 @@ ul.tree-children { } .tree-link .node-parent, .tree-link .node-leaf { - margin-right: 5px; + margin-right: 8px; } .tree-link.active i { color: #5e64ff; diff --git a/frappe/public/js/frappe/views/reports/print_tree.html b/frappe/public/js/frappe/views/reports/print_tree.html index 9300c8df64..c0bc55599a 100644 --- a/frappe/public/js/frappe/views/reports/print_tree.html +++ b/frappe/public/js/frappe/views/reports/print_tree.html @@ -1,91 +1,106 @@ - - - - - - - {{ title }} - - - - - + - - - ` ).appendTo(this.row); - this.row_index.find('input').on('keyup', (e) => { - clearTimeout(timer); - timer = setTimeout(() => { - let df = { - fieldtype: "Sr No" - }; + this.row_index.find('input').on('keyup', frappe.utils.debounce((e) => { + let df = { + fieldtype: "Sr No" + }; - this.grid.filter['row-index'] = { - df: df, - value: e.target.value - }; + this.grid.filter['row-index'] = { + df: df, + value: e.target.value + }; - if (e.target.value == "") { - delete this.grid.filter['row-index']; - } + if (e.target.value == "") { + delete this.grid.filter['row-index']; + } - this.grid.grid_sortable - .option('disabled', Object.keys(this.grid.filter).length !== 0); + this.grid.grid_sortable + .option('disabled', Object.keys(this.grid.filter).length !== 0); - this.grid.prevent_build = true; - me.grid.refresh(); - this.grid.prevent_build = false; - }, 500); - }); + this.grid.prevent_build = true; + me.grid.refresh(); + this.grid.prevent_build = false; + }, 500)); frappe.utils.only_allow_num_decimal(this.row_index.find('input')); } else { this.row_index.find('span').html(txt); @@ -647,7 +643,6 @@ export default class GridRow { } make_search_column(df, colsize) { - let timer = null; let title = ""; let input_class = ""; let is_disabled = ""; @@ -679,28 +674,25 @@ export default class GridRow { this.search_columns[df.fieldname] = $col; - $search_input.on('keyup', (e) => { - clearTimeout(timer); - timer = setTimeout(() => { - this.grid.filter[df.fieldname] = { - df: df, - value: e.target.value - }; + $search_input.on('keyup', frappe.utils.debounce((e) => { + this.grid.filter[df.fieldname] = { + df: df, + value: e.target.value + }; - if (e.target.value == '') { - delete this.grid.filter[df.fieldname]; - } + if (e.target.value == '') { + delete this.grid.filter[df.fieldname]; + } - this.grid.grid_sortable - .option('disabled', Object.keys(this.grid.filter).length !== 0); + this.grid.grid_sortable + .option('disabled', Object.keys(this.grid.filter).length !== 0); - this.grid.prevent_build = true; - this.grid.refresh(); - this.grid.prevent_build = false; - }, 500); - }); + this.grid.prevent_build = true; + this.grid.refresh(); + this.grid.prevent_build = false; + }, 500)); - ["Currency", "Float", "Int", "Percent", "Rating", "Check"].includes(df.fieldtype) && + ["Currency", "Float", "Int", "Percent", "Rating"].includes(df.fieldtype) && frappe.utils.only_allow_num_decimal($search_input); return $col; From 5a1bc4b1d61db755b2196da98bd9cea7144a198e Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 15 Mar 2022 17:23:46 +0530 Subject: [PATCH 090/139] fix: use boolean strings (T, F, true, false, 1, 0 etc) for Check fieldtype --- frappe/public/js/frappe/form/grid.js | 3 ++- frappe/public/js/frappe/utils/utils.js | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index 36461fb671..be20676183 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -549,7 +549,8 @@ export default class Grid { let fieldvalue = data[fieldname]; if (fieldtype === "Check") { - return (fieldvalue === parseInt(value || 0)) && data; + value = frappe.utils.string_to_boolean(value); + return (Boolean(fieldvalue) === value) && data; } else if (fieldtype === "Sr No" && data.idx.toString().includes(value)) { return data; } else if (fieldtype === "Duration" && fieldvalue) { diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index 339917ed77..b253f4da54 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1463,5 +1463,13 @@ Object.assign(frappe.utils, { e.preventDefault(); } }); + }, + + string_to_boolean(string) { + switch(string.toLowerCase().trim()){ + case "t": case "true": case "y": case "yes": case "1": return true; + case "f": case "false": case "n": case "no": case "0": case null: return false; + default: return string; + } } }); From 9268405d62aedc1846d4f9302a081b397b03566c Mon Sep 17 00:00:00 2001 From: shadrak gurupnor Date: Tue, 15 Mar 2022 17:36:33 +0530 Subject: [PATCH 091/139] feat: added redirect for support portal --- frappe/patches.txt | 2 +- frappe/utils/install.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/patches.txt b/frappe/patches.txt index a666480c90..82b1f497c2 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -146,7 +146,7 @@ frappe.patches.v13_0.update_duration_options frappe.patches.v13_0.replace_old_data_import # 2020-06-24 frappe.patches.v13_0.create_custom_dashboards_cards_and_charts frappe.patches.v13_0.rename_is_custom_field_in_dashboard_chart -frappe.patches.v13_0.add_standard_navbar_items # 2020-12-15 +frappe.patches.v13_0.add_standard_navbar_items # 2022-03-15 frappe.patches.v13_0.generate_theme_files_in_public_folder frappe.patches.v13_0.increase_password_length frappe.patches.v12_0.fix_email_id_formatting diff --git a/frappe/utils/install.py b/frappe/utils/install.py index a5fd39994f..3af77b885f 100644 --- a/frappe/utils/install.py +++ b/frappe/utils/install.py @@ -255,6 +255,12 @@ def add_standard_navbar_items(): 'item_type': 'Action', 'action': 'frappe.ui.toolbar.show_shortcuts(event)', 'is_standard': 1 + }, + { + 'item_label': 'Frappe Support', + 'item_type': 'Route', + 'route': 'https://frappe.io/support', + 'is_standard': 1 } ] From cac1fd40d4d74508294896743603cd883235dbf2 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 15 Mar 2022 17:39:02 +0530 Subject: [PATCH 092/139] fix(sider): expected space(s) --- frappe/public/js/frappe/utils/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index b253f4da54..03f3204abb 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -1466,7 +1466,7 @@ Object.assign(frappe.utils, { }, string_to_boolean(string) { - switch(string.toLowerCase().trim()){ + switch (string.toLowerCase().trim()) { case "t": case "true": case "y": case "yes": case "1": return true; case "f": case "false": case "n": case "no": case "0": case null: return false; default: return string; From a560d3856263f77fd54666f9fd62600daac67a35 Mon Sep 17 00:00:00 2001 From: Samuel Danieli <23150094+scdanieli@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:00:08 +0100 Subject: [PATCH 093/139] feat: add German translations --- frappe/translations/de.csv | 102 ++++++++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 19 deletions(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index afd1b101d6..1f98d1889b 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -146,7 +146,7 @@ Monthly,Monatlich, More,Weiter, More Information,Mehr Informationen, More...,Mehr..., -Move,Bewegen, +Move,Verschieben, My Account,Mein Konto, My Profile,Mein Profil, My Settings,Meine Einstellungen, @@ -175,7 +175,7 @@ Payment Gateway,Zahlungs-Gateways, Payment Gateway Name,Name des Zahlungsgateways, Payments,Zahlungen, Period,Periode, -Pincode,Postleitzahl (PLZ), +Pincode,Postleitzahl, Plan Name,Planname, Please enable pop-ups,Bitte Pop-ups aktivieren, Please select Company,Bitte Unternehmen auswählen, @@ -1486,7 +1486,7 @@ Linked,Verknüpft, Linked With,Verknüpft mit, Linked with {0},Verknüpft mit {0}, Links,Verknüpfungen, -List,Listenansicht, +List,Liste, List Filter,Listenfilter, List View,Listenansicht, List View Setting,Einstellungen zu Listenansicht, @@ -2427,7 +2427,7 @@ Sum,Summe, Sum of {0},Summe von {0}, Support Email Address Not Specified,Support-E-Mail-Adresse nicht angegeben, Suspend Sending,Senden unterbrechen, -Switch To Desk,Switch To Desk, +Switch To Desk,Zum Desk wechseln, Symbol,Symbol, Sync,Synchronisieren, Sync on Migrate,Sync auf Migrate, @@ -2870,8 +2870,8 @@ bullhorn,Megafon, ca-central-1,ca-central-1, camera,Kamera, cancelled this document,brach die Arbeit an diesem Dokument ab, -changed value of {0},Wert von {0} geändert, -changed values for {0},Werte von {0} geändert, +changed value of {0},hat den Wert von {0} geändert, +changed values for {0},hat die Werte von {0} geändert, chevron-down,Winkel nach unten, chevron-left,Winkel nach links, chevron-right,Winkel nach rechts, @@ -3431,7 +3431,7 @@ Mandatory Depends On,Obligatorisch Hängt von ab, Map Columns,Spalten zuordnen, Map columns from {0} to fields in {1},Ordnen Sie Spalten von {0} Feldern in {1} zu., Mapping column {0} to field {1},Spalte {0} dem Feld {1} zuordnen, -Mark all as Read,Markiere alle als gelesen, +Mark all as Read,Alle als gelesen markieren, Maximum Points,Maximale Punkte, Maximum points allowed after multiplying points with the multiplier value\n(Note: For no limit leave this field empty or set 0),Maximal zulässige Punkte nach Multiplikation der Punkte mit dem Multiplikatorwert (Hinweis: Für unbegrenzte Anzahl lassen Sie dieses Feld leer oder setzen Sie 0), Me,Mir, @@ -3485,7 +3485,7 @@ Page Shortcuts,Seitenkürzel, Parent Field (Tree),Elternfeld (Baum), Parent Field must be a valid fieldname,Das übergeordnete Feld muss ein gültiger Feldname sein, Pin Globally,Global anheften, -Places,Setzt, +Places,Orte, Please check the filter values set for Dashboard Chart: {},Bitte überprüfen Sie die für das Dashboard-Diagramm festgelegten Filterwerte: {}, Please enable pop-ups in your browser,Bitte aktivieren Sie Popups in Ihrem Browser, Please find attached {0}: {1},Im Anhang finden Sie {0}: {1}, @@ -3541,7 +3541,7 @@ Select Filters,Wählen Sie Filter, Select Google Calendar to which event should be synced.,"Wählen Sie Google Kalender aus, mit dem das Ereignis synchronisiert werden soll.", Select Google Contacts to which contact should be synced.,"Wählen Sie Google-Kontakte aus, mit denen der Kontakt synchronisiert werden soll.", Select Group By...,Wählen Sie Gruppieren nach ..., -Select Mandatory,Wählen Pflicht, +Select Mandatory,Verpflichtende auswählen, Select atleast 2 actions,Wählen Sie mindestens 2 Aktionen aus, Select list item,Listenelement auswählen, Select multiple list items,Wählen Sie mehrere Listenelemente aus, @@ -3664,8 +3664,8 @@ You need to install pycups to use this feature!,"Sie müssen Pycups installieren Your Target,Dein Ziel, "browse,","Durchsuche,", cancelled this document {0},stornierte dieses Dokument {0}, -changed value of {0} {1},geänderter Wert von {0} {1}, -changed values for {0} {1},geänderte Werte für {0} {1}, +changed value of {0} {1},hat den Wert von {0} {1} geändert, +changed values for {0} {1},hat die Werte von {0} {1} geändert, choose an,wähle ein, empty,leeren, of,von, @@ -3789,14 +3789,14 @@ Reset,Zurücksetzen, Review,Rezension, Room,Zimmer, Room Type,Zimmertyp, -Save,speichern, +Save,Speichern, Search results for,Suchergebnisse für, Select All,Alles auswählen, Send,Absenden, Sending,Versand, Server Error,Serverfehler, Set,Menge, -Setup,Einstellungen, +Setup,Einrichtung, Setup Wizard,Setup-Assistent, Size,Größe, Sr,Pos, @@ -3819,7 +3819,7 @@ Warehouse,Lager, Welcome to {0},Willkommen auf {0}, Year,Jahr, Yearly,Jährlich, -You,Benutzer, +You,Sie, You can also copy-paste this link in your browser,Sie können diese Verknüpfung in Ihren Browser kopieren, and,und, {0} Name,{0} Name, @@ -3953,7 +3953,7 @@ lock,sperren, logged in,Angemeldet, message,Mitteilung, module,Modul, -move,Bewegung, +move,verschieben, music,Musik, new,Neu, now,jetzt, @@ -4135,9 +4135,9 @@ Using this console may allow attackers to impersonate you and steal your informa yesterday,gestern, {0} years ago,Vor {0} Jahren, New Chart,Neues Diagramm, -New Shortcut,Neue Verknüpfung, +New Shortcut,Neuer Schnellzugriff, Edit Chart,Diagramm bearbeiten, -Edit Shortcut,Verknüpfung bearbeiten, +Edit Shortcut,Schnellzugriff bearbeiten, Couldn't Load Desk,Schreibtisch konnte nicht geladen werden, "Something went wrong while loading Desk. Please relaod the page. If the problem persists, contact the Administrator","Beim Laden von Desk ist ein Fehler aufgetreten. Bitte überarbeiten Sie die Seite . Wenn das Problem weiterhin besteht, wenden Sie sich an den Administrator", Customize Workspace,Arbeitsbereich anpassen, @@ -4228,7 +4228,7 @@ since last month,seit letztem Monat, since last year,seit letztem Jahr, Show,Show, New Number Card,Neue Zahlenkarte, -Your Shortcuts,Ihre Verknüpfungen, +Your Shortcuts,Ihre Schnellzugriffe, You haven't added any Dashboard Charts or Number Cards yet.,Sie haben noch keine Dashboard-Diagramme oder Zahlenkarten hinzugefügt., Click On Customize to add your first widget,"Klicken Sie auf Anpassen, um Ihr erstes Widget hinzuzufügen", Are you sure you want to reset all customizations?,Möchten Sie wirklich alle Anpassungen zurücksetzen?, @@ -4650,7 +4650,7 @@ Not permitted to view {0},{0} darf nicht angezeigt werden, Camera,Kamera, Invalid filter: {0},Ungültiger Filter: {0}, Let's Get Started,Lass uns anfangen, -Reports & Masters,Berichte & Meister, +Reports & Masters,Berichte & Stammdaten, New {0} {1} added to Dashboard {2},Neues {0} {1} zum Dashboard hinzugefügt {2}, New {0} {1} created,Neue {0} {1} erstellt, New {0} Created,Neu {0} erstellt, @@ -4715,3 +4715,67 @@ Reset sorting,Sortierung zurücksetzen, Sort Ascending,Aufsteigend sortieren, Sort Descending,Absteigend sortieren, Remove column,Spalte entfernen, +Set all public,Alle als öffentlich setzen, +Set all private,Alle als privat setzen, +Library,Bibliothek, +My Device,Mein Gerät, +Drag and drop files here or upload from,Ziehen Sie Dateien hierher oder laden Sie sie von, +days,Tage, +seconds,Sekunden, +minutes,Minuten, +Copy,Kopieren, +{} Assigned,{} Zugewiesen, +Hide Saved,Gespeicherte ausblenden, +Show Saved,Gespeicherte anzeigen, +{0} created this {1},{0} erstellte dies {1}, +{0} edited this {1},{0} bearbeitete dies {1}, +Toggle Full Width,Toggle Volle Breite, +Documentation,Dokumentation, +About,Über, +Search or type a command (Ctrl + G),Suchen oder Befehl eingeben (Strg + G), +{} Pending,{} Ausstehend, +{} Available,{} Verfügbar, +{} Open,{} Offen, +Password set,Passwort gesetzt, +Your new password has been set successfully.,Ihr Passwort wurde erfolgreich aktualisiert., +You hit the rate limit because of too many requests. Please try after sometime.,Sie haben die maximale Anzahl an Anfragen erreicht. Bitte versuchen Sie es später noch einmal., +"You need {0} permission to fetch values from {1} {2}","Sie benötigen eine {0}-Berechtigung, um die Werte von {1} {2} abzurufen", +Cannot Fetch Values,Werte können nicht abgerufen werden, +You do not have Read or Select Permissions for {},Sie haben keine Lese- oder Auswahlberechtigung für {}, +Or,Oder, +{0} changed values for {1},{0} hat die Werte von {1} geändert, +{0} changed values for {1} {2},{0} hat die Werte von {1} {2} geändert, +{0} cancelled this document,{0} dieses Dokument storniert, +{0} cancelled this document {1},{0} dieses Dokument storniert {1}, +{0} submitted this document,{0} hat dieses Dokument eingereicht, +{0} submitted this document {1},{0} hat das Dokument {1} eingereicht, +Customizations Discarded,Anpassungen verworfen, +No filters selected,Keine Filter ausgewählt, +You haven't created a {0} yet,Sie haben noch kein(en) {0} erstellt, +No Data...,Keine Daten..., +Don't have an account?,Sie haben noch kein Benutzerkonto?, +{0} changed value of {1},{0} hat den Wert von {1} geändert, +Basic Info,Grundlegende Informationen, +No.,Nr.,number +No.,Nein.,opposite of yes +There are no upcoming events for you.,Es sind keine Termine für Sie geplant., +No Upcoming Events,Keine bevorstehenden Termine, +Looks like you haven’t received any notifications.,Sieht aus, als hätten Sie keine Benachrichtigungen erhalten., +No New notifications,Keine neuen Benachrichtigungen, +Overview,Übersicht, +Connections,Verknüpfungen, +Save Customizations,Anpassungen speichern, +Apply Filters,Filter anwenden, +Add a Filter,Filter hinzufügen, +Reset Customizations,Anpassungen zurücksetzen, +{} wants to access the following details from your account,{} möchte Zugriff auf die folgenden Angaben von Ihrem Account, +{0} is not a field of doctype {1},{0} ist kein Feld in Doctype {1}, +{0} from {1} to {2} in row #{3},{0} von {1} zu/bis {2} in Zeile #{3}, +{0} from {1} to {2},{0} von {1} zu/bis {2}, +{0} changed {1} to {2},{0} wurde von {1} zu {2} geändert, +{0} Map,{0} Karte, +Use HTML,HTML verwenden, +Submit on Creation,Nach Erstellung buchen, +Show Absolute Values,Absolutwerte anzeigen, +Row #{0}: Could not find field {1} in {2} DocType,Zeile #{0}: Feld {1} existiert nicht in DocType {2}, +Repeat on Days,An Tagen wiederholen, From c5963b5b7c5c3e245ce6a9cf930c1794e01d85e0 Mon Sep 17 00:00:00 2001 From: Samuel Danieli <23150094+scdanieli@users.noreply.github.com> Date: Tue, 15 Mar 2022 17:17:32 +0100 Subject: [PATCH 094/139] fix: escape , --- frappe/translations/de.csv | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/translations/de.csv b/frappe/translations/de.csv index 1f98d1889b..1131a8b5d7 100644 --- a/frappe/translations/de.csv +++ b/frappe/translations/de.csv @@ -4760,7 +4760,7 @@ No.,Nr.,number No.,Nein.,opposite of yes There are no upcoming events for you.,Es sind keine Termine für Sie geplant., No Upcoming Events,Keine bevorstehenden Termine, -Looks like you haven’t received any notifications.,Sieht aus, als hätten Sie keine Benachrichtigungen erhalten., +"Looks like you haven’t received any notifications.","Sieht aus, als hätten Sie keine Benachrichtigungen erhalten.", No New notifications,Keine neuen Benachrichtigungen, Overview,Übersicht, Connections,Verknüpfungen, From 5f1eadeb7a64cf57dfad860613523f2d060058b3 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Wed, 16 Mar 2022 08:10:05 +0000 Subject: [PATCH 095/139] fix(BackupGenerator): set missing attribute for class object (backport #16273) (#16301) This is a semi-automatic backport of pull request #16273 done by [Mergify](https://mergify.com). --- frappe/utils/backups.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index d23804bef4..3a0c337042 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -183,8 +183,6 @@ class BackupGenerator: False, ) - self.todays_date = now_datetime().strftime("%Y%m%d_%H%M%S") - if not ( self.backup_path_conf and self.backup_path_db @@ -212,7 +210,7 @@ class BackupGenerator: partial = "-partial" if self.partial else "" ext = "tgz" if self.compress_files else "tar" enc = "-enc" if frappe.get_system_settings("encrypt_backup") else "" - + self.todays_date = now_datetime().strftime("%Y%m%d_%H%M%S") for_conf = f"{self.todays_date}-{self.site_slug}-site_config_backup{enc}.json" for_db = f"{self.todays_date}-{self.site_slug}{partial}-database{enc}.sql.gz" From 1a0fb216451f361279ec6ed95d63994d211582a9 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Tue, 15 Mar 2022 21:42:48 +0530 Subject: [PATCH 096/139] feat: MySQL TIMESTAMP functionality for QB --- frappe/query_builder/functions.py | 22 ++++++++++++-- frappe/tests/test_query_builder.py | 47 +++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/frappe/query_builder/functions.py b/frappe/query_builder/functions.py index c98df775b7..9d12358f0d 100644 --- a/frappe/query_builder/functions.py +++ b/frappe/query_builder/functions.py @@ -1,5 +1,5 @@ from pypika.functions import * -from pypika.terms import Function +from pypika.terms import Function, CustomFunction, ArithmeticExpression, Arithmetic from frappe.query_builder.utils import ImportMapper, db_type_is from frappe.query_builder.custom import GROUP_CONCAT, STRING_AGG, MATCH, TO_TSVECTOR from frappe.database.query import Query @@ -25,6 +25,24 @@ Match = ImportMapper( } ) +class _PostgresTimestamp(ArithmeticExpression): + def __init__(self, datepart, timepart, alias=None): + if isinstance(datepart, str): + datepart = Cast(datepart, "date") + if isinstance(timepart, str): + timepart = Cast(timepart, "time") + + super().__init__(operator=Arithmetic.add, + left=datepart, right=timepart, alias=alias) + + +CombineDatetime = ImportMapper( + { + db_type_is.MARIADB: CustomFunction("TIMESTAMP", ["date", "time"]), + db_type_is.POSTGRES: _PostgresTimestamp, + } +) + def _aggregate(function, dt, fieldname, filters, **kwargs): return ( @@ -46,4 +64,4 @@ def _avg(dt, fieldname, filters=None, **kwargs): return _aggregate(Avg, dt, fieldname, filters, **kwargs) def _sum(dt, fieldname, filters=None, **kwargs): - return _aggregate(Sum, dt, fieldname, filters, **kwargs) \ No newline at end of file + return _aggregate(Sum, dt, fieldname, filters, **kwargs) diff --git a/frappe/tests/test_query_builder.py b/frappe/tests/test_query_builder.py index ea700b183e..6b13da067e 100644 --- a/frappe/tests/test_query_builder.py +++ b/frappe/tests/test_query_builder.py @@ -3,7 +3,7 @@ from typing import Callable import frappe from frappe.query_builder.custom import ConstantColumn -from frappe.query_builder.functions import Coalesce, GroupConcat, Match +from frappe.query_builder.functions import Coalesce, GroupConcat, Match, CombineDatetime from frappe.query_builder.utils import db_type_is from frappe.query_builder import Case @@ -32,6 +32,27 @@ class TestCustomFunctionsMariaDB(unittest.TestCase): query.get_sql(), "SELECT `name`,'John' `User` FROM `tabDocType`" ) + def test_timestamp(self): + note = frappe.qb.DocType("Note") + self.assertEqual("TIMESTAMP(posting_date,posting_time)", CombineDatetime(note.posting_date, note.posting_time).get_sql()) + self.assertEqual("TIMESTAMP('2021-01-01','00:00:21')", CombineDatetime("2021-01-01", "00:00:21").get_sql()) + + todo = frappe.qb.DocType("ToDo") + select_query = (frappe.qb + .from_(note) + .join(todo).on(todo.refernce_name == note.name) + .select(CombineDatetime(note.posting_date, note.posting_time))) + self.assertIn("select timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`)", str(select_query).lower()) + + select_query = select_query.orderby(CombineDatetime(note.posting_date, note.posting_time)) + self.assertIn("order by timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`)", str(select_query).lower()) + + select_query = select_query.where(CombineDatetime(note.posting_date, note.posting_time) >= CombineDatetime("2021-01-01", "00:00:01")) + self.assertIn("timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`)>=timestamp('2021-01-01','00:00:01')", str(select_query).lower()) + + select_query = select_query.select(CombineDatetime(note.posting_date, note.posting_time, alias="timestamp")) + self.assertIn("timestamp(`tabnote`.`posting_date`,`tabnote`.`posting_time`) `timestamp`", str(select_query).lower()) + @run_only_if(db_type_is.POSTGRES) class TestCustomFunctionsPostgres(unittest.TestCase): @@ -52,6 +73,30 @@ class TestCustomFunctionsPostgres(unittest.TestCase): query.get_sql(), 'SELECT "name",\'John\' "User" FROM "tabDocType"' ) + def test_timestamp(self): + note = frappe.qb.DocType("Note") + self.assertEqual("posting_date+posting_time", CombineDatetime(note.posting_date, note.posting_time).get_sql()) + self.assertEqual("CAST('2021-01-01' AS DATE)+CAST('00:00:21' AS TIME)", CombineDatetime("2021-01-01", "00:00:21").get_sql()) + + todo = frappe.qb.DocType("ToDo") + select_query = (frappe.qb + .from_(note) + .join(todo).on(todo.refernce_name == note.name) + .select(CombineDatetime(note.posting_date, note.posting_time))) + self.assertIn('select "tabnote"."posting_date"+"tabnote"."posting_time"', str(select_query).lower()) + + select_query = select_query.orderby(CombineDatetime(note.posting_date, note.posting_time)) + self.assertIn('order by "tabnote"."posting_date"+"tabnote"."posting_time"', str(select_query).lower()) + + select_query = select_query.where( + CombineDatetime(note.posting_date, note.posting_time) >= CombineDatetime('2021-01-01', '00:00:01') + ) + self.assertIn("""where "tabnote"."posting_date"+"tabnote"."posting_time">=cast('2021-01-01' as date)+cast('00:00:01' as time)""", + str(select_query).lower()) + + select_query = select_query.select(CombineDatetime(note.posting_date, note.posting_time, alias="timestamp")) + self.assertIn('"tabnote"."posting_date"+"tabnote"."posting_time" "timestamp"', str(select_query).lower()) + class TestBuilderBase(object): def test_adding_tabs(self): From 2558c6bee0b2d7254a78d77919302d5bc2af84b9 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Mar 2022 18:30:22 +0530 Subject: [PATCH 097/139] feat: Drop site support for postgres --- frappe/database/__init__.py | 3 ++- frappe/database/postgres/setup_db.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frappe/database/__init__.py b/frappe/database/__init__.py index 7b26ac31b3..5db0537ed7 100644 --- a/frappe/database/__init__.py +++ b/frappe/database/__init__.py @@ -18,7 +18,8 @@ def setup_database(force, source_sql=None, verbose=None, no_mariadb_socket=False def drop_user_and_database(db_name, root_login=None, root_password=None): import frappe if frappe.conf.db_type == 'postgres': - pass + import frappe.database.postgres.setup_db + return frappe.database.postgres.setup_db.drop_user_and_database(db_name, root_login, root_password) else: import frappe.database.mariadb.setup_db return frappe.database.mariadb.setup_db.drop_user_and_database(db_name, root_login, root_password) diff --git a/frappe/database/postgres/setup_db.py b/frappe/database/postgres/setup_db.py index b3b2e0fd41..4b265e7660 100644 --- a/frappe/database/postgres/setup_db.py +++ b/frappe/database/postgres/setup_db.py @@ -95,3 +95,11 @@ def get_root_connection(root_login=None, root_password=None): frappe.local.flags.root_connection = frappe.database.get_db(user=root_login, password=root_password) return frappe.local.flags.root_connection + + +def drop_user_and_database(db_name, root_login, root_password): + root_conn = get_root_connection(frappe.flags.root_login or root_login, frappe.flags.root_password or root_password) + root_conn.commit() + root_conn.sql(f"SELECT pg_terminate_backend (pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = %s", (db_name, )) + root_conn.sql(f"DROP DATABASE IF EXISTS {db_name}") + root_conn.sql(f"DROP USER IF EXISTS {db_name}") From 776ba30a4d836469776e37af28dc48d5883376df Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 16 Mar 2022 19:06:41 +0530 Subject: [PATCH 098/139] fix: Add more verbosity for drop-site command --- frappe/commands/site.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frappe/commands/site.py b/frappe/commands/site.py index b54f369e34..63da4db093 100644 --- a/frappe/commands/site.py +++ b/frappe/commands/site.py @@ -677,7 +677,9 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site try: if not no_backup: - scheduled_backup(ignore_files=False, force=True) + click.secho(f"Taking backup of {site}", fg="green") + odb = scheduled_backup(ignore_files=False, force=True, verbose=True) + odb.print_summary() except Exception as err: if force: pass @@ -692,6 +694,7 @@ def _drop_site(site, db_root_username=None, db_root_password=None, archived_site click.echo("\n".join(messages)) sys.exit(1) + click.secho("Dropping site database and user", fg="green") drop_user_and_database(frappe.conf.db_name, db_root_username, db_root_password) archived_sites_path = archived_sites_path or os.path.join(frappe.get_app_path('frappe'), '..', '..', '..', 'archived', 'sites') From 734d0b4fe8f6ee0e810d7e5bb205329ad9acf80a Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 17 Mar 2022 01:35:46 +0100 Subject: [PATCH 099/139] test: frappe.db.exists --- frappe/tests/test_db.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index bbd09590be..504a1eb3b8 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -301,6 +301,14 @@ class TestDB(unittest.TestCase): # recover transaction to continue other tests raise Exception + def test_exists(self): + dt, dn = "User", "Administrator" + self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn) + self.assertEqual(frappe.db.exists(dt, dn), dn) + self.assertEqual(frappe.db.exists(dt, {"name": ("=", dn)}), dn) + self.assertEqual(frappe.db.exists({"doctype": dt, "name": ("like", "Admin%")}), dn) + self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn) + @run_only_if(db_type_is.MARIADB) class TestDDLCommandsMaria(unittest.TestCase): From 7e550e919ca51d5986314db9a6227cb988f5644f Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 17 Mar 2022 02:07:23 +0100 Subject: [PATCH 100/139] revert: don't check for doctype (861ff16) --- frappe/database/database.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 9b1828f811..5d91e0b4f1 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -908,7 +908,7 @@ class Database(object): exists("User", {"full_name": "Jane Doe"}) ``` """ - if dt == dn: + if dt != "DocType" and dt == dn: # single always exists (!) return dn From c26cf2547885b7b9c6d608a23c273d1501dd2f07 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Thu, 17 Mar 2022 02:46:00 +0100 Subject: [PATCH 101/139] fix: avoid invalid call to frappe.db.exists --- frappe/model/base_document.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 8a81aa5610..c251325bcb 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -963,7 +963,7 @@ class BaseDocument(object): from frappe.model.meta import get_default_df df = get_default_df(fieldname) - if not currency and df: + if df.fieldtype == "Currency" and not currency: currency = self.get(df.get("options")) if not frappe.db.exists('Currency', currency, cache=True): currency = None From 14a4e35d8d9eb5917fa680160fd5ca5b328ed2d2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 17 Mar 2022 09:50:14 +0530 Subject: [PATCH 102/139] fix: Typo in is_downgrade's user warning Fixes https://github.com/frappe/frappe/issues/16312 --- frappe/installer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/installer.py b/frappe/installer.py index 6ebab95a7d..d10dc78286 100644 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -611,7 +611,7 @@ def is_downgrade(sql_file_path, verbose=False): 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)) + print(f"Your site will be downgraded from Frappe {backup_version} to {current_version}") return downgrade From bc48c03da78e98557afe2560055fbeadd7e5ccc6 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 17 Mar 2022 11:18:12 +0530 Subject: [PATCH 103/139] test: failing list_paging UI test --- cypress/integration/list_paging.js | 3 +++ frappe/tests/ui_test_helpers.py | 11 ++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/cypress/integration/list_paging.js b/cypress/integration/list_paging.js index b6832f5a53..4a59024a7b 100644 --- a/cypress/integration/list_paging.js +++ b/cypress/integration/list_paging.js @@ -31,5 +31,8 @@ context('List Paging', () => { cy.get('.list-paging-area .btn-group .btn-paging[data-value="500"]').click(); cy.get('.list-paging-area .list-count').should('contain.text', '500 of'); + cy.get('.list-paging-area .btn-more').click(); + + cy.get('.list-paging-area .list-count').should('contain.text', '1000 of'); }); }); diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 473f9b22d3..42a7f48d79 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -138,11 +138,12 @@ def create_contact_records(): def create_multiple_todo_records(): if frappe.db.get_all('ToDo', {'description': 'Multiple ToDo 1'}): return - for index in range(501): - frappe.get_doc({ - 'doctype': 'ToDo', - 'description': 'Multiple ToDo {}'.format(index+1) - }).insert() + + query = "INSERT INTO `tabToDo` (`name`, `description`) VALUES ('1001', 'Multiple ToDo 1')" + for index in range(1000): + query = query + ", ('100{}', 'Multiple ToDo {}')".format(index+2,index+2) + + frappe.db.sql(query) def insert_contact(first_name, phone_number): doc = frappe.get_doc({ From 631e6f32604a8df389a89079f57e77213a52e52c Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 17 Mar 2022 12:03:46 +0530 Subject: [PATCH 104/139] fix: using bulk_insert instead of sql query --- frappe/tests/ui_test_helpers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index 42a7f48d79..75c28a8cd7 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -136,14 +136,14 @@ def create_contact_records(): @frappe.whitelist() def create_multiple_todo_records(): + values = [] if frappe.db.get_all('ToDo', {'description': 'Multiple ToDo 1'}): return - query = "INSERT INTO `tabToDo` (`name`, `description`) VALUES ('1001', 'Multiple ToDo 1')" - for index in range(1000): - query = query + ", ('100{}', 'Multiple ToDo {}')".format(index+2,index+2) + for index in range(1, 1002): + values.append(('100{}'.format(index), 'Multiple ToDo {}'.format(index))) - frappe.db.sql(query) + frappe.db.bulk_insert('ToDo', fields=['name', 'description'], values=set(values)) def insert_contact(first_name, phone_number): doc = frappe.get_doc({ From 0b8a2edee70cedb62059a29039eb06d3809efd19 Mon Sep 17 00:00:00 2001 From: Rushabh Mehta Date: Thu, 17 Mar 2022 14:54:17 +0530 Subject: [PATCH 105/139] fix(build): separate assets.json and assets-rtl.json to fix concurrency issue --- esbuild/esbuild.js | 22 ++++++++++++---------- frappe/utils/__init__.py | 35 +++++++++++++++++++++++------------ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index 43c01e88fb..ff31aa4b74 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -286,7 +286,7 @@ function get_watch_config() { notify_redis({ error }); } else { let { - assets_json, + new_assets_json, prev_assets_json } = await write_assets_json(result.metafile); @@ -294,7 +294,7 @@ function get_watch_config() { if (prev_assets_json) { changed_files = get_rebuilt_assets( prev_assets_json, - assets_json + new_assets_json ); let timestamp = new Date().toLocaleTimeString(); @@ -384,6 +384,7 @@ let prev_assets_json; let curr_assets_json; async function write_assets_json(metafile) { + let rtl = false; prev_assets_json = curr_assets_json; let out = {}; for (let output in metafile.outputs) { @@ -392,13 +393,14 @@ async function write_assets_json(metafile) { if (info.entryPoint) { let key = path.basename(info.entryPoint); if (key.endsWith('.css') && asset_path.includes('/css-rtl/')) { + rtl = true; key = `rtl_${key}`; } out[key] = asset_path; } } - let assets_json_path = path.resolve(assets_path, "assets.json"); + let assets_json_path = path.resolve(assets_path, `assets${rtl?'-rtl':''}.json`); let assets_json; try { assets_json = await fs.promises.readFile(assets_json_path, "utf-8"); @@ -407,21 +409,21 @@ async function write_assets_json(metafile) { } assets_json = JSON.parse(assets_json); // update with new values - assets_json = Object.assign({}, assets_json, out); - curr_assets_json = assets_json; + let new_assets_json = Object.assign({}, assets_json, out); + curr_assets_json = new_assets_json; await fs.promises.writeFile( assets_json_path, - JSON.stringify(assets_json, null, 4) + JSON.stringify(new_assets_json, null, 4) ); - await update_assets_json_in_cache(assets_json); + await update_assets_json_in_cache(); return { - assets_json, + new_assets_json, prev_assets_json }; } -function update_assets_json_in_cache(assets_json) { +function update_assets_json_in_cache() { // update assets_json cache in redis, so that it can be read directly by python return new Promise(resolve => { let client = get_redis_subscriber("redis_cache"); @@ -429,7 +431,7 @@ function update_assets_json_in_cache(assets_json) { client.on("error", _ => { log_warn("Cannot connect to redis_cache to update assets_json"); }); - client.set("assets_json", JSON.stringify(assets_json), err => { + client.del("assets_json", err => { client.unref(); resolve(); }); diff --git a/frappe/utils/__init__.py b/frappe/utils/__init__.py index 1233bcd30f..c361b5b430 100644 --- a/frappe/utils/__init__.py +++ b/frappe/utils/__init__.py @@ -796,22 +796,33 @@ def get_assets_json(): # using .get instead of .get_value to avoid pickle.loads try: - assets_json = cache.get("assets_json") - except ConnectionError: + if not frappe.conf.developer_mode: + assets_json = cache.get("assets_json").decode('utf-8') + else: + assets_json = None + except (UnicodeDecodeError, AttributeError, ConnectionError): assets_json = None - # if value found, decode it - if assets_json is not None: - try: - assets_json = assets_json.decode('utf-8') - except (UnicodeDecodeError, AttributeError): - assets_json = None - if not assets_json: - assets_json = frappe.read_file("assets/assets.json") - cache.set_value("assets_json", assets_json, shared=True) + # get merged assets.json and assets-rtl.json + assets_dict = frappe.parse_json( + frappe.read_file("assets/assets.json") + ) - frappe.local.assets_json = frappe.safe_decode(assets_json) + assets_rtl = frappe.read_file("assets/assets-rtl.json") + if assets_rtl: + assets_dict.update( + frappe.parse_json(assets_rtl) + ) + frappe.local.assets_json = frappe.as_json(assets_dict) + # save in cache + cache.set_value("assets_json", frappe.local.assets_json, + shared=True) + + return assets_dict + else: + # from cache, decode and send + frappe.local.assets_json = frappe.safe_decode(assets_json) return frappe.parse_json(frappe.local.assets_json) From d68187ab9679d3cc0fc95129716625154c5712db Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 17 Mar 2022 16:42:45 +0530 Subject: [PATCH 106/139] fix: empty state height --- .../public/js/frappe/web_form/web_form_list.js | 18 ++++++++---------- frappe/public/scss/website/index.scss | 13 +++++++++++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index b7e92052a4..1f3628ac38 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -141,23 +141,21 @@ export default class WebFormList { empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center"); frappe.has_permission(this.doctype, "", "create", () => { - new_button = `

`; - empty_state.innerHTML = `
+ empty_state.innerHTML = `
- Generic Empty State + Generic Empty State
-

${__("No {0} found", [__(this.doctype)])}

+

${__("No {0} found", [__(this.doctype)])}

${new_button}`; - this.wrapper.appendChild(empty_state); + this.wrapper.appendChild(empty_state); + }); } - } make_table_head() { diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 2cc0f64f76..e36e649eb7 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -311,3 +311,16 @@ h5.modal-title { .empty-list-icon { height: 70px; } + +.null-state { + height: 60px; + width: auto; + margin-bottom: var(--margin-md); + img { + fill: var(--fg-color); + } +} + +.no-result { + min-height: #{"calc(100vh - 284px)"}; +} From bbcb99d65d9e560d101b810918724b02e47d3ed2 Mon Sep 17 00:00:00 2001 From: Jannat Patel Date: Thu, 17 Mar 2022 16:55:14 +0530 Subject: [PATCH 107/139] fix: removed console --- frappe/public/js/frappe/web_form/web_form_list.js | 1 - 1 file changed, 1 deletion(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 1f3628ac38..09127a9f7f 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -141,7 +141,6 @@ export default class WebFormList { empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center"); frappe.has_permission(this.doctype, "", "create", () => { - console.log(this) new_button = ``; From c4be72c2d404f337ba10d747e27525feb66aa9b7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 18 Mar 2022 12:18:15 +0530 Subject: [PATCH 108/139] fix: Pass skip_dirty_trigger flag to the set_value and model trigger Also, renamed avoid_dirty to skip_dirty_trigger to be more explicit --- frappe/public/js/frappe/model/model.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index d0b1c729c6..3b95a4b3f1 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -412,7 +412,7 @@ $.extend(frappe.model, { } }, - set_value: function(doctype, docname, fieldname, value, fieldtype, avoid_dirty=false) { + set_value: function(doctype, docname, fieldname, value, fieldtype, skip_dirty_trigger=false) { /* help: Set a value locally (if changed) and execute triggers */ var doc; @@ -438,11 +438,11 @@ $.extend(frappe.model, { } doc[key] = value; - if (!avoid_dirty) tasks.push(() => frappe.model.trigger(key, value, doc)); + tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger)); } else { // execute link triggers (want to reselect to execute triggers) if(in_list(["Link", "Dynamic Link"], fieldtype) && doc) { - tasks.push(() => frappe.model.trigger(key, value, doc)); + tasks.push(() => frappe.model.trigger(key, value, doc, skip_dirty_trigger)); } } }); @@ -467,7 +467,7 @@ $.extend(frappe.model, { frappe.model.events[doctype][fieldname].push(fn); }, - trigger: function(fieldname, value, doc) { + trigger: function(fieldname, value, doc, skip_dirty_trigger=false) { const tasks = []; function enqueue_events(events) { @@ -477,7 +477,7 @@ $.extend(frappe.model, { if (!fn) continue; tasks.push(() => { - const return_value = fn(fieldname, value, doc); + const return_value = fn(fieldname, value, doc, skip_dirty_trigger); // if the trigger returns a promise, return it, // or use the default promise frappe.after_ajax From adb989fff2458cc92678ef6b6f5c224904be1864 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 18 Mar 2022 12:19:36 +0530 Subject: [PATCH 109/139] feat: Option to set form dirty even if form save is disabled --- frappe/public/js/frappe/form/form.js | 14 +++++++++----- frappe/public/js/frappe/form/toolbar.js | 6 +++--- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 494e3af705..7ec6677c7f 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -246,10 +246,12 @@ frappe.ui.form.Form = class FrappeForm { var me = this; // on main doc - frappe.model.on(me.doctype, "*", function(fieldname, value, doc) { + frappe.model.on(me.doctype, "*", function(fieldname, value, doc, skip_dirty_trigger=false) { // set input if (cstr(doc.name) === me.docname) { - me.dirty(); + if (!skip_dirty_trigger) { + me.dirty(); + } let field = me.fields_dict[fieldname]; field && field.refresh(fieldname); @@ -953,10 +955,12 @@ frappe.ui.form.Form = class FrappeForm { this.toolbar.set_primary_action(); } - disable_save() { + disable_save(set_dirty=false) { // IMPORTANT: this function should be called in refresh event this.save_disabled = true; this.toolbar.current_status = null; + // field changes should make form dirty + this.set_dirty = set_dirty; this.page.clear_primary_action(); } @@ -1447,7 +1451,7 @@ frappe.ui.form.Form = class FrappeForm { return doc; } - set_value(field, value, if_missing, avoid_dirty=false) { + set_value(field, value, if_missing, skip_dirty_trigger=false) { var me = this; var _set = function(f, v) { var fieldobj = me.fields_dict[f]; @@ -1467,7 +1471,7 @@ frappe.ui.form.Form = class FrappeForm { me.refresh_field(f); return Promise.resolve(); } else { - return frappe.model.set_value(me.doctype, me.doc.name, f, v, me.fieldtype, avoid_dirty); + return frappe.model.set_value(me.doctype, me.doc.name, f, v, me.fieldtype, skip_dirty_trigger); } } } else { diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index 016390a4e1..e55eb9fdeb 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -534,14 +534,14 @@ frappe.ui.form.Toolbar = class Toolbar { }); } show_title_as_dirty() { - if(this.frm.save_disabled) + if (this.frm.save_disabled && !this.frm.set_dirty) return; - if(this.frm.doc.__unsaved) { + if (this.frm.is_dirty()) { this.page.set_indicator(__("Not Saved"), "orange"); } - $(this.frm.wrapper).attr("data-state", this.frm.doc.__unsaved ? "dirty" : "clean"); + $(this.frm.wrapper).attr("data-state", this.frm.is_dirty() ? "dirty" : "clean"); } show_jump_to_field_dialog() { From fedcf48ada2bd17ce01f2238af9749b68a837437 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 18 Mar 2022 12:20:51 +0530 Subject: [PATCH 110/139] fix: Customize form issue where it remains "Not Saved" even after update fixes: https://github.com/frappe/frappe/issues/16068 --- frappe/custom/doctype/customize_form/customize_form.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 4862185b99..9cfe315e44 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -14,7 +14,6 @@ frappe.ui.form.on("Customize Form", { }, onload: function(frm) { - frm.disable_save(); frm.set_query("doc_type", function() { return { translate_values: false, @@ -110,7 +109,7 @@ frappe.ui.form.on("Customize Form", { }, refresh: function(frm) { - frm.disable_save(); + frm.disable_save(true); frm.page.clear_icons(); if (frm.doc.doc_type) { @@ -169,7 +168,7 @@ frappe.ui.form.on("Customize Form", { doc_type = localStorage.getItem("customize_doctype"); } if (doc_type) { - setTimeout(() => frm.set_value("doc_type", doc_type), 1000); + setTimeout(() => frm.set_value("doc_type", doc_type, false, true), 1000); } }, @@ -341,11 +340,11 @@ frappe.customize_form.confirm = function(msg, frm) { } frappe.customize_form.clear_locals_and_refresh = function(frm) { + delete frm.doc.__unsaved; // clear doctype from locals frappe.model.clear_doc("DocType", frm.doc.doc_type); delete frappe.meta.docfield_copy[frm.doc.doc_type]; - frm.refresh(); -} +}; extend_cscript(cur_frm.cscript, new frappe.model.DocTypeController({frm: cur_frm})); From 5187663d7370df3f548e0d83583a2f88220232fa Mon Sep 17 00:00:00 2001 From: hrwx Date: Sat, 19 Mar 2022 23:50:30 +0000 Subject: [PATCH 111/139] chore: prefix test for parse_email --- frappe/core/doctype/communication/test_communication.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/core/doctype/communication/test_communication.py b/frappe/core/doctype/communication/test_communication.py index d933c2f494..8012d8facf 100644 --- a/frappe/core/doctype/communication/test_communication.py +++ b/frappe/core/doctype/communication/test_communication.py @@ -4,8 +4,8 @@ import unittest from urllib.parse import quote import frappe -from frappe.email.doctype.email_queue.email_queue import EmailQueue from frappe.core.doctype.communication.communication import get_emails +from frappe.email.doctype.email_queue.email_queue import EmailQueue test_records = frappe.get_test_records('Communication') @@ -202,7 +202,7 @@ class TestCommunication(unittest.TestCase): self.assertIn(("Note", note.name), doc_links) - def parse_emails(self): + def test_parse_emails(self): emails = get_emails( [ 'comm_recipient+DocType+DocName@example.com', From 1934340a1b81313b191381206c3dc6aa3fdedac1 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 01:46:27 +0100 Subject: [PATCH 112/139] refactor: don't assign variable to itself --- frappe/handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100755 => 100644 frappe/handler.py diff --git a/frappe/handler.py b/frappe/handler.py old mode 100755 new mode 100644 index 3fd1c096e4..07c7d7a30c --- a/frappe/handler.py +++ b/frappe/handler.py @@ -250,7 +250,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): try: args = json.loads(args) except ValueError: - args = args + pass method_obj = getattr(doc, method) fn = getattr(method_obj, '__func__', method_obj) From 687ca6972d63a7505211b89ba4dc6b7f4efc174e Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 01:53:30 +0100 Subject: [PATCH 113/139] refactor: remove unused fallback --- frappe/public/js/frappe/views/reports/report_view.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/public/js/frappe/views/reports/report_view.js b/frappe/public/js/frappe/views/reports/report_view.js index f80d59350f..2747a2723d 100644 --- a/frappe/public/js/frappe/views/reports/report_view.js +++ b/frappe/public/js/frappe/views/reports/report_view.js @@ -1026,7 +1026,7 @@ frappe.views.ReportView = class ReportView extends frappe.views.ListView { } if (!docfield || docfield.report_hide) return; - let title = __(docfield ? docfield.label : toTitle(fieldname)); + let title = __(docfield.label); if (doctype !== this.doctype) { title += ` (${__(doctype)})`; } From 0d8733c462b99063a70eb50c3de2538608f613ba Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 02:19:07 +0100 Subject: [PATCH 114/139] refactor: remove unused import --- frappe/utils/backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index 3a0c337042..de7ce2e38b 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -15,7 +15,7 @@ import click # imports - module imports import frappe -from frappe import _, conf +from frappe import conf from frappe.utils import get_file_size, get_url, now, now_datetime, cint from frappe.utils.password import get_encryption_key From 4b8efc5ebd4e62a45aa6a4fecd68f607ab35d127 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 02:19:41 +0100 Subject: [PATCH 115/139] fix: typo in parameter name --- frappe/utils/backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index de7ce2e38b..dc5cdf43a2 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -505,7 +505,7 @@ download only after 24 hours.""" % { datetime_str.strftime("%d/%m/%Y %H:%M:%S") + """ - Backup ready to be downloaded""" ) - frappe.sendmail(recipients=recipient_list, msg=msg, subject=subject) + frappe.sendmail(recipients=recipient_list, message=msg, subject=subject) return recipient_list From 1d3ec4297402c4a6769af7731bf88e14d38999fb Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 02:20:27 +0100 Subject: [PATCH 116/139] fix: send_email doesn't take arguments --- frappe/utils/backups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frappe/utils/backups.py b/frappe/utils/backups.py index dc5cdf43a2..5197b20bd3 100644 --- a/frappe/utils/backups.py +++ b/frappe/utils/backups.py @@ -779,7 +779,7 @@ if __name__ == "__main__": db_type=db_type, db_port=db_port, ) - odb.send_email("abc.sql.gz") + odb.send_email() if cmd == "delete_temp_backups": delete_temp_backups() From 8a4f316ec5c3492b4f692f60079c2d299c5ff29d Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 02:35:35 +0100 Subject: [PATCH 117/139] refactor: remove useless pass, log error --- frappe/core/doctype/user/user.py | 4 ++-- frappe/translate.py | 2 -- frappe/utils/nestedset.py | 1 - 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index d08755f9a8..1ad977547c 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -253,8 +253,8 @@ class User(Document): self.email_new_password(new_password) except frappe.OutgoingEmailError: - print(frappe.get_traceback()) - pass # email server not set, don't send email + # email server not set, don't send email + frappe.log_error(frappe.get_traceback()) @Document.hook def validate_reset_password(self): diff --git a/frappe/translate.py b/frappe/translate.py index 292adc71c7..0367d33d3b 100644 --- a/frappe/translate.py +++ b/frappe/translate.py @@ -650,8 +650,6 @@ def extract_messages_from_code(code): if isinstance(e, InvalidIncludePath): frappe.clear_last_message() - pass - messages = [] pattern = r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)" diff --git a/frappe/utils/nestedset.py b/frappe/utils/nestedset.py index 98ad337043..2517761c45 100644 --- a/frappe/utils/nestedset.py +++ b/frappe/utils/nestedset.py @@ -227,7 +227,6 @@ class NestedSet(Document): update_nsm(self) except frappe.DoesNotExistError: if self.flags.on_rollback: - pass frappe.message_log.pop() else: raise From 9a2a2e7abea43d5854b92ea994cd62ac3f2f64f6 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 03:08:23 +0100 Subject: [PATCH 118/139] fix: assign result of concat --- frappe/public/js/frappe/list/list_settings.js | 2 +- frappe/public/js/frappe/utils/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js index 782a077a78..186a4370bc 100644 --- a/frappe/public/js/frappe/list/list_settings.js +++ b/frappe/public/js/frappe/list/list_settings.js @@ -375,7 +375,7 @@ export default class ListSettings { let me = this; if (me.removed_fields) { - me.removed_fields.concat(fields); + me.removed_fields = me.removed_fields.concat(fields); } else { me.removed_fields = fields; } diff --git a/frappe/public/js/frappe/utils/utils.js b/frappe/public/js/frappe/utils/utils.js index a944af523d..0514576380 100644 --- a/frappe/public/js/frappe/utils/utils.js +++ b/frappe/public/js/frappe/utils/utils.js @@ -231,7 +231,7 @@ Object.assign(frappe.utils, { if (tt && (tt.substr(0, 1)===">" || tt.substr(0, 4)===">")) { part.push(t); } else { - out.concat(part); + out = out.concat(part); out.push(t); part = []; } From f650408daa0da57d0ce9072eb4a5b2f244b0e4bd Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Sun, 20 Mar 2022 18:02:44 +0100 Subject: [PATCH 119/139] refactor: use frappe.parse_json --- frappe/handler.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/frappe/handler.py b/frappe/handler.py index 07c7d7a30c..ebc72da937 100644 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -225,11 +225,10 @@ def ping(): def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): """run a whitelisted controller method""" - import json - import inspect + from inspect import getfullargspec - if not args: - args = arg or "" + if not args and arg: + args = arg if dt: # not called from a doctype (from a page) if not dn: @@ -237,9 +236,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): doc = frappe.get_doc(dt, dn) else: - if isinstance(docs, str): - docs = json.loads(docs) - + docs = frappe.parse_json(docs) doc = frappe.get_doc(docs) doc._original_modified = doc.modified doc.check_if_latest() @@ -248,7 +245,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): throw_permission_error() try: - args = json.loads(args) + args = frappe.parse_json(args) except ValueError: pass @@ -257,7 +254,7 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): is_whitelisted(fn) is_valid_http_method(fn) - fnargs = inspect.getfullargspec(method_obj).args + fnargs = getfullargspec(method_obj).args if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): response = doc.run_method(method) From afd5956e31836634c6d63a6b3b68d30234588a91 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Mon, 21 Mar 2022 10:39:52 +0530 Subject: [PATCH 120/139] style: Fix formatting --- .../js/frappe/web_form/web_form_list.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 09127a9f7f..8d2959e49f 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -141,16 +141,26 @@ export default class WebFormList { empty_state.classList.add("no-result", "text-muted", "flex", "justify-center", "align-center"); frappe.has_permission(this.doctype, "", "create", () => { - new_button = ``; + new_button = ` + + `; - empty_state.innerHTML = `
-
- Generic Empty State -
-

${__("No {0} found", [__(this.doctype)])}

- ${new_button}`; + empty_state.innerHTML = ` +
+
+ Generic Empty State +
+

${__("No {0} found", [__(this.doctype)])}

+ ${new_button} +
+ `; this.wrapper.appendChild(empty_state); }); From b97cfed6d7678409019bdf02a48f3c8b8abdb770 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Sat, 19 Mar 2022 20:46:53 +0530 Subject: [PATCH 121/139] perf: limit rows to 1 for get_value and exists --- frappe/database/database.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index a3476c9538..099c9f1fde 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -384,7 +384,7 @@ class Database(object): """ ret = self.get_values(doctype, filters, fieldname, ignore, as_dict, debug, - order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct) + order_by, cache=cache, for_update=for_update, run=run, pluck=pluck, distinct=distinct, limit=1) if not run: return ret @@ -393,7 +393,7 @@ class Database(object): def get_values(self, doctype, filters=None, fieldname="name", ignore=None, as_dict=False, debug=False, order_by="KEEP_DEFAULT_ORDERING", update=None, cache=False, for_update=False, - run=True, pluck=False, distinct=False): + run=True, pluck=False, distinct=False, limit=None): """Returns multiple document properties. :param doctype: DocType name. @@ -423,14 +423,15 @@ class Database(object): if isinstance(filters, list): out = self._get_value_for_many_names( - doctype, - filters, - fieldname, - order_by, + doctype=doctype, + names=filters, + field=fieldname, + order_by=order_by, debug=debug, run=run, pluck=pluck, distinct=distinct, + limit=limit, ) else: @@ -444,17 +445,18 @@ class Database(object): if order_by: order_by = "modified" if order_by == "KEEP_DEFAULT_ORDERING" else order_by out = self._get_values_from_table( - fields, - filters, - doctype, - as_dict, - debug, - order_by, - update, + fields=fields, + filters=filters, + doctype=doctype, + as_dict=as_dict, + debug=debug, + order_by=order_by, + update=update, for_update=for_update, run=run, pluck=pluck, - distinct=distinct + distinct=distinct, + limit=limit, ) except Exception as e: if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): @@ -623,6 +625,7 @@ class Database(object): run=True, pluck=False, distinct=False, + limit=None, ): field_objects = [] @@ -641,6 +644,7 @@ class Database(object): field_objects=field_objects, fields=fields, distinct=distinct, + limit=limit, ) if ( fields == "*" @@ -654,7 +658,7 @@ class Database(object): ) return r - def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False): + def _get_value_for_many_names(self, doctype, names, field, order_by, debug=False, run=True, pluck=False, distinct=False, limit=None): names = list(filter(None, names)) if names: return self.get_all( @@ -667,6 +671,7 @@ class Database(object): as_list=1, run=run, distinct=distinct, + limit_page_length=limit ) else: return {} From 85428e817d2c2034b10ea6d18eb30d65555b3240 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Mar 2022 11:16:10 +0530 Subject: [PATCH 122/139] test: get_value(s) with limits --- frappe/tests/test_db.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 235211b6b8..1cd9287ec3 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -14,7 +14,7 @@ from frappe.database.database import Database from frappe.query_builder import Field from frappe.query_builder.functions import Concat_ws from frappe.tests.test_query_builder import db_type_is, run_only_if -from frappe.utils import add_days, now, random_string +from frappe.utils import add_days, now, random_string, cint from frappe.utils.testutils import clear_custom_fields @@ -84,6 +84,27 @@ class TestDB(unittest.TestCase): ), ) + def test_get_value_limits(self): + + # check both dict and list style filters + filters = [{"enabled": 1}, [["enabled", "=", 1]]] + for filter in filters: + self.assertEqual(1, len(frappe.db.get_values("User", filters=filter, limit=1))) + # count of last touched rows as per DB-API 2.0 https://peps.python.org/pep-0249/#rowcount + self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) + self.assertEqual(2, len(frappe.db.get_values("User", filters=filter, limit=2))) + self.assertGreaterEqual(2, cint(frappe.db._cursor.rowcount)) + + # without limits length == count + self.assertEqual(len(frappe.db.get_values("User", filters=filter)), + frappe.db.count("User", filter)) + + frappe.db.get_value("User", filters=filter) + self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) + + frappe.db.exists("User", filter) + self.assertGreaterEqual(1, cint(frappe.db._cursor.rowcount)) + def test_escape(self): frappe.db.escape("香港濟生堂製藥有限公司 - IT".encode("utf-8")) From 10fbb4330ac3c10839d8f32672fc2c563d6b549a Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 10 Mar 2022 12:58:13 +0530 Subject: [PATCH 123/139] fix: setting permissions to any role of some doctypes is not working (cherry picked from commit 6612232babd61852e90ccfc2ca836397d80506cf) # Conflicts: # frappe/permissions.py --- frappe/permissions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/permissions.py b/frappe/permissions.py index af17faba01..985d398749 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -7,7 +7,11 @@ import frappe.share from frappe import _, msgprint from frappe.utils import cint from frappe.query_builder import DocType +<<<<<<< HEAD +======= +import frappe.share +>>>>>>> 6612232bab (fix: setting permissions to any role of some doctypes is not working) rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share") @@ -466,6 +470,12 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali table = DocType("Custom DocPerm") frappe.qb.update(table).set(ptype, value).where(table.name == name).run() +<<<<<<< HEAD +======= + table = DocType("Custom DocPerm") + frappe.qb.update(table).set(ptype, value).where(table.name == name).run() + +>>>>>>> 6612232bab (fix: setting permissions to any role of some doctypes is not working) if validate: validate_permissions_for_doctype(doctype) From b2c0bf7a4ee491a9f871b8cd62d40be25b642214 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 17 Mar 2022 13:19:50 +0530 Subject: [PATCH 124/139] fix: remove tab \t and newlines \n from start of query and remove from middle (cherry picked from commit ac5effc7dd4d876d06daf945f0b8b77ecdd0c05f) # Conflicts: # frappe/database/database.py --- frappe/database/database.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frappe/database/database.py b/frappe/database/database.py index a3476c9538..f9a230b25c 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -116,8 +116,14 @@ class Database(object): """ query = str(query) +<<<<<<< HEAD if not run: return query +======= + + # remove \n \t from start of query and replace them with space anywhere in middle + query = re.sub(r'\s', ' ', query).lstrip() +>>>>>>> ac5effc7dd (fix: remove tab \t and newlines \n from start of query and remove from middle) if re.search(r'ifnull\(', query, flags=re.IGNORECASE): # replaces ifnull in query with coalesce From fbac6fbfb40e9e03aa67dc0df32328bdb96cd7a1 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Thu, 17 Mar 2022 15:06:20 +0530 Subject: [PATCH 125/139] fix: only remove \n\t from start and end (cherry picked from commit 7bb172365f2c9ae6cca98ccc4dfee7714b9c3f0c) # Conflicts: # frappe/database/database.py --- frappe/database/database.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/frappe/database/database.py b/frappe/database/database.py index f9a230b25c..3ce2b32fc8 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -121,9 +121,14 @@ class Database(object): return query ======= +<<<<<<< HEAD # remove \n \t from start of query and replace them with space anywhere in middle query = re.sub(r'\s', ' ', query).lstrip() >>>>>>> ac5effc7dd (fix: remove tab \t and newlines \n from start of query and remove from middle) +======= + # remove \n \t from start and end of query + query = re.sub(r'^\s*|\s*$', '', query) +>>>>>>> 7bb172365f (fix: only remove \n\t from start and end) if re.search(r'ifnull\(', query, flags=re.IGNORECASE): # replaces ifnull in query with coalesce From a832aa27af79e20b5ec47d21d0f8fffb8cf84f0f Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Mar 2022 11:26:36 +0530 Subject: [PATCH 126/139] chore: whitespace --- frappe/public/js/frappe/web_form/web_form_list.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/web_form/web_form_list.js b/frappe/public/js/frappe/web_form/web_form_list.js index 076f2f266d..1ad332e3c2 100644 --- a/frappe/public/js/frappe/web_form/web_form_list.js +++ b/frappe/public/js/frappe/web_form/web_form_list.js @@ -142,8 +142,8 @@ export default class WebFormList { frappe.has_permission(this.doctype, "", "create", () => { new_button = ` - @@ -152,9 +152,9 @@ export default class WebFormList { empty_state.innerHTML = `
- Generic Empty State

${__("No {0} found", [__(this.doctype)])}

From 1054a1203e0d3a79025b561d88cb342c6fdd6d10 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 21 Mar 2022 12:09:31 +0530 Subject: [PATCH 127/139] fix: assinging thread locals to global variables --- frappe/email/doctype/auto_email_report/auto_email_report.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 5ffde0c37b..abeb681a25 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -15,8 +15,6 @@ from frappe.utils.csvutils import to_csv from frappe.utils.xlsxutils import make_xlsx from frappe.desk.query_report import build_xlsx_data -max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 - class AutoEmailReport(Document): def autoname(self): @@ -46,6 +44,8 @@ class AutoEmailReport(Document): def validate_report_count(self): '''check that there are only 3 enabled reports per user''' count = frappe.db.sql('select count(*) from `tabAuto Email Report` where user=%s and enabled=1', self.user)[0][0] + max_reports_per_user = frappe.local.conf.max_reports_per_user or 3 + if count > max_reports_per_user + (-1 if self.flags.in_insert else 0): frappe.throw(_('Only {0} emailed reports are allowed per user').format(max_reports_per_user)) From dbb622fce177491a2d8d7ee92d1abbebeeba0333 Mon Sep 17 00:00:00 2001 From: Isaiah Galorport <86836253+icecliff@users.noreply.github.com> Date: Mon, 21 Mar 2022 16:43:06 +0800 Subject: [PATCH 128/139] fix: Other user must not able to delete other user's comment except System Manager (#16018) * fix: Other user must not able to delete other user's comment except Admin * Update frappe/public/js/frappe/form/footer/form_timeline.js Co-authored-by: Sagar Vora * fix: Close condition scope Co-authored-by: Sagar Vora Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> (cherry picked from commit e4137ca8a1bea4e35358e0ba9844042c3e4d334a) # Conflicts: # frappe/public/js/frappe/form/footer/form_timeline.js --- frappe/public/js/frappe/form/footer/form_timeline.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index d440874f36..7182aa667d 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -453,6 +453,7 @@ class FormTimeline extends BaseTimeline { let edit_wrapper = $(`
`).hide(); let edit_box = this.make_editable(edit_wrapper); let content_wrapper = comment_wrapper.find('.content'); +<<<<<<< HEAD let more_actions_wrapper = comment_wrapper.find('.more-actions'); if (frappe.model.can_delete("Comment")) { const delete_option = $(` @@ -461,6 +462,15 @@ class FormTimeline extends BaseTimeline { ${__("Delete")} +======= + + let delete_button = $(); + if (frappe.model.can_delete("Comment") && (frappe.session.user == doc.owner || frappe.user.has_role("System Manager"))) { + delete_button = $(` + +>>>>>>> e4137ca8a1 (fix: Other user must not able to delete other user's comment except System Manager (#16018)) `).click(() => this.delete_comment(doc.name)); more_actions_wrapper.find('.dropdown-menu').append(delete_option); } From 810867a0d53d35eec27cc7599c4e2ed980e61967 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Mon, 21 Mar 2022 14:18:58 +0530 Subject: [PATCH 129/139] fix: merge conflict --- .../public/js/frappe/form/footer/form_timeline.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index 7182aa667d..0070d384d7 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -453,24 +453,17 @@ class FormTimeline extends BaseTimeline { let edit_wrapper = $(`
`).hide(); let edit_box = this.make_editable(edit_wrapper); let content_wrapper = comment_wrapper.find('.content'); -<<<<<<< HEAD let more_actions_wrapper = comment_wrapper.find('.more-actions'); - if (frappe.model.can_delete("Comment")) { + if (frappe.model.can_delete("Comment") && ( + frappe.session.user == doc.owner || + frappe.user.has_role("System Manager") + )) { const delete_option = $(`
  • ${__("Delete")}
  • -======= - - let delete_button = $(); - if (frappe.model.can_delete("Comment") && (frappe.session.user == doc.owner || frappe.user.has_role("System Manager"))) { - delete_button = $(` - ->>>>>>> e4137ca8a1 (fix: Other user must not able to delete other user's comment except System Manager (#16018)) `).click(() => this.delete_comment(doc.name)); more_actions_wrapper.find('.dropdown-menu').append(delete_option); } From 880832671aec005d2f0f5bb256ec1a430416eaa6 Mon Sep 17 00:00:00 2001 From: Karthikeyan Singaravelan Date: Mon, 21 Mar 2022 11:03:59 +0000 Subject: [PATCH 130/139] fix: use assertEqual instead of assertEquals for Python 3.11 compatibility --- frappe/core/doctype/file/test_file.py | 6 +++--- frappe/email/doctype/notification/test_notification.py | 2 +- frappe/tests/test_base_document.py | 6 +++--- frappe/tests/test_db.py | 4 ++-- frappe/tests/test_document.py | 6 +++--- frappe/tests/test_search.py | 4 ++-- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/frappe/core/doctype/file/test_file.py b/frappe/core/doctype/file/test_file.py index d8e748a518..fb98a18d6e 100644 --- a/frappe/core/doctype/file/test_file.py +++ b/frappe/core/doctype/file/test_file.py @@ -382,7 +382,7 @@ class TestFile(unittest.TestCase): }).insert(ignore_permissions=True) test_file.make_thumbnail() - self.assertEquals(test_file.thumbnail_url, '/files/image_small.jpg') + self.assertEqual(test_file.thumbnail_url, '/files/image_small.jpg') # test web image without extension test_file = frappe.get_doc({ @@ -399,7 +399,7 @@ class TestFile(unittest.TestCase): test_file.reload() test_file.file_url = "/files/image_small.jpg" test_file.make_thumbnail(suffix="xs", crop=True) - self.assertEquals(test_file.thumbnail_url, '/files/image_small_xs.jpg') + self.assertEqual(test_file.thumbnail_url, '/files/image_small_xs.jpg') frappe.clear_messages() test_file.db_set('thumbnail_url', None) @@ -407,7 +407,7 @@ class TestFile(unittest.TestCase): test_file.file_url = frappe.utils.get_url('unknown.jpg') test_file.make_thumbnail(suffix="xs") self.assertEqual(json.loads(frappe.message_log[0]).get("message"), f"File '{frappe.utils.get_url('unknown.jpg')}' not found") - self.assertEquals(test_file.thumbnail_url, None) + self.assertEqual(test_file.thumbnail_url, None) def test_file_unzip(self): file_path = frappe.get_app_path('frappe', 'www/_test/assets/file.zip') diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index f05d35be3e..f6f216ada2 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -240,7 +240,7 @@ class TestNotification(unittest.TestCase): self.assertTrue(email_queue) # check if description is changed after alert since set_property_after_alert is set - self.assertEquals(todo.description, 'Changed by Notification') + self.assertEqual(todo.description, 'Changed by Notification') recipients = [d.recipient for d in email_queue.recipients] self.assertTrue('test2@example.com' in recipients) diff --git a/frappe/tests/test_base_document.py b/frappe/tests/test_base_document.py index 7e165e9045..fda795b5b6 100644 --- a/frappe/tests/test_base_document.py +++ b/frappe/tests/test_base_document.py @@ -7,12 +7,12 @@ class TestBaseDocument(unittest.TestCase): def test_docstatus(self): doc = BaseDocument({"docstatus": 0}) self.assertTrue(doc.docstatus.is_draft()) - self.assertEquals(doc.docstatus, 0) + self.assertEqual(doc.docstatus, 0) doc.docstatus = 1 self.assertTrue(doc.docstatus.is_submitted()) - self.assertEquals(doc.docstatus, 1) + self.assertEqual(doc.docstatus, 1) doc.docstatus = 2 self.assertTrue(doc.docstatus.is_cancelled()) - self.assertEquals(doc.docstatus, 2) + self.assertEqual(doc.docstatus, 2) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 1cd9287ec3..19b683aa75 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -386,7 +386,7 @@ class TestDDLCommandsMaria(unittest.TestCase): WHERE Key_name = '{index_name}'; """ ) - self.assertEquals(len(indexs_in_table), 2) + self.assertEqual(len(indexs_in_table), 2) class TestDBSetValue(unittest.TestCase): @@ -590,7 +590,7 @@ class TestDDLCommandsPost(unittest.TestCase): AND indexname = '{index_name}' ; """, ) - self.assertEquals(len(indexs_in_table), 1) + self.assertEqual(len(indexs_in_table), 1) @run_only_if(db_type_is.POSTGRES) def test_modify_query(self): diff --git a/frappe/tests/test_document.py b/frappe/tests/test_document.py index 55dbf001f9..169d1ebb2c 100644 --- a/frappe/tests/test_document.py +++ b/frappe/tests/test_document.py @@ -260,15 +260,15 @@ class TestDocument(unittest.TestCase): 'doctype': 'Test Formatted', 'currency': 100000 }) - self.assertEquals(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') + self.assertEqual(d.get_formatted('currency', currency='INR', format="#,###.##"), '₹ 100,000.00') def test_limit_for_get(self): doc = frappe.get_doc("DocType", "DocType") # assuming DocType has more than 3 Data fields - self.assertEquals(len(doc.get("fields", limit=3)), 3) + self.assertEqual(len(doc.get("fields", limit=3)), 3) # limit with filters - self.assertEquals(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) + self.assertEqual(len(doc.get("fields", filters={"fieldtype": "Data"}, limit=3)), 3) def test_virtual_fields(self): """Virtual fields are accessible via API and Form views, whenever .as_dict is invoked diff --git a/frappe/tests/test_search.py b/frappe/tests/test_search.py index f644f2dfcc..38a00f689a 100644 --- a/frappe/tests/test_search.py +++ b/frappe/tests/test_search.py @@ -70,10 +70,10 @@ class TestSearch(unittest.TestCase): result = frappe.response['results'] # Check whether the result is sorted or not - self.assertEquals(self.parent_doctype_name, result[0]['value']) + self.assertEqual(self.parent_doctype_name, result[0]['value']) # Check whether searching for parent also list out children - self.assertEquals(len(result), len(self.child_doctypes_names) + 1) + self.assertEqual(len(result), len(self.child_doctypes_names) + 1) #Search for the word "pay", part of the word "pays" (country) in french. def test_link_search_in_foreign_language(self): From aa0d10fe0ecc4df770f2f77a4e05db7880384ed8 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Mon, 21 Mar 2022 17:24:18 +0530 Subject: [PATCH 131/139] fix: First set in model then save attachment --- frappe/public/js/frappe/form/controls/attach.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index bd66225171..01af3bbc89 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -110,9 +110,9 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro return this.value || null; } - on_upload_complete(attachment) { + async on_upload_complete(attachment) { if(this.frm) { - this.parse_validate_and_set_in_model(attachment.file_url); + await this.parse_validate_and_set_in_model(attachment.file_url); this.frm.attachments.update_attachment(attachment); this.frm.doc.docstatus == 1 ? this.frm.save('Update') : this.frm.save(); } From d97c7e7cafcebb79840b9bc9a584ce9955ac70fd Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 21 Mar 2022 18:05:01 +0530 Subject: [PATCH 132/139] fix: resolved conflicts in permissions.py --- frappe/permissions.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/frappe/permissions.py b/frappe/permissions.py index 985d398749..a6c17fb59f 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -7,11 +7,7 @@ import frappe.share from frappe import _, msgprint from frappe.utils import cint from frappe.query_builder import DocType -<<<<<<< HEAD -======= -import frappe.share ->>>>>>> 6612232bab (fix: setting permissions to any role of some doctypes is not working) rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share") @@ -470,12 +466,6 @@ def update_permission_property(doctype, role, permlevel, ptype, value=None, vali table = DocType("Custom DocPerm") frappe.qb.update(table).set(ptype, value).where(table.name == name).run() -<<<<<<< HEAD -======= - table = DocType("Custom DocPerm") - frappe.qb.update(table).set(ptype, value).where(table.name == name).run() - ->>>>>>> 6612232bab (fix: setting permissions to any role of some doctypes is not working) if validate: validate_permissions_for_doctype(doctype) @@ -604,4 +594,4 @@ def is_parent_valid(child_doctype, parent_doctype): from frappe.core.utils import find parent_meta = frappe.get_meta(parent_doctype) child_table_field_exists = find(parent_meta.get_table_fields(), lambda d: d.options == child_doctype) - return not parent_meta.istable and child_table_field_exists \ No newline at end of file + return not parent_meta.istable and child_table_field_exists From 5c6c9bb5c461fcdde9cdfda664451b8332a7fc8c Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Mon, 21 Mar 2022 18:16:54 +0530 Subject: [PATCH 133/139] fix: resolved conflicts in database.py --- frappe/database/database.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index 3ce2b32fc8..7bf3b46bf0 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -116,19 +116,11 @@ class Database(object): """ query = str(query) -<<<<<<< HEAD if not run: return query -======= -<<<<<<< HEAD - # remove \n \t from start of query and replace them with space anywhere in middle - query = re.sub(r'\s', ' ', query).lstrip() ->>>>>>> ac5effc7dd (fix: remove tab \t and newlines \n from start of query and remove from middle) -======= # remove \n \t from start and end of query query = re.sub(r'^\s*|\s*$', '', query) ->>>>>>> 7bb172365f (fix: only remove \n\t from start and end) if re.search(r'ifnull\(', query, flags=re.IGNORECASE): # replaces ifnull in query with coalesce From 512c62248769ce3a92cf433ded2bb53db58f281c Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Mar 2022 20:51:35 +0100 Subject: [PATCH 134/139] test: make sure `exists` doesn't eat the doctype key --- frappe/tests/test_db.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index 19b683aa75..10c601db00 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -327,7 +327,13 @@ class TestDB(unittest.TestCase): self.assertEqual(frappe.db.exists(dt, dn, cache=True), dn) self.assertEqual(frappe.db.exists(dt, dn), dn) self.assertEqual(frappe.db.exists(dt, {"name": ("=", dn)}), dn) - self.assertEqual(frappe.db.exists({"doctype": dt, "name": ("like", "Admin%")}), dn) + + filters = {"doctype": dt, "name": ("like", "Admin%")} + self.assertEqual(frappe.db.exists(filters), dn) + self.assertEqual( + filters["doctype"], dt + ) # make sure that doctype was not removed from filters + self.assertEqual(frappe.db.exists(dt, [["name", "=", dn]]), dn) From 44a7c0dd9318d984b20ae95180f5cf35eb146c28 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Mar 2022 20:51:59 +0100 Subject: [PATCH 135/139] fix: copy dict before popping keys --- frappe/database/database.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/database/database.py b/frappe/database/database.py index b1b5abffca..82a7e6f919 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -919,8 +919,8 @@ class Database(object): return dn if isinstance(dt, dict): - _dt = dt.pop("doctype") - dt, dn = _dt, dt + dt = dt.copy() # don't modify the original dict + dt, dn = dt.pop("doctype"), dt return self.get_value(dt, dn, ignore=True, cache=cache) From 179c9f117c7ecf8c9c2882ecf192031331294bbf Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Mar 2022 21:12:05 +0100 Subject: [PATCH 136/139] perf: exists is already called in delete_doc --- frappe/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 86f8be35ea..80dd2f5f15 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -978,8 +978,7 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa def delete_doc_if_exists(doctype, name, force=0): """Delete document if exists.""" - if db.exists(doctype, name): - delete_doc(doctype, name, force=force) + delete_doc(doctype, name, force=force, ignore_missing=True) def reload_doctype(doctype, force=False, reset_permissions=False): """Reload DocType from model (`[module]/[doctype]/[name]/[name].json`) files.""" From 8cf2bf89534039b5b23a32177b469c1ddb1bd521 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Mon, 21 Mar 2022 23:09:58 +0100 Subject: [PATCH 137/139] refactor: call getfullargspec only once --- frappe/__init__.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frappe/__init__.py b/frappe/__init__.py index 86f8be35ea..1501fda81a 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1252,9 +1252,10 @@ def get_newargs(fn, kwargs): if hasattr(fn, 'fnargs'): fnargs = fn.fnargs else: - fnargs = inspect.getfullargspec(fn).args - fnargs.extend(inspect.getfullargspec(fn).kwonlyargs) - varkw = inspect.getfullargspec(fn).varkw + fullargspec = inspect.getfullargspec(fn) + fnargs = fullargspec.args + fnargs.extend(fullargspec.kwonlyargs) + varkw = fullargspec.varkw newargs = {} for a in kwargs: From 7a9536332eb862d359e5bf0d0b9819b758dc45d7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Tue, 22 Mar 2022 09:16:39 +0530 Subject: [PATCH 138/139] feat: Hide page head while scrolling down - To create more reading area in the form --- frappe/public/js/frappe/ui/page.js | 14 +++++++++----- frappe/public/scss/desk/page.scss | 1 + 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/frappe/public/js/frappe/ui/page.js b/frappe/public/js/frappe/ui/page.js index 91a2390cdb..e3134b1f38 100644 --- a/frappe/public/js/frappe/ui/page.js +++ b/frappe/public/js/frappe/ui/page.js @@ -47,13 +47,17 @@ frappe.ui.Page = class Page { } setup_scroll_handler() { - window.addEventListener('scroll', () => { - if (document.documentElement.scrollTop) { - $('.page-head').toggleClass('drop-shadow', true); + let last_scroll = 0; + window.addEventListener('scroll', frappe.utils.throttle(() => { + $('.page-head').toggleClass('drop-shadow', !!document.documentElement.scrollTop); + let current_scroll = document.documentElement.scrollTop; + if (current_scroll > 0 && last_scroll <= current_scroll) { + $('.page-head').css("top", "-15px"); } else { - $('.page-head').removeClass('drop-shadow'); + $('.page-head').css("top", "var(--navbar-height)"); } - }); + last_scroll = current_scroll; + }), 500); } get_empty_state(title, message, primary_action) { diff --git a/frappe/public/scss/desk/page.scss b/frappe/public/scss/desk/page.scss index f0a9152cfb..2df349cb6c 100644 --- a/frappe/public/scss/desk/page.scss +++ b/frappe/public/scss/desk/page.scss @@ -88,6 +88,7 @@ top: var(--navbar-height); background: var(--bg-color); margin-bottom: 5px; + transition: 0.5s top; .page-head-content { height: var(--page-head-height); } From d30f9e1d78b5e2e02ae9b7afedccd5451ab3d94f Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 22 Mar 2022 11:23:43 +0530 Subject: [PATCH 139/139] fix: wait until attach is cleared before saving --- frappe/public/js/frappe/form/controls/attach.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frappe/public/js/frappe/form/controls/attach.js b/frappe/public/js/frappe/form/controls/attach.js index 01af3bbc89..a91058a208 100644 --- a/frappe/public/js/frappe/form/controls/attach.js +++ b/frappe/public/js/frappe/form/controls/attach.js @@ -37,8 +37,8 @@ frappe.ui.form.ControlAttach = class ControlAttach extends frappe.ui.form.Contro if(this.frm) { me.parse_validate_and_set_in_model(null); me.refresh(); - me.frm.attachments.remove_attachment_by_filename(me.value, function() { - me.parse_validate_and_set_in_model(null); + me.frm.attachments.remove_attachment_by_filename(me.value, async () => { + await me.parse_validate_and_set_in_model(null); me.refresh(); me.frm.doc.docstatus == 1 ? me.frm.save('Update') : me.frm.save(); });