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}
+
+
+ | Id
+ | Time
+ | State
+ | Info
+ | Progress
+ |
+ ${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) {