From 9bfc97a823328163d7c632072528c163e66c9e23 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Tue, 23 Feb 2021 18:53:24 +0530 Subject: [PATCH 001/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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/104] 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 089/104] 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 090/104] 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 091/104] 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 092/104] 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 093/104] 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 094/104] 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 095/104] 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 096/104] 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 097/104] 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 14a4e35d8d9eb5917fa680160fd5ca5b328ed2d2 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 17 Mar 2022 09:50:14 +0530 Subject: [PATCH 098/104] 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 099/104] 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 100/104] 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 101/104] 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 c4be72c2d404f337ba10d747e27525feb66aa9b7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Fri, 18 Mar 2022 12:18:15 +0530 Subject: [PATCH 102/104] 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 103/104] 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 104/104] 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}));