diff --git a/codecov.yml b/codecov.yml index eb81252b61..41b22001a5 100644 --- a/codecov.yml +++ b/codecov.yml @@ -1,9 +1,13 @@ codecov: require_ci_to_pass: yes + +coverage: status: project: default: + target: auto threshold: 0.5% + comment: - layout: "diff, flags, files" + layout: "diff" require_changes: true diff --git a/cypress/integration/api.js b/cypress/integration/api.js index 7a5b1611b0..e8c39e6e25 100644 --- a/cypress/integration/api.js +++ b/cypress/integration/api.js @@ -31,8 +31,13 @@ context('API Resources', () => { }); it('Removes the Comments', () => { - cy.get_list('Comment').then(body => body.data.forEach(comment => { - cy.remove_doc('Comment', comment.name); - })); + cy.get_list('Comment').then(body => { + let comment_names = []; + body.data.map(comment => comment_names.push(comment.name)); + comment_names = [...new Set(comment_names)]; // remove duplicates + comment_names.forEach((comment_name) => { + cy.remove_doc('Comment', comment_name); + }); + }); }); }); diff --git a/cypress/integration/control_float.js b/cypress/integration/control_float.js new file mode 100644 index 0000000000..670d1fe73e --- /dev/null +++ b/cypress/integration/control_float.js @@ -0,0 +1,93 @@ +context("Control Float", () => { + before(() => { + cy.login(); + cy.visit("/app/website"); + }); + + function get_dialog_with_float() { + return cy.dialog({ + title: "Float Check", + fields: [ + { + fieldname: "float_number", + fieldtype: "Float", + Label: "Float" + } + ] + }); + } + + it("check value changes", () => { + get_dialog_with_float().as("dialog"); + + let data = get_data(); + data.forEach(x => { + cy.window() + .its("frappe") + .then(frappe => { + frappe.boot.sysdefaults.number_format = x.number_format; + }); + x.values.forEach(d => { + cy.get_field("float_number", "Float").clear(); + cy.fill_field("float_number", d.input, "Float").blur(); + cy.get_field("float_number", "Float").should( + "have.value", + d.blur_expected + ); + + cy.get_field("float_number", "Float").focus(); + cy.get_field("float_number", "Float").blur(); + cy.get_field("float_number", "Float").focus(); + cy.get_field("float_number", "Float").should( + "have.value", + d.focus_expected + ); + }); + }); + }); + + function get_data() { + return [ + { + number_format: "#.###,##", + values: [ + { + input: "364.87,334", + blur_expected: "36.487,334", + focus_expected: "36487.334" + }, + { + input: "36487,334", + blur_expected: "36.487,334", + focus_expected: "36487.334" + }, + { + input: "100", + blur_expected: "100,000", + focus_expected: "100" + } + ] + }, + { + number_format: "#,###.##", + values: [ + { + input: "364,87.334", + blur_expected: "36,487.334", + focus_expected: "36487.334" + }, + { + input: "36487.334", + blur_expected: "36,487.334", + focus_expected: "36487.334" + }, + { + input: "100", + blur_expected: "100.000", + focus_expected: "100" + } + ] + } + ]; + } +}); diff --git a/cypress/integration/datetime_field_form_validation.js b/cypress/integration/datetime_field_form_validation.js index 66fdde6863..ef47a0fbf7 100644 --- a/cypress/integration/datetime_field_form_validation.js +++ b/cypress/integration/datetime_field_form_validation.js @@ -1,19 +1,19 @@ -context('Datetime Field Validation', () => { - before(() => { - cy.login(); - cy.visit('/app/communication'); - cy.window().its('frappe').then(frappe => { - frappe.call("frappe.tests.ui_test_helpers.create_communication_records"); - }); - }); +// TODO: Enable this again +// currently this is flaky possibly because of different timezone in CI - // validating datetime field value when value is set from backend and get validated on form load. - it('datetime field form validation', () => { - cy.visit('/app/communication'); - cy.get('a[title="Test Form Communication 1"]').invoke('attr', 'data-name') - .then((name) => { - cy.visit(`/app/communication/${name}`); - cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); - }); - }); -}); \ No newline at end of file +// context('Datetime Field Validation', () => { +// before(() => { +// cy.login(); +// cy.visit('/app/communication'); +// }); + +// it('datetime field form validation', () => { +// // validating datetime field value when value is set from backend and get validated on form load. +// cy.window().its('frappe').then(frappe => { +// return frappe.xcall("frappe.tests.ui_test_helpers.create_communication_record"); +// }).then(doc => { +// cy.visit(`/app/communication/${doc.name}`); +// cy.get('.indicator-pill').should('contain', 'Open').should('have.class', 'red'); +// }); +// }); +// }); \ No newline at end of file diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 633d1335ab..298bb20432 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -7,11 +7,11 @@ context('List View', () => { }); }); it('enables "Actions" button', () => { - const actions = ['Approve', 'Reject', 'Edit', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; + const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; cy.go_to_list('ToDo'); cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); - cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => { + cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 9).each((el, index) => { cy.wrap(el).contains(actions[index]); }).then((elements) => { cy.intercept({ diff --git a/cypress/integration/sidebar.js b/cypress/integration/sidebar.js index e05f1877bf..cd771430c6 100644 --- a/cypress/integration/sidebar.js +++ b/cypress/integration/sidebar.js @@ -6,12 +6,12 @@ context('Sidebar', () => { }); it('Test for checking "Assigned To" counter value, adding filter and adding & removing an assignment', () => { - cy.click_sidebar_button(0); + cy.click_sidebar_button("Assigned To"); //To check if no filter is available in "Assigned To" dropdown cy.get('.empty-state').should('contain', 'No filters found'); - cy.click_sidebar_button(1); + cy.click_sidebar_button("Created By"); //To check if "Created By" dropdown contains filter cy.get('.group-by-item > .dropdown-item').should('contain', 'Me'); @@ -22,7 +22,7 @@ context('Sidebar', () => { cy.get_field('assign_to_me', 'Check').click(); cy.get('.modal-footer > .standard-actions > .btn-primary').click(); cy.visit('/app/doctype'); - cy.click_sidebar_button(0); + cy.click_sidebar_button("Assigned To"); //To check if filter is added in "Assigned To" dropdown after assignment cy.get('.group-by-field.show > .dropdown-menu > .group-by-item > .dropdown-item').should('contain', '1'); @@ -38,20 +38,19 @@ context('Sidebar', () => { cy.get('.fieldname-select-area > .awesomplete > .form-control').should('have.value', 'Assigned To'); cy.get('.condition').should('have.value', 'like'); cy.get('.filter-field > .form-group > .input-with-feedback').should('have.value', '%Administrator%'); + cy.click_filter_button(); //To remove the applied filter - cy.get('.filter-action-buttons > div > .btn-secondary').contains('Clear Filters').click(); - cy.click_filter_button(); - cy.get('.filter-selector > .btn').should('contain', 'Filter'); + cy.clear_filters(); //To remove the assignment cy.visit('/app/doctype'); cy.click_listview_row_item(0); cy.get('.assignments > .avatar-group > .avatar > .avatar-frame').click(); cy.get('.remove-btn').click({force: true}); - cy.get('.modal.show > .modal-dialog > .modal-content > .modal-header > .modal-actions > .btn-modal-close').click(); + cy.hide_dialog(); cy.visit('/app/doctype'); - cy.click_sidebar_button(0); + cy.click_sidebar_button("Assigned To"); cy.get('.empty-state').should('contain', 'No filters found'); }); }); \ No newline at end of file diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js index 7a8f3a159b..6387485220 100644 --- a/cypress/integration/timeline.js +++ b/cypress/integration/timeline.js @@ -4,11 +4,11 @@ context('Timeline', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/todo'); }); it('Adding new ToDo, adding new comment, verifying comment addition & deletion and deleting ToDo', () => { //Adding new ToDo + cy.visit('/app/todo'); cy.click_listview_primary_button('Add ToDo'); cy.findByRole('button', {name: 'Edit in full page'}).click(); cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); @@ -28,15 +28,15 @@ context('Timeline', () => { cy.get('.timeline-content').should('contain', 'Testing Timeline'); //Editing comment - cy.click_timeline_action_btn(0); + cy.click_timeline_action_btn("Edit"); cy.get('.timeline-content [data-fieldname="comment"] .ql-editor').first().type(' 123'); - cy.click_timeline_action_btn(0); + cy.click_timeline_action_btn("Save"); //To check if the edited comment text is visible in timeline content cy.get('.timeline-content').should('contain', 'Testing Timeline 123'); //Discarding comment - cy.click_timeline_action_btn(0); + cy.click_timeline_action_btn("Edit"); cy.findByRole('button', {name: 'Dismiss'}).click(); //To check if after discarding the timeline content is same as previous @@ -81,7 +81,7 @@ context('Timeline', () => { cy.visit('/app/custom-submittable-doctype'); cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click(); cy.findByRole('button', {name: 'Actions'}).click(); - cy.get('.actions-btn-group > .dropdown-menu > li > .grey-link').eq(7).click(); + cy.get('.actions-btn-group > .dropdown-menu > li > .dropdown-item').contains("Delete").click(); cy.click_modal_primary_button('Yes', {force: true, delay: 700}); //Deleting the custom doctype diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c941652487..47c37a56a0 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -187,7 +187,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { if (fieldtype === 'Select') { cy.get('@input').select(value); } else { - cy.get('@input').type(value, {waitForAnimations: false, force: true}); + cy.get('@input').type(value, {waitForAnimations: false, force: true, delay: 100}); } return cy.get('@input'); }); @@ -252,7 +252,8 @@ Cypress.Commands.add('new_form', doctype => { }); Cypress.Commands.add('go_to_list', doctype => { - cy.visit(`/app/list/${doctype}/list`); + let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); + cy.visit(`/app/${dt_in_route}`); }); Cypress.Commands.add('clear_cache', () => { @@ -316,7 +317,11 @@ Cypress.Commands.add('add_filter', () => { }); Cypress.Commands.add('clear_filters', () => { - cy.get('.filter-section .filter-button').click(); + cy.intercept({ + method: 'POST', + url: 'api/method/frappe.model.utils.user_settings.save' + }).as('filter-saved'); + cy.get('.filter-section .filter-button').click({force: true}); cy.wait(300); cy.get('.filter-popover').should('exist'); cy.get('.filter-popover').find('.clear-filters').click(); @@ -324,16 +329,15 @@ Cypress.Commands.add('clear_filters', () => { cy.window().its('cur_list').then(cur_list => { cur_list && cur_list.filter_area && cur_list.filter_area.clear(); }); - - + cy.wait('@filter-saved'); }); Cypress.Commands.add('click_modal_primary_button', (btn_name) => { cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true}); }); -Cypress.Commands.add('click_sidebar_button', (btn_no) => { - cy.get('.list-group-by-fields > .group-by-field > .btn').eq(btn_no).click(); +Cypress.Commands.add('click_sidebar_button', (btn_name) => { + cy.get('.list-group-by-fields .list-link > a').contains(btn_name).click({force: true}); }); Cypress.Commands.add('click_listview_row_item', (row_no) => { @@ -348,6 +352,6 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => { cy.get('.primary-action').contains(btn_name).click({force: true}); }); -Cypress.Commands.add('click_timeline_action_btn', (btn_no) => { - cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').eq(btn_no).first().click(); +Cypress.Commands.add('click_timeline_action_btn', (btn_name) => { + cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click(); }); \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index 7c6005a350..38904c68d0 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -618,8 +618,6 @@ def read_only(): try: retval = fn(*args, **get_newargs(fn, kwargs)) - except: - raise finally: if local and hasattr(local, 'primary_db'): local.db.close() @@ -629,6 +627,29 @@ def read_only(): return wrapper_fn return innfn +def write_only(): + # if replica connection exists, we have to replace it momentarily with the primary connection + def innfn(fn): + def wrapper_fn(*args, **kwargs): + primary_db = getattr(local, "primary_db", None) + replica_db = getattr(local, "replica_db", None) + in_read_only = getattr(local, "db", None) != primary_db + + # switch to primary connection + if in_read_only and primary_db: + local.db = local.primary_db + + try: + retval = fn(*args, **get_newargs(fn, kwargs)) + finally: + # switch back to replica connection + if in_read_only and replica_db: + local.db = replica_db + + return retval + return wrapper_fn + return innfn + def only_for(roles, message=False): """Raise `frappe.PermissionError` if the user does not have any of the given **Roles**. diff --git a/frappe/contacts/doctype/address/address.py b/frappe/contacts/doctype/address/address.py index 5f0619d170..5d0ed18d5f 100644 --- a/frappe/contacts/doctype/address/address.py +++ b/frappe/contacts/doctype/address/address.py @@ -65,7 +65,7 @@ class Address(Document): def has_link(self, doctype, name): for link in self.links: - if link.link_doctype==doctype and link.link_name== name: + if link.link_doctype == doctype and link.link_name == name: return True def has_common_link(self, doc): diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index a1aa8408bf..dfb9ff2973 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -47,14 +47,14 @@ class Contact(Document): def get_link_for(self, link_doctype): '''Return the link name, if exists for the given link DocType''' for link in self.links: - if link.link_doctype==link_doctype: + if link.link_doctype == link_doctype: return link.link_name return None def has_link(self, doctype, name): for link in self.links: - if link.link_doctype==doctype and link.link_name== name: + if link.link_doctype == doctype and link.link_name == name: return True def has_common_link(self, doc): diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py index 0f5776ce2f..d93da02d25 100644 --- a/frappe/core/doctype/access_log/access_log.py +++ b/frappe/core/doctype/access_log/access_log.py @@ -9,6 +9,7 @@ class AccessLog(Document): @frappe.whitelist() +@frappe.write_only() def make_access_log(doctype=None, document=None, method=None, file_type=None, report_name=None, filters=None, page=None, columns=None): diff --git a/frappe/core/doctype/user_type/user_type.py b/frappe/core/doctype/user_type/user_type.py index 2e5254b622..79a90933e7 100644 --- a/frappe/core/doctype/user_type/user_type.py +++ b/frappe/core/doctype/user_type/user_type.py @@ -36,8 +36,11 @@ class UserType(Document): if not self.user_doctypes: return - modules = frappe.get_all('DocType', fields=['distinct module as module'], - filters={'name': ('in', [d.document_type for d in self.user_doctypes])}) + modules = frappe.get_all("DocType", + fields=["module"], + filters={"name": ("in", [d.document_type for d in self.user_doctypes])}, + distinct=True, + ) self.set('user_type_modules', []) for row in modules: diff --git a/frappe/core/doctype/version/version.css b/frappe/core/doctype/version/version.css deleted file mode 100644 index 769b352585..0000000000 --- a/frappe/core/doctype/version/version.css +++ /dev/null @@ -1,21 +0,0 @@ -.version-info { - overflow: auto; -} - -.version-info pre { - border: 0px; - margin: 0px; - background-color: inherit; -} - -.version-info .table { - background-color: inherit; -} - -.version-info .success { - background-color: #dff0d8 !important; -} - -.version-info .danger { - background-color: #f2dede !important; -} diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index d4a119804b..71acefe17c 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -256,11 +256,11 @@ class MariaDBDatabase(Database): index_name=index_name )) - def add_index(self, doctype, fields, index_name=None): + def add_index(self, doctype: str, fields: List, index_name: str = None): """Creates an index with given fields if not already created. Index name will be `fieldname1_fieldname2_index`""" index_name = index_name or self.get_index_name(fields) - table_name = 'tab' + doctype + table_name = get_table_name(doctype) if not self.has_index(table_name, index_name): self.commit() self.sql("""ALTER TABLE `%s` diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index 00e60fb8d2..264d3bbf14 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -258,14 +258,14 @@ class PostgresDatabase(Database): return self.sql("""SELECT 1 FROM pg_indexes WHERE tablename='{table_name}' and indexname='{index_name}' limit 1""".format(table_name=table_name, index_name=index_name)) - def add_index(self, doctype, fields, index_name=None): + def add_index(self, doctype: str, fields: List, index_name: str = None): """Creates an index with given fields if not already created. Index name will be `fieldname1_fieldname2_index`""" + table_name = get_table_name(doctype) index_name = index_name or self.get_index_name(fields) - table_name = 'tab' + doctype + fields_str = '", "'.join(re.sub(r"\(.*\)", "", field) for field in fields) - self.commit() - self.sql("""CREATE INDEX IF NOT EXISTS "{}" ON `{}`("{}")""".format(index_name, table_name, '", "'.join(fields))) + self.sql_ddl(f'CREATE INDEX IF NOT EXISTS "{index_name}" ON `{table_name}` ("{fields_str}")') def add_unique(self, doctype, fields, constraint_name=None): if isinstance(fields, str): diff --git a/frappe/desk/doctype/system_console/system_console.js b/frappe/desk/doctype/system_console/system_console.js index 48dd2ba108..2d097f01ad 100644 --- a/frappe/desk/doctype/system_console/system_console.js +++ b/frappe/desk/doctype/system_console/system_console.js @@ -20,5 +20,46 @@ frappe.ui.form.on('System Console', { $btn.text(__('Execute')); }); }); + }, + + show_processlist: function(frm) { + if (frm.doc.show_processlist) { + // keep refreshing every 5 seconds + frm.events.refresh_processlist(frm); + frm.processlist_interval = setInterval(() => frm.events.refresh_processlist(frm), 5000); + } else { + if (frm.processlist_interval) { + + // end it + clearInterval(frm.processlist_interval); + } + } + }, + + refresh_processlist: function(frm) { + let timestamp = new Date(); + frappe.call('frappe.desk.doctype.system_console.system_console.show_processlist').then(r => { + let rows = ''; + for (let row of r.message) { + rows += ` + ${row.Id} + ${row.Time} + ${row.State} + ${row.Info} + ${row.Progress} + ` + } + frm.get_field('processlist').html(` +

Requested on: ${timestamp}

+ + + + ${rows}`); + }); } }); diff --git a/frappe/desk/doctype/system_console/system_console.json b/frappe/desk/doctype/system_console/system_console.json index 14e36e6fd3..753e672cdc 100644 --- a/frappe/desk/doctype/system_console/system_console.json +++ b/frappe/desk/doctype/system_console/system_console.json @@ -17,9 +17,13 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "execute_section", "console", "commit", - "output" + "output", + "database_processes_section", + "show_processlist", + "processlist" ], "fields": [ { @@ -40,13 +44,34 @@ "fieldname": "commit", "fieldtype": "Check", "label": "Commit" + }, + { + "fieldname": "execute_section", + "fieldtype": "Section Break", + "label": "Execute" + }, + { + "fieldname": "database_processes_section", + "fieldtype": "Section Break", + "label": "Database Processes" + }, + { + "default": "0", + "fieldname": "show_processlist", + "fieldtype": "Check", + "label": "Show Processlist" + }, + { + "fieldname": "processlist", + "fieldtype": "HTML", + "label": "processlist" } ], "hide_toolbar": 1, "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-08-21 14:44:35.296877", + "modified": "2021-09-09 13:10:14.237113", "modified_by": "Administrator", "module": "Desk", "name": "System Console", @@ -65,4 +90,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/desk/doctype/system_console/system_console.py b/frappe/desk/doctype/system_console/system_console.py index f7f31cc3ba..8382dc8638 100644 --- a/frappe/desk/doctype/system_console/system_console.py +++ b/frappe/desk/doctype/system_console/system_console.py @@ -33,4 +33,9 @@ class SystemConsole(Document): def execute_code(doc): console = frappe.get_doc(json.loads(doc)) console.run() - return console.as_dict() \ No newline at end of file + return console.as_dict() + +@frappe.whitelist() +def show_processlist(): + frappe.only_for('System Manager') + return frappe.db.sql('show full processlist', as_dict=1) diff --git a/frappe/hooks.py b/frappe/hooks.py index f3d25d6bf4..3cfdebc12e 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -164,7 +164,8 @@ doc_events = { "after_rename": "frappe.desk.notifications.clear_doctype_notifications", "on_cancel": [ "frappe.desk.notifications.clear_doctype_notifications", - "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions" + "frappe.workflow.doctype.workflow_action.workflow_action.process_workflow_actions", + "frappe.event_streaming.doctype.event_update_log.event_update_log.notify_consumers" ], "on_trash": [ "frappe.desk.notifications.clear_doctype_notifications", diff --git a/frappe/public/js/frappe/form/controls/float.js b/frappe/public/js/frappe/form/controls/float.js index 89f8f23cc5..e00f74238c 100644 --- a/frappe/public/js/frappe/form/controls/float.js +++ b/frappe/public/js/frappe/form/controls/float.js @@ -1,4 +1,17 @@ frappe.ui.form.ControlFloat = class ControlFloat extends frappe.ui.form.ControlInt { + + make_input() { + super.make_input(); + const change_handler = e => { + if (this.change) this.change(e); + else { + let value = this.get_input_value(); + this.parse_validate_and_set_in_model(value, e); + } + }; + // convert to number format on focusout since focus converts it to flt. + this.$input.on("focusout", change_handler); + } parse(value) { value = this.eval_expression(value); return isNaN(parseFloat(value)) ? null : flt(value, this.get_precision()); diff --git a/frappe/public/js/frappe/ui/messages.js b/frappe/public/js/frappe/ui/messages.js index 067fed233c..185d275ac3 100644 --- a/frappe/public/js/frappe/ui/messages.js +++ b/frappe/public/js/frappe/ui/messages.js @@ -140,7 +140,7 @@ frappe.msgprint = function(msg, title, is_minimizable) { return; } - if(data.alert) { + if(data.alert || data.toast) { frappe.show_alert(data); return; } @@ -361,7 +361,7 @@ frappe.hide_progress = function() { } // Floating Message -frappe.show_alert = function(message, seconds=7, actions={}) { +frappe.show_alert = frappe.toast = function(message, seconds=7, actions={}) { let indicator_icon_map = { 'orange': "solid-warning", 'yellow': "solid-warning", diff --git a/frappe/public/js/frappe/utils/number_format.js b/frappe/public/js/frappe/utils/number_format.js index 1c39f42ec5..32e3669caf 100644 --- a/frappe/public/js/frappe/utils/number_format.js +++ b/frappe/public/js/frappe/utils/number_format.js @@ -8,7 +8,12 @@ if (!window.frappe) window.frappe = {}; function flt(v, decimals, number_format) { if (v == null || v == '') return 0; - if (typeof v !== "number") { + if (!(typeof v === "number" || String(parseFloat(v)) == v)) { + // cases in which this block should not run + // 1. 'v' is already a number + // 2. v is already parsed but in string form + // if (typeof v !== "number") { + v = v + ""; // strip currency symbol if exists @@ -25,6 +30,7 @@ function flt(v, decimals, number_format) { v = 0; } + v = parseFloat(v); if (decimals != null) return _round(v, decimals); return v; diff --git a/frappe/public/scss/desk/index.scss b/frappe/public/scss/desk/index.scss index c5b8271a36..1d1124bd58 100644 --- a/frappe/public/scss/desk/index.scss +++ b/frappe/public/scss/desk/index.scss @@ -47,3 +47,4 @@ @import "link_preview"; @import "../common/quill"; @import "plyr"; +@import "version"; diff --git a/frappe/public/scss/desk/version.scss b/frappe/public/scss/desk/version.scss new file mode 100644 index 0000000000..ddcf1f07a5 --- /dev/null +++ b/frappe/public/scss/desk/version.scss @@ -0,0 +1,33 @@ +.version-info { + overflow: auto; + + pre { + border: 0px; + margin: 0px; + background-color: inherit; + } + + .table { + background-color: inherit; + } + + .success { + background-color: var(--green-100) !important; + } + + .danger { + background-color: var(--red-100) !important; + } +} + +[data-theme="dark"] { + .version-info { + .danger, .success { + color: var(--gray-900); + + td { + color: var(--gray-900); + } + } + } +} \ No newline at end of file diff --git a/frappe/public/scss/website/index.scss b/frappe/public/scss/website/index.scss index 823ec9b08a..eb6e83e7fe 100644 --- a/frappe/public/scss/website/index.scss +++ b/frappe/public/scss/website/index.scss @@ -8,6 +8,7 @@ @import "../common/flex"; @import "../common/buttons"; @import "../common/modal"; +@import "../desk/toast"; @import "../common/indicator"; @import "../common/controls"; @import "../common/awesomeplete"; diff --git a/frappe/tests/test_db.py b/frappe/tests/test_db.py index e153220a1d..72bec78db7 100644 --- a/frappe/tests/test_db.py +++ b/frappe/tests/test_db.py @@ -197,6 +197,7 @@ class TestDB(unittest.TestCase): frappe.delete_doc(test_doctype, doc) clear_custom_fields(test_doctype) + @run_only_if(db_type_is.MARIADB) class TestDDLCommandsMaria(unittest.TestCase): test_table_name = "TestNotes" @@ -205,7 +206,7 @@ class TestDDLCommandsMaria(unittest.TestCase): frappe.db.commit() frappe.db.sql( f""" - CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL,PRIMARY KEY (`id`)); + CREATE TABLE `tab{self.test_table_name}` (`id` INT NULL, content TEXT, PRIMARY KEY (`id`)); """ ) @@ -230,7 +231,10 @@ class TestDDLCommandsMaria(unittest.TestCase): def test_describe(self) -> None: self.assertEqual( - (("id", "int(11)", "NO", "PRI", None, ""),), + ( + ("id", "int(11)", "NO", "PRI", None, ""), + ("content", "text", "YES", "", None, ""), + ), frappe.db.describe(self.test_table_name), ) @@ -240,6 +244,17 @@ class TestDDLCommandsMaria(unittest.TestCase): self.assertGreater(len(test_table_description), 0) self.assertIn("varchar(255)", test_table_description[0]) + def test_add_index(self) -> None: + index_name = "test_index" + frappe.db.add_index(self.test_table_name, ["id", "content(50)"], index_name) + indexs_in_table = frappe.db.sql( + f""" + SHOW INDEX FROM tab{self.test_table_name} + WHERE Key_name = '{index_name}'; + """ + ) + self.assertEquals(len(indexs_in_table), 2) + @run_only_if(db_type_is.POSTGRES) class TestDDLCommandsPost(unittest.TestCase): @@ -248,7 +263,7 @@ class TestDDLCommandsPost(unittest.TestCase): def setUp(self) -> None: frappe.db.sql( f""" - CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL,PRIMARY KEY ("id")) + CREATE TABLE "tab{self.test_table_name}" ("id" INT NULL, content text, PRIMARY KEY ("id")) """ ) @@ -273,7 +288,9 @@ class TestDDLCommandsPost(unittest.TestCase): self.test_table_name = new_table_name def test_describe(self) -> None: - self.assertEqual([("id",)], frappe.db.describe(self.test_table_name)) + self.assertEqual( + [("id",), ("content",)], frappe.db.describe(self.test_table_name) + ) def test_change_type(self) -> None: frappe.db.change_column_type(self.test_table_name, "id", "varchar(255)") @@ -292,3 +309,15 @@ class TestDDLCommandsPost(unittest.TestCase): self.assertGreater(len(check_change), 0) self.assertIn("character varying", check_change[0]) + def test_add_index(self) -> None: + index_name = "test_index" + frappe.db.add_index(self.test_table_name, ["id", "content(50)"], index_name) + indexs_in_table = frappe.db.sql( + f""" + SELECT indexname + FROM pg_indexes + WHERE tablename = 'tab{self.test_table_name}' + AND indexname = '{index_name}' ; + """, + ) + self.assertEquals(len(indexs_in_table), 1) \ No newline at end of file diff --git a/frappe/tests/test_translate.py b/frappe/tests/test_translate.py index 9620978c4f..949e4f9d77 100644 --- a/frappe/tests/test_translate.py +++ b/frappe/tests/test_translate.py @@ -63,11 +63,12 @@ class TestTranslate(unittest.TestCase): Case 2: frappe.form_dict._lang is not set, but preferred_language cookie is """ - with patch.object(frappe.translate, "get_preferred_language_cookie", return_value=second_lang): - set_request(method="POST", path="/", headers=[("Accept-Language", third_lang)]) + with patch.object(frappe.translate, "get_preferred_language_cookie", return_value='fr'): + set_request(method="POST", path="/", headers=[("Accept-Language", 'hr')]) return_val = get_language() - - self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)]) + # system default language + self.assertEqual(return_val, 'en') + self.assertNotIn(return_val, [second_lang, get_parent_language(second_lang)]) def test_guest_request_language_resolution_with_cookie(self): """Test for frappe.translate.get_language diff --git a/frappe/tests/ui_test_helpers.py b/frappe/tests/ui_test_helpers.py index d8ad728136..9f6ad70a35 100644 --- a/frappe/tests/ui_test_helpers.py +++ b/frappe/tests/ui_test_helpers.py @@ -62,16 +62,15 @@ def create_todo_records(): }).insert() @frappe.whitelist() -def create_communication_records(): - if frappe.db.get_all('Communication', {'subject': 'Test Form Communication 1'}): - return - - frappe.get_doc({ +def create_communication_record(): + doc = frappe.get_doc({ "doctype": "Communication", "recipients": "test@gmail.com", "subject": "Test Form Communication 1", "communication_date": frappe.utils.now_datetime(), - }).insert() + }) + doc.insert() + return doc @frappe.whitelist() def setup_workflow(): diff --git a/frappe/translations/af.csv b/frappe/translations/af.csv index b383fea767..fb52b4038d 100644 --- a/frappe/translations/af.csv +++ b/frappe/translations/af.csv @@ -3262,7 +3262,7 @@ Drop,drop, Drop Here,Drop hier, Drop files here,Laat lêers hier neer, Dynamic Template,Dinamiese sjabloon, -ERPNext Role,ERPVolgende rol, +ERPNext Role,ERPNext rol, Email / Notifications,E-pos / kennisgewings, Email Account setup please enter your password for: {0},Voer u wagwoord in vir die e-posrekening vir: {0}, Email Address whose Google Contacts are to be synced.,E-posadres waarvan die Google-kontakte gesinkroniseer moet word., diff --git a/frappe/translations/am.csv b/frappe/translations/am.csv index 52ed7c5f11..4dfa1bd8cd 100644 --- a/frappe/translations/am.csv +++ b/frappe/translations/am.csv @@ -3262,7 +3262,7 @@ Drop,ጣል ያድርጉ።, Drop Here,እዚህ ጣል ያድርጉ።, Drop files here,ፋይሎችን እዚህ ይጣሉ።, Dynamic Template,ተለዋዋጭ አብነት, -ERPNext Role,የኢአርኤክስ ቀጣይ ሚና።, +ERPNext Role,ERPNext ሚና, Email / Notifications,ኢሜይል / ማስታወቂያዎች, Email Account setup please enter your password for: {0},የኢሜል አካውንት ማዋቀር እባክዎን ለሚከተለው ይለፍ ቃልዎን ያስገቡ ፦ {0}, Email Address whose Google Contacts are to be synced.,የጉግል አድራሻዎች የሚመሳሰሉበት የኢሜል አድራሻ ፡፡, diff --git a/frappe/translations/da.csv b/frappe/translations/da.csv index 32f2ac9f6d..4516ed82ca 100644 --- a/frappe/translations/da.csv +++ b/frappe/translations/da.csv @@ -3262,7 +3262,7 @@ Drop,Dråbe, Drop Here,Drop Here, Drop files here,Slip filer her, Dynamic Template,Dynamisk skabelon, -ERPNext Role,ERPNæste rolle, +ERPNext Role,ERPNext rolle, Email / Notifications,E-mail / underretninger, Email Account setup please enter your password for: {0},Opsætning af e-mail-konto: indtast venligst din adgangskode til: {0}, Email Address whose Google Contacts are to be synced.,"E-mail-adresse, hvis Google-kontakter skal synkroniseres.", diff --git a/frappe/translations/id.csv b/frappe/translations/id.csv index 6207ecaa05..c4eae3e145 100644 --- a/frappe/translations/id.csv +++ b/frappe/translations/id.csv @@ -3262,7 +3262,7 @@ Drop,Penurunan, Drop Here,Jatuhkan Di Sini, Drop files here,Letakkan file di sini, Dynamic Template,Template Dinamis, -ERPNext Role,Peran ERPN, +ERPNext Role,Peran ERPNext, Email / Notifications,Notifikasi email, Email Account setup please enter your password for: {0},"Pengaturan Akun Email, harap masukkan kata sandi Anda untuk: {0}", Email Address whose Google Contacts are to be synced.,Alamat Email yang Kontak Google-nya harus disinkronkan., diff --git a/frappe/translations/is.csv b/frappe/translations/is.csv index c06065b120..fd0b552701 100644 --- a/frappe/translations/is.csv +++ b/frappe/translations/is.csv @@ -3262,7 +3262,7 @@ Drop,Dropi, Drop Here,Sendu hér, Drop files here,Sendu skrár hér, Dynamic Template,Dynamískt sniðmát, -ERPNext Role,ERPNæsta hlutverk, +ERPNext Role,ERPNext hlutverk, Email / Notifications,Netfang / tilkynningar, Email Account setup please enter your password for: {0},Uppsetning tölvupóstreikninga vinsamlegast sláðu inn lykilorðið þitt fyrir: {0}, Email Address whose Google Contacts are to be synced.,Netfang þar sem samstillt er Google tengiliði, diff --git a/frappe/translations/it.csv b/frappe/translations/it.csv index f61d467ebe..1d4c1af0f2 100644 --- a/frappe/translations/it.csv +++ b/frappe/translations/it.csv @@ -3262,7 +3262,7 @@ Drop,Far cadere, Drop Here,Drop Here, Drop files here,Trascina i file qui, Dynamic Template,Modello dinamico, -ERPNext Role,ERPSuccessivo ruolo, +ERPNext Role,ruolo ERPNext, Email / Notifications,Notifiche di posta elettronica, Email Account setup please enter your password for: {0},"Impostazione dell'account e-mail, inserire la password per: {0}", Email Address whose Google Contacts are to be synced.,Indirizzo email i cui contatti Google devono essere sincronizzati., diff --git a/frappe/translations/ja.csv b/frappe/translations/ja.csv index 441cfa44ef..35029e0058 100644 --- a/frappe/translations/ja.csv +++ b/frappe/translations/ja.csv @@ -3262,7 +3262,7 @@ Drop,ドロップ, Drop Here,ここにドロップ, Drop files here,ここにファイルをドロップします, Dynamic Template,動的テンプレート, -ERPNext Role,ERP次のロール, +ERPNext Role,ERPNext の役割, Email / Notifications,メール/通知, Email Account setup please enter your password for: {0},メールアカウントのセットアップ:{0}のパスワードを入力してください, Email Address whose Google Contacts are to be synced.,Googleの連絡先を同期するメールアドレス。, diff --git a/frappe/translations/km.csv b/frappe/translations/km.csv index 70a719d63d..5e8e1fc2d5 100644 --- a/frappe/translations/km.csv +++ b/frappe/translations/km.csv @@ -3262,7 +3262,7 @@ Drop,ទម្លាក់។, Drop Here,ទម្លាក់នៅទីនេះ។, Drop files here,ទម្លាក់ឯកសារនៅទីនេះ។, Dynamic Template,គំរូឌីណាមិក, -ERPNext Role,តួនាទី ERP បន្ទាប់។, +ERPNext Role,ERPNext តួនាទី។, Email / Notifications,អ៊ីមែល / ការជូនដំណឹង, Email Account setup please enter your password for: {0},រៀបចំគណនីអ៊ីមែលសូមបញ្ចូលពាក្យសម្ងាត់របស់អ្នកសម្រាប់៖ {0}, Email Address whose Google Contacts are to be synced.,អាសយដ្ឋានអ៊ីមែលដែលទំនាក់ទំនងរបស់ Google នឹងត្រូវធ្វើសមកាលកម្ម។, diff --git a/frappe/translations/mr.csv b/frappe/translations/mr.csv index 383b7a1c1e..6e98f94434 100644 --- a/frappe/translations/mr.csv +++ b/frappe/translations/mr.csv @@ -3262,7 +3262,7 @@ Drop,थेंब, Drop Here,येथे ड्रॉप करा, Drop files here,फायली येथे सोडा, Dynamic Template,डायनॅमिक टेम्पलेट, -ERPNext Role,ईआरपीनेक्स्ट रोल, +ERPNext Role,ERPNext रोल, Email / Notifications,ईमेल / सूचना, Email Account setup please enter your password for: {0},ईमेल खाते सेटअप यासाठी आपला संकेतशब्द प्रविष्ट करा: {0}, Email Address whose Google Contacts are to be synced.,ज्यांचे Google संपर्क समक्रमित केले जातील असा ईमेल पत्ता., diff --git a/frappe/translations/rw.csv b/frappe/translations/rw.csv index e0a1142a6a..002d22e72b 100644 --- a/frappe/translations/rw.csv +++ b/frappe/translations/rw.csv @@ -3262,7 +3262,7 @@ Drop,Tera, Drop Here,Tera Hano, Drop files here,Tera dosiye hano, Dynamic Template,Icyitegererezo, -ERPNext Role,Uruhare rwa ERPN, +ERPNext Role,uruhare rwa ERPNext, Email / Notifications,Imeri / Amatangazo, Email Account setup please enter your password for: {0},Imeri ya konte ya imeri nyamuneka andika ijambo ryibanga rya: {0}, Email Address whose Google Contacts are to be synced.,Aderesi ya imeri abo Google igomba guhuza., diff --git a/frappe/translations/sk.csv b/frappe/translations/sk.csv index 1d9442ffdb..7583eec96e 100644 --- a/frappe/translations/sk.csv +++ b/frappe/translations/sk.csv @@ -3262,7 +3262,7 @@ Drop,Pokles, Drop Here,Drop sem, Drop files here,Sem presuňte súbory, Dynamic Template,Dynamická šablóna, -ERPNext Role,ERPĎalšia rola, +ERPNext Role,ERPNext rola, Email / Notifications,E-mail / Upozornenia, Email Account setup please enter your password for: {0},"Nastavenie e-mailového účtu, zadajte heslo pre: {0}", Email Address whose Google Contacts are to be synced.,"E-mailová adresa, ktorej kontakty Google sa majú synchronizovať.", diff --git a/frappe/translations/sq.csv b/frappe/translations/sq.csv index 3d3fe564b3..b26b104850 100644 --- a/frappe/translations/sq.csv +++ b/frappe/translations/sq.csv @@ -3262,7 +3262,7 @@ Drop,Drop, Drop Here,Hidh këtu, Drop files here,Hidh skedarët këtu, Dynamic Template,Modeli Dinamik, -ERPNext Role,Roli ERPN, +ERPNext Role,Roli i ERPNext, Email / Notifications,Email / njoftime, Email Account setup please enter your password for: {0},Konfigurimi i llogarisë email ju lutemi shkruani fjalëkalimin tuaj për: {0, Email Address whose Google Contacts are to be synced.,Adresa e Email-it Kontaktet e të cilit Google duhet të sinkronizohen., diff --git a/frappe/translations/sv.csv b/frappe/translations/sv.csv index fe8458de35..8ab480d659 100644 --- a/frappe/translations/sv.csv +++ b/frappe/translations/sv.csv @@ -3262,7 +3262,7 @@ Drop,Släppa, Drop Here,Släpp här, Drop files here,Släpp filer här, Dynamic Template,Dynamisk mall, -ERPNext Role,ERPNästa roll, +ERPNext Role,ERPNext roll, Email / Notifications,E-post / aviseringar, Email Account setup please enter your password for: {0},"Ange e-postkonto, ange ditt lösenord för: {0}", Email Address whose Google Contacts are to be synced.,E-postadress vars Google-kontakter ska synkroniseras., diff --git a/frappe/website/doctype/web_form/web_form.js b/frappe/website/doctype/web_form/web_form.js index 4d38d11891..d69d21c64d 100644 --- a/frappe/website/doctype/web_form/web_form.js +++ b/frappe/website/doctype/web_form/web_form.js @@ -47,7 +47,7 @@ frappe.ui.form.on("Web Form", { frm.add_custom_button(__('Get Fields'), () => { let webform_fieldtypes = frappe.meta.get_field('Web Form Field', 'fieldtype').options.split('\n'); - let fieldnames = (frm.doc.fields || []).map(d => d.fieldname); + let fieldnames = (frm.doc.web_form_fields || []).map(d => d.fieldname); frappe.model.with_doctype(frm.doc.doc_type, () => { let meta = frappe.get_meta(frm.doc.doc_type); for (let field of meta.fields) {
Id + Time + State + Info + Progress +