diff --git a/.github/helper/install.sh b/.github/helper/install.sh index 93189d2b1f..6c81d6298a 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -59,4 +59,4 @@ cd ../.. bench start & bench --site test_site reinstall --yes if [ "$TYPE" == "server" ]; then bench --site test_site_producer reinstall --yes; fi -bench build --app frappe +CI=Yes bench build --app frappe diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml index 90453cd1b4..02a01bf4e4 100644 --- a/.github/workflows/docs-checker.yml +++ b/.github/workflows/docs-checker.yml @@ -12,7 +12,7 @@ jobs: - name: 'Setup Environment' uses: actions/setup-python@v2 with: - python-version: 3.6 + python-version: 3.7 - name: 'Clone repo' uses: actions/checkout@v2 diff --git a/.github/workflows/patch-mariadb-tests.yml b/.github/workflows/patch-mariadb-tests.yml index 3ac5cfa349..8758c4e273 100644 --- a/.github/workflows/patch-mariadb-tests.yml +++ b/.github/workflows/patch-mariadb-tests.yml @@ -29,7 +29,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: '3.9' - name: Check if build should be run id: check-build @@ -102,4 +102,25 @@ jobs: cd ~/frappe-bench/ wget https://frappeframework.com/files/v10-frappe.sql.gz bench --site test_site --force restore ~/frappe-bench/v10-frappe.sql.gz + + source env/bin/activate + cd apps/frappe/ + git remote set-url upstream https://github.com/frappe/frappe.git + git fetch --all --tags + + taglist=$(git tag --sort version:refname | grep -v "beta") + last_release=$(echo "$taglist" | tail -1 | cut -d . -f 1 | cut -c 2-) + + for version in $(seq 12 "$last_release") + do + last_tag=$(echo "$taglist" | grep "v$version" | tail -1) + echo "Updating to $last_tag" + git checkout -q -f "$last_tag" + pip install -q -r requirements.txt + bench --site test_site migrate + done + + echo "Updating to last commit" + git checkout -q -f "$GITHUB_SHA" + bench setup requirements --python bench --site test_site migrate diff --git a/.github/workflows/publish-assets-develop.yml b/.github/workflows/publish-assets-develop.yml index a23885b508..f56d1460b5 100644 --- a/.github/workflows/publish-assets-develop.yml +++ b/.github/workflows/publish-assets-develop.yml @@ -18,7 +18,7 @@ jobs: node-version: 14 - uses: actions/setup-python@v2 with: - python-version: '3.6' + python-version: '3.9' - name: Set up bench and build assets run: | npm install -g yarn diff --git a/.github/workflows/publish-assets-releases.yml b/.github/workflows/publish-assets-releases.yml index a697517c23..2582632fa0 100644 --- a/.github/workflows/publish-assets-releases.yml +++ b/.github/workflows/publish-assets-releases.yml @@ -21,7 +21,7 @@ jobs: python-version: '12.x' - uses: actions/setup-python@v2 with: - python-version: '3.6' + python-version: '3.9' - name: Set up bench and build assets run: | npm install -g yarn diff --git a/.github/workflows/server-mariadb-tests.yml b/.github/workflows/server-mariadb-tests.yml index 0b187fc44c..588f357f26 100644 --- a/.github/workflows/server-mariadb-tests.yml +++ b/.github/workflows/server-mariadb-tests.yml @@ -38,7 +38,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: '3.9' - name: Check if build should be run id: check-build @@ -127,4 +127,5 @@ jobs: name: MariaDB fail_ci_if_error: true files: /home/runner/frappe-bench/sites/coverage.xml - verbose: true \ No newline at end of file + verbose: true + flags: server \ No newline at end of file diff --git a/.github/workflows/server-postgres-tests.yml b/.github/workflows/server-postgres-tests.yml index a5630121a4..78f379837b 100644 --- a/.github/workflows/server-postgres-tests.yml +++ b/.github/workflows/server-postgres-tests.yml @@ -41,7 +41,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: '3.9' - name: Check if build should be run id: check-build @@ -131,3 +131,4 @@ jobs: fail_ci_if_error: true files: /home/runner/frappe-bench/sites/coverage.xml verbose: true + flags: server diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index 0727b06043..fcc53ba59c 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -37,7 +37,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.7 + python-version: '3.9' - name: Check if build should be run id: check-build @@ -122,12 +122,36 @@ jobs: DB: mariadb TYPE: ui + - name: Instrument Source Code + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: cd ~/frappe-bench/apps/frappe/ && npx nyc instrument -x 'frappe/public/dist/**' -x 'frappe/public/js/lib/**' -x '**/*.bundle.js' --compact=false --in-place frappe + + - name: Build + if: ${{ steps.check-build.outputs.build == 'strawberry' }} + run: cd ~/frappe-bench/ && bench build --apps frappe + - name: Site Setup if: ${{ steps.check-build.outputs.build == 'strawberry' }} run: cd ~/frappe-bench/ && bench --site test_site execute frappe.utils.install.complete_setup_wizard - name: UI Tests if: ${{ steps.check-build.outputs.build == 'strawberry' }} - run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --headless --parallel --ci-build-id $GITHUB_RUN_ID + run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests frappe --with-coverage --headless --parallel --ci-build-id $GITHUB_RUN_ID env: CYPRESS_RECORD_KEY: 4a48f41c-11b3-425b-aa88-c58048fa69eb + + - name: Check If Coverage Report Exists + id: check_coverage + uses: andstor/file-existence-action@v1 + with: + files: "/home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml" + + - name: Upload Coverage Data + if: ${{ steps.check-build.outputs.build == 'strawberry' && steps.check_coverage.outputs.files_exists == 'true' }} + uses: codecov/codecov-action@v2 + with: + name: Cypress + fail_ci_if_error: true + directory: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/ + verbose: true + flags: ui-tests diff --git a/.gitignore b/.gitignore index 1ff3122d70..c9dd8f38f3 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,7 @@ coverage.xml *.cover .hypothesis/ .pytest_cache/ +.cypress-coverage # Translations *.mo diff --git a/README.md b/README.md index 6c2804d843..f8a1907da2 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ - + @@ -35,25 +35,29 @@ Full-stack web application framework that uses Python and MariaDB on the server side and a tightly integrated client side library. Built for [ERPNext](https://erpnext.com) -### Table of Contents -* [Installation](https://frappeframework.com/docs/user/en/installation) -* [Documentation](https://frappeframework.com/docs) +## Table of Contents +* [Installation](#installation) +* [Contributing](#contributing) +* [Resources](#resources) * [License](#license) -### Installation +## Installation * [Install via Docker](https://github.com/frappe/frappe_docker) * [Install via Frappe Bench](https://github.com/frappe/bench) +* [Offical Documentation](https://frappeframework.com/docs/user/en/installation) ## Contributing +1. [Code of Conduct](CODE_OF_CONDUCT.md) 1. [Contribution Guidelines](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines) +1. [Security Policy](SECURITY.md) 1. [Translations](https://translate.erpnext.com) -### Website +## Resources -For details and documentation, see the website -[https://frappeframework.com](https://frappeframework.com) +1. [frappeframework.com](https://frappeframework.com) - Official documentation of the Frappe Framework. +1. [frappe.school](https://frappe.school) - Pick from the various courses by the maintainers or from the community. -### License +## License This repository has been released under the [MIT License](LICENSE). diff --git a/codecov.yml b/codecov.yml index 41b22001a5..eeba1ff381 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,10 +4,28 @@ codecov: coverage: status: project: - default: + default: false + server: target: auto threshold: 0.5% + flags: + - server + ui-tests: + target: auto + threshold: 0.5% + flags: + - ui-tests comment: - layout: "diff" + layout: "diff, flags" require_changes: true + +flags: + server: + paths: + - ".*\\.py" + carryforward: true + ui-tests: + paths: + - ".*\\.js" + carryforward: true \ No newline at end of file diff --git a/cypress/fixtures/doctype_with_tab_break.js b/cypress/fixtures/doctype_with_tab_break.js new file mode 100644 index 0000000000..bc346e8fb8 --- /dev/null +++ b/cypress/fixtures/doctype_with_tab_break.js @@ -0,0 +1,59 @@ +export default { + name: 'Form With Tab Break', + custom: 1, + actions: [], + doctype: 'DocType', + engine: 'InnoDB', + fields: [ + { + fieldname: 'username', + fieldtype: 'Data', + label: 'Name', + options: 'Name' + }, + { + fieldname: 'tab', + fieldtype: 'Tab Break', + label: 'Tab 2', + }, + { + fieldname: 'Phone', + fieldtype: 'Data', + label: 'Phone', + options: 'Phone', + reqd: 1 + }, + ], + links: [ + { + "group": "Profile", + "link_doctype": "Contact", + "link_fieldname": "user" + }, + { + "group": "Profile", + "link_doctype": "Chat Profile", + "link_fieldname": "user" + }, + ], + modified_by: 'Administrator', + module: 'Custom', + owner: 'Administrator', + permissions: [ + { + create: 1, + delete: 1, + email: 1, + print: 1, + read: 1, + role: 'System Manager', + share: 1, + write: 1 + } + ], + quick_entry: 1, + autoname: "format: Test-{####}", + sort_field: 'modified', + sort_order: 'ASC', + track_changes: 1 +}; diff --git a/cypress/integration/dashboard_links.js b/cypress/integration/dashboard_links.js index b77965ee1a..16ffd41cf4 100644 --- a/cypress/integration/dashboard_links.js +++ b/cypress/integration/dashboard_links.js @@ -9,17 +9,20 @@ context('Dashboard links', () => { cy.clear_filters(); cy.visit('/app/user'); - cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click(); + cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true }); //To check if initially the dashboard contains only the "Contact" link and there is no counter cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); //Adding a new contact - cy.get('.btn[data-doctype="Contact"]').click(); + cy.get('.document-link-badge[data-doctype="Contact"]').click(); + cy.wait(300); + cy.findByRole('button', {name: 'Add Contact'}).should('be.visible'); + cy.findByRole('button', {name: 'Add Contact'}).click(); cy.get('[data-doctype="Contact"][data-fieldname="first_name"]').type('Admin'); cy.findByRole('button', {name: 'Save'}).click(); cy.visit('/app/user'); - cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click(); + cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true }); //To check if the counter for contact doc is "1" after adding the contact cy.get('[data-doctype="Contact"] > .count').should('contain', '1'); @@ -27,7 +30,7 @@ context('Dashboard links', () => { //Deleting the newly created contact cy.visit('/app/contact'); - cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click(); + cy.get('.list-subject > .select-like > .list-row-checkbox').eq(0).click({ force: true }); cy.findByRole('button', {name: 'Actions'}).click(); cy.get('.actions-btn-group [data-label="Delete"]').click(); cy.findByRole('button', {name: 'Yes'}).click({delay: 700}); @@ -36,7 +39,7 @@ context('Dashboard links', () => { //To check if the counter from the "Contact" doc link is removed cy.wait(700); cy.visit('/app/user'); - cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click(); + cy.get('.list-row-col > .level-item > .ellipsis').eq(0).click({ force: true }); cy.get('[data-doctype="Contact"]').should('contain', 'Contact'); }); @@ -51,13 +54,12 @@ context('Dashboard links', () => { cur_frm.dashboard.data.reports = [ { 'label': 'Reports', - 'items': ['Permitted Documents For User'] + 'items': ['Website Analytics'] } ]; cur_frm.dashboard.render_report_links(); - cy.get('[data-report="Permitted Documents For User"]').contains('Permitted Documents For User').click(); - cy.findByText('Permitted Documents For User'); - cy.findByPlaceholderText('User').should("have.value", "Administrator"); + cy.get('[data-report="Website Analytics"]').contains('Website Analytics').click(); + cy.findByText('Website Analytics'); }); }); }); diff --git a/cypress/integration/discussions.js b/cypress/integration/discussions.js new file mode 100644 index 0000000000..3c055d1923 --- /dev/null +++ b/cypress/integration/discussions.js @@ -0,0 +1,63 @@ +context('Discussions', () => { + before(() => { + cy.login(); + cy.visit('/app'); + return cy.window().its('frappe').then(frappe => { + return frappe.call('frappe.tests.ui_test_helpers.create_data_for_discussions'); + }); + }); + + const reply_through_modal = () => { + cy.visit('/test-page-discussions'); + + // Open the modal + cy.get('.reply').click(); + cy.wait(500); + cy.get('.discussion-modal').should('be.visible'); + + // Enter title + cy.get('.modal .topic-title').type('Discussion from tests') + .should('have.value', 'Discussion from tests'); + + // Enter comment + cy.get('.modal .comment-field') + .type('This is a discussion from the cypress ui tests.') + .should('have.value', 'This is a discussion from the cypress ui tests.'); + + // Submit + cy.get('.modal .submit-discussion').click(); + cy.wait(2000); + + // Check if discussion is added to page and content is visible + cy.get('.sidebar-parent:first .discussion-topic-title').should('have.text', 'Discussion from tests'); + cy.get('.discussion-on-page:visible').should('have.class', 'show'); + cy.get('.discussion-on-page:visible .reply-card .reply-text') + .should('have.text', 'This is a discussion from the cypress ui tests.\n'); + + }; + + const reply_through_comment_box = () => { + cy.get('.discussion-on-page:visible .comment-field') + .type('This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.') + .should('have.value', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.'); + + cy.get('.discussion-on-page:visible .submit-discussion').click(); + cy.wait(3000); + cy.get('.discussion-on-page:visible').should('have.class', 'show'); + cy.get('.discussion-on-page:visible').children(".reply-card").eq(1).children(".reply-text") + .should('have.text', 'This is a discussion from the cypress ui tests. \n\nThis comment was entered through the commentbox on the page.\n'); + }; + + const cancel_and_clear_comment_box = () => { + cy.get('.discussion-on-page:visible .comment-field') + .type('This is a discussion from the cypress ui tests.') + .should('have.value', 'This is a discussion from the cypress ui tests.'); + + cy.get('.discussion-on-page:visible .cancel-comment').click(); + cy.get('.discussion-on-page:visible .comment-field').should('have.value', ''); + }; + + it('reply through modal', reply_through_modal); + it('reply through comment box', reply_through_comment_box); + it('cancel and clear comment box', cancel_and_clear_comment_box); +}); diff --git a/cypress/integration/folder_navigation.js b/cypress/integration/folder_navigation.js index 1b7c02d98c..cec7edb59f 100644 --- a/cypress/integration/folder_navigation.js +++ b/cypress/integration/folder_navigation.js @@ -71,7 +71,7 @@ context('Folder Navigation', () => { it('Deleting Test Folder from the home', () => { //Deleting the Test Folder added in the home directory cy.visit('/app/file/view/home'); - cy.get('.level-left > .list-subject > .list-row-checkbox').eq(0).click({force: true, delay: 500}); + 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(); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index d20750b1d5..f860a742ef 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -8,7 +8,10 @@ context('Form', () => { }); it('create a new form', () => { cy.visit('/app/todo/new'); - cy.fill_field('description', 'this is a test todo', 'Text Editor'); + cy.get('[data-fieldname="description"] .ql-editor') + .first() + .click() + .type('this is a test todo'); cy.wait(300); cy.get('.page-title').should('contain', 'Not Saved'); cy.intercept({ diff --git a/cypress/integration/form_tab_break.js b/cypress/integration/form_tab_break.js new file mode 100644 index 0000000000..45c3c92084 --- /dev/null +++ b/cypress/integration/form_tab_break.js @@ -0,0 +1,31 @@ +import doctype_with_tab_break from '../fixtures/doctype_with_tab_break'; +const doctype_name = doctype_with_tab_break.name; +context("Form Tab Break", () => { + before(() => { + cy.login(); + cy.visit('/app/website'); + return cy.insert_doc('DocType', doctype_with_tab_break, true); + }); + it("Should switch tab and open correct tabs on validation error", () => { + cy.new_form(doctype_name); + // test tab switch + cy.findByRole("tab", {name: "Tab 2"}).click(); + cy.findByText("Phone"); + cy.findByRole("tab", {name: "Details"}).click(); + cy.findByText("Name"); + + // form should switch to the tab with un-filled mandatory field + cy.fill_field("username", "Test"); + cy.findByRole("button", {name: "Save"}).click(); + cy.findByText("Missing Fields"); + cy.hide_dialog(); + cy.findByText("Phone"); + cy.fill_field("phone", "12345678"); + cy.findByRole("button", {name: "Save"}).click(); + + // After save, first tab should have dashboard + cy.get(".form-tabs > .nav-item").eq(0).click(); + cy.findByText("Connections"); + + }); +}); \ No newline at end of file diff --git a/cypress/integration/grid_configuration.js b/cypress/integration/grid_configuration.js new file mode 100644 index 0000000000..7193d804c2 --- /dev/null +++ b/cypress/integration/grid_configuration.js @@ -0,0 +1,23 @@ +context('Grid Configuration', () => { + beforeEach(() => { + cy.login(); + cy.visit('/app/doctype/User'); + }); + it('Set user wise grid settings', () => { + cy.wait(100); + cy.get('.frappe-control[data-fieldname="fields"]').as('table'); + cy.get('@table').find('.icon-sm').click(); + cy.wait(100); + cy.get('.frappe-control[data-fieldname="fields_html"]').as('modal'); + cy.get('@modal').find('.add-new-fields').click(); + cy.wait(100); + cy.get('[type="checkbox"][data-unit="read_only"]').check(); + cy.findByRole('button', {name: 'Add'}).click(); + cy.wait(100); + cy.get('[data-fieldname="options"]').invoke('attr', 'value', '1'); + cy.get('.form-control.column-width[data-fieldname="options"]').trigger('change'); + cy.findByRole('button', {name: 'Update'}).click(); + cy.wait(200); + cy.get('[title="Read Only"').should('be.visible'); + }); +}); \ No newline at end of file diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index 298bb20432..ce9e87274b 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -6,6 +6,23 @@ context('List View', () => { return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); }); + + it('Keep checkbox checked after Bulk Update', () => { + cy.go_to_list('ToDo'); + cy.get('.list-row-container .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 .menu-item-label[data-label="Edit"]').click(); + + cy.get('.modal-body .form-control[data-fieldname="field"]').first().select('Due Date').wait(200); + cy.fill_field('value', '09-28-21', 'Date'); + + cy.get('.modal-footer .standard-actions .btn-primary').click(); + cy.wait(500); + + cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); + cy.get('.list-row-container .list-row-checkbox:checked').should('be.visible'); + }); + it('enables "Actions" button', () => { const actions = ['Approve', 'Reject', 'Edit', 'Export', 'Assign To', 'Apply Assignment Rule', 'Add Tags', 'Print', 'Delete']; cy.go_to_list('ToDo'); @@ -24,10 +41,11 @@ context('List View', () => { }).as('real-time-update'); cy.wrap(elements).contains('Approve').click(); cy.wait(['@bulk-approval', '@real-time-update']); - cy.hide_dialog(); + cy.wait(300); + cy.get_open_dialog().find('.btn-modal-close').click(); + cy.reload(); cy.clear_filters(); cy.get('.list-row-container:visible').should('contain', 'Approved'); }); }); }); - diff --git a/cypress/integration/navigation.js b/cypress/integration/navigation.js index ba45137cbd..c4d0638f26 100644 --- a/cypress/integration/navigation.js +++ b/cypress/integration/navigation.js @@ -13,6 +13,7 @@ context('Navigation', () => { it.only('Navigate to previous page after login', () => { cy.visit('/app/todo'); + cy.findByTitle('To Do').should('be.visible'); cy.request('/api/method/logout'); cy.reload(); cy.get('.btn-primary').contains('Login').click(); diff --git a/cypress/integration/timeline.js b/cypress/integration/timeline.js index 6387485220..3071330b61 100644 --- a/cypress/integration/timeline.js +++ b/cypress/integration/timeline.js @@ -11,6 +11,7 @@ context('Timeline', () => { cy.visit('/app/todo'); cy.click_listview_primary_button('Add ToDo'); cy.findByRole('button', {name: 'Edit in full page'}).click(); + cy.findByTitle('New ToDo').should('be.visible'); cy.get('[data-fieldname="description"] .ql-editor').eq(0).type('Test ToDo', {force: true}); cy.wait(200); cy.findByRole('button', {name: 'Save'}).click(); @@ -43,13 +44,14 @@ context('Timeline', () => { cy.get('.timeline-content').should('contain', 'Testing Timeline 123'); //Deleting the added comment - cy.get('.actions > .btn > .icon').first().click(); + cy.get('.more-actions > .action-btn').click(); + cy.get('.more-actions .dropdown-item').contains('Delete').click(); cy.findByRole('button', {name: 'Yes'}).click(); cy.click_modal_primary_button('Yes'); //Deleting the added ToDo - cy.get('.menu-btn-group button').eq(1).click(); - cy.get('.menu-btn-group [data-label="Delete"]').click(); + cy.get('.menu-btn-group [data-original-title="Menu"]').click(); + cy.get('.menu-btn-group .dropdown-item').contains('Delete').click(); cy.findByRole('button', {name: 'Yes'}).click(); }); diff --git a/cypress/integration/timeline_email.js b/cypress/integration/timeline_email.js index 82af24e822..dfe80e0019 100644 --- a/cypress/integration/timeline_email.js +++ b/cypress/integration/timeline_email.js @@ -5,14 +5,16 @@ context('Timeline Email', () => { cy.visit('/app/todo'); }); - it('Adding new ToDo, adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { - //Adding new ToDo + it('Adding new ToDo', () => { cy.click_listview_primary_button('Add ToDo'); cy.get('.custom-actions:visible > .btn').contains("Edit in full page").click({delay: 500}); cy.fill_field("description", "Test ToDo", "Text Editor"); cy.wait(500); cy.get('.primary-action').contains('Save').click({force: true}); cy.wait(700); + }); + + it('Adding email and verifying timeline content for email attachment, deleting attachment and ToDo', () => { cy.visit('/app/todo'); cy.get('.list-row > .level-left > .list-subject').eq(0).click(); @@ -41,11 +43,13 @@ context('Timeline Email', () => { cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .btn').click(); cy.get('#page-Communication > .page-head > .container > .row > .col > .standard-actions > .menu-btn-group > .dropdown-menu > li > .grey-link').eq(9).click(); cy.get('.modal.show > .modal-dialog > .modal-content > .modal-footer > .standard-actions > .btn-primary').click(); + cy.visit('/app/todo'); cy.get('.list-row > .level-left > .list-subject > .level-item.ellipsis > .ellipsis').eq(0).click(); //Removing the added attachment cy.get('.attachment-row > .data-pill > .remove-btn > .icon').click(); + cy.wait(500); cy.get('.modal-footer:visible > .standard-actions > .btn-primary').contains('Yes').click(); //To check if the removed attachment is shown in the timeline content diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js index 07d9804a73..9720faa666 100644 --- a/cypress/plugins/index.js +++ b/cypress/plugins/index.js @@ -11,7 +11,7 @@ // This function is called when a project is opened or re-opened (e.g. due to // the project's config changing) -module.exports = () => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config -}; +module.exports = (on, config) => { + require('@cypress/code-coverage/task')(on, config); + return config; +}; \ No newline at end of file diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 47c37a56a0..6484370946 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -353,5 +353,5 @@ Cypress.Commands.add('click_listview_primary_button', (btn_name) => { }); Cypress.Commands.add('click_timeline_action_btn', (btn_name) => { - cy.get('.timeline-content > .timeline-message-box > .justify-between > .actions > .btn').contains(btn_name).click(); + cy.get('.timeline-message-box .custom-actions > .btn').contains(btn_name).click(); }); \ No newline at end of file diff --git a/cypress/support/index.js b/cypress/support/index.js index 1bee72d2ca..9cd770a31e 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -15,6 +15,7 @@ // Import commands.js using ES2015 syntax: import './commands'; +import '@cypress/code-coverage/support'; // Alternatively you can use CommonJS syntax: diff --git a/esbuild/esbuild.js b/esbuild/esbuild.js index 9074beae06..bf4436358e 100644 --- a/esbuild/esbuild.js +++ b/esbuild/esbuild.js @@ -104,6 +104,9 @@ async function execute() { log_error("There were some problems during build"); log(); log(chalk.dim(e.stack)); + if (process.env.CI) { + process.kill(process.pid); + } return; } @@ -528,4 +531,4 @@ function log_rebuilt_assets(prev_assets, new_assets) { log(" " + filename); } log(); -} \ No newline at end of file +} diff --git a/frappe/__init__.py b/frappe/__init__.py index 38904c68d0..64e445973f 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -235,12 +235,13 @@ def connect_replica(): from frappe.database import get_db user = local.conf.db_name password = local.conf.db_password + port = local.conf.replica_db_port if local.conf.different_credentials_for_replica: user = local.conf.replica_db_name password = local.conf.replica_db_password - local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password) + local.replica_db = get_db(host=local.conf.replica_host, user=user, password=password, port=port) # swap db connections local.primary_db = local.db diff --git a/frappe/build.py b/frappe/build.py index dfbe20f31e..05fa213018 100644 --- a/frappe/build.py +++ b/frappe/build.py @@ -1,10 +1,11 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE import os import re import json import shutil import subprocess +from subprocess import getoutput from io import StringIO from tempfile import mkdtemp, mktemp from distutils.spawn import find_executable @@ -17,6 +18,8 @@ import psutil from urllib.parse import urlparse from simple_chalk import green from semantic_version import Version +from requests import head +from requests.exceptions import HTTPError timestamps = {} @@ -24,6 +27,12 @@ app_paths = None sites_path = os.path.abspath(os.getcwd()) +class AssetsNotDownloadedError(Exception): + pass + +class AssetsDontExistError(HTTPError): + pass + def download_file(url, prefix): from requests import get @@ -70,81 +79,94 @@ def build_missing_files(): bundle(build_mode, apps="frappe") -def get_assets_link(frappe_head): - from subprocess import getoutput - from requests import head - +def get_assets_link(frappe_head) -> str: tag = getoutput( - r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" - r" refs/tags/,,' -e 's/\^{}//'" - % frappe_head - ) + r"cd ../apps/frappe && git show-ref --tags -d | grep %s | sed -e 's,.*" + r" refs/tags/,,' -e 's/\^{}//'" + % frappe_head + ) if tag: # if tag exists, download assets from github release - url = "https://github.com/frappe/frappe/releases/download/{0}/assets.tar.gz".format(tag) + url = f"https://github.com/frappe/frappe/releases/download/{tag}/assets.tar.gz" else: - url = "http://assets.frappeframework.com/{0}.tar.gz".format(frappe_head) + url = f"http://assets.frappeframework.com/{frappe_head}.tar.gz" if not head(url): - raise ValueError("URL {0} doesn't exist".format(url)) + reference = f"Release {tag}" if tag else f"Commit {frappe_head}" + raise AssetsDontExistError(f"Assets for {reference} don't exist") return url +def fetch_assets(url, frappe_head): + click.secho("Retrieving assets...", fg="yellow") + + prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head) + assets_archive = download_file(url, prefix) + + if not assets_archive: + raise AssetsNotDownloadedError(f"Assets could not be retrived from {url}") + + print(f"\n{green('✔')} Downloaded Frappe assets from {url}") + + return assets_archive + + +def setup_assets(assets_archive): + import tarfile + directories_created = set() + + click.secho("\nExtracting assets...\n", fg="yellow") + with tarfile.open(assets_archive) as tar: + for file in tar: + if not file.isdir(): + dest = "." + file.name.replace("./frappe-bench/sites", "") + asset_directory = os.path.dirname(dest) + show = dest.replace("./assets/", "") + + if asset_directory not in directories_created: + if not os.path.exists(asset_directory): + os.makedirs(asset_directory, exist_ok=True) + directories_created.add(asset_directory) + + tar.makefile(file, dest) + print("{0} Restored {1}".format(green('✔'), show)) + + return directories_created + + def download_frappe_assets(verbose=True): """Downloads and sets up Frappe assets if they exist based on the current commit HEAD. Returns True if correctly setup else returns False. """ - from subprocess import getoutput - - assets_setup = False frappe_head = getoutput("cd ../apps/frappe && git rev-parse HEAD") - if frappe_head: + if not frappe_head: + return False + + try: + url = get_assets_link(frappe_head) + assets_archive = fetch_assets(url, frappe_head) + setup_assets(assets_archive) + build_missing_files() + return True + + except AssetsDontExistError as e: + click.secho(str(e), fg="yellow") + + except Exception as e: + # TODO: log traceback in bench.log + click.secho(str(e), fg="red") + + finally: try: - url = get_assets_link(frappe_head) - click.secho("Retrieving assets...", fg="yellow") - prefix = mkdtemp(prefix="frappe-assets-", suffix=frappe_head) - assets_archive = download_file(url, prefix) - print("\n{0} Downloaded Frappe assets from {1}".format(green('✔'), url)) - - if assets_archive: - import tarfile - directories_created = set() - - click.secho("\nExtracting assets...\n", fg="yellow") - with tarfile.open(assets_archive) as tar: - for file in tar: - if not file.isdir(): - dest = "." + file.name.replace("./frappe-bench/sites", "") - asset_directory = os.path.dirname(dest) - show = dest.replace("./assets/", "") - - if asset_directory not in directories_created: - if not os.path.exists(asset_directory): - os.makedirs(asset_directory, exist_ok=True) - directories_created.add(asset_directory) - - tar.makefile(file, dest) - print("{0} Restored {1}".format(green('✔'), show)) - - build_missing_files() - return True - else: - raise + shutil.rmtree(os.path.dirname(assets_archive)) except Exception: - # TODO: log traceback in bench.log - click.secho("An Error occurred while downloading assets...", fg="red") - assets_setup = False - finally: - try: - shutil.rmtree(os.path.dirname(assets_archive)) - except Exception: - pass + pass - return assets_setup + return False def symlink(target, link_name, overwrite=False): @@ -224,7 +246,7 @@ def bundle(mode, apps=None, hard_link=False, make_copy=False, restore=False, ver check_node_executable() frappe_app_path = frappe.get_app_path("frappe", "..") - frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env()) + frappe.commands.popen(command, cwd=frappe_app_path, env=get_node_env(), raise_err=True) def watch(apps=None): diff --git a/frappe/commands/__init__.py b/frappe/commands/__init__.py index 6fb33a51b7..82a71ce7b4 100644 --- a/frappe/commands/__init__.py +++ b/frappe/commands/__init__.py @@ -102,7 +102,7 @@ def get_commands(): from .site import commands as site_commands from .translate import commands as translate_commands from .utils import commands as utils_commands - from .redis import commands as redis_commands + from .redis_utils import commands as redis_commands clickable_link = ( "\x1b]8;;https://frappeframework.com/docs\afrappeframework.com\x1b]8;;\a" diff --git a/frappe/commands/redis.py b/frappe/commands/redis_utils.py similarity index 97% rename from frappe/commands/redis.py rename to frappe/commands/redis_utils.py index 38a46c2142..3556050782 100644 --- a/frappe/commands/redis.py +++ b/frappe/commands/redis_utils.py @@ -3,7 +3,7 @@ import os import click import frappe -from frappe.utils.rq import RedisQueue +from frappe.utils.redis_queue import RedisQueue from frappe.installer import update_site_config @click.command('create-rq-users') diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 90cd60c6ec..74af28e3ff 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -679,9 +679,10 @@ def run_parallel_tests(context, app, build_number, total_builds, with_coverage=F @click.argument('app') @click.option('--headless', is_flag=True, help="Run UI Test in headless mode") @click.option('--parallel', is_flag=True, help="Run UI Test in parallel mode") +@click.option('--with-coverage', is_flag=True, help="Generate coverage report") @click.option('--ci-build-id') @pass_context -def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None): +def run_ui_tests(context, app, headless=False, parallel=True, with_coverage=False, ci_build_id=None): "Run UI tests" site = get_site(context) app_base_path = os.path.abspath(os.path.join(frappe.get_app_path(app), '..')) @@ -691,6 +692,7 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None): # override baseUrl using env variable site_env = f'CYPRESS_baseUrl={site_url}' password_env = f'CYPRESS_adminPassword={admin_password}' if admin_password else '' + coverage_env = f'CYPRESS_coverage={str(with_coverage).lower()}' os.chdir(app_base_path) @@ -698,22 +700,23 @@ def run_ui_tests(context, app, headless=False, parallel=True, ci_build_id=None): cypress_path = f"{node_bin}/cypress" plugin_path = f"{node_bin}/../cypress-file-upload" testing_library_path = f"{node_bin}/../@testing-library" + coverage_plugin_path = f"{node_bin}/../@cypress/code-coverage" # check if cypress in path...if not, install it. if not ( os.path.exists(cypress_path) and os.path.exists(plugin_path) and os.path.exists(testing_library_path) + and os.path.exists(coverage_plugin_path) and cint(subprocess.getoutput("npm view cypress version")[:1]) >= 6 ): # install cypress click.secho("Installing Cypress...", fg="yellow") - frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile") + frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 @testing-library/cypress@^8 @cypress/code-coverage@^3 --no-lockfile") # run for headless mode run_or_open = 'run --browser firefox --record' if headless else 'open' - command = '{site_env} {password_env} {cypress} {run_or_open}' - formatted_command = command.format(site_env=site_env, password_env=password_env, cypress=cypress_path, run_or_open=run_or_open) + formatted_command = f'{site_env} {password_env} {coverage_env} {cypress_path} {run_or_open}' if parallel: formatted_command += ' --parallel' diff --git a/frappe/contacts/address_and_contact.py b/frappe/contacts/address_and_contact.py index 6b71ec50f9..79c3358665 100644 --- a/frappe/contacts/address_and_contact.py +++ b/frappe/contacts/address_and_contact.py @@ -178,4 +178,4 @@ def set_link_title(doc): for link in doc.links: if not link.link_title: linked_doc = frappe.get_doc(link.link_doctype, link.link_name) - link.link_title = linked_doc.get("title_field") or linked_doc.get("name") + link.link_title = linked_doc.get_title() or link.link_name diff --git a/frappe/core/doctype/access_log/access_log.py b/frappe/core/doctype/access_log/access_log.py index d93da02d25..f631353d56 100644 --- a/frappe/core/doctype/access_log/access_log.py +++ b/frappe/core/doctype/access_log/access_log.py @@ -1,6 +1,7 @@ -# Copyright (c) 2019, Frappe Technologies and contributors +# Copyright (c) 2021, Frappe Technologies and contributors # License: MIT. See LICENSE import frappe +from tenacity import retry, retry_if_exception_type, stop_after_attempt from frappe.model.document import Document @@ -10,25 +11,40 @@ 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): +@retry( + stop=stop_after_attempt(3), retry=retry_if_exception_type(frappe.DuplicateEntryError) +) +def make_access_log( + doctype=None, + document=None, + method=None, + file_type=None, + report_name=None, + filters=None, + page=None, + columns=None, +): user = frappe.session.user + in_request = frappe.request and frappe.request.method == "GET" - doc = frappe.get_doc({ - 'doctype': 'Access Log', - 'user': user, - 'export_from': doctype, - 'reference_document': document, - 'file_type': file_type, - 'report_name': report_name, - 'page': page, - 'method': method, - 'filters': frappe.utils.cstr(filters) if filters else None, - 'columns': columns - }) + doc = frappe.get_doc( + { + "doctype": "Access Log", + "user": user, + "export_from": doctype, + "reference_document": document, + "file_type": file_type, + "report_name": report_name, + "page": page, + "method": method, + "filters": frappe.utils.cstr(filters) if filters else None, + "columns": columns, + } + ) doc.insert(ignore_permissions=True) # `frappe.db.commit` added because insert doesnt `commit` when called in GET requests like `printview` - if frappe.request and frappe.request.method == 'GET': + # dont commit in test mode + if not frappe.flags.in_test or in_request: frappe.db.commit() diff --git a/frappe/core/doctype/docfield/docfield.json b/frappe/core/doctype/docfield/docfield.json index b240d29446..6910d615d3 100644 --- a/frappe/core/doctype/docfield/docfield.json +++ b/frappe/core/doctype/docfield/docfield.json @@ -1,544 +1,543 @@ { - "actions": [], - "autoname": "hash", - "creation": "2013-02-22 01:27:33", - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "label_and_type", - "label", - "fieldtype", - "fieldname", - "precision", - "length", - "non_negative", - "hide_days", - "hide_seconds", - "reqd", - "search_index", - "column_break_18", - "options", - "defaults_section", - "default", - "column_break_6", - "fetch_from", - "fetch_if_empty", - "visibility_section", - "hidden", - "bold", - "allow_in_quick_entry", - "translatable", - "print_hide", - "print_hide_if_no_value", - "report_hide", - "column_break_28", - "depends_on", - "collapsible", - "collapsible_depends_on", - "hide_border", - "list__search_settings_section", - "in_list_view", - "in_standard_filter", - "in_preview", - "column_break_35", - "in_filter", - "in_global_search", - "permissions", - "read_only", - "allow_on_submit", - "ignore_user_permissions", - "allow_bulk_edit", - "column_break_13", - "permlevel", - "ignore_xss_filter", - "constraints_section", - "unique", - "no_copy", - "set_only_once", - "remember_last_selected_value", - "column_break_38", - "mandatory_depends_on", - "read_only_depends_on", - "display", - "print_width", - "width", - "max_height", - "columns", - "column_break_22", - "description", - "oldfieldname", - "oldfieldtype" - ], - "fields": [ - { - "fieldname": "label_and_type", - "fieldtype": "Section Break" - }, - { - "bold": 1, - "fieldname": "label", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Label", - "oldfieldname": "label", - "oldfieldtype": "Data", - "print_width": "163", - "search_index": 1, - "width": "163" - }, - { - "bold": 1, - "default": "Data", - "fieldname": "fieldtype", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Type", - "oldfieldname": "fieldtype", - "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", - "reqd": 1, - "search_index": 1 - }, - { - "bold": 1, - "fieldname": "fieldname", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Name", - "oldfieldname": "fieldname", - "oldfieldtype": "Data", - "search_index": 1 - }, - { - "default": "0", - "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", - "fieldname": "reqd", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Mandatory", - "oldfieldname": "reqd", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", - "description": "Set non-standard precision for a Float or Currency field", - "fieldname": "precision", - "fieldtype": "Select", - "label": "Precision", - "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9", - "print_hide": 1 - }, - { - "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", - "fieldname": "length", - "fieldtype": "Int", - "label": "Length" - }, - { - "default": "0", - "fieldname": "search_index", - "fieldtype": "Check", - "label": "Index", - "oldfieldname": "search_index", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "in_list_view", - "fieldtype": "Check", - "label": "In List View", - "print_width": "70px", - "width": "70px" - }, - { - "default": "0", - "fieldname": "in_standard_filter", - "fieldtype": "Check", - "label": "In List Filter" - }, - { - "default": "0", - "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fieldname": "in_global_search", - "fieldtype": "Check", - "label": "In Global Search" - }, - { - "default": "0", - "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", - "fieldname": "in_preview", - "fieldtype": "Check", - "label": "In Preview" - }, - { - "default": "0", - "fieldname": "allow_in_quick_entry", - "fieldtype": "Check", - "label": "Allow in Quick Entry" - }, - { - "default": "0", - "fieldname": "bold", - "fieldtype": "Check", - "label": "Bold" - }, - { - "default": "0", - "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fieldname": "translatable", - "fieldtype": "Check", - "label": "Translatable" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype===\"Section Break\"", - "fieldname": "collapsible", - "fieldtype": "Check", - "label": "Collapsible", - "length": 255 - }, - { - "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible", - "fieldname": "collapsible_depends_on", - "fieldtype": "Code", - "label": "Collapsible Depends On (JS)", - "max_height": "3rem", - "options": "JS" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", - "fieldname": "options", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Options", - "oldfieldname": "options", - "oldfieldtype": "Text" - }, - { - "fieldname": "default", - "fieldtype": "Small Text", - "label": "Default", - "max_height": "3rem", - "oldfieldname": "default", - "oldfieldtype": "Text" - }, - { - "fieldname": "fetch_from", - "fieldtype": "Small Text", - "label": "Fetch From" - }, - { - "default": "0", - "fieldname": "fetch_if_empty", - "fieldtype": "Check", - "label": "Fetch only if value is not set" - }, - { - "fieldname": "permissions", - "fieldtype": "Section Break", - "label": "Permissions" - }, - { - "fieldname": "depends_on", - "fieldtype": "Code", - "label": "Display Depends On (JS)", - "length": 255, - "max_height": "3rem", - "oldfieldname": "depends_on", - "oldfieldtype": "Data", - "options": "JS" - }, - { - "default": "0", - "fieldname": "hidden", - "fieldtype": "Check", - "label": "Hidden", - "oldfieldname": "hidden", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "read_only", - "fieldtype": "Check", - "label": "Read Only", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "unique", - "fieldtype": "Check", - "label": "Unique" - }, - { - "default": "0", - "fieldname": "set_only_once", - "fieldtype": "Check", - "label": "Set only once" - }, - { - "default": "0", - "depends_on": "eval: doc.fieldtype == \"Table\"", - "fieldname": "allow_bulk_edit", - "fieldtype": "Check", - "label": "Allow Bulk Edit" - }, - { - "fieldname": "column_break_13", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "permlevel", - "fieldtype": "Int", - "label": "Perm Level", - "oldfieldname": "permlevel", - "oldfieldtype": "Int", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "ignore_user_permissions", - "fieldtype": "Check", - "label": "Ignore User Permissions" - }, - { - "default": "0", - "depends_on": "eval: parent.is_submittable", - "fieldname": "allow_on_submit", - "fieldtype": "Check", - "label": "Allow on Submit", - "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "report_hide", - "fieldtype": "Check", - "label": "Report Hide", - "oldfieldname": "report_hide", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "depends_on": "eval:(doc.fieldtype == 'Link')", - "fieldname": "remember_last_selected_value", - "fieldtype": "Check", - "label": "Remember Last Selected Value" - }, - { - "default": "0", - "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fieldname": "ignore_xss_filter", - "fieldtype": "Check", - "label": "Ignore XSS Filter" - }, - { - "fieldname": "display", - "fieldtype": "Section Break", - "label": "Display" - }, - { - "default": "0", - "fieldname": "in_filter", - "fieldtype": "Check", - "label": "In Filter", - "oldfieldname": "in_filter", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "no_copy", - "fieldtype": "Check", - "label": "No Copy", - "oldfieldname": "no_copy", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "fieldname": "print_hide", - "fieldtype": "Check", - "label": "Print Hide", - "oldfieldname": "print_hide", - "oldfieldtype": "Check", - "print_width": "50px", - "width": "50px" - }, - { - "default": "0", - "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fieldname": "print_hide_if_no_value", - "fieldtype": "Check", - "label": "Print Hide If No Value" - }, - { - "fieldname": "print_width", - "fieldtype": "Data", - "label": "Print Width", - "length": 10 - }, - { - "fieldname": "width", - "fieldtype": "Data", - "label": "Width", - "length": 10, - "oldfieldname": "width", - "oldfieldtype": "Data", - "print_width": "50px", - "width": "50px" - }, - { - "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fieldname": "columns", - "fieldtype": "Int", - "label": "Columns" - }, - { - "fieldname": "column_break_22", - "fieldtype": "Column Break" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "print_width": "300px", - "width": "300px" - }, - { - "fieldname": "oldfieldname", - "fieldtype": "Data", - "hidden": 1, - "oldfieldname": "oldfieldname", - "oldfieldtype": "Data" - }, - { - "fieldname": "oldfieldtype", - "fieldtype": "Data", - "hidden": 1, - "oldfieldname": "oldfieldtype", - "oldfieldtype": "Data" - }, - { - "fieldname": "mandatory_depends_on", - "fieldtype": "Code", - "label": "Mandatory Depends On (JS)", - "max_height": "3rem", - "options": "JS" - }, - { - "fieldname": "read_only_depends_on", - "fieldtype": "Code", - "label": "Read Only Depends On (JS)", - "max_height": "3rem", - "options": "JS" - }, - { - "fieldname": "column_break_38", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_days", - "fieldtype": "Check", - "label": "Hide Days" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_seconds", - "fieldtype": "Check", - "label": "Hide Seconds" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Section Break'", - "fieldname": "hide_border", - "fieldtype": "Check", - "label": "Hide Border" - }, - { - "default": "0", - "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", - "fieldname": "non_negative", - "fieldtype": "Check", - "label": "Non Negative" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, - { - "fieldname": "defaults_section", - "fieldtype": "Section Break", - "label": "Defaults", - "max_height": "2rem" - }, - { - "fieldname": "visibility_section", - "fieldtype": "Section Break", - "label": "Visibility" - }, - { - "fieldname": "column_break_28", - "fieldtype": "Column Break" - }, - { - "fieldname": "constraints_section", - "fieldtype": "Section Break", - "label": "Constraints" - }, - { - "fieldname": "max_height", - "fieldtype": "Data", - "label": "Max Height", - "length": 10 - }, - { - "fieldname": "list__search_settings_section", - "fieldtype": "Section Break", - "label": "List / Search Settings" - }, - { - "fieldname": "column_break_35", - "fieldtype": "Column Break" - } - ], - "idx": 1, - "index_web_pages_for_search": 1, - "istable": 1, - "links": [], - "modified": "2021-09-04 19:41:53.684094", - "modified_by": "Administrator", - "module": "Core", - "name": "DocField", - "naming_rule": "Random", - "owner": "Administrator", - "permissions": [], - "sort_field": "modified", - "sort_order": "ASC" + "actions": [], + "autoname": "hash", + "creation": "2013-02-22 01:27:33", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "label_and_type", + "label", + "fieldtype", + "fieldname", + "precision", + "length", + "non_negative", + "hide_days", + "hide_seconds", + "reqd", + "search_index", + "column_break_18", + "options", + "defaults_section", + "default", + "column_break_6", + "fetch_from", + "fetch_if_empty", + "visibility_section", + "hidden", + "bold", + "allow_in_quick_entry", + "translatable", + "print_hide", + "print_hide_if_no_value", + "report_hide", + "column_break_28", + "depends_on", + "collapsible", + "collapsible_depends_on", + "hide_border", + "list__search_settings_section", + "in_list_view", + "in_standard_filter", + "in_preview", + "column_break_35", + "in_filter", + "in_global_search", + "permissions", + "read_only", + "allow_on_submit", + "ignore_user_permissions", + "allow_bulk_edit", + "column_break_13", + "permlevel", + "ignore_xss_filter", + "constraints_section", + "unique", + "no_copy", + "set_only_once", + "remember_last_selected_value", + "column_break_38", + "mandatory_depends_on", + "read_only_depends_on", + "display", + "print_width", + "width", + "max_height", + "columns", + "column_break_22", + "description", + "oldfieldname", + "oldfieldtype" + ], + "fields": [{ + "fieldname": "label_and_type", + "fieldtype": "Section Break" + }, + { + "bold": 1, + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "oldfieldname": "label", + "oldfieldtype": "Data", + "print_width": "163", + "search_index": 1, + "width": "163" + }, + { + "bold": 1, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "search_index": 1 + }, + { + "default": "0", + "depends_on": "eval:!in_list([\"Section Break\", \"Column Break\", \"Button\", \"HTML\"], doc.fieldtype)", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Mandatory", + "oldfieldname": "reqd", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9", + "print_hide": 1 + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "default": "0", + "fieldname": "search_index", + "fieldtype": "Check", + "label": "Index", + "oldfieldname": "search_index", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View", + "print_width": "70px", + "width": "70px" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In List Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "0", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible", + "length": 255 + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\" && doc.collapsible", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "description": "For Links, enter the DocType as range.\nFor Select, enter list of Options, each on a new line.", + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "default", + "fieldtype": "Small Text", + "label": "Default", + "max_height": "3rem", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch only if value is not set" + }, + { + "fieldname": "permissions", + "fieldtype": "Section Break", + "label": "Permissions" + }, + { + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Display Depends On (JS)", + "length": 255, + "max_height": "3rem", + "oldfieldname": "depends_on", + "oldfieldtype": "Data", + "options": "JS" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden", + "oldfieldname": "hidden", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "set_only_once", + "fieldtype": "Check", + "label": "Set only once" + }, + { + "default": "0", + "depends_on": "eval: doc.fieldtype == \"Table\"", + "fieldname": "allow_bulk_edit", + "fieldtype": "Check", + "label": "Allow Bulk Edit" + }, + { + "fieldname": "column_break_13", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "label": "Perm Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "depends_on": "eval: parent.is_submittable", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "depends_on": "eval:(doc.fieldtype == 'Link')", + "fieldname": "remember_last_selected_value", + "fieldtype": "Check", + "label": "Remember Last Selected Value" + }, + { + "default": "0", + "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + }, + { + "fieldname": "display", + "fieldtype": "Section Break", + "label": "Display" + }, + { + "default": "0", + "fieldname": "in_filter", + "fieldtype": "Check", + "label": "In Filter", + "oldfieldname": "in_filter", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy", + "oldfieldname": "no_copy", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check", + "print_width": "50px", + "width": "50px" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "fieldname": "print_width", + "fieldtype": "Data", + "label": "Print Width", + "length": 10 + }, + { + "fieldname": "width", + "fieldtype": "Data", + "label": "Width", + "length": 10, + "oldfieldname": "width", + "oldfieldtype": "Data", + "print_width": "50px", + "width": "50px" + }, + { + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "fieldname": "oldfieldname", + "fieldtype": "Data", + "hidden": 1, + "oldfieldname": "oldfieldname", + "oldfieldtype": "Data" + }, + { + "fieldname": "oldfieldtype", + "fieldtype": "Data", + "hidden": 1, + "oldfieldname": "oldfieldtype", + "oldfieldtype": "Data" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On (JS)", + "max_height": "3rem", + "options": "JS" + }, + { + "fieldname": "column_break_38", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "fieldname": "defaults_section", + "fieldtype": "Section Break", + "label": "Defaults", + "max_height": "2rem" + }, + { + "fieldname": "visibility_section", + "fieldtype": "Section Break", + "label": "Visibility" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "constraints_section", + "fieldtype": "Section Break", + "label": "Constraints" + }, + { + "fieldname": "max_height", + "fieldtype": "Data", + "label": "Max Height", + "length": 10 + }, + { + "fieldname": "list__search_settings_section", + "fieldtype": "Section Break", + "label": "List / Search Settings" + }, + { + "fieldname": "column_break_35", + "fieldtype": "Column Break" + } + ], + "idx": 1, + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-09-04 19:41:23.684094", + "modified_by": "Administrator", + "module": "Core", + "name": "DocField", + "naming_rule": "Random", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/frappe/core/doctype/doctype/doctype.py b/frappe/core/doctype/doctype/doctype.py index 9bf21690fc..8f8a8ed287 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -274,6 +274,8 @@ class DocType(Document): d.fieldname = d.fieldname + '_section' elif d.fieldtype=='Column Break': d.fieldname = d.fieldname + '_column' + elif d.fieldtype=='Tab Break': + d.fieldname = d.fieldname + '_tab' else: d.fieldname = d.fieldtype.lower().replace(" ","_") + "_" + str(d.idx) else: diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index d9ecd85533..4df9ef3132 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -813,7 +813,7 @@ def extract_images_from_doc(doc, fieldname): doc.set(fieldname, content) -def extract_images_from_html(doc, content): +def extract_images_from_html(doc, content, is_private=False): frappe.flags.has_dataurl = False def _save_file(match): @@ -846,7 +846,8 @@ def extract_images_from_html(doc, content): "attached_to_doctype": doctype, "attached_to_name": name, "content": content, - "decode": False + "decode": False, + "is_private": is_private }) _file.save(ignore_permissions=True) file_url = _file.file_url diff --git a/frappe/core/doctype/package_release/package_release.py b/frappe/core/doctype/package_release/package_release.py index 1fb8796882..d23ae917c4 100644 --- a/frappe/core/doctype/package_release/package_release.py +++ b/frappe/core/doctype/package_release/package_release.py @@ -6,16 +6,27 @@ from frappe.model.document import Document from frappe.modules.export_file import export_doc import os import subprocess +from frappe.query_builder.functions import Max + class PackageRelease(Document): def set_version(self): # set the next patch release by default + doctype = frappe.qb.DocType("Package Release") if not self.major: - self.major = frappe.db.max('Package Release', 'major', dict(package=self.package)) + self.major = frappe.qb.from_(doctype) \ + .where(doctype.package == self.package) \ + .select(Max(doctype.minor)).run()[0][0] or 0 + if not self.minor: - self.minor = frappe.db.max('Package Release', 'minor', dict(package=self.package)) + self.minor = frappe.qb.from_(doctype) \ + .where(doctype.package == self.package) \ + .select(Max("minor")).run()[0][0] or 0 if not self.patch: - self.patch = frappe.db.max('Package Release', 'patch', dict(package=self.package)) + 1 + value = frappe.qb.from_(doctype) \ + .where(doctype.package == self.package) \ + .select(Max("patch")).run()[0][0] or 0 + self.patch = value + 1 def autoname(self): self.set_version() diff --git a/frappe/core/doctype/server_script/server_script.py b/frappe/core/doctype/server_script/server_script.py index 79fe7a9140..5b1aab1241 100644 --- a/frappe/core/doctype/server_script/server_script.py +++ b/frappe/core/doctype/server_script/server_script.py @@ -94,7 +94,7 @@ class ServerScript(Document): Args: doc (Document): Executes script with for a certain document's events """ - safe_exec(self.script, _locals={"doc": doc}) + safe_exec(self.script, _locals={"doc": doc}, restrict_commit_rollback=True) def execute_scheduled_method(self): """Specific to Scheduled Jobs via Server Scripts diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 6c028ff136..de858327a9 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -59,6 +59,26 @@ conditions = '1 = 1' reference_doctype = 'Note', script = ''' frappe.method_that_doesnt_exist("do some magic") +''' + ), + dict( + name='test_todo_commit', + script_type = 'DocType Event', + doctype_event = 'Before Save', + reference_doctype = 'ToDo', + disabled = 1, + script = ''' +frappe.db.commit() +''' + ), + dict( + name='test_cache_methods', + script_type = 'DocType Event', + doctype_event = 'Before Save', + reference_doctype = 'ToDo', + disabled = 1, + script = ''' +frappe.cache().set_value('test_key', doc.name) ''' ) ] @@ -119,3 +139,24 @@ class TestServerScript(unittest.TestCase): self.assertTrue("invalid python code" in str(se.exception).lower(), msg="Python code validation not working") + + def test_commit_in_doctype_event(self): + server_script = frappe.get_doc('Server Script', 'test_todo_commit') + server_script.disabled = 0 + server_script.save() + + self.assertRaises(AttributeError, frappe.get_doc(dict(doctype='ToDo', description='test me')).insert) + + server_script.disabled = 1 + server_script.save() + + def test_cache_methods_in_server_script(self): + server_script = frappe.get_doc('Server Script', 'test_cache_methods') + server_script.disabled = 0 + server_script.save() + + todo = frappe.get_doc(dict(doctype='ToDo', description='test me')).insert() + self.assertEqual(todo.name, frappe.cache().get_value('test_key')) + + server_script.disabled = 1 + server_script.save() diff --git a/frappe/core/doctype/sms_settings/sms_settings.json b/frappe/core/doctype/sms_settings/sms_settings.json index 073fb88bc7..d29949af45 100755 --- a/frappe/core/doctype/sms_settings/sms_settings.json +++ b/frappe/core/doctype/sms_settings/sms_settings.json @@ -1,238 +1,80 @@ { - "allow_copy": 1, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-01-10 16:34:24", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, + "actions": [], + "allow_copy": 1, + "creation": "2013-01-10 16:34:24", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "sms_gateway_url", + "message_parameter", + "receiver_parameter", + "static_parameters_section", + "parameters", + "use_post" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Eg. smsgateway.com/api/send_sms.cgi", - "fieldname": "sms_gateway_url", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "SMS Gateway URL", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Eg. smsgateway.com/api/send_sms.cgi", + "fieldname": "sms_gateway_url", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "SMS Gateway URL", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Enter url parameter for message", - "fieldname": "message_parameter", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Message Parameter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Enter url parameter for message", + "fieldname": "message_parameter", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Message Parameter", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Enter url parameter for receiver nos", - "fieldname": "receiver_parameter", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Receiver Parameter", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Enter url parameter for receiver nos", + "fieldname": "receiver_parameter", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Receiver Parameter", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "static_parameters_section", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "static_parameters_section", + "fieldtype": "Column Break", "width": "50%" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)", - "fieldname": "parameters", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Static Parameters", - "length": 0, - "no_copy": 0, - "options": "SMS Parameter", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "description": "Enter static url parameters here (Eg. sender=ERPNext, username=ERPNext, password=1234 etc.)", + "fieldname": "parameters", + "fieldtype": "Table", + "label": "Static Parameters", + "options": "SMS Parameter" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "use_post", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Use POST", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "default": "0", + "fieldname": "use_post", + "fieldtype": "Check", + "label": "Use POST" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-cog", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2021-03-02 18:06:00.868688", - "modified_by": "Administrator", - "module": "Core", - "name": "SMS Settings", - "owner": "Administrator", + ], + "icon": "fa fa-cog", + "idx": 1, + "issingle": 1, + "links": [], + "modified": "2021-09-21 19:45:26.809793", + "modified_by": "Administrator", + "module": "Core", + "name": "SMS Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "read": 1, + "role": "System Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "track_changes": 1, - "track_seen": 0 -} + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 147f4ddfee..e4b94cdbb6 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -788,7 +788,7 @@ def sign_up(email, full_name, redirect_to): return 2, _("Please ask your administrator to verify your sign-up") @frappe.whitelist(allow_guest=True) -@rate_limit(key='user', limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST']) +@rate_limit(limit=get_password_reset_limit, seconds = 24*60*60, methods=['POST']) def reset_password(user): if user=="Administrator": return 'not allowed' diff --git a/frappe/core/page/background_jobs/background_jobs.py b/frappe/core/page/background_jobs/background_jobs.py index 5d9bb815da..4d9deca526 100644 --- a/frappe/core/page/background_jobs/background_jobs.py +++ b/frappe/core/page/background_jobs/background_jobs.py @@ -67,7 +67,8 @@ def get_info(show_failed=False) -> List[Dict]: fail_registry = queue.failed_job_registry for job_id in fail_registry.get_job_ids(): job = queue.fetch_job(job_id) - add_job(job, queue.name) + if job: + add_job(job, queue.name) return jobs diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 41cc900a97..6b427fdebf 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -325,15 +325,15 @@ frappe.PermissionEngine = class PermissionEngine { .attr("data-doctype", d.parent) .attr("data-role", d.role) .attr("data-permlevel", d.permlevel) - .click(function () { + .on("click", () => { return frappe.call({ module: "frappe.core", page: "permission_manager", method: "remove", args: { - doctype: $(this).attr("data-doctype"), - role: $(this).attr("data-role"), - permlevel: $(this).attr("data-permlevel") + doctype: d.parent, + role: d.role, + permlevel: d.permlevel }, callback: (r) => { if (r.exc) { diff --git a/frappe/custom/doctype/custom_field/custom_field.json b/frappe/custom/doctype/custom_field/custom_field.json index a8b1fb0e23..235f11aad8 100644 --- a/frappe/custom/doctype/custom_field/custom_field.json +++ b/frappe/custom/doctype/custom_field/custom_field.json @@ -1,460 +1,458 @@ { - "actions": [], - "allow_import": 1, - "creation": "2013-01-10 16:34:01", - "description": "Adds a custom field to a DocType", - "doctype": "DocType", - "document_type": "Setup", - "engine": "InnoDB", - "field_order": [ - "dt", - "module", - "label", - "label_help", - "fieldname", - "insert_after", - "length", - "column_break_6", - "fieldtype", - "precision", - "hide_seconds", - "hide_days", - "options", - "fetch_from", - "fetch_if_empty", - "options_help", - "section_break_11", - "collapsible", - "collapsible_depends_on", - "default", - "depends_on", - "mandatory_depends_on", - "read_only_depends_on", - "properties", - "non_negative", - "reqd", - "unique", - "read_only", - "ignore_user_permissions", - "hidden", - "print_hide", - "print_hide_if_no_value", - "print_width", - "no_copy", - "allow_on_submit", - "in_list_view", - "in_standard_filter", - "in_global_search", - "in_preview", - "bold", - "report_hide", - "search_index", - "allow_in_quick_entry", - "ignore_xss_filter", - "translatable", - "hide_border", - "description", - "permlevel", - "width", - "columns" - ], - "fields": [ - { - "bold": 1, - "fieldname": "dt", - "fieldtype": "Link", - "in_filter": 1, - "in_list_view": 1, - "label": "Document", - "oldfieldname": "dt", - "oldfieldtype": "Link", - "options": "DocType", - "reqd": 1, - "search_index": 1 - }, - { - "bold": 1, - "fieldname": "label", - "fieldtype": "Data", - "in_filter": 1, - "label": "Label", - "no_copy": 1, - "oldfieldname": "label", - "oldfieldtype": "Data" - }, - { - "fieldname": "label_help", - "fieldtype": "HTML", - "label": "Label Help", - "oldfieldtype": "HTML" - }, - { - "fieldname": "fieldname", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Fieldname", - "no_copy": 1, - "oldfieldname": "fieldname", - "oldfieldtype": "Data", - "read_only": 1 - }, - { - "description": "Select the label after which you want to insert new field.", - "fieldname": "insert_after", - "fieldtype": "Select", - "label": "Insert After", - "no_copy": 1, - "oldfieldname": "insert_after", - "oldfieldtype": "Select" - }, - { - "fieldname": "column_break_6", - "fieldtype": "Column Break" - }, - { - "bold": 1, - "default": "Data", - "fieldname": "fieldtype", - "fieldtype": "Select", - "in_filter": 1, - "in_list_view": 1, - "label": "Field Type", - "oldfieldname": "fieldtype", - "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature", - "reqd": 1 - }, - { - "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", - "description": "Set non-standard precision for a Float or Currency field", - "fieldname": "precision", - "fieldtype": "Select", - "label": "Precision", - "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" - }, - { - "fieldname": "options", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Options", - "oldfieldname": "options", - "oldfieldtype": "Text" - }, - { - "fieldname": "fetch_from", - "fieldtype": "Small Text", - "label": "Fetch From" - }, - { - "default": "0", - "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", - "fieldname": "fetch_if_empty", - "fieldtype": "Check", - "label": "Fetch If Empty" - }, - { - "fieldname": "options_help", - "fieldtype": "HTML", - "label": "Options Help", - "oldfieldtype": "HTML" - }, - { - "fieldname": "section_break_11", - "fieldtype": "Section Break" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fieldname": "collapsible", - "fieldtype": "Check", - "label": "Collapsible" - }, - { - "depends_on": "eval:doc.fieldtype==\"Section Break\"", - "fieldname": "collapsible_depends_on", - "fieldtype": "Code", - "label": "Collapsible Depends On" - }, - { - "fieldname": "default", - "fieldtype": "Text", - "label": "Default Value", - "oldfieldname": "default", - "oldfieldtype": "Text" - }, - { - "fieldname": "depends_on", - "fieldtype": "Code", - "label": "Depends On", - "length": 255 - }, - { - "fieldname": "description", - "fieldtype": "Text", - "label": "Field Description", - "oldfieldname": "description", - "oldfieldtype": "Text", - "print_width": "300px", - "width": "300px" - }, - { - "default": "0", - "fieldname": "permlevel", - "fieldtype": "Int", - "label": "Permission Level", - "oldfieldname": "permlevel", - "oldfieldtype": "Int" - }, - { - "fieldname": "width", - "fieldtype": "Data", - "label": "Width", - "oldfieldname": "width", - "oldfieldtype": "Data" - }, - { - "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", - "fieldname": "columns", - "fieldtype": "Int", - "label": "Columns" - }, - { - "fieldname": "properties", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "print_width": "50%", - "width": "50%" - }, - { - "default": "0", - "fieldname": "reqd", - "fieldtype": "Check", - "in_list_view": 1, - "label": "Is Mandatory Field", - "oldfieldname": "reqd", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "unique", - "fieldtype": "Check", - "label": "Unique" - }, - { - "default": "0", - "fieldname": "read_only", - "fieldtype": "Check", - "label": "Read Only" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype===\"Link\"", - "fieldname": "ignore_user_permissions", - "fieldtype": "Check", - "label": "Ignore User Permissions" - }, - { - "default": "0", - "fieldname": "hidden", - "fieldtype": "Check", - "label": "Hidden" - }, - { - "default": "0", - "fieldname": "print_hide", - "fieldtype": "Check", - "label": "Print Hide", - "oldfieldname": "print_hide", - "oldfieldtype": "Check" - }, - { - "default": "0", - "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", - "fieldname": "print_hide_if_no_value", - "fieldtype": "Check", - "label": "Print Hide If No Value" - }, - { - "fieldname": "print_width", - "fieldtype": "Data", - "hidden": 1, - "label": "Print Width", - "no_copy": 1, - "print_hide": 1 - }, - { - "default": "0", - "fieldname": "no_copy", - "fieldtype": "Check", - "label": "No Copy", - "oldfieldname": "no_copy", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "allow_on_submit", - "fieldtype": "Check", - "label": "Allow on Submit", - "oldfieldname": "allow_on_submit", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "in_list_view", - "fieldtype": "Check", - "label": "In List View" - }, - { - "default": "0", - "fieldname": "in_standard_filter", - "fieldtype": "Check", - "label": "In Standard Filter" - }, - { - "default": "0", - "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", - "fieldname": "in_global_search", - "fieldtype": "Check", - "label": "In Global Search" - }, - { - "default": "0", - "fieldname": "bold", - "fieldtype": "Check", - "label": "Bold" - }, - { - "default": "0", - "fieldname": "report_hide", - "fieldtype": "Check", - "label": "Report Hide", - "oldfieldname": "report_hide", - "oldfieldtype": "Check" - }, - { - "default": "0", - "fieldname": "search_index", - "fieldtype": "Check", - "hidden": 1, - "label": "Index", - "no_copy": 1, - "print_hide": 1 - }, - { - "default": "0", - "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", - "fieldname": "ignore_xss_filter", - "fieldtype": "Check", - "label": "Ignore XSS Filter" - }, - { - "default": "1", - "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", - "fieldname": "translatable", - "fieldtype": "Check", - "label": "Translatable" - }, - { - "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", - "fieldname": "length", - "fieldtype": "Int", - "label": "Length" - }, - { - "fieldname": "mandatory_depends_on", - "fieldtype": "Code", - "label": "Mandatory Depends On", - "length": 255 - }, - { - "fieldname": "read_only_depends_on", - "fieldtype": "Code", - "label": "Read Only Depends On", - "length": 255 - }, - { - "default": "0", - "fieldname": "allow_in_quick_entry", - "fieldtype": "Check", - "label": "Allow in Quick Entry" - }, - { - "default": "0", - "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", - "fieldname": "in_preview", - "fieldtype": "Check", - "label": "In Preview" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_seconds", - "fieldtype": "Check", - "label": "Hide Seconds" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Duration'", - "fieldname": "hide_days", - "fieldtype": "Check", - "label": "Hide Days" - }, - { - "default": "0", - "depends_on": "eval:doc.fieldtype=='Section Break'", - "fieldname": "hide_border", - "fieldtype": "Check", - "label": "Hide Border" - }, - { - "default": "0", - "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", - "fieldname": "non_negative", - "fieldtype": "Check", - "label": "Non Negative" - }, - { - "fieldname": "module", - "fieldtype": "Link", - "label": "Module (for export)", - "options": "Module Def" - } - ], - "icon": "fa fa-glass", - "idx": 1, - "index_web_pages_for_search": 1, - "links": [], - "modified": "2021-09-04 12:45:22.810120", - "modified_by": "Administrator", - "module": "Custom", - "name": "Custom Field", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Administrator", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "search_fields": "dt,label,fieldtype,options", - "sort_field": "modified", - "sort_order": "ASC", - "track_changes": 1 + "actions": [], + "allow_import": 1, + "creation": "2013-01-10 16:34:01", + "description": "Adds a custom field to a DocType", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "dt", + "module", + "label", + "label_help", + "fieldname", + "insert_after", + "length", + "column_break_6", + "fieldtype", + "precision", + "hide_seconds", + "hide_days", + "options", + "fetch_from", + "fetch_if_empty", + "options_help", + "section_break_11", + "collapsible", + "collapsible_depends_on", + "default", + "depends_on", + "mandatory_depends_on", + "read_only_depends_on", + "properties", + "non_negative", + "reqd", + "unique", + "read_only", + "ignore_user_permissions", + "hidden", + "print_hide", + "print_hide_if_no_value", + "print_width", + "no_copy", + "allow_on_submit", + "in_list_view", + "in_standard_filter", + "in_global_search", + "in_preview", + "bold", + "report_hide", + "search_index", + "allow_in_quick_entry", + "ignore_xss_filter", + "translatable", + "hide_border", + "description", + "permlevel", + "width", + "columns" + ], + "fields": [{ + "bold": 1, + "fieldname": "dt", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "label": "Document", + "oldfieldname": "dt", + "oldfieldtype": "Link", + "options": "DocType", + "reqd": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "label", + "fieldtype": "Data", + "in_filter": 1, + "label": "Label", + "no_copy": 1, + "oldfieldname": "label", + "oldfieldtype": "Data" + }, + { + "fieldname": "label_help", + "fieldtype": "HTML", + "label": "Label Help", + "oldfieldtype": "HTML" + }, + { + "fieldname": "fieldname", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Fieldname", + "no_copy": 1, + "oldfieldname": "fieldname", + "oldfieldtype": "Data", + "read_only": 1 + }, + { + "description": "Select the label after which you want to insert new field.", + "fieldname": "insert_after", + "fieldtype": "Select", + "label": "Insert After", + "no_copy": 1, + "oldfieldname": "insert_after", + "oldfieldtype": "Select" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "bold": 1, + "default": "Data", + "fieldname": "fieldtype", + "fieldtype": "Select", + "in_filter": 1, + "in_list_view": 1, + "label": "Field Type", + "oldfieldname": "fieldtype", + "oldfieldtype": "Select", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRead Only\nRating\nSection Break\nSelect\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nSignature\nTab Break", + "reqd": 1 + }, + { + "depends_on": "eval:in_list([\"Float\", \"Currency\", \"Percent\"], doc.fieldtype)", + "description": "Set non-standard precision for a Float or Currency field", + "fieldname": "precision", + "fieldtype": "Select", + "label": "Precision", + "options": "\n0\n1\n2\n3\n4\n5\n6\n7\n8\n9" + }, + { + "fieldname": "options", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Options", + "oldfieldname": "options", + "oldfieldtype": "Text" + }, + { + "fieldname": "fetch_from", + "fieldtype": "Small Text", + "label": "Fetch From" + }, + { + "default": "0", + "description": "If checked, this field will be not overwritten based on Fetch From if a value already exists.", + "fieldname": "fetch_if_empty", + "fieldtype": "Check", + "label": "Fetch If Empty" + }, + { + "fieldname": "options_help", + "fieldtype": "HTML", + "label": "Options Help", + "oldfieldtype": "HTML" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible", + "fieldtype": "Check", + "label": "Collapsible" + }, + { + "depends_on": "eval:doc.fieldtype==\"Section Break\"", + "fieldname": "collapsible_depends_on", + "fieldtype": "Code", + "label": "Collapsible Depends On" + }, + { + "fieldname": "default", + "fieldtype": "Text", + "label": "Default Value", + "oldfieldname": "default", + "oldfieldtype": "Text" + }, + { + "fieldname": "depends_on", + "fieldtype": "Code", + "label": "Depends On", + "length": 255 + }, + { + "fieldname": "description", + "fieldtype": "Text", + "label": "Field Description", + "oldfieldname": "description", + "oldfieldtype": "Text", + "print_width": "300px", + "width": "300px" + }, + { + "default": "0", + "fieldname": "permlevel", + "fieldtype": "Int", + "label": "Permission Level", + "oldfieldname": "permlevel", + "oldfieldtype": "Int" + }, + { + "fieldname": "width", + "fieldtype": "Data", + "label": "Width", + "oldfieldname": "width", + "oldfieldtype": "Data" + }, + { + "description": "Number of columns for a field in a List View or a Grid (Total Columns should be less than 11)", + "fieldname": "columns", + "fieldtype": "Int", + "label": "Columns" + }, + { + "fieldname": "properties", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_width": "50%", + "width": "50%" + }, + { + "default": "0", + "fieldname": "reqd", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Is Mandatory Field", + "oldfieldname": "reqd", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "unique", + "fieldtype": "Check", + "label": "Unique" + }, + { + "default": "0", + "fieldname": "read_only", + "fieldtype": "Check", + "label": "Read Only" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype===\"Link\"", + "fieldname": "ignore_user_permissions", + "fieldtype": "Check", + "label": "Ignore User Permissions" + }, + { + "default": "0", + "fieldname": "hidden", + "fieldtype": "Check", + "label": "Hidden" + }, + { + "default": "0", + "fieldname": "print_hide", + "fieldtype": "Check", + "label": "Print Hide", + "oldfieldname": "print_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "depends_on": "eval:[\"Int\", \"Float\", \"Currency\", \"Percent\"].indexOf(doc.fieldtype)!==-1", + "fieldname": "print_hide_if_no_value", + "fieldtype": "Check", + "label": "Print Hide If No Value" + }, + { + "fieldname": "print_width", + "fieldtype": "Data", + "hidden": 1, + "label": "Print Width", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "no_copy", + "fieldtype": "Check", + "label": "No Copy", + "oldfieldname": "no_copy", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "allow_on_submit", + "fieldtype": "Check", + "label": "Allow on Submit", + "oldfieldname": "allow_on_submit", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "in_list_view", + "fieldtype": "Check", + "label": "In List View" + }, + { + "default": "0", + "fieldname": "in_standard_filter", + "fieldtype": "Check", + "label": "In Standard Filter" + }, + { + "default": "0", + "depends_on": "eval:([\"Data\", \"Select\", \"Table\", \"Text\", \"Text Editor\", \"Link\", \"Small Text\", \"Long Text\", \"Read Only\", \"Heading\", \"Dynamic Link\"].indexOf(doc.fieldtype) !== -1)", + "fieldname": "in_global_search", + "fieldtype": "Check", + "label": "In Global Search" + }, + { + "default": "0", + "fieldname": "bold", + "fieldtype": "Check", + "label": "Bold" + }, + { + "default": "0", + "fieldname": "report_hide", + "fieldtype": "Check", + "label": "Report Hide", + "oldfieldname": "report_hide", + "oldfieldtype": "Check" + }, + { + "default": "0", + "fieldname": "search_index", + "fieldtype": "Check", + "hidden": 1, + "label": "Index", + "no_copy": 1, + "print_hide": 1 + }, + { + "default": "0", + "description": "Don't HTML Encode HTML tags like <script> or just characters like < or >, as they could be intentionally used in this field", + "fieldname": "ignore_xss_filter", + "fieldtype": "Check", + "label": "Ignore XSS Filter" + }, + { + "default": "1", + "depends_on": "eval:['Data', 'Select', 'Text', 'Small Text', 'Text Editor'].includes(doc.fieldtype)", + "fieldname": "translatable", + "fieldtype": "Check", + "label": "Translatable" + }, + { + "depends_on": "eval:in_list(['Data', 'Link', 'Dynamic Link', 'Password', 'Select', 'Read Only', 'Attach', 'Attach Image', 'Int'], doc.fieldtype)", + "fieldname": "length", + "fieldtype": "Int", + "label": "Length" + }, + { + "fieldname": "mandatory_depends_on", + "fieldtype": "Code", + "label": "Mandatory Depends On", + "length": 255 + }, + { + "fieldname": "read_only_depends_on", + "fieldtype": "Code", + "label": "Read Only Depends On", + "length": 255 + }, + { + "default": "0", + "fieldname": "allow_in_quick_entry", + "fieldtype": "Check", + "label": "Allow in Quick Entry" + }, + { + "default": "0", + "depends_on": "eval:!in_list(['Table', 'Table MultiSelect'], doc.fieldtype);", + "fieldname": "in_preview", + "fieldtype": "Check", + "label": "In Preview" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_seconds", + "fieldtype": "Check", + "label": "Hide Seconds" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Duration'", + "fieldname": "hide_days", + "fieldtype": "Check", + "label": "Hide Days" + }, + { + "default": "0", + "depends_on": "eval:doc.fieldtype=='Section Break'", + "fieldname": "hide_border", + "fieldtype": "Check", + "label": "Hide Border" + }, + { + "default": "0", + "depends_on": "eval:in_list([\"Int\", \"Float\", \"Currency\"], doc.fieldtype)", + "fieldname": "non_negative", + "fieldtype": "Check", + "label": "Non Negative" + }, + { + "fieldname": "module", + "fieldtype": "Link", + "label": "Module (for export)", + "options": "Module Def" + } + ], + "icon": "fa fa-glass", + "idx": 1, + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-09-04 12:45:23.810120", + "modified_by": "Administrator", + "module": "Custom", + "name": "Custom Field", + "owner": "Administrator", + "permissions": [{ + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "dt,label,fieldtype,options", + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/custom/doctype/custom_field/custom_field.py b/frappe/custom/doctype/custom_field/custom_field.py index 61fc4486bd..bf606701da 100644 --- a/frappe/custom/doctype/custom_field/custom_field.py +++ b/frappe/custom/doctype/custom_field/custom_field.py @@ -18,7 +18,7 @@ class CustomField(Document): if not self.fieldname: label = self.label if not label: - if self.fieldtype in ["Section Break", "Column Break"]: + if self.fieldtype in ["Section Break", "Column Break", "Tab Break"]: label = self.fieldtype + "_" + str(self.idx) else: frappe.throw(_("Label is mandatory")) diff --git a/frappe/custom/doctype/customize_form_field/customize_form_field.json b/frappe/custom/doctype/customize_form_field/customize_form_field.json index 0a456b1026..986b99a7af 100644 --- a/frappe/custom/doctype/customize_form_field/customize_form_field.json +++ b/frappe/custom/doctype/customize_form_field/customize_form_field.json @@ -82,7 +82,7 @@ "label": "Type", "oldfieldname": "fieldtype", "oldfieldtype": "Select", - "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime", + "options": "Attach\nAttach Image\nBarcode\nButton\nCheck\nCode\nColor\nColumn Break\nCurrency\nData\nDate\nDatetime\nDuration\nDynamic Link\nFloat\nFold\nGeolocation\nHeading\nHTML\nHTML Editor\nIcon\nImage\nInt\nLink\nLong Text\nMarkdown Editor\nPassword\nPercent\nRating\nRead Only\nSection Break\nSelect\nSignature\nSmall Text\nTable\nTable MultiSelect\nText\nText Editor\nTime\nTab Break", "reqd": 1, "search_index": 1 }, @@ -428,7 +428,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-07-10 21:57:24.479749", + "modified": "2021-07-11 21:57:24.479749", "modified_by": "Administrator", "module": "Custom", "name": "Customize Form Field", diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py index 7b95408060..d71b7b0021 100644 --- a/frappe/custom/doctype/property_setter/property_setter.py +++ b/frappe/custom/doctype/property_setter/property_setter.py @@ -34,7 +34,7 @@ class PropertySetter(Document): fields=['fieldname', 'label', 'fieldtype'], filters={ 'parent': dt, - 'fieldtype': ['not in', ('Section Break', 'Column Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], + 'fieldtype': ['not in', ('Section Break', 'Column Break', 'Tab Break', 'HTML', 'Read Only', 'Fold') + frappe.model.table_fields], 'fieldname': ['!=', ''] }, order_by='label asc', diff --git a/frappe/database/database.py b/frappe/database/database.py index c48e86d301..e98cc22f41 100644 --- a/frappe/database/database.py +++ b/frappe/database/database.py @@ -14,8 +14,13 @@ import frappe.model.meta from frappe import _ from time import time -from frappe.utils import now, getdate, cast, get_datetime, get_table_name +from frappe.utils import now, getdate, cast, get_datetime from frappe.model.utils.link_count import flush_local_link_count +from frappe.query_builder.functions import Count +from frappe.query_builder.functions import Min, Max, Avg, Sum +from frappe.query_builder.utils import Column +from .query import Query +from pypika.terms import PseudoColumn class Database(object): @@ -55,6 +60,7 @@ class Database(object): self.password = password or frappe.conf.db_password self.value_cache = {} + self.query = Query() def setup_type_map(self): pass @@ -77,7 +83,7 @@ class Database(object): pass def sql(self, query, values=(), as_dict = 0, as_list = 0, formatted = 0, - debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False): + debug=0, ignore_ddl=0, as_utf8=0, auto_commit=0, update=None, explain=False, run=True): """Execute a SQL query and fetch all rows. :param query: SQL query. @@ -90,7 +96,7 @@ class Database(object): :param as_utf8: Encode values as UTF 8. :param auto_commit: Commit after executing the query. :param update: Update this dict to all rows (if returned `as_dict`). - + :param run: Returns query without executing it if False. Examples: # return customer names as dicts @@ -105,6 +111,9 @@ class Database(object): """ query = str(query) + if not run: + return query + if re.search(r'ifnull\(', query, flags=re.IGNORECASE): # replaces ifnull in query with coalesce query = re.sub(r'ifnull\(', 'coalesce(', query, flags=re.IGNORECASE) @@ -310,59 +319,6 @@ class Database(object): nres.append(nr) return nres - def build_conditions(self, filters): - """Convert filters sent as dict, lists to SQL conditions. filter's key - is passed by map function, build conditions like: - - * ifnull(`fieldname`, default_value) = %(fieldname)s - * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s - """ - conditions = [] - values = {} - def _build_condition(key): - """ - filter's key is passed by map function - build conditions like: - * ifnull(`fieldname`, default_value) = %(fieldname)s - * `fieldname` [=, !=, >, >=, <, <=] %(fieldname)s - """ - _operator = "=" - _rhs = " %(" + key + ")s" - value = filters.get(key) - values[key] = value - if isinstance(value, (list, tuple)): - # value is a tuple like ("!=", 0) - _operator = value[0] - values[key] = value[1] - if isinstance(value[1], (tuple, list)): - # value is a list in tuple ("in", ("A", "B")) - _rhs = " ({0})".format(", ".join(self.escape(v) for v in value[1])) - del values[key] - - if _operator not in ["=", "!=", ">", ">=", "<", "<=", "like", "in", "not in", "not like"]: - _operator = "=" - - if "[" in key: - split_key = key.split("[") - condition = "coalesce(`" + split_key[0] + "`, " + split_key[1][:-1] + ") " \ - + _operator + _rhs - else: - condition = "`" + key + "` " + _operator + _rhs - - conditions.append(condition) - - if isinstance(filters, int): - # docname is a number, convert to string - filters = str(filters) - - if isinstance(filters, str): - filters = { "name": filters } - - for f in filters: - _build_condition(f) - - return " and ".join(conditions), values - def get(self, doctype, filters=None, as_dict=True, cache=False): """Returns `get_value` with fieldname='*'""" return self.get_value(doctype, filters, "*", as_dict=as_dict, cache=cache) @@ -424,9 +380,8 @@ class Database(object): (doctype, filters, fieldname) in self.value_cache: return self.value_cache[(doctype, filters, fieldname)] - if not order_by: order_by = 'modified desc' - if isinstance(filters, list): + order_by = order_by or "modified_desc" out = self._get_value_for_many_names(doctype, filters, fieldname, debug=debug) else: @@ -439,6 +394,7 @@ class Database(object): if (filters is not None) and (filters!=doctype or doctype=="DocType"): try: + order_by = order_by or "modified" out = self._get_values_from_table(fields, filters, doctype, as_dict, debug, order_by, update, for_update=for_update) except Exception as e: if ignore and (frappe.db.is_missing_column(e) or frappe.db.is_table_missing(e)): @@ -567,32 +523,23 @@ class Database(object): return self.get_single_value(*args, **kwargs) def _get_values_from_table(self, fields, filters, doctype, as_dict, debug, order_by=None, update=None, for_update=False): - fl = [] + field_objects = [] + + for field in fields: + if "(" in field or " as " in field: + field_objects.append(PseudoColumn(field)) + else: + field_objects.append(field) + + criterion = self.query.build_conditions(table=doctype, filters=filters, orderby=order_by, for_update=for_update) + if isinstance(fields, (list, tuple)): - for f in fields: - if "(" in f or " as " in f: # function - fl.append(f) - else: - fl.append("`" + f + "`") - fl = ", ".join(fl) + query = criterion.select(*field_objects) else: - fl = fields if fields=="*": + query = criterion.select(fields) as_dict = True - - conditions, values = self.build_conditions(filters) - - order_by = ("order by " + order_by) if order_by else "" - - r = self.sql("select {fields} from `tab{doctype}` {where} {conditions} {order_by} {for_update}" - .format( - for_update = 'for update' if for_update else '', - fields = fl, - doctype = doctype, - where = "where" if conditions else "", - conditions = conditions, - order_by = order_by), - values, as_dict=as_dict, debug=debug, update=update) + r = self.sql(query, as_dict=as_dict, debug=debug, update=update) return r @@ -819,50 +766,34 @@ class Database(object): except Exception: return None + def min(self, dt, fieldname, filters=None, **kwargs): + return self.query.build_conditions(dt, filters=filters).select(Min(Column(fieldname))).run(**kwargs)[0][0] or 0 + + def max(self, dt, fieldname, filters=None, **kwargs): + return self.query.build_conditions(dt, filters=filters).select(Max(Column(fieldname))).run(**kwargs)[0][0] or 0 + + def avg(self, dt, fieldname, filters=None, **kwargs): + return self.query.build_conditions(dt, filters=filters).select(Avg(Column(fieldname))).run(**kwargs)[0][0] or 0 + + def sum(self, dt, fieldname, filters=None, **kwargs): + return self.query.build_conditions(dt, filters=filters).select(Sum(Column(fieldname))).run(**kwargs)[0][0] or 0 + def count(self, dt, filters=None, debug=False, cache=False): """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.build_conditions(table=dt, filters=filters).select(Count("*")) if filters: - conditions, filters = self.build_conditions(filters) - count = self.sql("""select count(*) - from `tab%s` where %s""" % (dt, conditions), filters, debug=debug)[0][0] + count = self.sql(query, debug=debug)[0][0] return count else: - count = self.sql("""select count(*) - from `tab%s`""" % (dt,))[0][0] - + count = self.sql(query, debug=debug)[0][0] if cache: frappe.cache().set_value('doctype:count:{}'.format(dt), count, expires_in_sec = 86400) - return count - def sum(self, dt, fieldname, filters=None): - return self._get_aggregation('SUM', dt, fieldname, filters) - - def avg(self, dt, fieldname, filters=None): - return self._get_aggregation('AVG', dt, fieldname, filters) - - def min(self, dt, fieldname, filters=None): - return self._get_aggregation('MIN', dt, fieldname, filters) - - def max(self, dt, fieldname, filters=None): - return self._get_aggregation('MAX', dt, fieldname, filters) - - def _get_aggregation(self, function, dt, fieldname, filters=None): - if not self.has_column(dt, fieldname): - frappe.throw(frappe._('Invalid column'), self.InvalidColumnName) - - query = f'SELECT {function}({fieldname}) AS value FROM `tab{dt}`' - values = () - if filters: - conditions, values = self.build_conditions(filters) - query = f"{query} WHERE {conditions}" - - return self.sql(query, values)[0][0] or 0 - @staticmethod def format_date(date): return getdate(date).strftime("%Y-%m-%d") @@ -984,16 +915,9 @@ class Database(object): """ values = () filters = filters or kwargs.get("conditions") - table = get_table_name(doctype) - query = f"DELETE FROM `{table}`" - + query = self.query.build_conditions(table=doctype, filters=filters).delete() if "debug" not in kwargs: kwargs["debug"] = debug - - if filters: - conditions, values = self.build_conditions(filters) - query = f"{query} WHERE {conditions}" - return self.sql(query, values, **kwargs) def truncate(self, doctype: str): diff --git a/frappe/database/mariadb/database.py b/frappe/database/mariadb/database.py index 5ed7991a82..2f6d640743 100644 --- a/frappe/database/mariadb/database.py +++ b/frappe/database/mariadb/database.py @@ -22,11 +22,11 @@ class MariaDBDatabase(Database): def setup_type_map(self): self.db_type = 'mariadb' self.type_map = { - 'Currency': ('decimal', '18,6'), + 'Currency': ('decimal', '21,9'), 'Int': ('int', '11'), 'Long Int': ('bigint', '20'), - 'Float': ('decimal', '18,6'), - 'Percent': ('decimal', '18,6'), + 'Float': ('decimal', '21,9'), + 'Percent': ('decimal', '21,9'), 'Check': ('int', '1'), 'Small Text': ('text', ''), 'Long Text': ('longtext', ''), @@ -51,7 +51,7 @@ class MariaDBDatabase(Database): 'Color': ('varchar', self.VARCHAR_LEN), 'Barcode': ('longtext', ''), 'Geolocation': ('longtext', ''), - 'Duration': ('decimal', '18,6'), + 'Duration': ('decimal', '21,9'), 'Icon': ('varchar', self.VARCHAR_LEN) } diff --git a/frappe/database/mariadb/setup_db.py b/frappe/database/mariadb/setup_db.py index 6be08c66bb..8088cc2331 100644 --- a/frappe/database/mariadb/setup_db.py +++ b/frappe/database/mariadb/setup_db.py @@ -34,25 +34,23 @@ def setup_database(force, source_sql, verbose, no_mariadb_socket=False): db_name = frappe.local.conf.db_name root_conn = get_root_connection(frappe.flags.root_login, frappe.flags.root_password) dbman = DbManager(root_conn) + dbman_kwargs = {} + if no_mariadb_socket: + dbman_kwargs["host"] = "%" + if force or (db_name not in dbman.get_database_list()): - dbman.delete_user(db_name) - if no_mariadb_socket: - dbman.delete_user(db_name, host="%") + dbman.delete_user(db_name, **dbman_kwargs) dbman.drop_database(db_name) else: raise Exception("Database %s already exists" % (db_name,)) - dbman.create_user(db_name, frappe.conf.db_password) - if no_mariadb_socket: - dbman.create_user(db_name, frappe.conf.db_password, host="%") + dbman.create_user(db_name, frappe.conf.db_password, **dbman_kwargs) if verbose: print("Created user %s" % db_name) dbman.create_database(db_name) if verbose: print("Created database %s" % db_name) - dbman.grant_all_privileges(db_name, db_name) - if no_mariadb_socket: - dbman.grant_all_privileges(db_name, db_name, host="%") + dbman.grant_all_privileges(db_name, db_name, **dbman_kwargs) dbman.flush_privileges() if verbose: print("Granted privileges to user %s and database %s" % (db_name, db_name)) diff --git a/frappe/database/postgres/database.py b/frappe/database/postgres/database.py index a06abb1013..bfa5515111 100644 --- a/frappe/database/postgres/database.py +++ b/frappe/database/postgres/database.py @@ -32,11 +32,11 @@ class PostgresDatabase(Database): def setup_type_map(self): self.db_type = 'postgres' self.type_map = { - 'Currency': ('decimal', '18,6'), + 'Currency': ('decimal', '21,9'), 'Int': ('bigint', None), 'Long Int': ('bigint', None), - 'Float': ('decimal', '18,6'), - 'Percent': ('decimal', '18,6'), + 'Float': ('decimal', '21,9'), + 'Percent': ('decimal', '21,9'), 'Check': ('smallint', None), 'Small Text': ('text', ''), 'Long Text': ('text', ''), @@ -61,7 +61,7 @@ class PostgresDatabase(Database): 'Color': ('varchar', self.VARCHAR_LEN), 'Barcode': ('text', ''), 'Geolocation': ('text', ''), - 'Duration': ('decimal', '18,6'), + 'Duration': ('decimal', '21,9'), 'Icon': ('varchar', self.VARCHAR_LEN) } diff --git a/frappe/database/query.py b/frappe/database/query.py new file mode 100644 index 0000000000..7d7de85646 --- /dev/null +++ b/frappe/database/query.py @@ -0,0 +1,267 @@ +import operator +from typing import Any, Dict, List, Tuple, Union + +import frappe +from frappe.query_builder import Criterion, Order, Field + + +def like(key: str, value: str) -> frappe.qb: + """Wrapper method for `LIKE` + + Args: + key (str): field + value (str): criterion + + Returns: + frappe.qb: `frappe.qb object with `LIKE` + """ + return Field(key).like(value) + + +def func_in(key: str, value: Union[List, Tuple]) -> frappe.qb: + """Wrapper method for `IN` + + Args: + key (str): field + value (Union[int, str]): criterion + + Returns: + frappe.qb: `frappe.qb object with `IN` + """ + return Field(key).isin(value) + + +def not_like(key: str, value: str) -> frappe.qb: + """Wrapper method for `NOT LIKE` + + Args: + key (str): field + value (str): criterion + + Returns: + frappe.qb: `frappe.qb object with `NOT LIKE` + """ + return Field(key).not_like(value) + + +def func_not_in(key: str, value: Union[List, Tuple]): + """Wrapper method for `NOT IN` + + Args: + key (str): field + value (Union[int, str]): criterion + + Returns: + frappe.qb: `frappe.qb object with `NOT IN` + """ + return Field(key).notin(value) + + +def func_regex(key: str, value: str) -> frappe.qb: + """Wrapper method for `REGEX` + + Args: + key (str): field + value (str): criterion + + Returns: + frappe.qb: `frappe.qb object with `REGEX` + """ + return Field(key).regex(value) + + +def func_between(key: str, value: Union[List, Tuple]) -> frappe.qb: + """Wrapper method for `BETWEEN` + + Args: + key (str): field + value (Union[int, str]): criterion + + Returns: + frappe.qb: `frappe.qb object with `BETWEEN` + """ + return Field(key)[slice(*value)] + +def make_function(key: Any, value: Union[int, str]): + """returns fucntion query + + Args: + key (Any): field + value (Union[int, str]): criterion + + Returns: + frappe.qb: frappe.qb object + """ + return OPERATOR_MAP[value[0]](key, value[1]) + + +def change_orderby(order: str): + """Convert orderby to standart Order object + + Args: + order (str): Field, order + + Returns: + tuple: field, order + """ + order = order.split() + if order[1].lower() == "asc": + orderby, order = order[0], Order.asc + return orderby, order + orderby, order = order[0], Order.desc + return orderby, order + + +OPERATOR_MAP = { + "+": operator.add, + "=": operator.eq, + "-": operator.sub, + "!=": operator.ne, + "<": operator.lt, + ">": operator.gt, + "<=": operator.le, + ">=": operator.ge, + "in": func_in, + "not in": func_not_in, + "like": like, + "not like": not_like, + "regex": func_regex, + "between": func_between + } + + +class Query: + def get_condition(self, table: str, **kwargs) -> frappe.qb: + """Get initial table object + + Args: + table (str): DocType + + Returns: + frappe.qb: DocType with initial condition + """ + if kwargs.get("update"): + return frappe.qb.update(table) + if kwargs.get("into"): + return frappe.qb.into(table) + return frappe.qb.from_(table) + + def criterion_query(self, table: str, criterion: Criterion, **kwargs) -> frappe.qb: + """Generate filters from Criterion objects + + Args: + table (str): DocType + criterion (Criterion): Filters + + Returns: + frappe.qb: condition object + """ + condition = self.get_condition(table, **kwargs) + return condition.where(criterion) + + def add_conditions(self, conditions: frappe.qb, **kwargs): + """Adding additional conditions + + Args: + conditions (frappe.qb): built conditions + + Returns: + conditions (frappe.qb): frappe.qb object + """ + if kwargs.get("orderby"): + orderby = kwargs.get("orderby") + order = kwargs.get("order") if kwargs.get("order") else Order.desc + if isinstance(orderby, str) and len(orderby.split()) > 1: + orderby, order = change_orderby(orderby) + conditions = conditions.orderby(orderby, order=order) + + if kwargs.get("limit"): + conditions = conditions.limit(kwargs.get("limit")) + + if kwargs.get("distinct"): + conditions = conditions.distinct() + + if kwargs.get("for_update"): + conditions = conditions.for_update() + + return conditions + + def misc_query(self, table: str, filters: Union[List, Tuple] = None, **kwargs): + """Build conditions using the given Lists or Tuple filters + + Args: + table (str): DocType + filters (Union[List, Tuple], optional): Filters. Defaults to None. + """ + conditions = self.get_condition(table, **kwargs) + if not filters: + return conditions + if isinstance(filters, list): + for f in filters: + if not isinstance(f, (list, tuple)): + _operator = OPERATOR_MAP[filters[1]] + if not isinstance(filters[0], str): + conditions = make_function(filters[0], filters[2]) + break + 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])) + + conditions = self.add_conditions(conditions, **kwargs) + return conditions + + def dict_query(self, table: str, filters: Dict[str, Union[str, int]] = None, **kwargs) -> frappe.qb: + """Build conditions using the given dictionary filters + + Args: + table (str): DocType + filters (Dict[str, Union[str, int]], optional): Filters. Defaults to None. + + Returns: + frappe.qb: conditions object + """ + conditions = self.get_condition(table, **kwargs) + if not filters: + return conditions + + for key in filters: + value = filters.get(key) + _operator = OPERATOR_MAP["="] + + if not isinstance(key, str): + conditions = conditions.where(make_function(key, value)) + continue + 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])) + else: + _operator = OPERATOR_MAP[value[0]] + conditions = conditions.where(_operator(Field(key), value[1])) + else: + conditions = conditions.where(_operator(Field(key), value)) + conditions = self.add_conditions(conditions, **kwargs) + return conditions + + def build_conditions(self, table: str, filters: Union[Dict[str, Union[str, int]], str, int] = None, **kwargs) -> frappe.qb: + """Build conditions for sql query + + Args: + filters (Union[Dict[str, Union[str, int]], str, int]): conditions in Dict + table (str): DocType + + Returns: + frappe.qb: frappe.qb conditions object + """ + if isinstance(filters, Criterion): + return self.criterion_query(table, filters, **kwargs) + + if isinstance(filters, int) or isinstance(filters, str): + filters = {"name": str(filters)} + + if isinstance(filters, (list, tuple)): + return self.misc_query(table, filters, **kwargs) + + return self.dict_query(filters=filters, table=table, **kwargs) diff --git a/frappe/database/schema.py b/frappe/database/schema.py index 31f11dbd5e..ce9fcb4147 100644 --- a/frappe/database/schema.py +++ b/frappe/database/schema.py @@ -303,6 +303,8 @@ def get_definition(fieldtype, precision=None, length=None): size = d[1] if d[1] else None if size: + # This check needs to exist for backward compatibility. + # Till V13, default size used for float, currency and percent are (18, 6). if fieldtype in ["Float", "Currency", "Percent"] and cint(precision) > 6: size = '21,9' diff --git a/frappe/desk/doctype/note/note.json b/frappe/desk/doctype/note/note.json index 8d476e83fe..69a9518ac4 100644 --- a/frappe/desk/doctype/note/note.json +++ b/frappe/desk/doctype/note/note.json @@ -1,322 +1,106 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "beta": 0, - "creation": "2013-05-24 13:41:00", - "custom": 0, - "description": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "description": "", - "fieldname": "public", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Public", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "public", - "fieldname": "notify_on_login", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Notify users with a popup when they log in", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "notify_on_login", - "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", - "fieldname": "notify_on_every_login", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Notify Users On Every Login", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.notify_on_login && doc.public", - "fieldname": "expire_notification_on", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expire Notification On", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "description": "Help: To link to another record in the system, use \"#Form/Note/[Note Name]\" as the Link URL. (don't use \"http://\")", - "fieldname": "content", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Content", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "seen_by_section", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Seen By", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "seen_by", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Seen By Table", - "length": 0, - "no_copy": 0, - "options": "Note Seen By", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-file-text", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-21 15:15:44.909636", - "modified_by": "Administrator", - "module": "Desk", - "name": "Note", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "All", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 1, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 - } \ No newline at end of file + "actions": [], + "allow_rename": 1, + "creation": "2013-05-24 13:41:00", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "title", + "public", + "notify_on_login", + "notify_on_every_login", + "expire_notification_on", + "content", + "seen_by_section", + "seen_by" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_global_search": 1, + "in_list_view": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "default": "0", + "fieldname": "public", + "fieldtype": "Check", + "label": "Public", + "print_hide": 1 + }, + { + "bold": 1, + "default": "0", + "depends_on": "public", + "fieldname": "notify_on_login", + "fieldtype": "Check", + "label": "Notify users with a popup when they log in" + }, + { + "bold": 1, + "default": "0", + "depends_on": "notify_on_login", + "description": "If enabled, users will be notified every time they login. If not enabled, users will only be notified once.", + "fieldname": "notify_on_every_login", + "fieldtype": "Check", + "label": "Notify Users On Every Login" + }, + { + "depends_on": "eval:doc.notify_on_login && doc.public", + "fieldname": "expire_notification_on", + "fieldtype": "Date", + "label": "Expire Notification On", + "search_index": 1 + }, + { + "bold": 1, + "description": "Help: To link to another record in the system, use \"/app/note/[Note Name]\" as the Link URL. (don't use \"http://\")", + "fieldname": "content", + "fieldtype": "Text Editor", + "in_global_search": 1, + "label": "Content" + }, + { + "collapsible": 1, + "fieldname": "seen_by_section", + "fieldtype": "Section Break", + "label": "Seen By" + }, + { + "fieldname": "seen_by", + "fieldtype": "Table", + "label": "Seen By Table", + "options": "Note Seen By" + } + ], + "icon": "fa fa-file-text", + "idx": 1, + "links": [], + "modified": "2021-09-18 10:57:51.352643", + "modified_by": "Administrator", + "module": "Desk", + "name": "Note", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "All", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py index 44bb780681..aff1bd6973 100644 --- a/frappe/desk/doctype/tag/tag.py +++ b/frappe/desk/doctype/tag/tag.py @@ -128,46 +128,35 @@ def delete_tags_for_document(doc): }) def update_tags(doc, tags): - """ - Adds tags for documents - :param doc: Document to be added to global tags - """ + """Adds tags for documents + :param doc: Document to be added to global tags + """ new_tags = {tag.strip() for tag in tags.split(",") if tag} - - for tag in new_tags: - if not frappe.db.exists("Tag Link", {"parenttype": doc.doctype, "parent": doc.name, "tag": tag}): - frappe.get_doc({ - "doctype": "Tag Link", - "document_type": doc.doctype, - "document_name": doc.name, - "parenttype": doc.doctype, - "parent": doc.name, - "title": doc.get_title() or '', - "tag": tag - }).insert(ignore_permissions=True) - existing_tags = [tag.tag for tag in frappe.get_list("Tag Link", filters={ "document_type": doc.doctype, "document_name": doc.name }, fields=["tag"])] - deleted_tags = get_deleted_tags(new_tags, existing_tags) + added_tags = set(new_tags) - set(existing_tags) + for tag in added_tags: + frappe.get_doc({ + "doctype": "Tag Link", + "document_type": doc.doctype, + "document_name": doc.name, + "parenttype": doc.doctype, + "parent": doc.name, + "title": doc.get_title() or '', + "tag": tag + }).insert(ignore_permissions=True) - if deleted_tags: - for tag in deleted_tags: - delete_tag_for_document(doc.doctype, doc.name, tag) - -def get_deleted_tags(new_tags, existing_tags): - - return list(set(existing_tags) - set(new_tags)) - -def delete_tag_for_document(dt, dn, tag): - frappe.db.delete("Tag Link", { - "document_type": dt, - "document_name": dn, - "tag": tag - }) + deleted_tags = list(set(existing_tags) - set(new_tags)) + for tag in deleted_tags: + frappe.db.delete("Tag Link", { + "document_type": doc.doctype, + "document_name": doc.name, + "tag": tag + }) @frappe.whitelist() def get_documents_for_tag(tag): diff --git a/frappe/desk/doctype/tag_link/tag_link.json b/frappe/desk/doctype/tag_link/tag_link.json index 00a7349c5c..9142279fa3 100644 --- a/frappe/desk/doctype/tag_link/tag_link.json +++ b/frappe/desk/doctype/tag_link/tag_link.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-09-24 13:25:36.435685", "doctype": "DocType", "editable_grid": 1, @@ -44,7 +45,8 @@ "read_only": 1 } ], - "modified": "2019-10-03 16:42:35.932409", + "links": [], + "modified": "2021-09-20 16:53:37.217998", "modified_by": "Administrator", "module": "Desk", "name": "Tag Link", @@ -61,6 +63,17 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1, + "write": 1 } ], "read_only": 1, diff --git a/frappe/desk/form/utils.py b/frappe/desk/form/utils.py index a4dcee4ab3..bd04e1915c 100644 --- a/frappe/desk/form/utils.py +++ b/frappe/desk/form/utils.py @@ -66,7 +66,7 @@ def add_comment(reference_doctype, reference_name, content, comment_email, comme comment_type='Comment', comment_by=comment_by )) - doc.content = extract_images_from_html(doc, content) + doc.content = extract_images_from_html(doc, content, is_private=True) doc.insert(ignore_permissions=True) follow_document(doc.reference_doctype, doc.reference_name, frappe.session.user) diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index b3fccf84f9..076d672db5 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -141,7 +141,7 @@ class Leaderboard { } create_date_range_field() { - let timespan_field = $(this.parent).find(`.frappe-control[data-original-title=${__('Timespan')}]`); + let timespan_field = $(this.parent).find(`.frappe-control[data-original-title="${__('Timespan')}"]`); this.date_range_field = $(`
`).insertAfter(timespan_field).hide(); let date_field = frappe.ui.form.make_control({ diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index d758ebb590..c75d730b2f 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -121,7 +121,7 @@ def validate_filters(data, filters): def setup_group_by(data): '''Add columns for aggregated values e.g. count(name)''' - if data.group_by: + if data.group_by and data.aggregate_function: if data.aggregate_function.lower() not in ('count', 'sum', 'avg'): frappe.throw(_('Invalid aggregate function')) diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 8cfd75d839..e20f38c74a 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -226,7 +226,7 @@ }, { "default": "UNSEEN", - "depends_on": "eval: doc.enable_incoming", + "depends_on": "eval: doc.enable_incoming && doc.use_imap", "fieldname": "email_sync_option", "fieldtype": "Select", "hide_days": 1, @@ -236,7 +236,7 @@ }, { "default": "250", - "depends_on": "eval: doc.enable_incoming", + "depends_on": "eval: doc.enable_incoming && doc.use_imap", "description": "Total number of emails to sync in initial sync process ", "fieldname": "initial_sync_count", "fieldtype": "Select", @@ -567,7 +567,7 @@ "icon": "fa fa-inbox", "index_web_pages_for_search": 1, "links": [], - "modified": "2021-08-31 15:23:25.714366", + "modified": "2021-09-21 16:44:25.728637", "modified_by": "Administrator", "module": "Email", "name": "Email Account", @@ -589,4 +589,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/email/doctype/notification/notification.py b/frappe/email/doctype/notification/notification.py index f523d835e9..6b4ee92043 100644 --- a/frappe/email/doctype/notification/notification.py +++ b/frappe/email/doctype/notification/notification.py @@ -146,6 +146,7 @@ def get_context(context): if doc.meta.get_field(fieldname).fieldtype in frappe.model.numeric_fieldtypes: value = frappe.utils.cint(value) + doc.reload() doc.set(fieldname, value) doc.flags.updater_reference = { 'doctype': self.doctype, diff --git a/frappe/email/doctype/notification/test_notification.py b/frappe/email/doctype/notification/test_notification.py index 8e814e0245..f05d35be3e 100644 --- a/frappe/email/doctype/notification/test_notification.py +++ b/frappe/email/doctype/notification/test_notification.py @@ -20,6 +20,8 @@ class TestNotification(unittest.TestCase): notification.event = 'Value Change' notification.value_changed = 'status' notification.send_to_all_assignees = 1 + notification.set_property_after_alert = 'description' + notification.property_value = 'Changed by Notification' notification.save() if not frappe.db.exists('Notification', {'name': 'Contact Status Update'}, 'name'): @@ -237,6 +239,9 @@ class TestNotification(unittest.TestCase): self.assertTrue(email_queue) + # check if description is changed after alert since set_property_after_alert is set + self.assertEquals(todo.description, 'Changed by Notification') + recipients = [d.recipient for d in email_queue.recipients] self.assertTrue('test2@example.com' in recipients) self.assertTrue('test1@example.com' in recipients) @@ -269,4 +274,7 @@ class TestNotification(unittest.TestCase): self.assertTrue('test2@example.com' in recipients) self.assertTrue('test1@example.com' in recipients) - + @classmethod + def tearDownClass(cls): + frappe.delete_doc_if_exists("Notification", "ToDo Status Update") + frappe.delete_doc_if_exists("Notification", "Contact Status Update") \ No newline at end of file diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index 0be15e461b..05771a89d3 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -408,8 +408,9 @@ def sync_dependencies(document, producer_site): child_table = doc.get(df.fieldname) for entry in child_table: child_doc = producer_site.get_doc(entry.doctype, entry.name) - child_doc = frappe._dict(child_doc) - set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site) + if child_doc: + child_doc = frappe._dict(child_doc) + set_dependencies(child_doc, frappe.get_meta(entry.doctype).get_link_fields(), producer_site) def sync_link_dependencies(doc, link_fields, producer_site): set_dependencies(doc, link_fields, producer_site) diff --git a/frappe/handler.py b/frappe/handler.py index 352a9672bf..ea654517c3 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -223,7 +223,10 @@ def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): doc = frappe.get_doc(dt, dn) else: - doc = frappe.get_doc(json.loads(docs)) + if isinstance(docs, str): + docs = json.loads(docs) + + doc = frappe.get_doc(docs) doc._original_modified = doc.modified doc.check_if_latest() diff --git a/frappe/installer.py b/frappe/installer.py index f0bf0cb51c..e1b67ee10c 100755 --- a/frappe/installer.py +++ b/frappe/installer.py @@ -29,6 +29,10 @@ def _new_site( ): """Install a new Frappe site""" + from frappe.commands.scheduler import _is_scheduler_enabled + from frappe.utils import get_site_path, scheduler, touch_file + + if not force and os.path.exists(site): print("Site {0} already exists".format(site)) sys.exit(1) @@ -37,14 +41,11 @@ def _new_site( print("--no-mariadb-socket requires db_type to be set to mariadb.") sys.exit(1) - if not db_name: - import hashlib - db_name = "_" + hashlib.sha1(site.encode()).hexdigest()[:16] - frappe.init(site=site) - from frappe.commands.scheduler import _is_scheduler_enabled - from frappe.utils import get_site_path, scheduler, touch_file + if not db_name: + import hashlib + db_name = "_" + hashlib.sha1(os.path.realpath(frappe.get_site_path()).encode()).hexdigest()[:16] try: # enable scheduler post install? @@ -455,9 +456,21 @@ def convert_archive_content(sql_file_path): if frappe.conf.db_type == "mariadb": # ever since mariaDB 10.6, row_format COMPRESSED has been deprecated and removed # this step is added to ease restoring sites depending on older mariaDB servers - contents = open(sql_file_path).read() - with open(sql_file_path, "w") as f: - f.write(contents.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC")) + from frappe.utils import random_string + from pathlib import Path + + old_sql_file_path = Path(f"{sql_file_path}_{random_string(10)}") + sql_file_path = Path(sql_file_path) + + os.rename(sql_file_path, old_sql_file_path) + sql_file_path.unlink(missing_ok=True) + sql_file_path.touch() + + with open(old_sql_file_path) as r, open(sql_file_path, "a") as w: + for line in r: + w.write(line.replace("ROW_FORMAT=COMPRESSED", "ROW_FORMAT=DYNAMIC")) + + old_sql_file_path.unlink(missing_ok=True) def extract_sql_gzip(sql_gz_path): diff --git a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py index 9ae16a31f8..9bbab9db9b 100644 --- a/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py +++ b/frappe/integrations/doctype/razorpay_settings/razorpay_settings.py @@ -371,6 +371,7 @@ def capture_payment(is_sandbox=False, sanbox_response=None): doc = frappe.get_doc("Integration Request", doc.name) doc.status = "Failed" doc.error = frappe.get_traceback() + doc.save() frappe.log_error(doc.error, '{0} Failed'.format(doc.name)) diff --git a/frappe/model/__init__.py b/frappe/model/__init__.py index ce0a208d50..b460db29a7 100644 --- a/frappe/model/__init__.py +++ b/frappe/model/__init__.py @@ -41,6 +41,7 @@ data_fieldtypes = ( no_value_fields = ( 'Section Break', 'Column Break', + 'Tab Break', 'HTML', 'Table', 'Table MultiSelect', @@ -53,6 +54,7 @@ no_value_fields = ( display_fieldtypes = ( 'Section Break', 'Column Break', + 'Tab Break', 'HTML', 'Button', 'Image', diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 5605ac61ed..1826cca9a3 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -267,7 +267,12 @@ class BaseDocument(object): if isinstance(d[fieldname], list) and df.fieldtype not in table_fields: frappe.throw(_('Value for {0} cannot be a list').format(_(df.label))) - if convert_dates_to_str and isinstance(d[fieldname], (datetime.datetime, datetime.time, datetime.timedelta)): + if convert_dates_to_str and isinstance(d[fieldname], ( + datetime.datetime, + datetime.date, + datetime.time, + datetime.timedelta + )): d[fieldname] = str(d[fieldname]) if d[fieldname] == None and ignore_nulls: diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index fd74a8cfe4..978f3062c5 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -4,6 +4,7 @@ from typing import List import frappe.defaults +from frappe.query_builder.utils import Column import frappe.share from frappe import _ import frappe.permissions @@ -491,7 +492,7 @@ class DatabaseQuery(object): f.value = date_range fallback = "'0001-01-01 00:00:00'" - if f.operator in ('>', '<') and (f.fieldname in ('creation', 'modified')): + if (f.fieldname in ('creation', 'modified')): value = cstr(f.value) fallback = "NULL" @@ -547,8 +548,12 @@ class DatabaseQuery(object): value = flt(f.value) fallback = 0 + if isinstance(f.value, Column): + quote = '"' if frappe.conf.db_type == 'postgres' else "`" + value = f"{tname}.{quote}{f.value.name}{quote}" + # escape value - if isinstance(value, str) and not f.operator.lower() == 'between': + elif isinstance(value, str) and not f.operator.lower() == 'between': value = f"{frappe.db.escape(value, percent=False)}" if ( diff --git a/frappe/printing/doctype/network_printer_settings/__init__.py b/frappe/printing/doctype/network_printer_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.js b/frappe/printing/doctype/network_printer_settings/network_printer_settings.js new file mode 100644 index 0000000000..043afd388f --- /dev/null +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.js @@ -0,0 +1,29 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Network Printer Settings', { + onload (frm) { + frm.trigger("connect_print_server"); + }, + server_ip (frm) { + frm.trigger("connect_print_server"); + }, + port (frm) { + frm.trigger("connect_print_server"); + }, + connect_print_server (frm) { + if (frm.doc.server_ip && frm.doc.port) { + frappe.call({ + "doc": frm.doc, + "method": "get_printers_list", + "args": { + ip: frm.doc.server_ip, + port: frm.doc.port + }, + callback: function(data) { + frm.set_df_property('printer_name', 'options', [""].concat(data.message)); + } + }); + } + } +}); diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.json b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json new file mode 100644 index 0000000000..11f1382225 --- /dev/null +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.json @@ -0,0 +1,76 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2021-09-17 11:26:06.943999", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "server_ip", + "port", + "column_break_4", + "printer_name" + ], + "fields": [ + { + "default": "localhost", + "fieldname": "server_ip", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Server IP", + "reqd": 1 + }, + { + "default": "631", + "fieldname": "port", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Port", + "reqd": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "printer_name", + "fieldtype": "Select", + "label": "Printer Name", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-10-07 11:23:13.799402", + "modified_by": "Administrator", + "module": "Printing", + "name": "Network Printer Settings", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "All", + "share": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/printing/doctype/network_printer_settings/network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py new file mode 100644 index 0000000000..e42ed818c7 --- /dev/null +++ b/frappe/printing/doctype/network_printer_settings/network_printer_settings.py @@ -0,0 +1,37 @@ +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.document import Document +from frappe import _ + +class NetworkPrinterSettings(Document): + @frappe.whitelist() + def get_printers_list(self,ip="localhost",port=631): + printer_list = [] + try: + import cups + except ImportError: + frappe.throw(_('''This feature can not be used as dependencies are missing. + Please contact your system manager to enable this by installing pycups!''')) + return + try: + cups.setServer(self.server_ip) + cups.setPort(self.port) + conn = cups.Connection() + printers = conn.getPrinters() + for printer_id,printer in printers.items(): + printer_list.append({ + 'value': printer_id, + 'label': printer['printer-make-and-model'] + }) + + except RuntimeError: + frappe.throw(_("Failed to connect to server")) + except frappe.ValidationError: + frappe.throw(_("Failed to connect to server")) + return printer_list + +@frappe.whitelist() +def get_network_printer_settings(): + return frappe.db.get_list('Network Printer Settings', pluck='name') diff --git a/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py b/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py new file mode 100644 index 0000000000..86509b239f --- /dev/null +++ b/frappe/printing/doctype/network_printer_settings/test_network_printer_settings.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt + +# import frappe +import unittest + +class TestNetworkPrinterSettings(unittest.TestCase): + pass diff --git a/frappe/printing/doctype/print_settings/print_settings.js b/frappe/printing/doctype/print_settings/print_settings.js index 9616892a31..b1311166ee 100644 --- a/frappe/printing/doctype/print_settings/print_settings.js +++ b/frappe/printing/doctype/print_settings/print_settings.js @@ -15,27 +15,5 @@ frappe.ui.form.on('Print Settings', { }, onload: function(frm) { frm.script_manager.trigger("print_style"); - }, - server_ip: function(frm) { - frm.trigger("connect_print_server"); - }, - port:function(frm) { - frm.trigger("connect_print_server"); - }, - connect_print_server:function(frm) { - if(frm.doc.server_ip && frm.doc.port){ - frappe.call({ - "doc": frm.doc, - "method": "get_printers", - "args": { - ip: frm.doc.server_ip, - port: frm.doc.port - }, - callback: function(data) { - frm.set_df_property('printer_name', 'options', [""].concat(data.message)); - }, - error: (data) => frm.set_value("enable_print_server", 0) - }); - } } }); diff --git a/frappe/printing/doctype/print_settings/print_settings.json b/frappe/printing/doctype/print_settings/print_settings.json index 31962be050..babbae248d 100644 --- a/frappe/printing/doctype/print_settings/print_settings.json +++ b/frappe/printing/doctype/print_settings/print_settings.json @@ -19,9 +19,6 @@ "allow_print_for_cancelled", "server_printer", "enable_print_server", - "server_ip", - "printer_name", - "port", "raw_printing_section", "enable_raw_printing", "print_style_section", @@ -107,29 +104,11 @@ }, { "default": "0", + "depends_on": "enable_print_server", "fieldname": "enable_print_server", "fieldtype": "Check", - "label": "Enable Print Server" - }, - { - "default": "localhost", - "depends_on": "enable_print_server", - "fieldname": "server_ip", - "fieldtype": "Data", - "label": "Server IP" - }, - { - "depends_on": "enable_print_server", - "fieldname": "printer_name", - "fieldtype": "Select", - "label": "Printer Name" - }, - { - "default": "631", - "depends_on": "enable_print_server", - "fieldname": "port", - "fieldtype": "Int", - "label": "Port" + "label": "Enable Print Server", + "mandatory_depends_on": "enable_print_server" }, { "fieldname": "raw_printing_section", @@ -183,7 +162,7 @@ "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2021-02-15 14:16:18.474254", + "modified": "2021-09-17 12:59:14.783694", "modified_by": "Administrator", "module": "Printing", "name": "Print Settings", diff --git a/frappe/printing/doctype/print_settings/print_settings.py b/frappe/printing/doctype/print_settings/print_settings.py index a7e59c9078..ff00317cf8 100644 --- a/frappe/printing/doctype/print_settings/print_settings.py +++ b/frappe/printing/doctype/print_settings/print_settings.py @@ -12,26 +12,6 @@ class PrintSettings(Document): def on_update(self): frappe.clear_cache() - @frappe.whitelist() - def get_printers(self,ip="localhost",port=631): - printer_list = [] - try: - import cups - except ImportError: - frappe.throw(_("You need to install pycups to use this feature!")) - return - try: - cups.setServer(self.server_ip) - cups.setPort(self.port) - conn = cups.Connection() - printers = conn.getPrinters() - printer_list = printers.keys() - except RuntimeError: - frappe.throw(_("Failed to connect to server")) - except frappe.ValidationError: - frappe.throw(_("Failed to connect to server")) - return printer_list - @frappe.whitelist() def is_print_server_enabled(): if not hasattr(frappe.local, 'enable_print_server'): diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index da34dfda96..c1ab56b181 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -165,10 +165,7 @@ frappe.ui.form.PrintView = class { frappe.set_route('Form', 'Print Settings'); }); - if ( - frappe.model.get_doc(':Print Settings', 'Print Settings') - .enable_raw_printing == '1' - ) { + if (this.print_settings.enable_raw_printing == '1') { this.page.add_menu_item(__('Raw Printing Setting'), () => { this.printer_setting_dialog(); }); @@ -179,6 +176,12 @@ frappe.ui.form.PrintView = class { this.edit_print_format() ); } + + if (cint(this.print_settings.enable_print_server)) { + this.page.add_menu_item(__('Select Network Printer'), () => + this.network_printer_setting_dialog() + ); + } } show(frm) { @@ -460,72 +463,108 @@ frappe.ui.form.PrintView = class { printit() { let me = this; - frappe.call({ - method: - 'frappe.printing.doctype.print_settings.print_settings.is_print_server_enabled', - callback: function(data) { - if (data.message) { - frappe.call({ - method: 'frappe.utils.print_format.print_by_server', - args: { - doctype: me.frm.doc.doctype, - name: me.frm.doc.name, - print_format: me.selected_format(), - no_letterhead: me.with_letterhead(), - letterhead: this.get_letterhead(), - }, - callback: function() {}, - }); - } else if (me.get_mapped_printer().length === 1) { - // printer is already mapped in localstorage (applies for both raw and pdf ) - if (me.is_raw_printing()) { - me.get_raw_commands(function(out) { - frappe.ui.form - .qz_connect() - .then(function() { - let printer_map = me.get_mapped_printer()[0]; - let data = [out.raw_commands]; - let config = qz.configs.create(printer_map.printer); - return qz.print(config, data); - }) - .then(frappe.ui.form.qz_success) - .catch((err) => { - frappe.ui.form.qz_fail(err); - }); + + if (cint(me.print_settings.enable_print_server)) { + if (localStorage.getItem('network_printer')) { + me.print_by_server(); + } else { + me.network_printer_setting_dialog(() => me.print_by_server()); + } + } else if (me.get_mapped_printer().length === 1) { + // printer is already mapped in localstorage (applies for both raw and pdf ) + if (me.is_raw_printing()) { + me.get_raw_commands(function(out) { + frappe.ui.form + .qz_connect() + .then(function() { + let printer_map = me.get_mapped_printer()[0]; + let data = [out.raw_commands]; + let config = qz.configs.create(printer_map.printer); + return qz.print(config, data); + }) + .then(frappe.ui.form.qz_success) + .catch((err) => { + frappe.ui.form.qz_fail(err); }); - } else { - frappe.show_alert( + }); + } else { + frappe.show_alert( + { + message: __('PDF printing via "Raw Print" is not supported.'), + subtitle: __( + 'Please remove the printer mapping in Printer Settings and try again.' + ), + indicator: 'info', + }, + 14 + ); + //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing. + } + } else if (me.is_raw_printing()) { + // printer not mapped in localstorage and the current print format is raw printing + frappe.show_alert( + { + message: __('Printer mapping not set.'), + subtitle: __( + 'Please set a printer mapping for this print format in the Printer Settings' + ), + indicator: 'warning', + }, + 14 + ); + me.printer_setting_dialog(); + } else { + me.render_page('/printview?', true); + } + } + + print_by_server() { + let me = this; + if (localStorage.getItem('network_printer')) { + frappe.call({ + method: 'frappe.utils.print_format.print_by_server', + args: { + doctype: me.frm.doc.doctype, + name: me.frm.doc.name, + printer_setting: localStorage.getItem('network_printer'), + print_format: me.selected_format(), + no_letterhead: me.with_letterhead(), + letterhead: me.get_letterhead(), + }, + callback: function() {}, + }); + } + } + network_printer_setting_dialog(callback) { + frappe.call({ + method: 'frappe.printing.doctype.network_printer_settings.network_printer_settings.get_network_printer_settings', + callback: function(r) { + if (r.message) { + let d = new frappe.ui.Dialog({ + title: __('Select Network Printer'), + fields: [ { - message: __('PDF printing via "Raw Print" is not supported.'), - subtitle: __( - 'Please remove the printer mapping in Printer Settings and try again.' - ), - indicator: 'info', - }, - 14 - ); - //Note: need to solve "Error: Cannot parse (FILE) as a PDF file" to enable qz pdf printing. - } - } else if (me.is_raw_printing()) { - // printer not mapped in localstorage and the current print format is raw printing - frappe.show_alert( - { - message: __('Printer mapping not set.'), - subtitle: __( - 'Please set a printer mapping for this print format in the Printer Settings' - ), - indicator: 'warning', + "label": "Printer", + "fieldname": "printer", + "fieldtype": "Select", + "reqd": 1, + "options": r.message + } + ], + primary_action: function() { + localStorage.setItem('network_printer', d.get_values().printer); + if (typeof callback == "function") { + callback(); + } + d.hide(); }, - 14 - ); - me.printer_setting_dialog(); - } else { - me.render_page('/printview?', true); + primary_action_label: __('Select') + }); + d.show(); } }, }); } - render_page(method, printit = false) { let w = window.open( frappe.urllib.get_full_url( diff --git a/frappe/printing/page/print_format_builder/print_format_builder.js b/frappe/printing/page/print_format_builder/print_format_builder.js index ca2a8bc378..b73ff31d32 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder.js +++ b/frappe/printing/page/print_format_builder/print_format_builder.js @@ -261,7 +261,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { } else if(f.fieldtype==="Column Break") { set_column(); - } else if(!in_list(["Section Break", "Column Break", "Fold"], f.fieldtype) + } else if (!in_list(["Section Break", "Column Break", "Tab Break", "Fold"], f.fieldtype) && f.label) { if(!column) set_column(); @@ -298,7 +298,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { init_visible_columns(f) { f.visible_columns = [] $.each(frappe.get_meta(f.options).fields, function(i, _f) { - if(!in_list(["Section Break", "Column Break"], _f.fieldtype) && + if (!in_list(["Section Break", "Column Break", "Tab Break"], _f.fieldtype) && !_f.print_hide && f.label) { // column names set as fieldname|width @@ -606,7 +606,7 @@ frappe.PrintFormatBuilder = class PrintFormatBuilder { // add remaining fields $.each(doc_fields, function(j, f) { if (f && !in_list(column_names, f.fieldname) - && !in_list(["Section Break", "Column Break"], f.fieldtype) && f.label) { + && !in_list(["Section Break", "Column Break", "Tab Break"], f.fieldtype) && f.label) { fields.push(f); } }) diff --git a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html index 1ebb87ac31..c608eecbbd 100644 --- a/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html +++ b/frappe/printing/page/print_format_builder/print_format_builder_sidebar.html @@ -4,7 +4,7 @@