diff --git a/.mergify.yml b/.mergify.yml
index 7f4c084e30..f1333362a8 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -6,6 +6,7 @@ pull_request_rules:
- author!=surajshetty3416
- author!=gavindsouza
- author!=deepeshgarg007
+ - author!=ankush
- or:
- base=version-13
- base=version-12
@@ -13,7 +14,7 @@ pull_request_rules:
close:
comment:
message: |
- @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
+ @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: Automatic merge on CI success and review
@@ -53,7 +54,7 @@ pull_request_rules:
{{ title }} (#{{ number }})
{{ body }}
-
+
- name: backport to develop
conditions:
- label="backport develop"
@@ -92,4 +93,4 @@ pull_request_rules:
branches:
- version-12-hotfix
assignees:
- - "{{ author }}"
\ No newline at end of file
+ - "{{ author }}"
diff --git a/cypress/integration/control_color.js b/cypress/integration/control_color.js
new file mode 100644
index 0000000000..8d55003618
--- /dev/null
+++ b/cypress/integration/control_color.js
@@ -0,0 +1,77 @@
+context('Control Color', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app/website');
+ });
+
+ function get_dialog_with_color() {
+ return cy.dialog({
+ title: 'Color',
+ fields: [{
+ label: 'Color',
+ fieldname: 'color',
+ fieldtype: 'Color'
+ }]
+ });
+ }
+
+ it('Verifying if the color control is selecting correct', () => {
+ get_dialog_with_color().as('dialog');
+ cy.findByPlaceholderText('Choose a color').click();
+
+ ///Selecting a color from the color palette
+ cy.get('[style="background-color: rgb(79, 157, 217);"]').click();
+
+ //Checking if the css attribute is correct
+ cy.get('.color-map').should('have.css', 'color', 'rgb(79, 157, 217)');
+ cy.get('.hue-map').should('have.css', 'color', 'rgb(0, 145, 255)');
+
+ //Checking if the correct color is being selected
+ cy.get('@dialog').then(dialog => {
+ let value = dialog.get_value('color');
+ expect(value).to.equal('#4F9DD9');
+ });
+
+ //Selecting a color
+ cy.get('[style="background-color: rgb(203, 41, 41);"]').click();
+
+ //Checking if the correct css is being selected
+ cy.get('.color-map').should('have.css', 'color', 'rgb(203, 41, 41)');
+ cy.get('.hue-map').should('have.css', 'color', 'rgb(255, 0, 0)');
+
+ //Checking if the correct color is being selected
+ cy.get('@dialog').then(dialog => {
+ let value = dialog.get_value('color');
+ expect(value).to.equal('#CB2929');
+ });
+
+ //Selecting color from the palette
+ cy.get('.color-map > .color-selector').click(65, 87, {force: true});
+ cy.get('.color-map').should('have.css', 'color', 'rgb(56, 0, 0)');
+
+ //Checking if the expected color is selected and getting displayed
+ cy.get('@dialog').then(dialog => {
+ let value = dialog.get_value('color');
+ expect(value).to.equal('#380000');
+ });
+
+ //Selecting the color from the hue map
+ cy.get('.hue-map > .hue-selector').click(35, -1, {force: true});
+ cy.get('.color-map').should('have.css', 'color', 'rgb(56, 45, 0)');
+ cy.get('.hue-map').should('have.css', 'color', 'rgb(255, 204, 0)');
+ cy.get('.color-map > .color-selector').click(55, 12, {force: true});
+ cy.get('.color-map').should('have.css', 'color', 'rgb(46, 37, 0)');
+
+ //Checking if the correct color is being displayed
+ cy.get('@dialog').then(dialog => {
+ let value = dialog.get_value('color');
+ expect(value).to.equal('#2e2500');
+ });
+
+ //Clearing the field and checking if the field contains the placeholder "Choose a color"
+ cy.get('.input-with-feedback').click({force: true});
+ cy.get_field('color', 'Color').type('{selectall}').clear();
+ cy.get_field('color', 'Color').invoke('attr', 'placeholder').should('contain', 'Choose a color');
+
+ });
+});
\ No newline at end of file
diff --git a/cypress/integration/control_data.js b/cypress/integration/control_data.js
index 021b9032c1..01f9168667 100644
--- a/cypress/integration/control_data.js
+++ b/cypress/integration/control_data.js
@@ -59,7 +59,7 @@ context('Data Control', () => {
//Checking for the error message
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', '@@### is not a valid Name');
- cy.get('.modal').type('{esc}');
+ cy.hide_dialog();
cy.get_field('name1', 'Data').clear({force: true});
cy.fill_field('name1', 'Komal{}/!', 'Data');
@@ -67,10 +67,10 @@ context('Data Control', () => {
cy.findByRole('button', {name: 'Save'}).click();
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'Komal{}/! is not a valid Name');
+ cy.hide_dialog();
});
it('Verifying data control by inputting different patterns for "Email" field', () => {
- cy.get('.modal-actions > .btn-modal-close').trigger("click");
cy.get_field('name1', 'Data').clear({force: true});
cy.fill_field('name1', 'Komal', 'Data');
cy.get_field('email', 'Data').clear({force: true});
@@ -79,17 +79,17 @@ context('Data Control', () => {
cy.findByRole('button', {name: 'Save'}).click();
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'komal is not a valid Email Address');
- cy.get('.modal-actions > .btn-modal-close').trigger("click");
+ cy.hide_dialog();
cy.get_field('email', 'Data').clear({force: true});
cy.fill_field('email', 'komal@test', 'Data');
cy.get('.frappe-control[data-fieldname="email"]').should('have.class', 'has-error');
cy.findByRole('button', {name: 'Save'}).click();
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'komal@test is not a valid Email Address');
+ cy.hide_dialog();
});
it('Verifying data control by inputting different patterns for "Phone" field', () => {
- cy.get('.modal-actions > .btn-modal-close').trigger("click");
cy.get_field('email', 'Data').clear({force: true});
cy.fill_field('email', 'komal@test.com', 'Data');
cy.get_field('phone', 'Data').clear({force: true});
@@ -98,7 +98,7 @@ context('Data Control', () => {
cy.findByRole('button', {name: 'Save'}).click({force: true});
cy.get('.modal-title').should('have.text', 'Message');
cy.get('.msgprint').should('have.text', 'komal is not a valid Phone Number');
- cy.get('.modal-actions > .btn-modal-close').trigger("click");
+ cy.hide_dialog();
});
it('Inputting correct data and saving the doc', () => {
@@ -124,6 +124,5 @@ context('Data Control', () => {
cy.get('.actions-btn-group > .btn').contains('Actions').click();
cy.get('.actions-btn-group > .dropdown-menu [data-label="Delete"]').click();
cy.click_modal_primary_button('Yes');
- cy.get('.btn-modal-close').click();
});
});
\ No newline at end of file
diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js
index 7a7e94d2f5..44153f7e4a 100644
--- a/cypress/integration/control_link.js
+++ b/cypress/integration/control_link.js
@@ -20,7 +20,21 @@ context('Control Link', () => {
'label': 'Select ToDo',
'fieldname': 'link',
'fieldtype': 'Link',
- 'options': 'ToDo'
+ 'options': 'ToDo',
+ }
+ ]
+ });
+ }
+
+ function get_dialog_with_user_link() {
+ return cy.dialog({
+ title: 'Link',
+ fields: [
+ {
+ 'label': 'Select User',
+ 'fieldname': 'link',
+ 'fieldtype': 'Link',
+ 'options': 'User',
}
]
});
@@ -29,6 +43,24 @@ context('Control Link', () => {
it('should set the valid value', () => {
get_dialog_with_link().as('dialog');
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "User",
+ "property": "translate_link_fields",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "0"
+ }, true);
+
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "ToDo",
+ "property": "show_title_field_in_link",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "0"
+ }, true);
+
cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
@@ -88,7 +120,8 @@ context('Control Link', () => {
cy.get('@input').type(todos[0]).blur();
cy.wait('@validate_link');
cy.get('@input').focus();
- cy.findByTitle('Open Link')
+ cy.wait(500); // wait for arrow to show
+ cy.get('.frappe-control[data-fieldname=link] .btn-open')
.should('be.visible')
.click();
cy.location('pathname').should('eq', `/app/todo/${todos[0]}`);
@@ -96,7 +129,15 @@ context('Control Link', () => {
});
it('show title field in link', () => {
- get_dialog_with_link().as('dialog');
+
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "User",
+ "property": "translate_link_fields",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "0"
+ }, true);
cy.insert_doc("Property Setter", {
"doctype": "Property Setter",
@@ -107,6 +148,10 @@ context('Control Link', () => {
"value": "1"
}, true);
+ cy.clear_cache();
+ cy.wait(500);
+
+ get_dialog_with_link().as('dialog');
cy.window().its('frappe').then(frappe => {
if (!frappe.boot) {
frappe.boot = {
@@ -134,8 +179,6 @@ context('Control Link', () => {
expect(value).to.eq(todos[0]);
expect(label).to.eq('this is a test todo for link');
-
- cy.remove_doc("Property Setter", "ToDo-main-show_title_field_in_link");
});
});
});
@@ -143,6 +186,7 @@ context('Control Link', () => {
it('should update dependant fields (via fetch_from)', () => {
cy.get('@todos').then(todos => {
cy.visit(`/app/todo/${todos[0]}`);
+ cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
cy.intercept('POST', '/api/method/frappe.client.validate_link').as('validate_link');
cy.get('.frappe-control[data-fieldname=assigned_by] input').focus().as('input');
@@ -167,7 +211,9 @@ context('Control Link', () => {
.should("eq", null);
// set valid value again
- cy.get('@input').clear().type('Administrator', {delay: 100}).blur();
+ cy.get('@input').clear().focus();
+ cy.wait('@search_link');
+ cy.get('@input').type('Administrator', {delay: 100}).blur();
cy.wait('@validate_link');
cy.window()
@@ -214,4 +260,130 @@ context('Control Link', () => {
"contain", ""
);
});
+
+ it('show translated text for link with show_title_field_in_link enabled', () => {
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "ToDo",
+ "property": "translate_link_fields",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "1"
+ }, true);
+
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "ToDo",
+ "property": "show_title_field_in_link",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "1"
+ }, true);
+
+ cy.window().its('frappe').then(frappe => {
+ cy.insert_doc("Translation", {
+ doctype: "Translation",
+ language: frappe.boot.lang,
+ source_text: "this is a test todo for link",
+ translated_text: "this is a translated test todo for link",
+ });
+ });
+
+ cy.clear_cache();
+ cy.wait(500);
+
+ cy.window().its('frappe').then(frappe => {
+ if (!frappe.boot) {
+ frappe.boot = {
+ link_title_doctypes: ['ToDo'],
+ translatable_doctypes: ['ToDo']
+ };
+ } else {
+ frappe.boot.link_title_doctypes = ['ToDo'];
+ frappe.boot.translatable_doctypes = ['ToDo'];
+ }
+ });
+
+ get_dialog_with_link().as('dialog');
+ cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
+
+ cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
+ cy.wait('@search_link');
+ cy.get('@input').type('todo for link', { delay: 100 });
+ cy.wait('@search_link');
+ cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
+ cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
+ cy.get('.frappe-control[data-fieldname=link] input').blur();
+ cy.get('@dialog').then(dialog => {
+ cy.get('@todos').then(todos => {
+ let field = dialog.get_field('link');
+ let value = field.get_value();
+ let label = field.get_label_value();
+
+ expect(value).to.eq(todos[0]);
+ expect(label).to.eq('this is a translated test todo for link');
+ });
+ });
+ });
+
+ it('show translated text for link with show_title_field_in_link disabled', () => {
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "User",
+ "property": "translate_link_fields",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "1"
+ }, true);
+
+ cy.insert_doc("Property Setter", {
+ "doctype": "Property Setter",
+ "doc_type": "ToDo",
+ "property": "show_title_field_in_link",
+ "property_type": "Check",
+ "doctype_or_field": "DocType",
+ "value": "0"
+ }, true);
+
+ cy.window().its('frappe').then(frappe => {
+ cy.insert_doc("Translation", {
+ doctype: "Translation",
+ language: frappe.boot.lang,
+ source_text: "test@erpnext.com",
+ translated_text: "translatedtest@erpnext.com",
+ });
+ });
+
+ cy.clear_cache();
+ cy.wait(500);
+
+ cy.window().its('frappe').then(frappe => {
+ if (!frappe.boot) {
+ frappe.boot = {
+ translatable_doctypes: ['User']
+ };
+ } else {
+ frappe.boot.translatable_doctypes = ['User'];
+ }
+ });
+
+ get_dialog_with_user_link().as('dialog');
+ cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link');
+
+ cy.get('.frappe-control[data-fieldname=link] input').focus().as('input');
+ cy.wait('@search_link');
+ cy.get('@input').type('test@erpnext.com', { delay: 100 });
+ cy.wait('@search_link');
+ cy.get('.frappe-control[data-fieldname=link] ul').should('be.visible');
+ cy.get('.frappe-control[data-fieldname=link] input').type('{enter}', { delay: 100 });
+ cy.get('.frappe-control[data-fieldname=link] input').blur();
+ cy.get('@dialog').then(dialog => {
+ let field = dialog.get_field('link');
+ let value = field.get_value();
+ let label = field.get_label_value();
+
+ expect(value).to.eq('test@erpnext.com');
+ expect(label).to.eq('translatedtest@erpnext.com');
+ });
+ });
});
diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js
index cec7edb59f..bab14f5441 100644
--- a/cypress/integration/folder_navigation.js
+++ b/cypress/integration/folder_navigation.js
@@ -17,8 +17,8 @@ context('Folder Navigation', () => {
//Adding folder (Test Folder)
cy.get('.menu-btn-group > .btn').click();
cy.get('.menu-btn-group [data-label="New Folder"]').click();
- cy.get('form > [data-fieldname="value"]').type('Test Folder');
- cy.findByRole('button', {name: 'Create'}).click();
+ cy.fill_field('value', 'Test Folder');
+ cy.click_modal_primary_button('Create');
});
it('Navigating the nested folders, checking if the URL formed is correct, checking if the added content in the child folder is correct', () => {
@@ -32,8 +32,8 @@ context('Folder Navigation', () => {
//Adding folder inside the attachments folder
cy.get('.menu-btn-group > .btn').click();
cy.get('.menu-btn-group [data-label="New Folder"]').click();
- cy.get('form > [data-fieldname="value"]').type('Test Folder');
- cy.findByRole('button', {name: 'Create'}).click();
+ cy.fill_field('value', 'Test Folder');
+ cy.click_modal_primary_button('Create');
//Navigating inside the added folder in the Attachments folder
cy.get('[title="Test Folder"] > span').click();
@@ -46,26 +46,31 @@ context('Folder Navigation', () => {
cy.findByRole('button', {name: 'Add File'}).eq(0).click({force: true});
cy.get('.file-uploader').findByText('Link').click();
cy.get('.input-group > .form-control').type('https://wallpaperplay.com/walls/full/8/2/b/72402.jpg');
- cy.findByRole('button', {name: 'Upload'}).click();
+ cy.click_modal_primary_button('Upload');
//To check if the added file is present in the Test Folder
cy.get('span.level-item > span').should('contain', 'Test Folder');
cy.get('.list-row-container').eq(0).should('contain.text', '72402.jpg');
cy.get('.list-row-checkbox').eq(0).click();
+ cy.intercept({
+ method: 'POST',
+ url: 'api/method/frappe.desk.reportview.delete_items'
+ }).as('file_deleted');
+
//Deleting the added file from the Test folder
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
- cy.wait(700);
- cy.findByRole('button', {name: 'Yes'}).click();
- cy.wait(700);
+ cy.click_modal_primary_button('Yes');
+ cy.wait('@file_deleted');
//Deleting the Test Folder
cy.visit('/app/file/view/home/Attachments');
cy.get('.list-row-checkbox').eq(0).click();
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
- cy.findByRole('button', {name: 'Yes'}).click();
+ cy.click_modal_primary_button('Yes');
+ cy.wait('@file_deleted');
});
it('Deleting Test Folder from the home', () => {
@@ -74,6 +79,6 @@ context('Folder Navigation', () => {
cy.get('.level-left > .list-subject > .file-select >.list-row-checkbox').eq(0).click({force: true, delay: 500});
cy.findByRole('button', {name: 'Actions'}).click();
cy.get('.actions-btn-group [data-label="Delete"]').click();
- cy.findByRole('button', {name: 'Yes'}).click();
+ cy.click_modal_primary_button('Yes');
});
});
diff --git a/cypress/integration/form_tour.js b/cypress/integration/form_tour.js
index e87a0e5528..507a07ab1a 100644
--- a/cypress/integration/form_tour.js
+++ b/cypress/integration/form_tour.js
@@ -1,7 +1,7 @@
context.skip('Form Tour', () => {
before(() => {
cy.login();
- cy.visit('/app/form-tour');
+ cy.visit('/app');
return cy.window().its('frappe').then(frappe => {
return frappe.call("frappe.tests.ui_test_helpers.create_form_tour");
});
diff --git a/cypress/integration/kanban.js b/cypress/integration/kanban.js
index 50fc41afe3..6ebab5d008 100644
--- a/cypress/integration/kanban.js
+++ b/cypress/integration/kanban.js
@@ -26,7 +26,7 @@ context('Kanban Board', () => {
cy.click_listview_primary_button('Add ToDo');
- cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor');
+ cy.fill_field('description', 'Test Kanban ToDo', 'Text Editor').wait(300);
cy.get('.modal-footer .btn-primary').last().click();
cy.wait('@save-todo');
diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js
index f873461efb..ae93354964 100644
--- a/cypress/integration/table_multiselect.js
+++ b/cypress/integration/table_multiselect.js
@@ -48,4 +48,4 @@ context('Table MultiSelect', () => {
cy.get('@existing_value').find('.btn-link-to-form').click();
cy.location('pathname').should('contain', '/user/test@erpnext.com');
});
-});
+});
\ No newline at end of file
diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js
index 6c4733400d..cb4d43a96a 100644
--- a/cypress/integration/timeline.js
+++ b/cypress/integration/timeline.js
@@ -78,12 +78,5 @@ context('Timeline', () => {
cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
cy.click_modal_primary_button('Yes');
-
- //Deleting the custom doctype
- cy.visit('/app/doctype');
- cy.select_listview_row_checkbox(0);
- cy.get('.page-actions').findByRole('button', {name: 'Actions'}).click();
- cy.get('.page-actions .actions-btn-group [data-label="Delete"]').click();
- cy.click_modal_primary_button('Yes');
});
});
\ No newline at end of file
diff --git a/cypress/integration/workspace_blocks.js b/cypress/integration/workspace_blocks.js
new file mode 100644
index 0000000000..ba707499c9
--- /dev/null
+++ b/cypress/integration/workspace_blocks.js
@@ -0,0 +1,150 @@
+context('Workspace Blocks', () => {
+ before(() => {
+ cy.login();
+ cy.visit('/app');
+ });
+
+ it('Create Test Page', () => {
+ cy.intercept({
+ method: 'POST',
+ url: 'api/method/frappe.desk.doctype.workspace.workspace.new_page'
+ }).as('new_page');
+
+ cy.get('.codex-editor__redactor .ce-block');
+ cy.get('.custom-actions button[data-label="Create%20Workspace"]').click();
+ cy.fill_field('title', 'Test Block Page', 'Data');
+ cy.fill_field('icon', 'edit', 'Icon');
+ cy.get_open_dialog().find('.modal-header').click();
+ cy.get_open_dialog().find('.btn-primary').click();
+
+ // check if sidebar item is added in private section
+ cy.get('.sidebar-item-container[item-name="Test Block Page"]').should('have.attr', 'item-public', '0');
+
+ cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
+ cy.wait(300);
+ cy.get('.sidebar-item-container[item-name="Test Block Page"]').should('have.attr', 'item-public', '0');
+
+ cy.wait('@new_page');
+ });
+
+ it('Quick List Block', () => {
+ cy.create_records([
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 1',
+ status: 'Open'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 2',
+ status: 'Open'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 3',
+ status: 'Open'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 4',
+ status: 'Open'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 5',
+ status: 'Closed'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 6',
+ status: 'Closed'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 7',
+ status: 'Closed'
+ },
+ {
+ doctype: 'ToDo',
+ description: 'Quick List ToDo 8',
+ status: 'Closed'
+ }
+ ]);
+
+ cy.get('.codex-editor__redactor .ce-block');
+ cy.get('.standard-actions .btn-secondary[data-label=Edit]').click();
+
+ // test quick list creation
+ cy.get('.ce-block').first().click({force: true}).type('{enter}');
+ cy.get('.block-list-container .block-list-item').contains('Quick List').click();
+
+ cy.get_open_dialog().find('.modal-header').click();
+
+ cy.fill_field('document_type', 'ToDo', 'Link').blur();
+ cy.fill_field('label', 'ToDo', 'Data').blur();
+
+ cy.get_open_dialog().find('.filter-edit-area').should('contain', 'No filters selected');
+ cy.get_open_dialog().find('.filter-area .add-filter').click();
+
+ cy.get_open_dialog().find('.fieldname-select-area input').type('Status{enter}').blur();
+ cy.get_open_dialog().find('select.input-with-feedback').select('Open');
+
+ cy.get_open_dialog().find('.modal-header').click();
+ cy.get_open_dialog().find('.btn-primary').click();
+
+ cy.get('.standard-actions .btn-primary[data-label="Save"]').click();
+
+
+ cy.get('.codex-editor__redactor .ce-block');
+
+ cy.get('.ce-block .quick-list-widget-box').first().as('todo-quick-list');
+
+ cy.get('@todo-quick-list').find('.quick-list-item .status').should('contain', 'Open');
+
+ // test filter-list
+ cy.get('@todo-quick-list').find('.widget-control .filter-list').click();
+
+ cy.get_open_dialog().find('select.input-with-feedback').select('Closed');
+ cy.get_open_dialog().find('.modal-header').click();
+ cy.get_open_dialog().find('.btn-primary').click();
+
+ cy.get('@todo-quick-list').find('.quick-list-item .status').should('contain', 'Closed');
+
+
+ // test refresh-list
+ cy.intercept({
+ method: 'POST',
+ url: 'api/method/frappe.desk.reportview.get'
+ }).as('refresh-list');
+
+ cy.get('@todo-quick-list').find('.widget-control .refresh-list').click();
+ cy.wait('@refresh-list');
+
+
+ // test add-new
+ cy.get('@todo-quick-list').find('.widget-control .add-new').click();
+ cy.url().should('include', `/todo/new-todo-1`);
+ cy.go('back');
+
+
+ // test quick-list-item
+ cy.get('@todo-quick-list').find('.quick-list-item .title')
+ .first()
+ .invoke('attr', 'title')
+ .then(title => {
+ cy.get('@todo-quick-list').find('.quick-list-item').contains(title).click();
+ cy.get_field('description', 'Text Editor').should('contain', title);
+ });
+ cy.go('back');
+
+
+ // test see-all
+ cy.get('@todo-quick-list').find('.widget-footer .see-all').click();
+
+ cy.get('.standard-filter-section select[data-fieldname="status"]')
+ .invoke('val')
+ .should('eq', 'Open');
+ cy.go('back');
+ });
+
+});
\ No newline at end of file
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
index 95b5cbb670..7686626fea 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.js
@@ -341,7 +341,8 @@ Cypress.Commands.add('clear_filters', () => {
});
Cypress.Commands.add('click_modal_primary_button', (btn_name) => {
- cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).trigger('click', {force: true});
+ cy.wait(400);
+ cy.get('.modal-footer > .standard-actions > .btn-primary').contains(btn_name).click({force: true});
});
Cypress.Commands.add('click_sidebar_button', (btn_name) => {
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 8bd7783283..17a945c875 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -10,28 +10,19 @@ be used to build database driven apps.
Read the documentation: https://frappeframework.com/docs
"""
-import os
-import warnings
-
-STANDARD_USERS = ("Guest", "Administrator")
-
-_dev_server = os.environ.get("DEV_SERVER", False)
-
-if _dev_server:
- warnings.simplefilter("always", DeprecationWarning)
- warnings.simplefilter("always", PendingDeprecationWarning)
-
import importlib
import inspect
import json
+import os
import sys
-from typing import TYPE_CHECKING, Dict, List, Union
+import warnings
+from typing import TYPE_CHECKING, Dict, List, Optional, Union
import click
from werkzeug.local import Local, release_local
from frappe.query_builder import get_query_builder, patch_query_aggregation, patch_query_execute
-from frappe.utils.data import cstr
+from frappe.utils.data import cstr, sbool
# Local application imports
from .exceptions import *
@@ -45,11 +36,17 @@ from .utils.jinja import (
from .utils.lazy_loader import lazy_import
__version__ = "14.0.0-dev"
-
__title__ = "Frappe Framework"
-local = Local()
controllers = {}
+local = Local()
+STANDARD_USERS = ("Guest", "Administrator")
+
+_dev_server = int(sbool(os.environ.get("DEV_SERVER", False)))
+
+if _dev_server:
+ warnings.simplefilter("always", DeprecationWarning)
+ warnings.simplefilter("always", PendingDeprecationWarning)
class _dict(dict):
@@ -435,7 +432,7 @@ def msgprint(
if as_table and type(msg) in (list, tuple):
out.as_table = 1
- if as_list and type(msg) in (list, tuple) and len(msg) > 1:
+ if as_list and type(msg) in (list, tuple):
out.as_list = 1
if flags.print_messages and out.message:
@@ -973,7 +970,7 @@ def get_precision(doctype, fieldname, currency=None, doc=None):
return get_field_precision(get_meta(doctype).get_field(fieldname), doc, currency)
-def generate_hash(txt=None, length=None):
+def generate_hash(txt: Optional[str] = None, length: Optional[int] = None) -> str:
"""Generates random hash for given text + current timestamp + random string."""
import hashlib
import time
diff --git a/frappe/boot.py b/frappe/boot.py
index 62122ed4e5..a23a7e6ac3 100644
--- a/frappe/boot.py
+++ b/frappe/boot.py
@@ -100,6 +100,7 @@ def get_bootinfo():
bootinfo.desk_settings = get_desk_settings()
bootinfo.app_logo_url = get_app_logo()
bootinfo.link_title_doctypes = get_link_title_doctypes()
+ bootinfo.translatable_doctypes = get_translatable_doctypes()
return bootinfo
@@ -408,3 +409,11 @@ def set_time_zone(bootinfo):
"user": bootinfo.get("user_info", {}).get(frappe.session.user, {}).get("time_zone", None)
or get_time_zone(),
}
+
+
+def get_translatable_doctypes():
+ dts = frappe.get_all("DocType", {"translate_link_fields": 1}, pluck="name")
+ custom_dts = frappe.get_all(
+ "Property Setter", {"property": "translate_link_fields", "value": "1"}, pluck="doc_type"
+ )
+ return dts + custom_dts
diff --git a/frappe/client.py b/frappe/client.py
index 1bad2bed2f..f753da6f57 100644
--- a/frappe/client.py
+++ b/frappe/client.py
@@ -2,6 +2,7 @@
# License: MIT. See LICENSE
import json
import os
+from typing import TYPE_CHECKING
import frappe
import frappe.model
@@ -11,6 +12,9 @@ from frappe.desk.reportview import validate_args
from frappe.model.db_query import check_parent_permission
from frappe.utils import get_safe_filters
+if TYPE_CHECKING:
+ from frappe.model.document import Document
+
"""
Handle RESTful requests that are mapped to the `/api/resource` route.
@@ -189,18 +193,7 @@ def insert(doc=None):
if isinstance(doc, str):
doc = json.loads(doc)
- doc = frappe._dict(doc)
- if frappe.is_table(doc.doctype):
- if not (doc.parenttype and doc.parent and doc.parentfield):
- frappe.throw(_("parenttype, parent and parentfield are required to insert a child record"))
- # inserting a child record
- parent = frappe.get_doc(doc.parenttype, doc.parent)
- parent.append(doc.parentfield, doc)
- parent.save()
- return parent.as_dict()
- else:
- doc = frappe.get_doc(doc).insert()
- return doc.as_dict()
+ return insert_doc(doc).as_dict()
@frappe.whitelist(methods=["POST", "PUT"])
@@ -211,21 +204,12 @@ def insert_many(docs=None):
if isinstance(docs, str):
docs = json.loads(docs)
- out = []
-
if len(docs) > 200:
frappe.throw(_("Only 200 inserts allowed in one request"))
+ out = set()
for doc in docs:
- if doc.get("parenttype"):
- # inserting a child record
- parent = frappe.get_doc(doc.parenttype, doc.parent)
- parent.append(doc.parentfield, doc)
- parent.save()
- out.append(parent.name)
- else:
- doc = frappe.get_doc(doc).insert()
- out.append(doc.name)
+ out.add(insert_doc(doc).name)
return out
@@ -496,3 +480,23 @@ def validate_link(doctype: str, docname: str, fields=None):
)
return values
+
+
+def insert_doc(doc) -> "Document":
+ """Inserts document and returns parent document object with appended child document
+ if `doc` is child document else returns the inserted document object
+
+ :param doc: doc to insert (dict)"""
+
+ doc = frappe._dict(doc)
+ if frappe.is_table(doc.doctype):
+ if not (doc.parenttype and doc.parent and doc.parentfield):
+ frappe.throw(_("Parenttype, Parent and Parentfield are required to insert a child record"))
+
+ # inserting a child record
+ parent = frappe.get_doc(doc.parenttype, doc.parent)
+ parent.append(doc.parentfield, doc)
+ parent.save()
+ return parent
+
+ return frappe.get_doc(doc).insert()
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index fa9ab4be59..628a10d67e 100644
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -54,7 +54,6 @@ def new_site(
db_root_password=None,
admin_password=None,
verbose=False,
- install_apps=None,
source_sql=None,
force=None,
no_mariadb_socket=False,
@@ -398,8 +397,9 @@ def _reinstall(
@click.command("install-app")
@click.argument("apps", nargs=-1)
+@click.option("--force", is_flag=True, default=False)
@pass_context
-def install_app(context, apps):
+def install_app(context, apps, force=False):
"Install a new app to site, supports multiple apps"
from frappe.installer import install_app as _install_app
@@ -414,7 +414,7 @@ def install_app(context, apps):
for app in apps:
try:
- _install_app(app, verbose=context.verbose)
+ _install_app(app, verbose=context.verbose, force=force)
except frappe.IncompatibleApp as err:
err_msg = ":\n{}".format(err) if str(err) else ""
print("App {} is Incompatible with Site {}{}".format(app, site, err_msg))
@@ -825,7 +825,7 @@ def _drop_site(
try:
if not no_backup:
click.secho(f"Taking backup of {site}", fg="green")
- odb = scheduled_backup(ignore_files=False, force=True, verbose=True)
+ odb = scheduled_backup(ignore_files=False, ignore_conf=True, force=True, verbose=True)
odb.print_summary()
except Exception as err:
if force:
@@ -923,7 +923,6 @@ def set_user_password(site, user, password, logout_all_sessions=False):
update_password(user=user, pwd=password, logout_all_sessions=logout_all_sessions)
frappe.db.commit()
- password = None
finally:
frappe.destroy()
diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py
index 1eb9f1cf33..036594926e 100644
--- a/frappe/contacts/address_and_contact.py
+++ b/frappe/contacts/address_and_contact.py
@@ -17,7 +17,7 @@ def load_address_and_contact(doc, key=None):
["Dynamic Link", "link_name", "=", doc.name],
["Dynamic Link", "parenttype", "=", "Address"],
]
- address_list = frappe.get_list("Address", filters=filters, fields=["*"])
+ address_list = frappe.get_list("Address", filters=filters, fields=["*"], order_by="creation asc")
address_list = [a.update({"display": get_address_display(a)}) for a in address_list]
diff --git a/frappe/core/doctype/communication/communication.py b/frappe/core/doctype/communication/communication.py
index f0e80c2207..667d3ee135 100644
--- a/frappe/core/doctype/communication/communication.py
+++ b/frappe/core/doctype/communication/communication.py
@@ -152,8 +152,6 @@ class Communication(Document, CommunicationEmailMixin):
if not email_body:
return
- email_body = email_body[0]
-
user_email_signature = (
frappe.db.get_value(
"User",
diff --git a/frappe/core/doctype/doctype/doctype.json b/frappe/core/doctype/doctype/doctype.json
index b1579f35cd..4e110202d2 100644
--- a/frappe/core/doctype/doctype/doctype.json
+++ b/frappe/core/doctype/doctype/doctype.json
@@ -47,6 +47,7 @@
"view_settings",
"title_field",
"show_title_field_in_link",
+ "translate_link_fields",
"search_fields",
"default_print_format",
"sort_field",
@@ -591,6 +592,12 @@
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
+ },
+ {
+ "default": "0",
+ "fieldname": "translate_link_fields",
+ "fieldtype": "Check",
+ "label": "Translate Link Fields"
}
],
"icon": "fa fa-bolt",
@@ -673,7 +680,7 @@
"link_fieldname": "reference_doctype"
}
],
- "modified": "2022-02-15 21:47:16.467217",
+ "modified": "2022-02-28 21:56:52.116915",
"modified_by": "Administrator",
"module": "Core",
"name": "DocType",
@@ -708,5 +715,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
- "track_changes": 1
+ "track_changes": 1,
+ "translate_link_fields": 1
}
\ No newline at end of file
diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py
index 6bd7f2306f..4e21f3bcb4 100644
--- a/frappe/core/doctype/doctype/doctype.py
+++ b/frappe/core/doctype/doctype/doctype.py
@@ -100,6 +100,7 @@ class DocType(Document):
self.set_default_in_list_view()
self.set_default_translatable()
validate_series(self)
+ self.set("can_change_name_type", validate_autoincrement_autoname(self))
self.validate_document_type()
validate_fields(self)
@@ -124,12 +125,6 @@ class DocType(Document):
if self.default_print_format and not self.custom:
frappe.throw(_("Standard DocType cannot have default print format, use Customize Form"))
- if check_if_can_change_name_type(self):
- change_name_column_type(
- self.name,
- "bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
- )
-
def validate_field_name_conflicts(self):
"""Check if field names dont conflict with controller properties and methods"""
core_doctypes = [
@@ -374,6 +369,10 @@ class DocType(Document):
def on_update(self):
"""Update database schema, make controller templates if `custom` is not set and clear cache."""
+
+ if self.get("can_change_name_type"):
+ self.setup_autoincrement_and_sequence()
+
try:
frappe.db.updatedb(self.name, Meta(self))
except Exception as e:
@@ -413,6 +412,17 @@ class DocType(Document):
clear_linked_doctype_cache()
+ def setup_autoincrement_and_sequence(self):
+ """Changes name type and makes sequence on change (if required)"""
+
+ name_type = f"varchar({frappe.db.VARCHAR_LEN})"
+
+ if self.autoname == "autoincrement":
+ name_type = "bigint"
+ frappe.db.create_sequence(self.name, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
+
+ change_name_column_type(self.name, name_type)
+
def sync_global_search(self):
"""If global search settings are changed, rebuild search properties for this table"""
global_search_fields_before_update = [
@@ -903,26 +913,25 @@ def validate_series(dt, autoname=None, name=None):
frappe.throw(_("Series {0} already used in {1}").format(prefix, used_in[0][0]))
-def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
- def get_autoname_before_save(doctype: str, to_be_customized_dt: str) -> str:
- if doctype == "Customize Form":
- property_value = frappe.db.get_value(
- "Property Setter", {"doc_type": to_be_customized_dt, "property": "autoname"}, "value"
- )
+def validate_autoincrement_autoname(dt: DocType) -> bool:
+ """Checks if can doctype can change to/from autoincrement autoname"""
+ def get_autoname_before_save(dt: DocType) -> str:
+ if dt.name == "Customize Form":
+ property_value = frappe.db.get_value(
+ "Property Setter", {"doc_type": dt.doc_type, "property": "autoname"}, "value"
+ )
# initially no property setter is set,
# hence getting autoname value from the doctype itself
if not property_value:
- return frappe.db.get_value("DocType", to_be_customized_dt, "autoname") or ""
+ return frappe.db.get_value("DocType", dt.doc_type, "autoname") or ""
return property_value
return getattr(dt.get_doc_before_save(), "autoname", "")
- doctype_name = dt.doc_type if dt.doctype == "Customize Form" else dt.name
-
if not dt.is_new():
- autoname_before_save = get_autoname_before_save(dt.doctype, doctype_name)
+ autoname_before_save = get_autoname_before_save(dt)
is_autoname_autoincrement = dt.autoname == "autoincrement"
if (
@@ -930,23 +939,35 @@ def check_if_can_change_name_type(dt: DocType, raise_err: bool = True) -> bool:
and autoname_before_save != "autoincrement"
or (not is_autoname_autoincrement and autoname_before_save == "autoincrement")
):
- if not frappe.get_all(doctype_name, limit=1):
+
+ if frappe.get_meta(dt.name).issingle:
+ if dt.name == "Customize Form":
+ frappe.throw(_("Cannot change to/from autoincrement autoname in Customize Form"))
+
+ return False
+
+ if not frappe.get_all(dt.name, limit=1):
# allow changing the column type if there is no data
return True
- if raise_err:
- frappe.throw(
- _("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
- )
+ frappe.throw(
+ _("Can only change to/from Autoincrement naming rule when there is no data in the doctype")
+ )
return False
def change_name_column_type(doctype_name: str, type: str) -> None:
- return frappe.db.change_column_type(
- doctype_name, "name", type, True if frappe.db.db_type == "mariadb" else False
+ """Changes name column type"""
+
+ args = (
+ (doctype_name, "name", type, False, True)
+ if (frappe.db.db_type == "postgres")
+ else (doctype_name, "name", type, True)
)
+ frappe.db.change_column_type(*args)
+
def validate_links_table_fieldnames(meta):
"""Validate fieldnames in Links table"""
diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py
index 59475a95a7..11f5ef8a69 100644
--- a/frappe/core/doctype/doctype/test_doctype.py
+++ b/frappe/core/doctype/doctype/test_doctype.py
@@ -38,6 +38,52 @@ class TestDocType(unittest.TestCase):
doc = new_doctype(name).insert()
doc.delete()
+ def test_making_sequence_on_change(self):
+ frappe.delete_doc_if_exists("DocType", self._testMethodName)
+ dt = new_doctype(self._testMethodName).insert(ignore_permissions=True)
+ autoname = dt.autoname
+
+ # change autoname
+ dt.autoname = "autoincrement"
+ dt.save()
+
+ # check if name type has been changed
+ self.assertEqual(
+ frappe.db.sql(
+ f"""select data_type FROM information_schema.columns
+ where column_name = 'name' and table_name = 'tab{self._testMethodName}'"""
+ )[0][0],
+ "bigint",
+ )
+
+ if frappe.db.db_type == "mariadb":
+ table_name = "information_schema.tables"
+ conditions = f"table_type = 'sequence' and table_name = '{self._testMethodName}_id_seq'"
+ else:
+ table_name = "information_schema.sequences"
+ conditions = f"sequence_name = '{self._testMethodName}_id_seq'"
+
+ # check if sequence table is created
+ self.assertTrue(
+ frappe.db.sql(
+ f"""select * from {table_name}
+ where {conditions}"""
+ )
+ )
+
+ # change the autoname/naming rule back to original
+ dt.autoname = autoname
+ dt.save()
+
+ # check if name type has changed
+ self.assertEqual(
+ frappe.db.sql(
+ f"""select data_type FROM information_schema.columns
+ where column_name = 'name' and table_name = 'tab{self._testMethodName}'"""
+ )[0][0],
+ "varchar" if frappe.db.db_type == "mariadb" else "character varying",
+ )
+
def test_doctype_unique_constraint_dropped(self):
if frappe.db.exists("DocType", "With_Unique"):
frappe.delete_doc("DocType", "With_Unique")
diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py
index 3547a03832..8ca0b9ea10 100755
--- a/frappe/core/doctype/file/file.py
+++ b/frappe/core/doctype/file/file.py
@@ -608,7 +608,7 @@ def on_doctype_update():
def make_home_folder():
home = frappe.get_doc(
{"doctype": "File", "is_folder": 1, "is_home_folder": 1, "file_name": _("Home")}
- ).insert()
+ ).insert(ignore_if_duplicate=True)
frappe.get_doc(
{
@@ -618,7 +618,7 @@ def make_home_folder():
"is_attachments_folder": 1,
"file_name": _("Attachments"),
}
- ).insert()
+ ).insert(ignore_if_duplicate=True)
@frappe.whitelist()
diff --git a/frappe/core/doctype/role/role.js b/frappe/core/doctype/role/role.js
index f436c8c166..595e857d02 100644
--- a/frappe/core/doctype/role/role.js
+++ b/frappe/core/doctype/role/role.js
@@ -1,8 +1,15 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
-// MIT License. See license.txt
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+// MIT License. See LICENSE
frappe.ui.form.on('Role', {
refresh: function(frm) {
+ if (frm.doc.name === "All") {
+ frm.dashboard.add_comment(
+ __("Role 'All' will be given to all System Users."),
+ "yellow"
+ );
+ }
+
frm.set_df_property('is_custom', 'read_only', frappe.session.user !== 'Administrator');
frm.add_custom_button("Role Permissions Manager", function() {
diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py
index 0e2eac16ba..5fd59e1014 100644
--- a/frappe/core/doctype/server_script/server_script.py
+++ b/frappe/core/doctype/server_script/server_script.py
@@ -2,7 +2,6 @@
# Copyright (c) 2019, Frappe Technologies and contributors
# License: MIT. See LICENSE
-import ast
from types import FunctionType, MethodType, ModuleType
from typing import Dict, List
@@ -17,6 +16,7 @@ class ServerScript(Document):
frappe.only_for("Script Manager", True)
self.sync_scheduled_jobs()
self.clear_scheduled_events()
+ self.check_if_compilable_in_restricted_context()
def on_update(self):
frappe.cache().delete_value("server_script_map")
@@ -60,6 +60,15 @@ class ServerScript(Document):
for scheduled_job in self.scheduled_jobs:
frappe.delete_doc("Scheduled Job Type", scheduled_job.name)
+ def check_if_compilable_in_restricted_context(self):
+ """Check compilation errors and send them back as warnings."""
+ from RestrictedPython import compile_restricted
+
+ try:
+ compile_restricted(self.script)
+ except Exception as e:
+ frappe.msgprint(str(e), title=_("Compilation warning"))
+
def execute_method(self) -> Dict:
"""Specific to API endpoint Server Scripts
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 8436c24065..1dc5e55e04 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -11,7 +11,6 @@
"language",
"column_break_3",
"time_zone",
- "is_first_startup",
"enable_onboarding",
"setup_complete",
"date_and_number_format",
@@ -72,7 +71,8 @@
"column_break_64",
"max_auto_email_report_per_user",
"system_updates_section",
- "disable_system_update_notification"
+ "disable_system_update_notification",
+ "disable_change_log_notification"
],
"fields": [
{
@@ -104,14 +104,6 @@
"read_only": 1,
"reqd": 1
},
- {
- "default": "0",
- "fieldname": "is_first_startup",
- "fieldtype": "Check",
- "hidden": 1,
- "label": "Is First Startup",
- "read_only": 1
- },
{
"default": "0",
"fieldname": "setup_complete",
@@ -505,12 +497,18 @@
"fieldname": "max_auto_email_report_per_user",
"fieldtype": "Int",
"label": "Max auto email report per user"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_change_log_notification",
+ "fieldtype": "Check",
+ "label": "Disable Change Log Notification"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2022-04-21 09:11:35.218721",
+ "modified": "2022-05-09 18:53:35.218721",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js
index 4ce2c73fa3..3ec6795f0e 100644
--- a/frappe/custom/doctype/customize_form/customize_form.js
+++ b/frappe/custom/doctype/customize_form/customize_form.js
@@ -16,7 +16,6 @@ frappe.ui.form.on("Customize Form", {
onload: function(frm) {
frm.set_query("doc_type", function() {
return {
- translate_values: false,
filters: [
["DocType", "issingle", "=", 0],
["DocType", "custom", "=", 0],
diff --git a/frappe/custom/doctype/customize_form/customize_form.json b/frappe/custom/doctype/customize_form/customize_form.json
index a0bc994c45..0011f51af4 100644
--- a/frappe/custom/doctype/customize_form/customize_form.json
+++ b/frappe/custom/doctype/customize_form/customize_form.json
@@ -29,6 +29,7 @@
"view_settings_section",
"title_field",
"show_title_field_in_link",
+ "translate_link_fields",
"image_field",
"default_print_format",
"column_break_29",
@@ -287,7 +288,7 @@
"label": "Naming"
},
{
- "description": "Naming Options:\n
field:[fieldname] - By Field
\n
autoincrement - Uses Databases' Auto Increment feature
naming_series: - By Naming Series (field called naming_series must be present
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
+ "description": "Naming Options:\n
field:[fieldname] - By Field
naming_series: - By Naming Series (field called naming_series must be present)
Prompt - Prompt user for a name
[series] - Series by prefix (separated by a dot); for example PRE.#####
\n
format:EXAMPLE-{MM}morewords{fieldname1}-{fieldname2}-{#####} - Replace all braced words (fieldnames, date words (DD, MM, YY), series) with their value. Outside braces, any characters can be used.
",
"fieldname": "autoname",
"fieldtype": "Data",
"label": "Auto Name"
@@ -311,6 +312,12 @@
"fieldname": "show_title_field_in_link",
"fieldtype": "Check",
"label": "Show Title in Link Fields"
+ },
+ {
+ "default": "0",
+ "fieldname": "translate_link_fields",
+ "fieldtype": "Check",
+ "label": "Translate Link Fields"
}
],
"hide_toolbar": 1,
@@ -319,7 +326,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2022-04-21 15:36:16.772277",
+ "modified": "2022-05-13 15:36:16.772277",
"modified_by": "Administrator",
"module": "Custom",
"name": "Customize Form",
diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py
index 262542fd4b..e92fd50ea8 100644
--- a/frappe/custom/doctype/customize_form/customize_form.py
+++ b/frappe/custom/doctype/customize_form/customize_form.py
@@ -11,9 +11,8 @@ import frappe
import frappe.translate
from frappe import _
from frappe.core.doctype.doctype.doctype import (
- change_name_column_type,
check_email_append_to,
- check_if_can_change_name_type,
+ validate_autoincrement_autoname,
validate_fields_for_doctype,
validate_series,
)
@@ -163,7 +162,7 @@ class CustomizeForm(Document):
return
validate_series(self, self.autoname, self.doc_type)
- can_change_name_type = check_if_can_change_name_type(self)
+ validate_autoincrement_autoname(self)
self.flags.update_db = False
self.flags.rebuild_doctype_for_global_search = False
self.set_property_setters()
@@ -172,12 +171,6 @@ class CustomizeForm(Document):
validate_fields_for_doctype(self.doc_type)
check_email_append_to(self)
- if can_change_name_type:
- change_name_column_type(
- self.doc_type,
- "bigint" if self.autoname == "autoincrement" else f"varchar({frappe.db.VARCHAR_LEN})",
- )
-
if self.flags.update_db:
frappe.db.updatedb(self.doc_type)
@@ -584,6 +577,7 @@ doctype_properties = {
"naming_rule": "Data",
"autoname": "Data",
"show_title_field_in_link": "Check",
+ "translate_link_fields": "Check",
}
docfield_properties = {
diff --git a/frappe/custom/doctype/customize_form/test_customize_form.py b/frappe/custom/doctype/customize_form/test_customize_form.py
index 5a1f629beb..b00f45f5d2 100644
--- a/frappe/custom/doctype/customize_form/test_customize_form.py
+++ b/frappe/custom/doctype/customize_form/test_customize_form.py
@@ -396,3 +396,10 @@ class TestCustomizeForm(unittest.TestCase):
d.label = ""
d.run_method("save_customization")
self.assertEqual(d.label, "")
+
+ def test_change_to_autoincrement_autoname(self):
+ d = self.get_customize_form("Event")
+ d.autoname = "autoincrement"
+
+ with self.assertRaises(frappe.ValidationError):
+ d.run_method("save_customization")
diff --git a/frappe/database/database.py b/frappe/database/database.py
index ca4b5a5310..1a03ac3889 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -1019,13 +1019,13 @@ class Database(object):
return self.get_value(dt, dn, ignore=True, cache=cache)
- def count(self, dt, filters=None, debug=False, cache=False):
+ def count(self, dt, filters=None, debug=False, cache=False, distinct: bool = True):
"""Returns `COUNT(*)` for given DocType and filters."""
if cache and not filters:
cache_count = frappe.cache().get_value("doctype:count:{}".format(dt))
if cache_count is not None:
return cache_count
- query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"))
+ query = self.query.get_sql(table=dt, filters=filters, fields=Count("*"), distinct=distinct)
if filters:
count = self.sql(query, debug=debug)[0][0]
return count
diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py
index 28d78471d2..7505ef3a7f 100644
--- a/frappe/database/mariadb/database.py
+++ b/frappe/database/mariadb/database.py
@@ -19,6 +19,15 @@ class MariaDBDatabase(Database):
DataError = pymysql.err.DataError
REGEX_CHARACTER = "regexp"
+ # NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
+ # it drops the cache and uses the next non cached value in setval query and
+ # puts that in the backup file, which will start the counter
+ # from that value when inserting any new record in the doctype.
+ # By default the cache is 1000 which will mess up the sequence when
+ # using the system after a restore.
+ # issue link: https://jira.mariadb.org/browse/MDEV-21786
+ SEQUENCE_CACHE = 50
+
def setup_type_map(self):
self.db_type = "mariadb"
self.type_map = {
diff --git a/frappe/database/mariadb/framework_mariadb.sql b/frappe/database/mariadb/framework_mariadb.sql
index f2a1206c7c..dc91873a82 100644
--- a/frappe/database/mariadb/framework_mariadb.sql
+++ b/frappe/database/mariadb/framework_mariadb.sql
@@ -226,6 +226,7 @@ CREATE TABLE `tabDocType` (
`sender_field` varchar(255) DEFAULT NULL,
`show_title_field_in_link` int(1) NOT NULL DEFAULT 0,
`migration_hash` varchar(255) DEFAULT NULL,
+ `translate_link_fields` int(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`name`)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC CHARACTER SET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/frappe/database/mariadb/schema.py b/frappe/database/mariadb/schema.py
index 784fa23c13..f402b4ec74 100644
--- a/frappe/database/mariadb/schema.py
+++ b/frappe/database/mariadb/schema.py
@@ -40,14 +40,7 @@ class MariaDBTable(DBTable):
not self.meta.issingle and self.meta.autoname == "autoincrement"
) or self.doctype in log_types:
- # NOTE: using a very small cache - as during backup, if the sequence was used in anyform,
- # it drops the cache and uses the next non cached value in setval func and
- # puts that in the backup file, which will start the counter
- # from that value when inserting any new record in the doctype.
- # By default the cache is 1000 which will mess up the sequence when
- # using the system after a restore.
- # issue link: https://jira.mariadb.org/browse/MDEV-21786
- frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=50)
+ frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
# NOTE: not used nextval func as default as the ability to restore
# database with sequences has bugs in mariadb and gives a scary error.
diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py
index d69e0bea94..8bd4113823 100644
--- a/frappe/database/postgres/database.py
+++ b/frappe/database/postgres/database.py
@@ -31,6 +31,12 @@ class PostgresDatabase(Database):
InterfaceError = psycopg2.InterfaceError
REGEX_CHARACTER = "~"
+ # NOTE; The sequence cache for postgres is per connection.
+ # Since we're opening and closing connections for every transaction this results in skipping the cache
+ # to the next non-cached value hence not using cache in postgres.
+ # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
+ SEQUENCE_CACHE = 0
+
def setup_type_map(self):
self.db_type = "postgres"
self.type_map = {
@@ -209,18 +215,19 @@ class PostgresDatabase(Database):
)
def change_column_type(
- self, doctype: str, column: str, type: str, nullable: bool = False
+ self, doctype: str, column: str, type: str, nullable: bool = False, use_cast: bool = False
) -> Union[List, Tuple]:
table_name = get_table_name(doctype)
null_constraint = "SET NOT NULL" if not nullable else "DROP NOT NULL"
+ using_cast = f'using "{column}"::{type}' if use_cast else ""
# postgres allows ddl in transactions but since we've currently made
# things same as mariadb (raising exception on ddl commands if the transaction has any writes),
# hence using sql_ddl here for committing and then moving forward.
return self.sql_ddl(
f"""ALTER TABLE "{table_name}"
- ALTER COLUMN "{column}" TYPE {type},
- ALTER COLUMN "{column}" {null_constraint}"""
+ ALTER COLUMN "{column}" TYPE {type} {using_cast},
+ ALTER COLUMN "{column}" {null_constraint}"""
)
def create_auth_table(self):
diff --git a/frappe/database/postgres/framework_postgres.sql b/frappe/database/postgres/framework_postgres.sql
index 2cae3ab82f..99e94a226f 100644
--- a/frappe/database/postgres/framework_postgres.sql
+++ b/frappe/database/postgres/framework_postgres.sql
@@ -231,6 +231,7 @@ CREATE TABLE "tabDocType" (
"sender_field" varchar(255) DEFAULT NULL,
"show_title_field_in_link" smallint NOT NULL DEFAULT 0,
"migration_hash" varchar(255) DEFAULT NULL,
+ "translate_link_fields" smallint NOT NULL DEFAULT 0,
PRIMARY KEY ("name")
) ;
diff --git a/frappe/database/postgres/schema.py b/frappe/database/postgres/schema.py
index 2abd5f37c7..ef7ba33e12 100644
--- a/frappe/database/postgres/schema.py
+++ b/frappe/database/postgres/schema.py
@@ -34,11 +34,7 @@ class PostgresTable(DBTable):
not self.meta.issingle and self.meta.autoname == "autoincrement"
) or self.doctype in log_types:
- # The sequence cache is per connection.
- # Since we're opening and closing connections for every transaction this results in skipping the cache
- # to the next non-cached value hence not using cache in postgres.
- # ref: https://stackoverflow.com/questions/21356375/postgres-9-0-4-sequence-skipping-numbers
- frappe.db.create_sequence(self.doctype, check_not_exists=True)
+ frappe.db.create_sequence(self.doctype, check_not_exists=True, cache=frappe.db.SEQUENCE_CACHE)
name_column = "name bigint primary key"
# TODO: set docstatus length
diff --git a/frappe/database/query.py b/frappe/database/query.py
index 136f5c86b6..b107759af0 100644
--- a/frappe/database/query.py
+++ b/frappe/database/query.py
@@ -4,10 +4,10 @@ from typing import Any, Dict, List, Tuple, Union
import frappe
from frappe import _
-from frappe.query_builder import Criterion, Field, Order
+from frappe.query_builder import Criterion, Field, Order, Table
-def like(key: str, value: str) -> frappe.qb:
+def like(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `LIKE`
Args:
@@ -17,10 +17,10 @@ def like(key: str, value: str) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `LIKE`
"""
- return Field(key).like(value)
+ return key.like(value)
-def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
+def func_in(key: Field, value: Union[List, Tuple]) -> frappe.qb:
"""Wrapper method for `IN`
Args:
@@ -30,10 +30,10 @@ def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `IN`
"""
- return Field(key).isin(value)
+ return key.isin(value)
-def not_like(key: str, value: str) -> frappe.qb:
+def not_like(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `NOT LIKE`
Args:
@@ -43,10 +43,10 @@ def not_like(key: str, value: str) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `NOT LIKE`
"""
- return Field(key).not_like(value)
+ return key.not_like(value)
-def func_not_in(key: str, value: Union[List, Tuple]):
+def func_not_in(key: Field, value: Union[List, Tuple]):
"""Wrapper method for `NOT IN`
Args:
@@ -56,10 +56,10 @@ def func_not_in(key: str, value: Union[List, Tuple]):
Returns:
frappe.qb: `frappe.qb object with `NOT IN`
"""
- return Field(key).notin(value)
+ return key.notin(value)
-def func_regex(key: str, value: str) -> frappe.qb:
+def func_regex(key: Field, value: str) -> frappe.qb:
"""Wrapper method for `REGEX`
Args:
@@ -69,10 +69,10 @@ def func_regex(key: str, value: str) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `REGEX`
"""
- return Field(key).regex(value)
+ return key.regex(value)
-def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
+def func_between(key: Field, value: Union[List, Tuple]) -> frappe.qb:
"""Wrapper method for `BETWEEN`
Args:
@@ -82,7 +82,12 @@ def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb:
Returns:
frappe.qb: `frappe.qb object with `BETWEEN`
"""
- return Field(key)[slice(*value)]
+ return key[slice(*value)]
+
+
+def func_is(key, value):
+ "Wrapper for IS"
+ return Field(key).isnotnull() if value.lower() == "set" else Field(key).isnull()
def make_function(key: Any, value: Union[int, str]):
@@ -135,11 +140,14 @@ OPERATOR_MAP = {
"not like": not_like,
"regex": func_regex,
"between": func_between,
+ "is": func_is,
}
class Query:
- def get_condition(self, table: str, **kwargs) -> frappe.qb:
+ tables: dict = {}
+
+ def get_condition(self, table: Union[str, Table], **kwargs) -> frappe.qb:
"""Get initial table object
Args:
@@ -148,11 +156,20 @@ class Query:
Returns:
frappe.qb: DocType with initial condition
"""
+ table_object = self.get_table(table)
if kwargs.get("update"):
- return frappe.qb.update(table)
+ return frappe.qb.update(table_object)
if kwargs.get("into"):
- return frappe.qb.into(table)
- return frappe.qb.from_(table)
+ return frappe.qb.into(table_object)
+ return frappe.qb.from_(table_object)
+
+ def get_table(self, table_name: Union[str, Table]) -> Table:
+ if isinstance(table_name, Table):
+ return table_name
+ table_name = table_name.strip('"').strip("'")
+ if table_name not in self.tables:
+ self.tables[table_name] = frappe.qb.DocType(table_name)
+ return self.tables[table_name]
def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb:
"""Generate filters from Criterion objects
@@ -217,8 +234,13 @@ class Query:
conditions = conditions.where(_operator(Field(filters[0]), filters[2]))
break
else:
- _operator = OPERATOR_MAP[f[1]]
- conditions = conditions.where(_operator(Field(f[0]), f[2]))
+ _operator = OPERATOR_MAP[f[-2]]
+ if len(f) == 4:
+ table_object = self.get_table(f[0])
+ _field = table_object[f[1]]
+ else:
+ _field = Field(f[0])
+ conditions = conditions.where(_operator(_field, f[-1]))
return self.add_conditions(conditions, **kwargs)
@@ -249,7 +271,7 @@ class Query:
if isinstance(value, (list, tuple)):
if isinstance(value[1], (list, tuple)) or value[0] in list(OPERATOR_MAP.keys())[-4:]:
_operator = OPERATOR_MAP[value[0]]
- conditions = conditions.where(_operator(key, value[1]))
+ conditions = conditions.where(_operator(Field(key), value[1]))
else:
_operator = OPERATOR_MAP[value[0]]
conditions = conditions.where(_operator(Field(key), value[1]))
@@ -293,10 +315,19 @@ class Query:
self,
table: str,
fields: Union[List, Tuple],
- filters: Union[Dict[str, Union[str, int]], str, int] = None,
+ filters: Union[Dict[str, Union[str, int]], str, int, List[Union[List, str, int]]] = None,
**kwargs,
):
+ # Clean up state before each query
+ self.tables = {}
criterion = self.build_conditions(table, filters, **kwargs)
+
+ if len(self.tables) > 1:
+ primary_table = self.tables[table]
+ del self.tables[table]
+ for table_object in self.tables.values():
+ criterion = criterion.left_join(table_object).on(table_object.parent == primary_table.name)
+
if isinstance(fields, (list, tuple)):
query = criterion.select(*kwargs.get("field_objects", fields))
diff --git a/frappe/database/sequence.py b/frappe/database/sequence.py
index ede4689485..6a352d20d1 100644
--- a/frappe/database/sequence.py
+++ b/frappe/database/sequence.py
@@ -5,7 +5,7 @@ def create_sequence(
doctype_name: str,
*,
slug: str = "_id_seq",
- temporary=False,
+ temporary: bool = False,
check_not_exists: bool = False,
cycle: bool = False,
cache: int = 0,
@@ -51,7 +51,7 @@ def create_sequence(
else:
query += " cycle"
- db.sql(query)
+ db.sql_ddl(query)
return sequence_name
diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py
index 4c82fe8c73..ca0d1e2353 100644
--- a/frappe/desk/desktop.py
+++ b/frappe/desk/desktop.py
@@ -166,6 +166,8 @@ class Workspace:
self.onboardings = {"items": self.get_onboardings()}
+ self.quick_lists = {"items": self.get_quick_lists()}
+
def _doctype_contains_a_record(self, name):
exists = self.table_counts.get(name, False)
@@ -284,6 +286,21 @@ class Workspace:
return items
+ @handle_not_exist
+ def get_quick_lists(self):
+ items = []
+ quick_lists = self.doc.quick_lists
+
+ for item in quick_lists:
+ new_item = item.as_dict().copy()
+
+ # Translate label
+ new_item["label"] = _(item.label) if item.label else _(item.document_type)
+
+ items.append(new_item)
+
+ return items
+
@handle_not_exist
def get_onboardings(self):
if self.onboarding_list:
@@ -336,6 +353,7 @@ def get_desktop_page(page):
"shortcuts": workspace.shortcuts,
"cards": workspace.cards,
"onboardings": workspace.onboardings,
+ "quick_lists": workspace.quick_lists,
}
except DoesNotExistError:
frappe.log_error("Workspace Missing")
@@ -452,6 +470,8 @@ def save_new_widget(doc, page, blocks, new_widgets):
doc.charts.extend(new_widget(widgets.chart, "Workspace Chart", "charts"))
if widgets.shortcut:
doc.shortcuts.extend(new_widget(widgets.shortcut, "Workspace Shortcut", "shortcuts"))
+ if widgets.quick_list:
+ doc.quick_lists.extend(new_widget(widgets.quick_list, "Workspace Quick List", "quick_lists"))
if widgets.card:
doc.build_links_table_from_card(widgets.card)
@@ -481,12 +501,12 @@ def save_new_widget(doc, page, blocks, new_widgets):
def clean_up(original_page, blocks):
page_widgets = {}
- for wid in ["shortcut", "card", "chart"]:
+ for wid in ["shortcut", "card", "chart", "quick_list"]:
# get list of widget's name from blocks
page_widgets[wid] = [x["data"][wid + "_name"] for x in loads(blocks) if x["type"] == wid]
- # shortcut & chart cleanup
- for wid in ["shortcut", "chart"]:
+ # shortcut, chart & quick_list cleanup
+ for wid in ["shortcut", "chart", "quick_list"]:
updated_widgets = []
original_page.get(wid + "s").reverse()
diff --git a/frappe/desk/doctype/event/event.json b/frappe/desk/doctype/event/event.json
index 2f67c36fc0..bce3b1e65a 100644
--- a/frappe/desk/doctype/event/event.json
+++ b/frappe/desk/doctype/event/event.json
@@ -277,7 +277,7 @@
"icon": "fa fa-calendar",
"idx": 1,
"links": [],
- "modified": "2021-11-18 05:06:24.881742",
+ "modified": "2022-05-12 05:43:27.935510",
"modified_by": "Administrator",
"module": "Desk",
"name": "Event",
@@ -312,6 +312,7 @@
"sender_field": "sender",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"subject_field": "subject",
"title_field": "subject",
"track_changes": 1,
diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json
index fa8b81f5fd..032de9de4e 100644
--- a/frappe/desk/doctype/workspace/workspace.json
+++ b/frappe/desk/doctype/workspace/workspace.json
@@ -26,6 +26,8 @@
"shortcuts",
"tab_break_18",
"links",
+ "quick_lists_tab",
+ "quick_lists",
"roles_tab",
"roles"
],
@@ -155,11 +157,22 @@
"fieldname": "roles_tab",
"fieldtype": "Tab Break",
"label": "Roles"
+ },
+ {
+ "fieldname": "quick_lists_tab",
+ "fieldtype": "Tab Break",
+ "label": "Quick Lists"
+ },
+ {
+ "fieldname": "quick_lists",
+ "fieldtype": "Table",
+ "label": "Quick Lists",
+ "options": "Workspace Quick List"
}
],
"in_create": 1,
"links": [],
- "modified": "2022-01-27 12:06:13.111743",
+ "modified": "2022-05-12 13:00:03.925605",
"modified_by": "Administrator",
"module": "Desk",
"name": "Workspace",
@@ -189,5 +202,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace_quick_list/__init__.py b/frappe/desk/doctype/workspace_quick_list/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json
new file mode 100644
index 0000000000..1542ebe03c
--- /dev/null
+++ b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.json
@@ -0,0 +1,60 @@
+{
+ "actions": [],
+ "creation": "2022-05-12 12:58:41.824496",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "column_break_1",
+ "label",
+ "section_break_4",
+ "quick_list_filter"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "DocType",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "label",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Label",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "quick_list_filter",
+ "fieldtype": "Code",
+ "in_list_view": 1,
+ "label": "Quick List Filter",
+ "options": "JSON"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2022-05-12 13:48:40.617623",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Workspace Quick List",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py
new file mode 100644
index 0000000000..9f26424115
--- /dev/null
+++ b/frappe/desk/doctype/workspace_quick_list/workspace_quick_list.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class WorkspaceQuickList(Document):
+ pass
diff --git a/frappe/desk/notifications.py b/frappe/desk/notifications.py
index e92a7492ce..2a987f5539 100644
--- a/frappe/desk/notifications.py
+++ b/frappe/desk/notifications.py
@@ -249,9 +249,9 @@ def get_open_count(doctype, name, items=None):
if frappe.flags.in_migrate or frappe.flags.in_install:
return {"count": []}
- frappe.has_permission(doc=frappe.get_doc(doctype, name), throw=True)
-
- meta = frappe.get_meta(doctype)
+ doc = frappe.get_doc(doctype, name)
+ doc.check_permission()
+ meta = doc.meta
links = meta.get_dashboard_data()
# compile all items in a list
@@ -266,7 +266,6 @@ def get_open_count(doctype, name, items=None):
out = []
for d in items:
if d in links.get("internal_links", {}):
- # internal link
continue
filters = get_filters_for(d)
diff --git a/frappe/desk/page/setup_wizard/setup_wizard.py b/frappe/desk/page/setup_wizard/setup_wizard.py
index f85d24704f..3f849bbcaa 100755
--- a/frappe/desk/page/setup_wizard/setup_wizard.py
+++ b/frappe/desk/page/setup_wizard/setup_wizard.py
@@ -269,7 +269,6 @@ def add_all_roles_to(name):
def disable_future_access():
frappe.db.set_default("desktop:home_page", "workspace")
frappe.db.set_value("System Settings", "System Settings", "setup_complete", 1)
- frappe.db.set_value("System Settings", "System Settings", "is_first_startup", 1)
# Enable onboarding after install
frappe.db.set_value("System Settings", "System Settings", "enable_onboarding", 1)
@@ -334,11 +333,6 @@ def load_user_details():
}
-@frappe.whitelist()
-def reset_is_first_startup():
- frappe.db.set_value("System Settings", "System Settings", "is_first_startup", 0)
-
-
def prettify_args(args):
# remove attachments
for key, val in args.items():
diff --git a/frappe/handler.py b/frappe/handler.py
index 7b010eb716..44249323ef 100644
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -25,6 +25,7 @@ ALLOWED_MIMETYPES = (
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.spreadsheet",
+ "text/plain",
)
@@ -202,7 +203,7 @@ def upload_file():
if not file_url and (frappe.session.user == "Guest" or (user and not user.has_desk_access())):
filetype = guess_type(filename)[0]
if filetype not in ALLOWED_MIMETYPES:
- frappe.throw(_("You can only upload JPG, PNG, PDF, or Microsoft documents."))
+ frappe.throw(_("You can only upload JPG, PNG, PDF, TXT or Microsoft documents."))
if method:
method = frappe.get_attr(method)
diff --git a/frappe/hooks.py b/frappe/hooks.py
index f7a67dc7ec..ee8417a3ec 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -4,8 +4,6 @@ app_name = "frappe"
app_title = "Frappe Framework"
app_publisher = "Frappe Technologies"
app_description = "Full stack web framework with Python, Javascript, MariaDB, Redis, Node"
-app_icon = "octicon octicon-circuit-board"
-app_color = "orange"
source_link = "https://github.com/frappe/frappe"
app_license = "MIT"
app_logo_url = "/assets/frappe/images/frappe-framework-logo.svg"
diff --git a/frappe/installer.py b/frappe/installer.py
index 634d6287f8..5cd46e618d 100644
--- a/frappe/installer.py
+++ b/frappe/installer.py
@@ -7,6 +7,8 @@ import sys
from collections import OrderedDict
from typing import Dict, List, Tuple
+import click
+
import frappe
from frappe.defaults import _clear_cache
from frappe.utils import is_git_url
@@ -80,7 +82,13 @@ def _new_site(
)
for app in apps_to_install:
- install_app(app, verbose=verbose, set_as_patched=not source_sql)
+ # NOTE: not using force here for 2 reasons:
+ # 1. It's not really needed here as we've freshly installed a new db
+ # 2. If someone uses a sql file to do restore and that file already had
+ # installed_apps then it might cause problems as that sql file can be of any previous version(s)
+ # which might be incompatible with the current version and using force might cause problems.
+ # Example: the DocType DocType might not have `migration_hash` column which will cause failure in the restore.
+ install_app(app, verbose=verbose, set_as_patched=not source_sql, force=False)
os.remove(installing)
@@ -226,7 +234,7 @@ def parse_app_name(name: str) -> str:
return repo
-def install_app(name, verbose=False, set_as_patched=True):
+def install_app(name, verbose=False, set_as_patched=True, force=False):
from frappe.core.doctype.scheduled_job_type.scheduled_job_type import sync_jobs
from frappe.model.sync import sync_for
from frappe.modules.utils import sync_customizations
@@ -243,7 +251,7 @@ def install_app(name, verbose=False, set_as_patched=True):
if app_hooks.required_apps:
for app in app_hooks.required_apps:
required_app = parse_app_name(app)
- install_app(required_app, verbose=verbose)
+ install_app(required_app, verbose=verbose, force=force)
frappe.flags.in_install = name
frappe.clear_cache()
@@ -251,8 +259,8 @@ def install_app(name, verbose=False, set_as_patched=True):
if name not in frappe.get_all_apps():
raise Exception("App not in apps.txt")
- if name in installed_apps:
- frappe.msgprint(frappe._("App {0} already installed").format(name))
+ if not force and name in installed_apps:
+ click.secho(f"App {name} already installed", fg="yellow")
return
print("\nInstalling {0}...".format(name))
@@ -266,9 +274,9 @@ def install_app(name, verbose=False, set_as_patched=True):
return
if name != "frappe":
- add_module_defs(name)
+ add_module_defs(name, ignore_if_duplicate=force)
- sync_for(name, force=True, reset_permissions=True)
+ sync_for(name, force=force, reset_permissions=True)
add_to_installed_apps(name)
@@ -315,7 +323,6 @@ def remove_from_installed_apps(app_name):
def remove_app(app_name, dry_run=False, yes=False, no_backup=False, force=False):
"""Remove app and all linked to the app's module with the app from a site."""
- import click
site = frappe.local.site
app_hooks = frappe.get_hooks(app_name=app_name)
@@ -573,13 +580,13 @@ def make_site_dirs():
os.makedirs(path, exist_ok=True)
-def add_module_defs(app):
+def add_module_defs(app, ignore_if_duplicate=False):
modules = frappe.get_module_list(app)
for module in modules:
d = frappe.new_doc("Module Def")
d.app_name = app
d.module_name = module
- d.insert(ignore_permissions=True, ignore_if_duplicate=True)
+ d.insert(ignore_permissions=True, ignore_if_duplicate=ignore_if_duplicate)
def remove_missing_apps():
@@ -752,11 +759,9 @@ def partial_restore(sql_file_path, verbose=False):
elif frappe.conf.db_type == "postgres":
import warnings
- from click import style
-
from frappe.database.postgres.setup_db import import_db_from_sql
- warn = style(
+ warn = click.style(
"Delete the tables you want to restore manually before attempting"
" partial restore operation for PostreSQL databases",
fg="yellow",
@@ -798,8 +803,6 @@ def validate_database_sql(path, _raise=True):
error_message = "Table `tabDefaultValue` not found in file."
if error_message:
- import click
-
click.secho(error_message, fg="red")
if _raise and (missing_table or empty_file):
diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py
index acb63b5bfa..005b7e3741 100644
--- a/frappe/model/db_query.py
+++ b/frappe/model/db_query.py
@@ -920,12 +920,12 @@ def cast_name(column: str) -> str:
kwargs = {"string": column, "flags": re.IGNORECASE}
if "cast(" not in column.lower() and "::" not in column:
- if re.search(r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", **kwargs):
+ if re.search(r"locate\([^,]+,\s*[`\"]?name[`\"]?\s*\)", **kwargs):
return re.sub(
- r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\)", r"locate(\1, cast(\2 as varchar))", **kwargs
+ r"locate\(([^,]+),\s*([`\"]?name[`\"]?)\s*\)", r"locate(\1, cast(\2 as varchar))", **kwargs
)
- elif match := re.search(r"(strpos|ifnull|coalesce)\(\s*([`\"]?name[`\"]?)\s*,", **kwargs):
+ elif match := re.search(r"(strpos|ifnull|coalesce)\(\s*[`\"]?name[`\"]?\s*,", **kwargs):
func = match.groups()[0]
return re.sub(rf"{func}\(\s*([`\"]?name[`\"]?)\s*,", rf"{func}(cast(\1 as varchar),", **kwargs)
diff --git a/frappe/model/sync.py b/frappe/model/sync.py
index a56d1f267f..4c535b2811 100644
--- a/frappe/model/sync.py
+++ b/frappe/model/sync.py
@@ -80,6 +80,7 @@ def sync_for(app_name, force=0, reset_permissions=False):
"workspace_link",
"workspace_chart",
"workspace_shortcut",
+ "workspace_quick_list",
"workspace",
]:
files.append(os.path.join(FRAPPE_PATH, "desk", "doctype", desk_module, f"{desk_module}.json"))
diff --git a/frappe/modules/import_file.py b/frappe/modules/import_file.py
index 00483bf6a5..d39f98f966 100644
--- a/frappe/modules/import_file.py
+++ b/frappe/modules/import_file.py
@@ -144,7 +144,6 @@ def import_file_by_path(
import_doc(
docdict=doc,
- force=force,
data_import=data_import,
pre_process=pre_process,
ignore_version=ignore_version,
@@ -203,7 +202,6 @@ def update_modified(original_modified, doc):
def import_doc(
docdict,
- force=False,
data_import=False,
pre_process=None,
ignore_version=None,
diff --git a/frappe/patches.txt b/frappe/patches.txt
index 845ccee09a..4dc8c1b698 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -189,6 +189,8 @@ frappe.patches.v14_0.save_ratings_in_fraction #23-12-2021
frappe.patches.v14_0.transform_todo_schema
frappe.patches.v14_0.remove_post_and_post_comment
frappe.patches.v14_0.reset_creation_datetime
+frappe.patches.v14_0.remove_is_first_startup
+frappe.patches.v14_0.reload_workspace_child_tables
[post_model_sync]
frappe.patches.v14_0.drop_data_import_legacy
diff --git a/frappe/patches/v14_0/reload_workspace_child_tables.py b/frappe/patches/v14_0/reload_workspace_child_tables.py
new file mode 100644
index 0000000000..c22774d94c
--- /dev/null
+++ b/frappe/patches/v14_0/reload_workspace_child_tables.py
@@ -0,0 +1,13 @@
+import frappe
+
+
+def execute():
+ child_tables = frappe.get_all(
+ "DocField",
+ pluck="options",
+ filters={"fieldtype": ["in", frappe.model.table_fields], "parent": "Workspace"},
+ )
+
+ for child_table in child_tables:
+ if child_table != "Has Role":
+ frappe.reload_doc("desk", "doctype", child_table, force=True)
diff --git a/frappe/patches/v14_0/remove_is_first_startup.py b/frappe/patches/v14_0/remove_is_first_startup.py
new file mode 100644
index 0000000000..cae38ce2ab
--- /dev/null
+++ b/frappe/patches/v14_0/remove_is_first_startup.py
@@ -0,0 +1,8 @@
+import frappe
+
+
+def execute():
+ singles = frappe.qb.Table("tabSingles")
+ frappe.qb.from_(singles).delete().where(
+ (singles.doctype == "System Settings") & (singles.field == "is_first_startup")
+ ).run()
diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js
index 72e8010605..a8cbe020f3 100644
--- a/frappe/public/js/frappe/desk.js
+++ b/frappe/public/js/frappe/desk.js
@@ -511,7 +511,8 @@ frappe.Application = class Application {
// "version": "12.2.0"
// }];
- if (!Array.isArray(change_log) || !change_log.length || window.Cypress) {
+ if (!Array.isArray(change_log) || !change_log.length ||
+ window.Cypress || cint(frappe.boot.sysdefaults.disable_change_log_notification)) {
return;
}
diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js
index 5a5af389ee..5545a453e9 100644
--- a/frappe/public/js/frappe/form/controls/base_input.js
+++ b/frappe/public/js/frappe/form/controls/base_input.js
@@ -65,7 +65,11 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
};
var update_input = function() {
- me.set_input(me.value);
+ if (me.doctype && me.docname) {
+ me.set_input(me.value);
+ } else {
+ me.set_input(me.value || null);
+ }
};
if (me.disp_status != "None") {
@@ -155,6 +159,13 @@ frappe.ui.form.ControlInput = class ControlInput extends frappe.ui.form.Control
this.$wrapper.find(".help-box").html("");
}
set_mandatory(value) {
+ // do not set has-error class on form load
+ if (this.frm && this.frm.cscript && this.frm.cscript.is_onload) return;
+
+ // do not set has-error class while dialog is rendered
+ // set has-error if dialog primary button is clicked
+ if (this.layout && this.layout.is_dialog && !this.layout.primary_action_fulfilled) return;
+
this.$wrapper.toggleClass("has-error", Boolean(this.df.reqd && is_null(value)));
}
set_invalid () {
diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js
index 0f80371706..bea7e77bd1 100644
--- a/frappe/public/js/frappe/form/controls/date.js
+++ b/frappe/public/js/frappe/form/controls/date.js
@@ -137,13 +137,13 @@ frappe.ui.form.ControlDate = class ControlDate extends frappe.ui.form.ControlDat
});
}
parse(value) {
- if(value) {
- return frappe.datetime.user_to_str(value);
+ if (value) {
+ return frappe.datetime.user_to_str(value, false, true);
}
}
format_for_input(value) {
- if(value) {
- return frappe.datetime.str_to_user(value);
+ if (value) {
+ return frappe.datetime.str_to_user(value, false, true);
}
return "";
}
diff --git a/frappe/public/js/frappe/form/controls/datetime.js b/frappe/public/js/frappe/form/controls/datetime.js
index d1a06a6ac6..9b10465d7b 100644
--- a/frappe/public/js/frappe/form/controls/datetime.js
+++ b/frappe/public/js/frappe/form/controls/datetime.js
@@ -45,8 +45,6 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co
}
format_for_input(value) {
if (!value) return "";
-
-
return frappe.datetime.str_to_user(value, false);
}
set_description() {
diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js
index 2081a301c3..ebaf36fe4e 100644
--- a/frappe/public/js/frappe/form/controls/link.js
+++ b/frappe/public/js/frappe/form/controls/link.js
@@ -36,6 +36,9 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
if(!me.$input.val()) {
me.$input.val("").trigger("input");
+
+ // hide link arrow to doctype if none is set
+ me.$link.toggle(false);
}
}, 500);
});
@@ -78,6 +81,12 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
this.set_link_title(value);
}
+ get_translated(value) {
+ return this.is_translatable() ? __(value) : value;
+ }
+ is_translatable() {
+ return in_list(frappe.boot?.translatable_doctypes || [], this.get_options());
+ }
set_link_title(value) {
let doctype = this.get_options();
@@ -89,25 +98,32 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
link_title = frappe.utils
.fetch_link_title(doctype, value)
.then(link_title => {
- this.set_input_value(link_title);
- this.title_value_map[link_title] = value;
+ this.translate_and_set_input_value(link_title, value);
});
} else {
- this.set_input_value(link_title);
- this.title_value_map[link_title] = value;
+ this.translate_and_set_input_value(link_title, value);
}
} else {
- this.set_input_value(value);
+ this.translate_and_set_input_value(value, value)
}
}
+ translate_and_set_input_value(link_title, value) {
+ let translated_link_text = this.get_translated(link_title)
+ this.title_value_map[translated_link_text] = value;
+
+ this.set_input_value(translated_link_text);
+ }
parse_validate_and_set_in_model(value, e, label) {
if (this.parse) value = this.parse(value, label);
if (label) {
- this.label = label;
+ this.label = this.get_translated(label);
frappe.utils.add_link_title(this.df.options, value, label);
}
- return this.validate_and_set_in_model(value, e);
+ return this.validate_and_set_in_model(value, e, true);
+ }
+ parse(value) {
+ return strip_html(value);
}
get_input_value() {
if (this.$input) {
@@ -164,7 +180,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
return false;
}
setup_awesomeplete() {
- var me = this;
+ let me = this;
this.$input.cache = {};
@@ -173,14 +189,14 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
maxItems: 99,
autoFirst: true,
list: [],
- replace: function (suggestion) {
+ replace: function (item) {
// Override Awesomeplete replace function as it is used to set the input value
// https://github.com/LeaVerou/awesomplete/issues/17104#issuecomment-359185403
- this.input.value = suggestion.label || suggestion.value;
+ this.input.value = me.get_translated(item.label || item.value);
},
data: function (item) {
return {
- label: item.label || item.value,
+ label: me.get_translated(item.label || item.value),
value: item.value
};
},
@@ -188,11 +204,11 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
return true;
},
item: function (item) {
- var d = this.get_item(item.value);
+ let d = this.get_item(item.value);
if(!d.label) { d.label = d.value; }
- var _label = (me.translate_values) ? __(d.label) : d.label;
- var html = d.html || "" + _label + "";
+ let _label = me.get_translated(d.label);
+ let html = d.html || "" + _label + "";
if(d.description && d.value!==d.description) {
html += ' ' + __(d.description) + '';
}
@@ -296,18 +312,30 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
}
let value = me.get_input_value();
let label = me.get_label_value();
+ let last_value = me.last_value || "";
+ let last_label = me.label || "";
- if (value !== me.last_value || me.label !== label) {
+ if (value !== last_value || label !== last_label) {
me.parse_validate_and_set_in_model(value, null, label);
}
});
this.$input.on("awesomplete-open", () => {
this.autocomplete_open = true;
+
+ if (!me.get_label_value()) {
+ // hide link arrow to doctype if none is set
+ me.$link.toggle(false);
+ }
});
- this.$input.on("awesomplete-close", () => {
+ this.$input.on("awesomplete-close", (e) => {
this.autocomplete_open = false;
+
+ if (!me.get_label_value()) {
+ // hide link arrow to doctype if none is set
+ me.$link.toggle(false);
+ }
});
this.$input.on("awesomplete-select", function(e) {
@@ -317,7 +345,7 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
me.autocomplete_open = false;
// prevent selection on tab
- var TABKEY = 9;
+ let TABKEY = 9;
if (e.keyCode === TABKEY) {
e.preventDefault();
me.awesomplete.close();
@@ -347,6 +375,24 @@ frappe.ui.form.ControlLink = class ControlLink extends frappe.ui.form.ControlDat
me.$input.val("");
}
});
+
+ this.$input.on("focus", function () {
+ if (!frappe.boot.translated_search_doctypes.includes(me.df.options)) {
+ me.show_untranslated();
+ }
+ });
+
+ this.$input.keydown((e) => {
+ let BACKSPACE = 8;
+ if (e.keyCode === BACKSPACE && !frappe.boot.translated_search_doctypes.includes(me.df.options)) {
+ me.show_untranslated();
+ }
+ });
+ }
+
+ show_untranslated() {
+ let value = this.get_input_value();
+ this.is_translatable() && this.set_input_value(value);
}
merge_duplicates(results) {
@@ -590,5 +636,4 @@ if (Awesomplete) {
return item.value === value;
});
};
-}
-
+}
\ No newline at end of file
diff --git a/frappe/public/js/frappe/form/controls/select.js b/frappe/public/js/frappe/form/controls/select.js
index 7df2bbfbaa..363c0d957c 100644
--- a/frappe/public/js/frappe/form/controls/select.js
+++ b/frappe/public/js/frappe/form/controls/select.js
@@ -73,7 +73,8 @@ frappe.ui.form.ControlSelect = class ControlSelect extends frappe.ui.form.Contro
if(this.$input) {
var selected = this.$input.find(":selected").val();
- this.$input.empty().add_options(options || []);
+ this.$input.empty();
+ frappe.ui.form.add_options(this.$input, options || []);
if(value===undefined && selected) {
this.$input.val(selected);
@@ -101,39 +102,47 @@ frappe.ui.form.ControlSelect = class ControlSelect extends frappe.ui.form.Contro
}
};
+frappe.ui.form.add_options = function(input, options_list) {
+ let $select = $(input);
+ if (!Array.isArray(options_list)) {
+ return $select;
+ }
+ // create options
+ for(var i=0, j=options_list.length; i').html(cstr(label))
+ .attr('value', value)
+ .prop('disabled', is_disabled)
+ .prop('selected', is_selected)
+ .appendTo($select.get(0));
+ }
+ // select the first option
+ $select.get(0).selectedIndex = 0;
+ $select.trigger('select-change');
+ return $select;
+};
+
// add