diff --git a/.codacy.yml b/.codacy.yml deleted file mode 100644 index 4754a63e7e..0000000000 --- a/.codacy.yml +++ /dev/null @@ -1,2 +0,0 @@ -exclude_paths: - - '**.sql' \ No newline at end of file diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index f5edb47a13..0000000000 --- a/.deepsource.toml +++ /dev/null @@ -1,17 +0,0 @@ -version = 1 - -test_patterns = [ - "**/test_*.py" -] - -exclude_patterns = [ - "frappe/patches/**", - "*.min.js" -] - -[[analyzers]] -name = "python" -enabled = true - - [analyzers.meta] - runtime_version = "3.x.x" diff --git a/.eslintignore b/.eslintignore index 2fd65d307d..baf9bb2cc5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,5 +5,4 @@ frappe/core/doctype/doctype/boilerplate/* frappe/core/doctype/report/boilerplate/* frappe/public/js/frappe/class.js frappe/templates/includes/* -frappe/tests/testcafe/* -frappe/www/website_script.js \ No newline at end of file +frappe/www/website_script.js diff --git a/.eslintrc b/.eslintrc index 69c731b079..a2538feab5 100644 --- a/.eslintrc +++ b/.eslintrc @@ -87,6 +87,7 @@ "open_url_post": true, "toTitle": true, "lstrip": true, + "rstrip": true, "strip": true, "strip_html": true, "replace_all": true, diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index e1f16970fe..5be3a87884 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -15,7 +15,7 @@ If your issue is not clear or does not meet the guidelines, then it will be clos ### General Issue Guidelines 1. **Search existing Issues:** Before raising a Issue, search if it has been raised before. Maybe add a 👍 or give additional help by creating a mockup if it is not already created. -2. **Report each issue separately:** Don't club multiple, unreleated issues in one note. +2. **Report each issue separately:** Don't club multiple, unrelated issues in one note. 3. **Brief:** Please don't include long explanations. Use screenshots and bullet points instead of descriptive paragraphs. ### Bug Report Guidelines diff --git a/.github/helper/translation.py b/.github/helper/translation.py index 340f4f8772..9146b3b32b 100644 --- a/.github/helper/translation.py +++ b/.github/helper/translation.py @@ -2,7 +2,7 @@ import re import sys errors_encounter = 0 -pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") +pattern = re.compile(r"_\(([\"']{,3})(?P((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P((?!\11).)*)\11)*)*\)") words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]") start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}") f_string_pattern = re.compile(r"_\(f[\"']") @@ -28,7 +28,7 @@ for _file in files_to_scan: has_f_string = f_string_pattern.search(line) if has_f_string: errors_encounter += 1 - print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}') + print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}') continue else: continue @@ -36,7 +36,7 @@ for _file in files_to_scan: match = pattern.search(line) error_found = False - if not match and line.endswith(',\n'): + if not match and line.endswith((',\n', '[\n')): # concat remaining text to validate multiline pattern line = "".join(file_lines[line_number - 1:]) line = line[start_matches.start() + 1:] @@ -44,11 +44,11 @@ for _file in files_to_scan: if not match: error_found = True - print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}') + print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}') if not error_found and not words_pattern.search(line): error_found = True - print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}') + print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}') if error_found: errors_encounter += 1 diff --git a/.gitignore b/.gitignore index 900ae1c7b7..766288fe2e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ locale *.swp *.egg-info dist/ -build/ +# build/ frappe/docs/current .vscode node_modules @@ -28,7 +28,7 @@ __pycache__/ # Distribution / packaging .Python -build/ +# build/ develop-eggs/ dist/ downloads/ diff --git a/.stylelintrc b/.stylelintrc new file mode 100644 index 0000000000..1e05d1fb41 --- /dev/null +++ b/.stylelintrc @@ -0,0 +1,9 @@ +{ + "extends": ["stylelint-config-recommended"], + "plugins": ["stylelint-scss"], + "rules": { + "at-rule-no-unknown": null, + "scss/at-rule-no-unknown": true, + "no-descending-specificity": null + } +} \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 63895675ea..53ad56a948 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,19 +31,18 @@ matrix: - name: "Python 3.7 MariaDB" python: 3.7 env: DB=mariadb TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --verbose --site test_site run-tests --coverage - name: "Python 3.7 PostgreSQL" python: 3.7 env: DB=postgres TYPE=server - script: bench --site test_site run-tests --coverage + script: bench --verbose --site test_site run-tests --coverage - name: "Cypress" python: 3.7 env: DB=mariadb TYPE=ui before_script: - bench --site test_site execute frappe.utils.install.complete_setup_wizard - - bench --site test_site_producer execute frappe.utils.install.complete_setup_wizard script: bench --site test_site run-ui-tests frappe --headless before_install: @@ -75,8 +74,10 @@ install: - mkdir ~/frappe-bench/sites/test_site - cp $TRAVIS_BUILD_DIR/.travis/consumer_db/$DB.json ~/frappe-bench/sites/test_site/site_config.json - - mkdir ~/frappe-bench/sites/test_site_producer - - cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json + - if [ $TYPE == "server" ]; then + mkdir ~/frappe-bench/sites/test_site_producer; + cp $TRAVIS_BUILD_DIR/.travis/producer_db/$DB.json ~/frappe-bench/sites/test_site_producer/site_config.json; + fi - if [ $DB == "mariadb" ];then mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"; @@ -104,11 +105,11 @@ install: - cd ./frappe-bench - - sed -i 's/watch:/# watch:/g' Procfile - - sed -i 's/schedule:/# schedule:/g' Procfile + - sed -i 's/^watch:/# watch:/g' Procfile + - sed -i 's/^schedule:/# schedule:/g' Procfile - - if [ $TYPE == "server" ]; then sed -i 's/socketio:/# socketio:/g' Procfile; fi - - if [ $TYPE == "server" ]; then sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile; fi + - if [ $TYPE == "server" ]; then sed -i 's/^socketio:/# socketio:/g' Procfile; fi + - if [ $TYPE == "server" ]; then sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; fi - if [ $TYPE == "ui" ]; then bench setup requirements --node; fi @@ -119,7 +120,7 @@ install: - bench start & - bench --site test_site reinstall --yes - - bench --site test_site_producer reinstall --yes + - if [ $TYPE == "server" ]; then bench --site test_site_producer reinstall --yes; fi - bench build --app frappe after_script: diff --git a/CODEOWNERS b/CODEOWNERS index 5753d85cfa..1afa3f72e3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -4,12 +4,12 @@ # the repo. Unless a later match takes precedence, * @frappe/frappe-review-team -website/ @scmmishra -web_form/ @scmmishra -templates/ @scmmishra -www/ @scmmishra +website/ @prssanna +web_form/ @prssanna +templates/ @surajshetty3416 +www/ @surajshetty3416 integrations/ @nextchamp-saqib -patches/ @sahil28297 +patches/ @surajshetty3416 dashboard/ @prssanna email/ @saurabh6790 event_streaming/ @ruchamahabal diff --git a/ci/fix-mariadb.sh b/ci/fix-mariadb.sh deleted file mode 100755 index 886ec5e0d0..0000000000 --- a/ci/fix-mariadb.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -# stolen from http://cgit.drupalcode.org/octopus/commit/?id=db4f837 -includedir=`mysql_config --variable=pkgincludedir` -thiscwd=`pwd` -_THIS_DB_VERSION=`mysql -V 2>&1 | tr -d "\n" | cut -d" " -f6 | awk '{ print $1}' | cut -d"-" -f1 | awk '{ print $1}' | sed "s/[\,']//g"` -if [ "$_THIS_DB_VERSION" = "5.5.40" ] && [ ! -e "$includedir-$_THIS_DB_VERSION-fixed.log" ] ; then - cd $includedir - sudo patch -p1 < $thiscwd/ci/my_config.h.patch &> /dev/null - sudo touch $includedir-$_THIS_DB_VERSION-fixed.log -fi diff --git a/ci/my_config.h.patch b/ci/my_config.h.patch deleted file mode 100644 index 5247b5b39b..0000000000 --- a/ci/my_config.h.patch +++ /dev/null @@ -1,22 +0,0 @@ -diff -burp a/my_config.h b/my_config.h ---- a/my_config.h 2014-10-09 19:32:46.000000000 -0400 -+++ b/my_config.h 2014-10-09 19:35:12.000000000 -0400 -@@ -641,17 +641,4 @@ - #define SIZEOF_TIME_T 8 - /* #undef TIME_T_UNSIGNED */ - --/* -- stat structure (from ) is conditionally defined -- to have different layout and size depending on the defined macros. -- The correct macro is defined in my_config.h, which means it MUST be -- included first (or at least before - so, practically, -- before including any system headers). -- -- __GLIBC__ is defined in --*/ --#ifdef __GLIBC__ --#error MUST be included first! --#endif -- - #endif - diff --git a/cypress.json b/cypress.json index 97ac41bb61..f2508ca66e 100644 --- a/cypress.json +++ b/cypress.json @@ -3,5 +3,9 @@ "projectId": "92odwv", "adminPassword": "admin", "defaultCommandTimeout": 20000, - "pageLoadTimeout": 15000 + "pageLoadTimeout": 15000, + "retries": { + "runMode": 2, + "openMode": 2 + } } diff --git a/cypress/integration/api.js b/cypress/integration/api.js index 767cfbb55e..7a5b1611b0 100644 --- a/cypress/integration/api.js +++ b/cypress/integration/api.js @@ -2,7 +2,7 @@ context('API Resources', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); it('Creates two Comments', () => { diff --git a/cypress/integration/awesome_bar.js b/cypress/integration/awesome_bar.js index 55c8015bae..3e12101532 100644 --- a/cypress/integration/awesome_bar.js +++ b/cypress/integration/awesome_bar.js @@ -2,7 +2,7 @@ context('Awesome Bar', () => { before(() => { cy.visit('/login'); cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); beforeEach(() => { diff --git a/cypress/integration/control_barcode.js b/cypress/integration/control_barcode.js index 674d825504..1df5e64f0e 100644 --- a/cypress/integration/control_barcode.js +++ b/cypress/integration/control_barcode.js @@ -1,7 +1,7 @@ context('Control Barcode', () => { beforeEach(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function get_dialog_with_barcode() { diff --git a/cypress/integration/control_duration.js b/cypress/integration/control_duration.js index 4d974018d3..266d421e70 100644 --- a/cypress/integration/control_duration.js +++ b/cypress/integration/control_duration.js @@ -1,7 +1,7 @@ context('Control Duration', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function get_dialog_with_duration(hide_days = 0, hide_seconds = 0) { diff --git a/cypress/integration/control_link.js b/cypress/integration/control_link.js index 702c8430d6..8f9257e9c4 100644 --- a/cypress/integration/control_link.js +++ b/cypress/integration/control_link.js @@ -1,11 +1,11 @@ context('Control Link', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); beforeEach(() => { - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.create_records({ doctype: 'ToDo', description: 'this is a test todo for link' @@ -29,8 +29,7 @@ context('Control Link', () => { it('should set the valid value', () => { get_dialog_with_link().as('dialog'); - cy.server(); - cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); + cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); cy.get('.frappe-control[data-fieldname=link] input').focus().as('input'); cy.wait('@search_link'); @@ -50,8 +49,7 @@ context('Control Link', () => { it('should unset invalid value', () => { get_dialog_with_link().as('dialog'); - cy.server(); - cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); cy.get('.frappe-control[data-fieldname=link] input') .type('invalid value', { delay: 100 }) @@ -63,9 +61,8 @@ context('Control Link', () => { it('should route to form on arrow click', () => { get_dialog_with_link().as('dialog'); - cy.server(); - cy.route('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); - cy.route('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); + cy.intercept('GET', '/api/method/frappe.desk.form.utils.validate_link*').as('validate_link'); + cy.intercept('POST', '/api/method/frappe.desk.search.search_link').as('search_link'); cy.get('@todos').then(todos => { cy.get('.frappe-control[data-fieldname=link] input').as('input'); diff --git a/cypress/integration/control_rating.js b/cypress/integration/control_rating.js index 02d8e0639e..31c036d240 100644 --- a/cypress/integration/control_rating.js +++ b/cypress/integration/control_rating.js @@ -1,7 +1,7 @@ context('Control Rating', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); function get_dialog_with_rating() { diff --git a/cypress/integration/datetime.js b/cypress/integration/datetime.js index 74d823a81f..b310526c7c 100644 --- a/cypress/integration/datetime.js +++ b/cypress/integration/datetime.js @@ -4,7 +4,7 @@ const doctype_name = datetime_doctype.name; context('Control Date, Time and DateTime', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.insert_doc('DocType', datetime_doctype, true); }); @@ -42,7 +42,7 @@ context('Control Date, Time and DateTime', () => { .should('be.visible'); cy.get( '.datepickers-container .datepicker.active .datepicker--cell-day.-current-' - ).click(); + ).click({ force: true }); cy.window() .its('cur_frm') diff --git a/cypress/integration/depends_on.js b/cypress/integration/depends_on.js index 7031634d98..d33babb134 100644 --- a/cypress/integration/depends_on.js +++ b/cypress/integration/depends_on.js @@ -1,9 +1,33 @@ context('Depends On', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { - return frappe.call('frappe.tests.ui_test_helpers.create_doctype', { + return frappe.xcall('frappe.tests.ui_test_helpers.create_child_doctype', { + name: 'Child Test Depends On', + fields: [ + { + "label": "Child Test Field", + "fieldname": "child_test_field", + "fieldtype": "Data", + "in_list_view": 1, + }, + { + "label": "Child Dependant Field", + "fieldname": "child_dependant_field", + "fieldtype": "Data", + "in_list_view": 1, + }, + { + "label": "Child Display Dependant Field", + "fieldname": "child_display_dependant_field", + "fieldtype": "Data", + "in_list_view": 1, + }, + ] + }); + }).then(frappe => { + return frappe.xcall('frappe.tests.ui_test_helpers.create_doctype', { name: 'Test Depends On', fields: [ { @@ -24,6 +48,13 @@ context('Depends On', () => { "fieldtype": "Data", 'depends_on': "eval:doc.test_field=='Value'" }, + { + "label": "Child Test Depends On Field", + "fieldname": "child_test_depends_on_field", + "fieldtype": "Table", + 'read_only_depends_on': "eval:doc.test_field=='Some Other Value'", + 'options': "Child Test Depends On" + }, ] }); }); @@ -48,6 +79,30 @@ context('Depends On', () => { cy.get('body').click(); cy.get('.control-input [data-fieldname="dependant_field"]').should('not.be.disabled'); }); + it('should set the table and its fields as read only depending on other fields value', () => { + cy.new_form('Test Depends On'); + cy.fill_field('dependant_field', 'Some Value'); + //cy.fill_field('test_field', 'Some Other Value'); + cy.get('.frappe-control[data-fieldname="child_test_depends_on_field"]').as('table'); + cy.get('@table').find('button.grid-add-row').click(); + cy.get('@table').find('[data-idx="1"]').as('row1'); + cy.get('@row1').find('.btn-open-row').click(); + cy.get('@row1').find('.form-in-grid').as('row1-form_in_grid'); + //cy.get('@row1-form_in_grid').find('') + cy.fill_table_field('child_test_depends_on_field', '1', 'child_test_field', 'Some Value'); + cy.fill_table_field('child_test_depends_on_field', '1', 'child_dependant_field', 'Some Other Value'); + + cy.get('@row1-form_in_grid').find('.grid-collapse-row').click(); + + // set the table to read-only + cy.fill_field('test_field', 'Some Other Value'); + + // grid row form fields should be read-only + cy.get('@row1').find('.btn-open-row').click(); + + cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_test_field"]').should('be.disabled'); + cy.get('@row1-form_in_grid').find('.control-input [data-fieldname="child_dependant_field"]').should('be.disabled'); + }); it('should display the field depending on other fields value', () => { cy.new_form('Test Depends On'); cy.get('.control-input [data-fieldname="display_dependant_field"]').should('not.be.visible'); diff --git a/cypress/integration/file_uploader.js b/cypress/integration/file_uploader.js index 5ef041b797..2f457983de 100644 --- a/cypress/integration/file_uploader.js +++ b/cypress/integration/file_uploader.js @@ -1,7 +1,7 @@ context('FileUploader', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app'); }); function open_upload_dialog() { @@ -14,40 +14,32 @@ context('FileUploader', () => { open_upload_dialog(); cy.get_open_dialog().should('contain', 'Drag and drop files'); cy.hide_dialog(); - cy.get('body').click(); }); it('should accept dropped files', () => { open_upload_dialog(); - cy.fixture('example.json').then(fileContent => { - cy.get_open_dialog().find('.file-upload-area').upload({ - fileContent, - fileName: 'example.json', - mimeType: 'application/json' - }, { - subjectType: 'drag-n-drop', - force: true - }); - cy.get_open_dialog().find('.file-name').should('contain', 'example.json'); - cy.server(); - cy.route('POST', '/api/method/upload_file').as('upload_file'); - cy.get_open_dialog().find('.btn-modal-primary').click(); - cy.wait('@upload_file').its('status').should('be', 200); - cy.get('.modal:visible').should('not.exist'); + cy.get_open_dialog().find('.file-upload-area').attachFile('example.json', { + subjectType: 'drag-n-drop', }); + + cy.get_open_dialog().find('.file-name').should('contain', 'example.json'); + cy.intercept('POST', '/api/method/upload_file').as('upload_file'); + cy.get_open_dialog().find('.btn-modal-primary').click(); + cy.wait('@upload_file').its('response.statusCode').should('eq', 200); + cy.get('.modal:visible').should('not.exist'); }); it('should accept uploaded files', () => { open_upload_dialog(); cy.get_open_dialog().find('.btn-file-upload div:contains("Library")').click(); + cy.get('.file-filter').type('example.json'); cy.get_open_dialog().find('.tree-label:contains("example.json")').first().click(); - cy.server(); - cy.route('POST', '/api/method/upload_file').as('upload_file'); + cy.intercept('POST', '/api/method/upload_file').as('upload_file'); cy.get_open_dialog().find('.btn-primary').click(); cy.wait('@upload_file').its('response.body.message') - .should('have.property', 'file_url', '/private/files/example.json'); + .should('have.property', 'file_name', 'example.json'); cy.get('.modal:visible').should('not.exist'); }); @@ -56,8 +48,7 @@ context('FileUploader', () => { cy.get_open_dialog().find('.btn-file-upload div:contains("Link")').click(); cy.get_open_dialog().find('.file-web-link input').type('https://github.com', { delay: 100, force: true }); - cy.server(); - cy.route('POST', '/api/method/upload_file').as('upload_file'); + cy.intercept('POST', '/api/method/upload_file').as('upload_file'); cy.get_open_dialog().find('.btn-primary').click(); cy.wait('@upload_file').its('response.body.message') .should('have.property', 'file_url', 'https://github.com'); diff --git a/cypress/integration/form.js b/cypress/integration/form.js index f574770520..9c63fe4e8b 100644 --- a/cypress/integration/form.js +++ b/cypress/integration/form.js @@ -1,7 +1,7 @@ context('Form', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.call("frappe.tests.ui_test_helpers.create_contact_records"); }); @@ -11,13 +11,12 @@ context('Form', () => { cy.fill_field('description', 'this is a test todo', 'Text Editor').blur(); cy.wait(300); cy.get('.page-title').should('contain', 'Not Saved'); - cy.server(); - cy.route({ + cy.intercept({ method: 'POST', url: 'api/method/frappe.desk.form.save.savedocs' }).as('form_save'); cy.get('.primary-action').click(); - cy.wait('@form_save').its('status').should('eq', 200); + cy.wait('@form_save').its('response.statusCode').should('eq', 200); cy.visit('/app/todo'); cy.get('.title-text').should('be.visible').and('contain', 'To Do'); cy.get('.list-row').should('contain', 'this is a test todo'); @@ -30,21 +29,20 @@ context('Form', () => { cy.visit('/app/contact/Test Form Contact 3'); cy.get('.prev-doc').should('be.visible').click(); cy.get('.msgprint-dialog .modal-body').contains('No further records').should('be.visible'); - cy.get('.btn-modal-close:visible').click(); + cy.hide_dialog(); cy.get('.next-doc').click(); cy.wait(200); + cy.hide_dialog(); cy.contains('Test Form Contact 2').should('not.exist'); - cy.get('.title-text').should('contain', 'Test Form Contact 1'); + cy.get('.title-text').should('contain', 'Test Form Contact 3'); // clear filters - cy.window().its('frappe').then((frappe) => { - let list_view = frappe.get_list_view('Contact'); - list_view.filter_area.filter_list.clear_filters(); - }); + cy.visit('/app/contact'); + cy.clear_filters(); }); it('validates behaviour of Data options validations in child table', () => { // test email validations for set_invalid controller let website_input = 'website.in'; - let expectBackgroundColor = 'rgb(255, 220, 220)'; + let expectBackgroundColor = 'rgb(255, 245, 245)'; cy.visit('/app/contact/new'); cy.get('.frappe-control[data-fieldname="email_ids"]').as('table'); diff --git a/cypress/integration/grid_pagination.js b/cypress/integration/grid_pagination.js index 87c0fb0af4..8f6b79c1f4 100644 --- a/cypress/integration/grid_pagination.js +++ b/cypress/integration/grid_pagination.js @@ -1,11 +1,11 @@ context('Grid Pagination', () => { beforeEach(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.call("frappe.tests.ui_test_helpers.create_contact_phone_nos_records"); }); diff --git a/cypress/integration/list_view.js b/cypress/integration/list_view.js index a1ba1cf25e..633d1335ab 100644 --- a/cypress/integration/list_view.js +++ b/cypress/integration/list_view.js @@ -1,7 +1,7 @@ context('List View', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); return cy.window().its('frappe').then(frappe => { return frappe.xcall("frappe.tests.ui_test_helpers.setup_workflow"); }); @@ -11,20 +11,21 @@ context('List View', () => { cy.go_to_list('ToDo'); cy.get('.list-row-container:contains("Pending") .list-row-checkbox').click({ multiple: true, force: true }); cy.get('.actions-btn-group button').contains('Actions').should('be.visible').click(); - cy.get('.dropdown-menu li:visible').should('have.length', 8).each((el, index) => { + cy.get('.dropdown-menu li:visible .dropdown-item').should('have.length', 8).each((el, index) => { cy.wrap(el).contains(actions[index]); }).then((elements) => { - cy.server(); - cy.route({ + cy.intercept({ method: 'POST', url: 'api/method/frappe.model.workflow.bulk_workflow_approval' }).as('bulk-approval'); - cy.route({ + cy.intercept({ method: 'POST', url: 'api/method/frappe.desk.reportview.get' }).as('real-time-update'); cy.wrap(elements).contains('Approve').click(); cy.wait(['@bulk-approval', '@real-time-update']); + cy.hide_dialog(); + cy.clear_filters(); cy.get('.list-row-container:visible').should('contain', 'Approved'); }); }); diff --git a/cypress/integration/list_view_settings.js b/cypress/integration/list_view_settings.js index cdb1c5d778..52512b911e 100644 --- a/cypress/integration/list_view_settings.js +++ b/cypress/integration/list_view_settings.js @@ -1,7 +1,7 @@ context('List View Settings', () => { beforeEach(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); it('Default settings', () => { cy.visit('/app/List/DocType/List'); @@ -14,8 +14,8 @@ context('List View Settings', () => { cy.wait(300); cy.get('.list-count').should('contain', "20 of"); cy.get('.menu-btn-group button').click(); - cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click(); - cy.get('.modal-dialog').should('contain', 'Settings'); + cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click(); + cy.get('.modal-dialog').should('contain', 'DocType Settings'); cy.get('input[data-fieldname="disable_count"]').check({ force: true }); cy.get('input[data-fieldname="disable_sidebar_stats"]').check({ force: true }); @@ -27,8 +27,8 @@ context('List View Settings', () => { cy.get('.list-sidebar .list-tags').should('not.exist'); cy.get('.menu-btn-group button').click({ force: true }); - cy.get('.dropdown-menu li').filter(':visible').contains('Settings').click(); - cy.get('.modal-dialog').should('contain', 'Settings'); + cy.get('.dropdown-menu li').filter(':visible').contains('List Settings').click(); + cy.get('.modal-dialog').should('contain', 'DocType Settings'); cy.get('input[data-fieldname="disable_count"]').uncheck({ force: true }); cy.get('input[data-fieldname="disable_sidebar_stats"]').uncheck({ force: true }); cy.get('button').filter(':visible').contains('Save').click(); diff --git a/cypress/integration/login.js b/cypress/integration/login.js index 4eb8933771..6b109dd18d 100644 --- a/cypress/integration/login.js +++ b/cypress/integration/login.js @@ -2,7 +2,7 @@ context('Login', () => { beforeEach(() => { cy.request('/api/method/logout'); cy.visit('/login'); - cy.location().should('be', '/login'); + cy.location('pathname').should('eq', '/login'); }); it('greets with login screen', () => { @@ -35,7 +35,7 @@ context('Login', () => { cy.get('#login_password').type(Cypress.config('adminPassword')); cy.get('.btn-login:visible').click(); - cy.location('pathname').should('eq', '/app/space/Home'); + cy.location('pathname').should('eq', '/app'); cy.window().its('frappe.session.user').should('eq', 'Administrator'); }); diff --git a/cypress/integration/query_report.js b/cypress/integration/query_report.js index d0ca844362..e2a1c3fc79 100644 --- a/cypress/integration/query_report.js +++ b/cypress/integration/query_report.js @@ -1,7 +1,7 @@ context('Query Report', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); }); it('add custom column in report', () => { diff --git a/cypress/integration/recorder.js b/cypress/integration/recorder.js index 2c71dbe64d..7236200741 100644 --- a/cypress/integration/recorder.js +++ b/cypress/integration/recorder.js @@ -4,73 +4,69 @@ context('Recorder', () => { }); it('Navigate to Recorder', () => { - cy.visit('/app/space/Website'); + cy.visit('/app'); cy.awesomebar('recorder'); - cy.get('h1').should('contain', 'Recorder'); - cy.location('pathname').should('eq', '#recorder'); + cy.get('h3').should('contain', 'Recorder'); + cy.url().should('include', '/recorder/detail'); }); - // it('Recorder Empty State', () => { - // cy.visit('/app/recorder'); - // cy.get('.title-text').should('contain', 'Recorder'); + it('Recorder Empty State', () => { + cy.visit('/app/recorder'); + cy.get('.title-text').should('contain', 'Recorder'); - // cy.get('.indicator').should('contain', 'Inactive').should('have.class', 'red'); + cy.get('.indicator-pill').should('contain', 'Inactive').should('have.class', 'red'); - // cy.get('.primary-action').should('contain', 'Start'); - // cy.get('.btn-secondary').should('contain', 'Clear'); + cy.get('.primary-action').should('contain', 'Start'); + cy.get('.btn-secondary').should('contain', 'Clear'); - // cy.get('.msg-box').should('contain', 'Inactive'); - // cy.get('.msg-box .btn-primary').should('contain', 'Start Recording'); - // }); + cy.get('.msg-box').should('contain', 'Inactive'); + cy.get('.msg-box .btn-primary').should('contain', 'Start Recording'); + }); - // it('Recorder Start', () => { - // cy.visit('/app/recorder'); - // cy.get('.primary-action').should('contain', 'Start').click(); - // cy.get('.indicator').should('contain', 'Active').should('have.class', 'green'); + it('Recorder Start', () => { + cy.visit('/app/recorder'); + cy.get('.primary-action').should('contain', 'Start').click(); + cy.get('.indicator-pill').should('contain', 'Active').should('have.class', 'green'); - // cy.get('.msg-box').should('contain', 'No Requests'); + cy.get('.msg-box').should('contain', 'No Requests'); - // cy.server(); - // cy.visit('/app/List/DocType/List'); - // cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); - // cy.wait('@list_refresh'); + cy.visit('/app/List/DocType/List'); + cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); + cy.wait('@list_refresh'); - // cy.get('.title-text').should('contain', 'DocType'); - // cy.get('.list-count').should('contain', '20 of '); + cy.get('.title-text').should('contain', 'DocType'); + cy.get('.list-count').should('contain', '20 of '); - // cy.visit('/app/recorder'); - // cy.get('.title-text').should('contain', 'Recorder'); - // cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get'); + cy.visit('/app/recorder'); + cy.get('.title-text').should('contain', 'Recorder'); + cy.get('.result-list').should('contain', '/api/method/frappe.desk.reportview.get'); - // cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); - // cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); - // cy.get('.msg-box').should('contain', 'Inactive'); - // }); + cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); + cy.wait(500); + cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); + cy.get('.msg-box').should('contain', 'Inactive'); + }); - // it('Recorder View Request', () => { - // cy.visit('/app/recorder'); - // cy.get('.primary-action').should('contain', 'Start').click(); + it('Recorder View Request', () => { + cy.visit('/app/recorder'); + cy.get('.primary-action').should('contain', 'Start').click(); - // cy.server(); - // cy.visit('/app/List/DocType/List'); - // cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); - // cy.wait('@list_refresh'); + cy.visit('/app/List/DocType/List'); + cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); + cy.wait('@list_refresh'); - // cy.get('.title-text').should('contain', 'DocType'); - // cy.get('.list-count').should('contain', '20 of '); + cy.get('.title-text').should('contain', 'DocType'); + cy.get('.list-count').should('contain', '20 of '); - // temporarily commenting out theses tests as they seem to be - // randomly failing maybe due a backround event + cy.visit('/app/recorder'); - // cy.visit('/app/recorder'); + cy.get('.list-row-container span').contains('/api/method/frappe').click(); - // cy.get('.list-row-container span').contains('/api/method/frappe').click(); + cy.url().should('include', '/recorder/request'); + cy.get('form').should('contain', '/api/method/frappe'); - // cy.location('hash').should('contain', '#recorder/request/'); - // cy.get('form').should('contain', '/api/method/frappe'); - - // cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); - // cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); - // cy.location('hash').should('eq', '#recorder'); - // }); + cy.get('#page-recorder .primary-action').should('contain', 'Stop').click(); + cy.wait(200); + cy.get('#page-recorder .btn-secondary').should('contain', 'Clear').click(); + }); }); \ No newline at end of file diff --git a/cypress/integration/relative_time_filters.js b/cypress/integration/relative_time_filters.js index 33ea49f2d2..80e6387d99 100644 --- a/cypress/integration/relative_time_filters.js +++ b/cypress/integration/relative_time_filters.js @@ -4,50 +4,43 @@ context('Relative Timeframe', () => { }); before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.window().its('frappe').then(frappe => { frappe.call("frappe.tests.ui_test_helpers.create_todo_records"); }); }); it('sets relative timespan filter for last week and filters list', () => { cy.visit('/app/List/ToDo/List'); + cy.clear_filters(); cy.get('.list-row:contains("this is fourth todo")').should('exist'); cy.add_filter(); - // cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); cy.get('.fieldname-select-area').should('exist'); cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); cy.get('select.condition.form-control').select("Timespan"); cy.get('.filter-field select.input-with-feedback.form-control').select("last week"); - cy.server(); - cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); - // cy.get('.filter-box .btn:contains("Apply")').click(); + cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); cy.get('.filter-popover .apply-filters').click({ force: true }); cy.wait('@list_refresh'); cy.get('.list-row-container').its('length').should('eq', 1); cy.get('.list-row-container').should('contain', 'this is second todo'); - cy.route('POST', '/api/method/frappe.model.utils.user_settings.save') + cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save') .as('save_user_settings'); cy.clear_filters(); cy.wait('@save_user_settings'); }); it('sets relative timespan filter for next week and filters list', () => { cy.visit('/app/List/ToDo/List'); + cy.clear_filters(); cy.get('.list-row:contains("this is fourth todo")').should('exist'); - // cy.get('.tag-filters-area .btn:contains("Add Filter")').click(); cy.add_filter(); cy.get('.fieldname-select-area input').type("Due Date{enter}", { delay: 100 }); cy.get('select.condition.form-control').select("Timespan"); cy.get('.filter-field select.input-with-feedback.form-control').select("next week"); - cy.server(); - cy.route('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); - // cy.get('.filter-box .btn:contains("Apply")').click(); + cy.intercept('POST', '/api/method/frappe.desk.reportview.get').as('list_refresh'); cy.get('.filter-popover .apply-filters').click({ force: true }); cy.wait('@list_refresh'); - // cy.get('.list-row-container').its('length').should('eq', 1); - // cy.get('.list-row').should('contain', 'this is first todo'); - cy.route('POST', '/api/method/frappe.model.utils.user_settings.save') + cy.intercept('POST', '/api/method/frappe.model.utils.user_settings.save') .as('save_user_settings'); - // cy.get('.remove-filter').click(); cy.clear_filters(); cy.wait('@save_user_settings'); }); diff --git a/cypress/integration/report_view.js b/cypress/integration/report_view.js index 00816a4fa6..ea76246ae2 100644 --- a/cypress/integration/report_view.js +++ b/cypress/integration/report_view.js @@ -4,7 +4,7 @@ const doctype_name = custom_submittable_doctype.name; context('Report View', () => { before(() => { cy.login(); - cy.visit('/app/space/Website'); + cy.visit('/app/website'); cy.insert_doc('DocType', custom_submittable_doctype, true); cy.clear_cache(); cy.insert_doc(doctype_name, { @@ -16,8 +16,7 @@ context('Report View', () => { }, true).as('doc'); }); it('Field with enabled allow_on_submit should be editable.', () => { - cy.server(); - cy.route('POST', 'api/method/frappe.client.set_value').as('value-update'); + cy.intercept('POST', 'api/method/frappe.client.set_value').as('value-update'); cy.visit(`/app/List/${doctype_name}/Report`); // check status column added from docstatus cy.get('.dt-row-0 > .dt-cell--col-3').should('contain', 'Submitted'); diff --git a/cypress/integration/table_multiselect.js b/cypress/integration/table_multiselect.js index be3f52f971..8b83a0d914 100644 --- a/cypress/integration/table_multiselect.js +++ b/cypress/integration/table_multiselect.js @@ -17,11 +17,10 @@ context('Table MultiSelect', () => { .as('selected-value'); cy.get('@selected-value').should('contain', 'test@erpnext.com'); - cy.server(); - cy.route('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form'); + cy.intercept('POST', '/api/method/frappe.desk.form.save.savedocs').as('save_form'); // trigger save cy.get('.primary-action').click(); - cy.wait('@save_form').its('status').should('eq', 200); + cy.wait('@save_form').its('response.statusCode').should('eq', 200); cy.get('@selected-value').should('contain', 'test@erpnext.com'); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index f95bbeeeb5..7f0afdf035 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -160,7 +160,7 @@ Cypress.Commands.add('remove_doc', (doctype, name) => { Cypress.Commands.add('create_records', doc => { return cy - .call('frappe.tests.ui_test_helpers.create_if_not_exists', { doc }) + .call('frappe.tests.ui_test_helpers.create_if_not_exists', {doc}) .then(r => r.message); }); @@ -186,7 +186,7 @@ Cypress.Commands.add('fill_field', (fieldname, value, fieldtype = 'Data') => { if (fieldtype === 'Select') { cy.get('@input').select(value); } else { - cy.get('@input').type(value, { waitForAnimations: false, force: true }); + cy.get('@input').type(value, {waitForAnimations: false, force: true}); } return cy.get('@input'); }); @@ -204,15 +204,47 @@ Cypress.Commands.add('get_field', (fieldname, fieldtype = 'Data') => { return cy.get(selector); }); +Cypress.Commands.add('fill_table_field', (tablefieldname, row_idx, fieldname, value, fieldtype = 'Data') => { + cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as('input'); + + if (['Date', 'Time', 'Datetime'].includes(fieldtype)) { + cy.get('@input').click().wait(200); + cy.get('.datepickers-container .datepicker.active').should('exist'); + } + if (fieldtype === 'Time') { + cy.get('@input').clear().wait(200); + } + + if (fieldtype === 'Select') { + cy.get('@input').select(value); + } else { + cy.get('@input').type(value, {waitForAnimations: false, force: true}); + } + return cy.get('@input'); +}); + +Cypress.Commands.add('get_table_field', (tablefieldname, row_idx, fieldname, fieldtype = 'Data') => { + let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; + selector += ` [data-idx="${row_idx}"]`; + selector += ` .form-in-grid`; + + if (fieldtype === 'Text Editor') { + selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } else if (fieldtype === 'Code') { + selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; + } else { + selector += ` .form-control[data-fieldname="${fieldname}"]`; + } + + return cy.get(selector); +}); + Cypress.Commands.add('awesomebar', text => { - cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, { delay: 100 }); + cy.get('#navbar-search').type(`${text}{downarrow}{enter}`, {delay: 100}); }); Cypress.Commands.add('new_form', doctype => { - let dt_in_route = doctype.toLowerCase().replace(/ /g, '-') - // // let route = `${dt_in_route}/new-${dt_in_route}-1`; - // let route = `${dt_in_route}/new`; - // let route = `${doctype.toLowerCase().replace(' ', '-')}/new`; + let dt_in_route = doctype.toLowerCase().replace(/ /g, '-'); cy.visit(`/app/${dt_in_route}/new`); cy.get('body').should('have.attr', 'data-route', `Form/${doctype}/new-${dt_in_route}-1`); cy.get('body').should('have.attr', 'data-ajax-state', 'complete'); @@ -244,8 +276,7 @@ Cypress.Commands.add('get_open_dialog', () => { Cypress.Commands.add('hide_dialog', () => { cy.wait(200); - cy.get_open_dialog() - .find('.btn-modal-close').click() + cy.get_open_dialog().find('.btn-modal-close').click(); cy.get('.modal:visible').should('not.exist'); }); @@ -289,4 +320,8 @@ Cypress.Commands.add('clear_filters', () => { cy.wait(300); cy.get('.filter-popover').should('exist'); cy.get('.filter-popover').find('.clear-filters').click(); + cy.get('.filter-section .filter-button').click(); + cy.window().its('cur_list').then(cur_list => { + cur_list && cur_list.filter_area && cur_list.filter_area.clear(); + }); }); diff --git a/cypress/support/index.js b/cypress/support/index.js index 8035ef117b..1bee72d2ca 100644 --- a/cypress/support/index.js +++ b/cypress/support/index.js @@ -21,5 +21,5 @@ import './commands'; // require('./commands') Cypress.Cookies.defaults({ - whitelist: 'sid' + preserve: 'sid' }); \ No newline at end of file diff --git a/frappe/__init__.py b/frappe/__init__.py index 4cde8bfe0e..9b3ffc4662 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -1,8 +1,14 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # MIT License. See license.txt """ -globals attached to frappe module -+ some utility functions that should probably be moved +Frappe - Low Code Open Source Framework in Python and JS + +Frappe, pronounced fra-pay, is a full stack, batteries-included, web +framework written in Python and Javascript with MariaDB as the database. +It is the framework which powers ERPNext. It is pretty generic and can +be used to build database driven apps. + +Read the documentation: https://frappeframework.com/docs """ from __future__ import unicode_literals, print_function @@ -27,6 +33,7 @@ __version__ = '13.0.0-dev' __title__ = "Frappe Framework" local = Local() +controllers = {} class _dict(dict): """dict like object that exposes keys as attributes""" @@ -327,7 +334,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, :param is_minimizable: [optional] Allow users to minimize the modal :param wide: [optional] Show wide modal """ - from frappe.utils import encode + from frappe.utils import strip_html_tags msg = safe_decode(msg) out = _dict(message=msg) @@ -354,7 +361,7 @@ def msgprint(msg, title=None, raise_exception=0, as_table=False, as_list=False, out.as_list = 1 if flags.print_messages and out.message: - print(f"Message: {repr(out.message).encode('utf-8')}") + print(f"Message: {strip_html_tags(out.message)}") if title: out.title = title @@ -465,7 +472,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message attachments=None, content=None, doctype=None, name=None, reply_to=None, cc=[], bcc=[], message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, retry=1, now=None, read_receipt=None, is_notification=False, - inline_images=None, template=None, args=None, header=None, print_letterhead=False): + inline_images=None, template=None, args=None, header=None, print_letterhead=False, with_container=False): """Send email using user's default **Email Account** or global default **Email Account**. @@ -491,6 +498,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message :param template: Name of html template from templates/emails folder :param args: Arguments for rendering the template :param header: Append header in email + :param with_container: Wraps email inside a styled container """ text_content = None if template: @@ -513,7 +521,7 @@ def sendmail(recipients=[], sender="", subject="No Subject", message="No Message attachments=attachments, reply_to=reply_to, cc=cc, bcc=bcc, message_id=message_id, in_reply_to=in_reply_to, send_after=send_after, expose_recipients=expose_recipients, send_priority=send_priority, communication=communication, now=now, read_receipt=read_receipt, is_notification=is_notification, - inline_images=inline_images, header=header, print_letterhead=print_letterhead) + inline_images=inline_images, header=header, print_letterhead=print_letterhead, with_container=with_container) whitelisted = [] guest_methods = [] @@ -628,6 +636,21 @@ def clear_cache(user=None, doctype=None): local.role_permissions = {} +def only_has_select_perm(doctype, user=None, ignore_permissions=False): + if ignore_permissions: + return False + + if not user: + user = local.session.user + + import frappe.permissions + permissions = frappe.permissions.get_role_permissions(doctype, user=user) + + if permissions.get('select') and not permissions.get('read'): + return True + else: + return False + def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=False, throw=False): """Raises `frappe.PermissionError` if not permitted. @@ -948,10 +971,6 @@ def get_installed_apps(sort=False, frappe_last=False): if not local.all_apps: local.all_apps = cache().get_value('all_apps', get_all_apps) - #cache bench apps - if not cache().get_value('all_apps'): - cache().set_value('all_apps', local.all_apps) - installed = json.loads(db.get_global("installed_apps") or "[]") if sort: diff --git a/frappe/app.py b/frappe/app.py index 82471c4e32..adf2bfa8c9 100644 --- a/frappe/app.py +++ b/frappe/app.py @@ -7,8 +7,8 @@ import os from six import iteritems import logging -from werkzeug.wrappers import Request from werkzeug.local import LocalManager +from werkzeug.wrappers import Request, Response from werkzeug.exceptions import HTTPException, NotFound from werkzeug.middleware.profiler import ProfilerMiddleware from werkzeug.middleware.shared_data import SharedDataMiddleware @@ -57,19 +57,22 @@ def application(request): frappe.monitor.start() frappe.rate_limiter.apply() - if frappe.local.form_dict.cmd: + if request.method == "OPTIONS": + response = Response() + + elif frappe.form_dict.cmd: response = frappe.handler.handle() - elif frappe.request.path.startswith("/api/"): + elif request.path.startswith("/api/"): response = frappe.api.handle() - elif frappe.request.path.startswith('/backups'): + elif request.path.startswith('/backups'): response = frappe.utils.response.download_backup(request.path) - elif frappe.request.path.startswith('/private/files/'): + elif request.path.startswith('/private/files/'): response = frappe.utils.response.download_private_file(request.path) - elif frappe.local.request.method in ('GET', 'HEAD', 'POST'): + elif request.method in ('GET', 'HEAD', 'POST'): response = frappe.website.render.render() else: @@ -88,13 +91,9 @@ def application(request): rollback = after_request(rollback) finally: - if frappe.local.request.method in ("POST", "PUT") and frappe.db and rollback: + if request.method in ("POST", "PUT") and frappe.db and rollback: frappe.db.rollback() - # set cookies - if response and hasattr(frappe.local, 'cookie_manager'): - frappe.local.cookie_manager.flush_cookies(response=response) - frappe.rate_limiter.update() frappe.monitor.stop(response) frappe.recorder.dump() @@ -110,9 +109,7 @@ def application(request): "http_status_code": getattr(response, "status_code", "NOTFOUND") }) - if response and hasattr(frappe.local, 'rate_limiter'): - response.headers.extend(frappe.local.rate_limiter.headers()) - + process_response(response) frappe.destroy() return response @@ -134,7 +131,46 @@ def init_request(request): make_form_dict(request) - frappe.local.http_request = frappe.auth.HTTPRequest() + if request.method != "OPTIONS": + frappe.local.http_request = frappe.auth.HTTPRequest() + +def process_response(response): + if not response: + return + + # set cookies + if hasattr(frappe.local, 'cookie_manager'): + frappe.local.cookie_manager.flush_cookies(response=response) + + # rate limiter headers + if hasattr(frappe.local, 'rate_limiter'): + response.headers.extend(frappe.local.rate_limiter.headers()) + + # CORS headers + if hasattr(frappe.local, 'conf') and frappe.conf.allow_cors: + set_cors_headers(response) + +def set_cors_headers(response): + origin = frappe.request.headers.get('Origin') + if not origin: + return + + allow_cors = frappe.conf.allow_cors + if allow_cors != "*": + if not isinstance(allow_cors, list): + allow_cors = [allow_cors] + + if origin not in allow_cors: + return + + response.headers.extend({ + 'Access-Control-Allow-Origin': origin, + 'Access-Control-Allow-Credentials': 'true', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', + 'Access-Control-Allow-Headers': ('Authorization,DNT,X-Mx-ReqToken,' + 'Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,' + 'Cache-Control,Content-Type') + }) def make_form_dict(request): import json diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.js b/frappe/automation/doctype/auto_repeat/auto_repeat.js index 30b3b17fb4..7028ac486d 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.js +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.js @@ -54,10 +54,12 @@ frappe.ui.form.on('Auto Repeat', { toggle_submit_on_creation: function(frm) { // submit on creation checkbox - frappe.model.with_doctype(frm.doc.reference_doctype, () => { - let meta = frappe.get_meta(frm.doc.reference_doctype); - frm.toggle_display('submit_on_creation', meta.is_submittable); - }); + if (frm.doc.reference_doctype) { + frappe.model.with_doctype(frm.doc.reference_doctype, () => { + let meta = frappe.get_meta(frm.doc.reference_doctype); + frm.toggle_display('submit_on_creation', meta.is_submittable); + }); + } }, template: function(frm) { @@ -100,10 +102,7 @@ frappe.ui.form.on('Auto Repeat', { frappe.auto_repeat.render_schedule = function(frm) { if (!frm.is_dirty() && frm.doc.status !== 'Disabled') { - frappe.call({ - method: "get_auto_repeat_schedule", - doc: frm.doc - }).done((r) => { + frm.call("get_auto_repeat_schedule").then(r => { frm.dashboard.wrapper.empty(); frm.dashboard.add_section( frappe.render_template("auto_repeat_schedule", { diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.json b/frappe/automation/doctype/auto_repeat/auto_repeat.json index 80975dd4f5..74965346fd 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.json +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.json @@ -23,6 +23,8 @@ "repeat_on_last_day", "column_break_12", "next_schedule_date", + "section_break_16", + "repeat_on_days", "notification", "notify_by_email", "recipients", @@ -189,15 +191,27 @@ "fieldtype": "Check", "label": "Repeat on Last Day of the Month" }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "repeat_on_days", + "fieldtype": "Table", + "label": "Repeat on Days", + "options": "Auto Repeat Day" + }, { "default": "0", "fieldname": "submit_on_creation", "fieldtype": "Check", "label": "Submit on Creation" + }, + { + "depends_on": "eval:doc.frequency==='Weekly';", + "fieldname": "section_break_16", + "fieldtype": "Section Break" } ], "links": [], - "modified": "2020-12-10 10:43:13.449172", + "modified": "2021-01-12 09:24:49.719611", "modified_by": "Administrator", "module": "Automation", "name": "Auto Repeat", diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 31d6539e61..281e699640 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from datetime import timedelta from frappe.desk.form import assign_to from frappe.utils.jinja import validate_template from dateutil.relativedelta import relativedelta @@ -13,9 +14,12 @@ from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_ from frappe.model.document import Document from frappe.core.doctype.communication.email import make from frappe.utils.background_jobs import get_jobs +from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated +from frappe.contacts.doctype.contact.contact import get_contacts_linked_from +from frappe.contacts.doctype.contact.contact import get_contacts_linking_to month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12} - +week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6} class AutoRepeat(Document): def validate(self): @@ -24,6 +28,7 @@ class AutoRepeat(Document): self.validate_submit_on_creation() self.validate_dates() self.validate_email_id() + self.validate_auto_repeat_days() self.set_dates() self.update_auto_repeat_id() self.unlink_if_applicable() @@ -49,7 +54,7 @@ class AutoRepeat(Document): if self.disabled: self.next_schedule_date = None else: - self.next_schedule_date = get_next_schedule_date(self.start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, self.end_date) + self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date) def unlink_if_applicable(self): if self.status == 'Completed' or self.disabled: @@ -88,6 +93,12 @@ class AutoRepeat(Document): else: frappe.throw(_("'Recipients' not specified")) + def validate_auto_repeat_days(self): + auto_repeat_days = self.get_auto_repeat_days() + if not len(set(auto_repeat_days)) == len(auto_repeat_days): + repeated_days = get_repeated(auto_repeat_days) + frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days))) + def update_auto_repeat_id(self): #check if document is already on auto repeat auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat") @@ -113,7 +124,7 @@ class AutoRepeat(Document): end_date = getdate(self.end_date) if not self.end_date: - next_date = get_next_schedule_date(start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day) + next_date = self.get_next_schedule_date(schedule_date=start_date) row = { "reference_document": self.reference_document, "frequency": self.frequency, @@ -122,8 +133,7 @@ class AutoRepeat(Document): schedule_details.append(row) if self.end_date: - next_date = get_next_schedule_date( - start_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, for_full_schedule=True) + next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True) while (getdate(next_date) < getdate(end_date)): row = { @@ -132,8 +142,7 @@ class AutoRepeat(Document): "next_scheduled_date" : next_date } schedule_details.append(row) - next_date = get_next_schedule_date( - next_date, self.frequency, self.start_date, self.repeat_on_day, self.repeat_on_last_day, end_date, for_full_schedule=True) + next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True) return schedule_details @@ -211,6 +220,75 @@ class AutoRepeat(Document): new_doc.set('from_date', from_date) new_doc.set('to_date', to_date) + def get_next_schedule_date(self, schedule_date, for_full_schedule=False): + """ + Returns the next schedule date for auto repeat after a recurring document has been created. + Adds required offset to the schedule_date param and returns the next schedule date. + + :param schedule_date: The date when the last recurring document was created. + :param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule. + """ + if month_map.get(self.frequency): + month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1 + else: + month_count = 0 + + day_count = 0 + if month_count and self.repeat_on_last_day: + day_count = 31 + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count and self.repeat_on_day: + day_count = self.repeat_on_day + next_date = get_next_date(self.start_date, month_count, day_count) + elif month_count: + next_date = get_next_date(self.start_date, month_count) + else: + days = self.get_days(schedule_date) + next_date = add_days(schedule_date, days) + + # next schedule date should be after or on current date + if not for_full_schedule: + while getdate(next_date) < getdate(today()): + if month_count: + month_count += month_map.get(self.frequency, 0) + next_date = get_next_date(self.start_date, month_count, day_count) + else: + days = self.get_days(next_date) + next_date = add_days(next_date, days) + + return next_date + + def get_days(self, schedule_date): + if self.frequency == "Weekly": + days = self.get_offset_for_weekly_frequency(schedule_date) + else: + # daily frequency + days = 1 + + return days + + def get_offset_for_weekly_frequency(self, schedule_date): + # if weekdays are not set, offset is 7 from current schedule date + if not self.repeat_on_days: + return 7 + + repeat_on_days = self.get_auto_repeat_days() + current_schedule_day = getdate(schedule_date).weekday() + weekdays = list(week_map.keys()) + + # if repeats on more than 1 day or + # start date's weekday is not in repeat days, then get next weekday + # else offset is 7 + if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days: + weekday = get_next_weekday(current_schedule_day, repeat_on_days) + next_weekday_number = week_map.get(weekday, 0) + # offset for upcoming weekday + return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days + return 7 + + def get_auto_repeat_days(self): + return [d.day for d in self.get('repeat_on_days', [])] + def send_notification(self, new_doc): """Notify concerned people about recurring document generation""" subject = self.subject or '' @@ -252,13 +330,8 @@ class AutoRepeat(Document): def fetch_linked_contacts(self): if self.reference_doctype and self.reference_document: - res = frappe.db.get_all('Contact', - fields=['email_id'], - filters=[ - ['Dynamic Link', 'link_doctype', '=', self.reference_doctype], - ['Dynamic Link', 'link_name', '=', self.reference_document] - ]) - + res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) + res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id']) email_ids = list(set([d.email_id for d in res])) if not email_ids: frappe.msgprint(_('No contacts linked to document'), alert=True) @@ -291,42 +364,24 @@ class AutoRepeat(Document): ) -def get_next_schedule_date(schedule_date, frequency, start_date, repeat_on_day=None, repeat_on_last_day=False, end_date=None, for_full_schedule=False): - if month_map.get(frequency): - month_count = month_map.get(frequency) + month_diff(schedule_date, start_date) - 1 - else: - month_count = 0 - - day_count = 0 - if month_count and repeat_on_last_day: - day_count = 31 - next_date = get_next_date(start_date, month_count, day_count) - elif month_count and repeat_on_day: - day_count = repeat_on_day - next_date = get_next_date(start_date, month_count, day_count) - elif month_count: - next_date = get_next_date(start_date, month_count) - else: - days = 7 if frequency == 'Weekly' else 1 - next_date = add_days(schedule_date, days) - - # next schedule date should be after or on current date - if not for_full_schedule: - while getdate(next_date) < getdate(today()): - if month_count: - month_count += month_map.get(frequency) - next_date = get_next_date(start_date, month_count, day_count) - elif days: - next_date = add_days(next_date, days) - - return next_date - - def get_next_date(dt, mcount, day=None): dt = getdate(dt) dt += relativedelta(months=mcount, day=day) return dt + +def get_next_weekday(current_schedule_day, weekdays): + days = list(week_map.keys()) + if current_schedule_day > 0: + days = days[(current_schedule_day + 1):] + days[:current_schedule_day] + else: + days = days[(current_schedule_day + 1):] + + for entry in days: + if entry in weekdays: + return entry + + #called through hooks def make_auto_repeat_entry(): enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries' @@ -337,6 +392,7 @@ def make_auto_repeat_entry(): data = get_auto_repeat_entries(date) frappe.enqueue(enqueued_method, data=data) + def create_repeated_entries(data): for d in data: doc = frappe.get_doc('Auto Repeat', d.name) @@ -346,10 +402,11 @@ def create_repeated_entries(data): if schedule_date == current_date and not doc.disabled: doc.create_documents() - schedule_date = get_next_schedule_date(schedule_date, doc.frequency, doc.start_date, doc.repeat_on_day, doc.repeat_on_last_day, doc.end_date) + schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date) if schedule_date and not doc.disabled: frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date) + def get_auto_repeat_entries(date=None): if not date: date = getdate(today()) @@ -358,6 +415,7 @@ def get_auto_repeat_entries(date=None): ['status', '=', 'Active'] ]) + #called through hooks def set_auto_repeat_as_completed(): auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']}) @@ -367,6 +425,7 @@ def set_auto_repeat_as_completed(): doc.status = 'Completed' doc.save() + @frappe.whitelist() def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None): if not start_date: diff --git a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py index e40b12e3b9..0d6229cd9e 100644 --- a/frappe/automation/doctype/auto_repeat/test_auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/test_auto_repeat.py @@ -7,10 +7,9 @@ import unittest import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_field -from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries +from frappe.automation.doctype.auto_repeat.auto_repeat import get_auto_repeat_entries, create_repeated_entries, week_map from frappe.utils import today, add_days, getdate, add_months - def add_custom_fields(): df = dict( fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender', @@ -42,6 +41,52 @@ class TestAutoRepeat(unittest.TestCase): self.assertEqual(todo.get('description'), new_todo.get('description')) + def test_weekly_auto_repeat(self): + todo = frappe.get_doc( + dict(doctype='ToDo', description='test weekly todo', assigned_by='Administrator')).insert() + + doc = make_auto_repeat(reference_doctype='ToDo', + frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7)) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + frappe.db.commit() + + todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + new_todo = frappe.db.get_value('ToDo', + {'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name') + + new_todo = frappe.get_doc('ToDo', new_todo) + + self.assertEqual(todo.get('description'), new_todo.get('description')) + + def test_weekly_auto_repeat_with_weekdays(self): + todo = frappe.get_doc( + dict(doctype='ToDo', description='test auto repeat with weekdays', assigned_by='Administrator')).insert() + + weekdays = list(week_map.keys()) + current_weekday = getdate().weekday() + days = [ + {'day': weekdays[current_weekday]}, + {'day': weekdays[(current_weekday + 2) % 7]} + ] + doc = make_auto_repeat(reference_doctype='ToDo', + frequency='Weekly', reference_document=todo.name, start_date=add_days(today(), -7), days=days) + + self.assertEqual(doc.next_schedule_date, today()) + data = get_auto_repeat_entries(getdate(today())) + create_repeated_entries(data) + frappe.db.commit() + + todo = frappe.get_doc(doc.reference_doctype, doc.reference_document) + self.assertEqual(todo.auto_repeat, doc.name) + + doc.reload() + self.assertEqual(doc.next_schedule_date, add_days(getdate(), 2)) + def test_monthly_auto_repeat(self): start_date = today() end_date = add_months(start_date, 12) @@ -144,7 +189,8 @@ def make_auto_repeat(**args): 'notify_by_email': args.notify or 0, 'recipients': args.recipients or "", 'subject': args.subject or "", - 'message': args.message or "" + 'message': args.message or "", + 'repeat_on_days': args.days or [] }).insert(ignore_permissions=True) return doc diff --git a/frappe/core/page/desktop/__init__.py b/frappe/automation/doctype/auto_repeat_day/__init__.py similarity index 100% rename from frappe/core/page/desktop/__init__.py rename to frappe/automation/doctype/auto_repeat_day/__init__.py diff --git a/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json new file mode 100644 index 0000000000..6f5c3060cd --- /dev/null +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.json @@ -0,0 +1,33 @@ +{ + "actions": [], + "creation": "2020-11-10 22:30:53.690228", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "day" + ], + "fields": [ + { + "fieldname": "day", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Day", + "options": "Monday\nTuesday\nWednesday\nThursday\nFriday\nSaturday\nSunday", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-11-10 22:30:53.690228", + "modified_by": "Administrator", + "module": "Automation", + "name": "Auto Repeat Day", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.py b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py similarity index 88% rename from frappe/desk/doctype/desk_shortcut/desk_shortcut.py rename to frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py index bbf0b2e074..3a7ced1370 100644 --- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.py +++ b/frappe/automation/doctype/auto_repeat_day/auto_repeat_day.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class DeskShortcut(Document): +class AutoRepeatDay(Document): pass diff --git a/frappe/boot.py b/frappe/boot.py index 2f4569dee9..8cf75e02bb 100644 --- a/frappe/boot.py +++ b/frappe/boot.py @@ -109,7 +109,7 @@ def load_conf_settings(bootinfo): def load_desktop_data(bootinfo): from frappe.desk.desktop import get_desk_sidebar_items - bootinfo.allowed_workspaces = get_desk_sidebar_items(flatten=True, cache=False) + bootinfo.allowed_workspaces = get_desk_sidebar_items() bootinfo.module_page_map = get_controller("Workspace").get_module_page_map() bootinfo.dashboards = frappe.get_all("Dashboard") @@ -250,13 +250,12 @@ def add_home_page(bootinfo, docs): try: page = frappe.desk.desk_page.get(home_page) + docs.append(page) + bootinfo['home_page'] = page.name except (frappe.DoesNotExistError, frappe.PermissionError): if frappe.message_log: frappe.message_log.pop() - page = frappe.desk.desk_page.get('space') - - bootinfo['home_page'] = page.name - docs.append(page) + bootinfo['home_page'] = 'Workspaces' def add_timezone_info(bootinfo): system = bootinfo.sysdefaults.get("time_zone") diff --git a/frappe/cache_manager.py b/frappe/cache_manager.py index 27e6543235..bad879d2fa 100644 --- a/frappe/cache_manager.py +++ b/frappe/cache_manager.py @@ -13,7 +13,7 @@ common_default_keys = ["__default", "__global"] doctype_map_keys = ('energy_point_rule_map', 'assignment_rule_map', 'milestone_tracker_map', 'event_consumer_document_type_map') -global_cache_keys = ("app_hooks", "installed_apps", +global_cache_keys = ("app_hooks", "installed_apps", 'all_apps', "app_modules", "module_app", "system_settings", 'scheduler_events', 'time_zone', 'webhooks', 'active_domains', 'active_modules', 'assignment_rule', 'server_script_map', 'wkhtmltopdf_version', @@ -68,6 +68,7 @@ def clear_defaults_cache(user=None): frappe.cache().delete_key("defaults") def clear_doctype_cache(doctype=None): + clear_controller_cache(doctype) cache = frappe.cache() if getattr(frappe.local, 'meta_cache') and (doctype in frappe.local.meta_cache): @@ -99,6 +100,15 @@ def clear_doctype_cache(doctype=None): for name in doctype_cache_keys: cache.delete_value(name) +def clear_controller_cache(doctype=None): + if not doctype: + del frappe.controllers + frappe.controllers = {} + return + + for site_controllers in frappe.controllers.values(): + site_controllers.pop(doctype, None) + def get_doctype_map(doctype, name, filters=None, order_by=None): cache = frappe.cache() cache_key = frappe.scrub(doctype) + '_map' diff --git a/frappe/commands/utils.py b/frappe/commands/utils.py index 31b84ee98a..e9fa7217a8 100644 --- a/frappe/commands/utils.py +++ b/frappe/commands/utils.py @@ -571,10 +571,11 @@ def run_ui_tests(context, app, headless=False): plugin_path = "{0}/cypress-file-upload".format(node_bin) # check if cypress in path...if not, install it. - if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)): + if not (os.path.exists(cypress_path) or os.path.exists(plugin_path)) \ + or not subprocess.getoutput("npm view cypress version").startswith("6."): # install cypress click.secho("Installing Cypress...", fg="yellow") - frappe.commands.popen("yarn add cypress@3 cypress-file-upload@^3.1 --no-lockfile") + frappe.commands.popen("yarn add cypress@^6 cypress-file-upload@^5 --no-lockfile") # run for headless mode run_or_open = 'run --browser chrome --record --key 4a48f41c-11b3-425b-aa88-c58048fa69eb' if headless else 'open' diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 987ba7d3d6..42fa039f74 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -256,3 +256,27 @@ def get_contact_with_phone_number(number): def get_contact_name(email_id): contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1) return contact[0].parent if contact else None + +def get_contacts_linking_to(doctype, docname, fields=None): + """Return a list of contacts containing a link to the given document.""" + return frappe.get_list('Contact', fields=fields, filters=[ + ['Dynamic Link', 'link_doctype', '=', doctype], + ['Dynamic Link', 'link_name', '=', docname] + ]) + +def get_contacts_linked_from(doctype, docname, fields=None): + """Return a list of contacts that are contained in (linked from) the given document.""" + link_fields = frappe.get_meta(doctype).get('fields', { + 'fieldtype': 'Link', + 'options': 'Contact' + }) + if not link_fields: + return [] + + contact_names = frappe.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields]) + if not contact_names: + return [] + + return frappe.get_list('Contact', fields=fields, filters={ + 'name': ('in', contact_names) + }) diff --git a/frappe/core/doctype/comment/comment.py b/frappe/core/doctype/comment/comment.py index 49b339731e..e4fd181733 100644 --- a/frappe/core/doctype/comment/comment.py +++ b/frappe/core/doctype/comment/comment.py @@ -27,6 +27,8 @@ class Comment(Document): def on_update(self): update_comment_in_doc(self) + if self.is_new(): + self.notify_change('update') def on_trash(self): self.remove_comment_from_cache() @@ -163,7 +165,7 @@ def update_comments_in_parent(reference_doctype, reference_name, _comments): try: # use sql, so that we do not mess with the timestamp frappe.db.sql("""update `tab{0}` set `_comments`=%s where name=%s""".format(reference_doctype), # nosec - (json.dumps(_comments[-50:]), reference_name)) + (json.dumps(_comments[-100:]), reference_name)) except Exception as e: if frappe.db.is_column_missing(e) and getattr(frappe.local, 'request', None): diff --git a/frappe/core/doctype/communication/communication.js b/frappe/core/doctype/communication/communication.js index 2ca3e7dea0..07674d16ae 100644 --- a/frappe/core/doctype/communication/communication.js +++ b/frappe/core/doctype/communication/communication.js @@ -99,8 +99,7 @@ frappe.ui.form.on("Communication", { } }, - show_relink_dialog: function(frm){ - var lib = "frappe.email"; + show_relink_dialog: function(frm) { var d = new frappe.ui.Dialog ({ title: __("Relink Communication"), fields: [{ @@ -138,8 +137,10 @@ frappe.ui.form.on("Communication", { } }); }, - function () { - frappe.show_alert({message:__('Document not Relinked'), 'indicator': 'info'}) + function() { + frappe.show_alert({ + message: __('Document not Relinked'), 'indicator': 'info' + }); } ); } diff --git a/frappe/core/doctype/custom_docperm/custom_docperm.json b/frappe/core/doctype/custom_docperm/custom_docperm.json index f8f7f58be1..93f5431903 100644 --- a/frappe/core/doctype/custom_docperm/custom_docperm.json +++ b/frappe/core/doctype/custom_docperm/custom_docperm.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "hash", "creation": "2017-01-11 04:21:35.217943", @@ -13,6 +14,7 @@ "column_break_2", "permlevel", "section_break_4", + "select", "read", "write", "create", @@ -211,9 +213,16 @@ "fieldtype": "Data", "label": "Reference Document Type", "read_only": 1 + }, + { + "default": "0", + "fieldname": "select", + "fieldtype": "Check", + "label": "Select" } ], - "modified": "2019-10-31 16:58:16.157079", + "links": [], + "modified": "2020-12-03 15:20:48.296730", "modified_by": "Administrator", "module": "Core", "name": "Custom DocPerm", diff --git a/frappe/core/doctype/data_import/importer.py b/frappe/core/doctype/data_import/importer.py index 7880648b6f..dde3dfaee9 100644 --- a/frappe/core/doctype/data_import/importer.py +++ b/frappe/core/doctype/data_import/importer.py @@ -751,7 +751,7 @@ class Row: self.warnings.append( { "row": self.row_number, - "message": _("{0} is a mandatory field asdadsf").format(id_field.label), + "message": _("{0} is a mandatory field").format(id_field.label), } ) return diff --git a/frappe/core/doctype/deleted_document/deleted_document_list.js b/frappe/core/doctype/deleted_document/deleted_document_list.js index 268936e8a8..92413bfdf4 100644 --- a/frappe/core/doctype/deleted_document/deleted_document_list.js +++ b/frappe/core/doctype/deleted_document/deleted_document_list.js @@ -9,15 +9,16 @@ frappe.listview_settings["Deleted Document"] = { args: { docnames }, callback: function (r) { if (r.message) { - function body(docnames) { + let body = (docnames) => { const html = docnames.map(docname => { return `
  • ${docname}
  • `; }); return "
      " + html.join(""); - } - function message(title, docnames) { + }; + + let message = (title, docnames) => { return (docnames.length > 0) ? title + body(docnames) + "
    ": ""; - } + }; const { restored, invalid, failed } = r.message; const restored_summary = message(__("Documents restored successfully"), restored); diff --git a/frappe/core/doctype/docperm/docperm.json b/frappe/core/doctype/docperm/docperm.json index 1a23118a29..4411a67435 100644 --- a/frappe/core/doctype/docperm/docperm.json +++ b/frappe/core/doctype/docperm/docperm.json @@ -1,775 +1,229 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, + "actions": [], "autoname": "hash", - "beta": 0, "creation": "2013-02-22 01:27:33", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "role_and_level", + "role", + "if_owner", + "column_break_2", + "permlevel", + "section_break_4", + "select", + "read", + "write", + "create", + "delete", + "column_break_8", + "submit", + "cancel", + "amend", + "additional_permissions", + "report", + "export", + "import", + "set_user_permissions", + "column_break_19", + "share", + "print", + "email" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "role_and_level", "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": "Role and Level", - "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 + "label": "Role and Level" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "role", "fieldtype": "Link", - "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": "Role", - "length": 0, - "no_copy": 0, "oldfieldname": "role", "oldfieldtype": "Link", "options": "Role", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "Apply this rule if the User is the Owner", "fieldname": "if_owner", "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": "If user is the owner", - "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 + "label": "If user is the owner" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_2", - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "0", "fieldname": "permlevel", "fieldtype": "Int", - "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": "Level", - "length": 0, - "no_copy": 0, "oldfieldname": "permlevel", "oldfieldtype": "Int", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "40px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "40px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_4", "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": "Permissions", - "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 + "label": "Permissions" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "read", "fieldtype": "Check", - "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": "Read", - "length": 0, - "no_copy": 0, "oldfieldname": "read", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "write", "fieldtype": "Check", - "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": "Write", - "length": 0, - "no_copy": 0, "oldfieldname": "write", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "create", "fieldtype": "Check", - "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": "Create", - "length": 0, - "no_copy": 0, "oldfieldname": "create", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "delete", "fieldtype": "Check", - "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": "Delete", - "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 + "label": "Delete" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_8", - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "submit", "fieldtype": "Check", - "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": "Submit", - "length": 0, - "no_copy": 0, "oldfieldname": "submit", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "cancel", "fieldtype": "Check", - "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": "Cancel", - "length": 0, - "no_copy": 0, "oldfieldname": "cancel", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "amend", "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": "Amend", - "length": 0, - "no_copy": 0, "oldfieldname": "amend", "oldfieldtype": "Check", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "additional_permissions", "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": "Additional Permissions", - "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 + "label": "Additional Permissions" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "report", "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": "Report", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "32px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "32px" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "export", "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": "Export", - "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 + "label": "Export" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "import", "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": "Import", - "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 + "label": "Import" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "description": "This role update User Permissions for a user", "fieldname": "set_user_permissions", "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": "Set User Permissions", - "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 + "label": "Set User Permissions" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_19", - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "share", "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": "Share", - "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 + "label": "Share" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "print", "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": "Print", - "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 + "label": "Print" }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "email", "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": "Email", - "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 + "label": "Email" + }, + { + "default": "0", + "fieldname": "select", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Select" } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2018-05-29 11:54:38.613936", + "links": [], + "modified": "2020-12-03 15:15:30.488212", "modified_by": "Administrator", "module": "Core", "name": "DocPerm", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0 + "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 a59bef9fc7..cbcfa350f5 100644 --- a/frappe/core/doctype/doctype/doctype.py +++ b/frappe/core/doctype/doctype/doctype.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import re, copy, os, shutil import json -from frappe.cache_manager import clear_user_cache +from frappe.cache_manager import clear_user_cache, clear_controller_cache # imports - third party imports import six @@ -26,6 +26,7 @@ from frappe.database.schema import validate_column_name, validate_column_length from frappe.model.docfield import supports_translation from frappe.modules.import_file import get_file_path from frappe.model.meta import Meta +from frappe.desk.utils import validate_route_conflict class InvalidFieldNameError(frappe.ValidationError): pass class UniqueFieldnameError(frappe.ValidationError): pass @@ -288,9 +289,15 @@ class DocType(Document): self.update_fields_to_fetch() - from frappe import conf - allow_doctype_export = frappe.flags.allow_doctype_export or (not frappe.flags.in_test and conf.get('developer_mode')) - if not self.custom and not frappe.flags.in_import and allow_doctype_export: + allow_doctype_export = ( + not self.custom + and not frappe.flags.in_import + and ( + frappe.conf.developer_mode + or frappe.flags.allow_doctype_export + ) + ) + if allow_doctype_export: self.export_doc() self.make_controller_template() @@ -368,13 +375,10 @@ class DocType(Document): if merge: frappe.throw(_("DocType can not be merged")) - # Do not rename and move files and folders for custom doctype - if not self.custom and not frappe.flags.in_test and not frappe.flags.in_patch: - self.rename_files_and_folders(old, new) - def after_rename(self, old, new, merge=False): """Change table name using `RENAME TABLE` if table exists. Or update `doctype` property for Single type.""" + if self.issingle: frappe.db.sql("""update tabSingles set doctype=%s where doctype=%s""", (new, old)) frappe.db.sql("""update tabSingles set value=%s @@ -384,6 +388,18 @@ class DocType(Document): "mariadb": f"RENAME TABLE `tab{old}` TO `tab{new}`", "postgres": f"ALTER TABLE `tab{old}` RENAME TO `tab{new}`" }) + frappe.db.commit() + + # Do not rename and move files and folders for custom doctype + if not self.custom: + if not frappe.flags.in_patch: + self.rename_files_and_folders(old, new) + + clear_controller_cache(old) + + def after_delete(self): + if not self.custom: + clear_controller_cache(self.name) def rename_files_and_folders(self, old, new): # move files @@ -640,7 +656,7 @@ class DocType(Document): flags = {"flags": re.ASCII} if six.PY3 else {} # a DocType name should not start or end with an empty space - if re.match("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): + if re.search("^[ \t\n\r]+|[ \t\n\r]+$", name, **flags): frappe.throw(_("DocType's name should not start or end with whitespace"), frappe.NameError) # a DocType's name should not start with a number or underscore @@ -648,6 +664,8 @@ class DocType(Document): if not re.match("^(?![\W])[^\d_\s][\w ]+$", name, **flags): frappe.throw(_("DocType's name should start with a letter and it can only consist of letters, numbers, spaces and underscores"), frappe.NameError) + validate_route_conflict(self.doctype, self.name) + def validate_links_table_fieldnames(meta): """Validate fieldnames in Links table""" if frappe.flags.in_patch: return @@ -984,10 +1002,10 @@ def validate_fields(meta): check_sort_field(meta) check_image_field(meta) -def validate_permissions_for_doctype(doctype, for_remove=False): +def validate_permissions_for_doctype(doctype, for_remove=False, alert=False): """Validates if permissions are set correctly.""" doctype = frappe.get_doc("DocType", doctype) - validate_permissions(doctype, for_remove) + validate_permissions(doctype, for_remove, alert=alert) # save permissions for perm in doctype.get("permissions"): @@ -1010,9 +1028,10 @@ def clear_permissions_cache(doctype): """, doctype): frappe.clear_cache(user=user) -def validate_permissions(doctype, for_remove=False): +def validate_permissions(doctype, for_remove=False, alert=False): permissions = doctype.get("permissions") - if not permissions: + # Some DocTypes may not have permissions by default, don't show alert for them + if not permissions and alert: frappe.msgprint(_('No Permissions Specified'), alert=True, indicator='orange') issingle = issubmittable = isimportable = False if doctype: @@ -1024,7 +1043,7 @@ def validate_permissions(doctype, for_remove=False): return _("For {0} at level {1} in {2} in row {3}").format(d.role, d.permlevel, d.parent, d.idx) def check_atleast_one_set(d): - if not d.read and not d.write and not d.submit and not d.cancel and not d.create: + if not d.select and not d.read and not d.write and not d.submit and not d.cancel and not d.create: frappe.throw(_("{0}: No basic permissions set").format(get_txt(d))) def check_double(d): diff --git a/frappe/core/doctype/doctype/patches/set_route.py b/frappe/core/doctype/doctype/patches/set_route.py index 655935f861..c052a51f38 100644 --- a/frappe/core/doctype/doctype/patches/set_route.py +++ b/frappe/core/doctype/doctype/patches/set_route.py @@ -1,7 +1,7 @@ import frappe -from frappe.desk.utils import get_doctype_route +from frappe.desk.utils import slug def execute(): for doctype in frappe.get_all('DocType', ['name', 'route'], dict(istable=0)): if not doctype.route: - frappe.db.set_value('DocType', doctype.name, 'route', get_doctype_route(doctype.name), update_modified = False) \ No newline at end of file + frappe.db.set_value('DocType', doctype.name, 'route', slug(doctype.name), update_modified = False) \ No newline at end of file diff --git a/frappe/core/doctype/doctype/test_doctype.py b/frappe/core/doctype/doctype/test_doctype.py index 047d1aa6e2..ec88a2d14c 100644 --- a/frappe/core/doctype/doctype/test_doctype.py +++ b/frappe/core/doctype/doctype/test_doctype.py @@ -5,14 +5,13 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError, - IllegalMandatoryError, - DoctypeLinkError, - WrongOptionsDoctypeLinkError, - HiddenAndMandatoryWithoutDefaultError, - CannotIndexedError, - InvalidFieldNameError, - CannotCreateStandardDoctypeError, +from frappe.core.doctype.doctype.doctype import (UniqueFieldnameError, + IllegalMandatoryError, + DoctypeLinkError, + WrongOptionsDoctypeLinkError, + HiddenAndMandatoryWithoutDefaultError, + CannotIndexedError, + InvalidFieldNameError, validate_links_table_fieldnames) # test_records = frappe.get_test_records('DocType') diff --git a/frappe/core/doctype/document_naming_rule/document_naming_rule.py b/frappe/core/doctype/document_naming_rule/document_naming_rule.py index 3ff47facc3..4b34293af6 100644 --- a/frappe/core/doctype/document_naming_rule/document_naming_rule.py +++ b/frappe/core/doctype/document_naming_rule/document_naming_rule.py @@ -6,8 +6,19 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document from frappe.utils.data import evaluate_filters +from frappe import _ class DocumentNamingRule(Document): + def validate(self): + self.validate_fields_in_conditions() + + def validate_fields_in_conditions(self): + if self.has_value_changed("document_type"): + docfields = [x.fieldname for x in frappe.get_meta(self.document_type).fields] + for condition in self.conditions: + if condition.field not in docfields: + frappe.throw(_("{0} is not a field of doctype {1}").format(frappe.bold(condition.field), frappe.bold(self.document_type))) + def apply(self, doc): ''' Apply naming rules for the given document. Will set `name` if the rule is matched. diff --git a/frappe/core/doctype/file/file.py b/frappe/core/doctype/file/file.py index 8614740d26..445ca1184d 100755 --- a/frappe/core/doctype/file/file.py +++ b/frappe/core/doctype/file/file.py @@ -456,7 +456,7 @@ class File(Document): def save_file(self, content=None, decode=False, ignore_existing_file_check=False): file_exists = False self.content = content - + if decode: if isinstance(content, text_type): self.content = content.encode("utf-8") @@ -467,19 +467,19 @@ class File(Document): if not self.is_private: self.is_private = 0 - + self.content_type = mimetypes.guess_type(self.file_name)[0] - + self.file_size = self.check_max_file_size() - + if ( self.content_type and "image" in self.content_type and frappe.get_system_settings("strip_exif_metadata_from_uploaded_images") ): - self.content = strip_exif_data(self.content, self.content_type) + self.content = strip_exif_data(self.content, self.content_type) self.content_hash = get_content_hash(self.content) - + duplicate_file = None # check if a file exists with the same content hash and is also in the same folder (public or private) @@ -940,10 +940,33 @@ def validate_filename(filename): return fname @frappe.whitelist() -def get_files_in_folder(folder): - return frappe.db.get_all('File', +def get_files_in_folder(folder, start=0, page_length=20): + start = cint(start) + page_length = cint(page_length) + + files = frappe.db.get_all('File', { 'folder': folder }, - ['name', 'file_name', 'file_url', 'is_folder', 'modified'] + ['name', 'file_name', 'file_url', 'is_folder', 'modified'], + start=start, + page_length=page_length + 1 + ) + return { + 'files': files[:page_length], + 'has_more': len(files) > page_length + } + +@frappe.whitelist() +def get_files_by_search_text(text): + if not text: + return [] + + text = '%' + cstr(text).lower() + '%' + return frappe.db.get_all('File', + fields=['name', 'file_name', 'file_url', 'is_folder', 'modified'], + filters={'is_folder': False}, + or_filters={'file_name': ('like', text), 'file_url': text, 'name': ('like', text)}, + order_by='modified desc', + limit=20 ) def update_existing_file_docs(doc): diff --git a/frappe/core/doctype/module_def/module_def.py b/frappe/core/doctype/module_def/module_def.py index 930c46e60b..7e63572162 100644 --- a/frappe/core/doctype/module_def/module_def.py +++ b/frappe/core/doctype/module_def/module_def.py @@ -43,7 +43,7 @@ class ModuleDef(Document): def on_trash(self): """Delete module name from modules.txt""" - if frappe.flags.in_uninstall or self.custom: + if not frappe.conf.get('developer_mode') or frappe.flags.in_uninstall or self.custom: return modules = None diff --git a/frappe/core/page/space/__init__.py b/frappe/core/doctype/module_profile/__init__.py similarity index 100% rename from frappe/core/page/space/__init__.py rename to frappe/core/doctype/module_profile/__init__.py diff --git a/frappe/core/doctype/module_profile/module_profile.js b/frappe/core/doctype/module_profile/module_profile.js new file mode 100644 index 0000000000..9c92042dda --- /dev/null +++ b/frappe/core/doctype/module_profile/module_profile.js @@ -0,0 +1,19 @@ +// Copyright (c) 2020, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Module Profile', { + refresh: function(frm) { + if (has_common(frappe.user_roles, ["Administrator", "System Manager"])) { + if (!frm.module_editor && frm.doc.__onload && frm.doc.__onload.all_modules) { + let module_area = $('
    ') + .appendTo(frm.fields_dict.module_html.wrapper); + + frm.module_editor = new frappe.ModuleEditor(frm, module_area); + } + } + + if (frm.module_editor) { + frm.module_editor.refresh(); + } + } +}); diff --git a/frappe/core/doctype/module_profile/module_profile.json b/frappe/core/doctype/module_profile/module_profile.json new file mode 100644 index 0000000000..0e4e56962e --- /dev/null +++ b/frappe/core/doctype/module_profile/module_profile.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "autoname": "field:module_profile_name", + "creation": "2020-12-22 22:00:30.614475", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "module_profile_name", + "module_html", + "block_modules" + ], + "fields": [ + { + "fieldname": "module_profile_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Module Profile Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "module_html", + "fieldtype": "HTML", + "label": "Module HTML" + }, + { + "fieldname": "block_modules", + "fieldtype": "Table", + "hidden": 1, + "label": "Block Modules", + "options": "Block Module", + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-01-03 15:36:52.622696", + "modified_by": "Administrator", + "module": "Core", + "name": "Module Profile", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/core/doctype/module_profile/module_profile.py b/frappe/core/doctype/module_profile/module_profile.py new file mode 100644 index 0000000000..4f392353ac --- /dev/null +++ b/frappe/core/doctype/module_profile/module_profile.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from frappe.model.document import Document + +class ModuleProfile(Document): + def onload(self): + from frappe.config import get_modules_from_all_apps + self.set_onload('all_modules', + [m.get("module_name") for m in get_modules_from_all_apps()]) diff --git a/frappe/core/doctype/module_profile/test_module_profile.py b/frappe/core/doctype/module_profile/test_module_profile.py new file mode 100644 index 0000000000..400053d22c --- /dev/null +++ b/frappe/core/doctype/module_profile/test_module_profile.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals +import frappe +import unittest + +class TestModuleProfile(unittest.TestCase): + def test_make_new_module_profile(self): + if not frappe.db.get_value('Module Profile', '_Test Module Profile'): + frappe.get_doc({ + 'doctype': 'Module Profile', + 'module_profile_name': '_Test Module Profile', + 'block_modules': [ + {'module': 'Accounts'} + ] + }).insert() + + # add to user and check + if not frappe.db.get_value('User', 'test-for-module_profile@example.com'): + new_user = frappe.get_doc({ + 'doctype': 'User', + 'email':'test-for-module_profile@example.com', + 'first_name':'Test User' + }).insert() + else: + new_user = frappe.get_doc('User', 'test-for-module_profile@example.com') + + new_user.module_profile = '_Test Module Profile' + new_user.save() + + self.assertEqual(new_user.block_modules[0].module, 'Accounts') diff --git a/frappe/core/doctype/page/page.py b/frappe/core/doctype/page/page.py index 2d616542f3..bdec350efd 100644 --- a/frappe/core/doctype/page/page.py +++ b/frappe/core/doctype/page/page.py @@ -9,6 +9,7 @@ from frappe.build import html_to_js_template from frappe.model.utils import render_include from frappe import conf, _, safe_decode from frappe.desk.form.meta import get_code_files_via_hooks, get_js +from frappe.desk.utils import validate_route_conflict from frappe.core.doctype.custom_role.custom_role import get_custom_allowed_roles from six import text_type @@ -33,10 +34,7 @@ class Page(Document): self.name += '-' + str(cnt) def validate(self): - if frappe.db.get_value('DocType', self.name): - frappe.throw( - _("{} is the name of a DocType. DocType names cannot be the same as a Page name, please choose another name.").format(self.page_name) - ) + validate_route_conflict(self.doctype, self.name) if self.is_new() and not getattr(conf,'developer_mode', 0): frappe.throw(_("Not in Developer Mode")) diff --git a/frappe/core/doctype/page/patches/drop_unused_pages.py b/frappe/core/doctype/page/patches/drop_unused_pages.py new file mode 100644 index 0000000000..93b47cebcc --- /dev/null +++ b/frappe/core/doctype/page/patches/drop_unused_pages.py @@ -0,0 +1,5 @@ +import frappe + +def execute(): + for name in ('desktop', 'space'): + frappe.delete_doc('Page', name) \ No newline at end of file diff --git a/frappe/core/doctype/page/test_page.py b/frappe/core/doctype/page/test_page.py index 78659f1ffd..f7b3952a5b 100644 --- a/frappe/core/doctype/page/test_page.py +++ b/frappe/core/doctype/page/test_page.py @@ -8,4 +8,6 @@ import unittest test_records = frappe.get_test_records('Page') class TestPage(unittest.TestCase): - pass + def test_naming(self): + self.assertRaises(frappe.NameError, frappe.get_doc(dict(doctype='Page', page_name='DocType', module='Core')).insert) + self.assertRaises(frappe.NameError, frappe.get_doc(dict(doctype='Page', page_name='Settings', module='Core')).insert) diff --git a/frappe/core/doctype/report_filter/report_filter.json b/frappe/core/doctype/report_filter/report_filter.json index 9d277db11d..964294b96e 100644 --- a/frappe/core/doctype/report_filter/report_filter.json +++ b/frappe/core/doctype/report_filter/report_filter.json @@ -44,7 +44,7 @@ }, { "fieldname": "options", - "fieldtype": "Data", + "fieldtype": "Small Text", "label": "Options" }, { @@ -58,7 +58,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-17 16:15:46.937267", + "modified": "2020-12-05 19:20:00.503097", "modified_by": "Administrator", "module": "Core", "name": "Report Filter", diff --git a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py index a09d679bcc..375ea02e0e 100644 --- a/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py +++ b/frappe/core/doctype/role/patches/v13_set_default_desk_properties.py @@ -2,6 +2,7 @@ import frappe from ..role import desk_properties def execute(): + frappe.reload_doctype('role') for role in frappe.get_all('Role', ['name', 'desk_access']): role_doc = frappe.get_doc('Role', role.name) for key in desk_properties: diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py index bac68e30ab..7adfeba8d9 100644 --- a/frappe/core/doctype/role/role.py +++ b/frappe/core/doctype/role/role.py @@ -31,6 +31,9 @@ class Role(Document): def set_desk_properties(self): # set if desk_access is not allowed, unset all desk properties + if self.name == 'Guest': + self.desk_access = 0 + if not self.desk_access: for key in desk_properties: self.set(key, 0) diff --git a/frappe/core/doctype/server_script/server_script.json b/frappe/core/doctype/server_script/server_script.json index 94a48f196c..9aa7b5afe5 100644 --- a/frappe/core/doctype/server_script/server_script.json +++ b/frappe/core/doctype/server_script/server_script.json @@ -47,7 +47,7 @@ "fieldname": "doctype_event", "fieldtype": "Select", "label": "DocType Event", - "options": "Before Insert\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" + "options": "Before Insert\nBefore Validate\nBefore Save\nAfter Save\nBefore Submit\nAfter Submit\nBefore Cancel\nAfter Cancel\nBefore Delete\nAfter Delete\nBefore Save (Submitted Document)\nAfter Save (Submitted Document)" }, { "depends_on": "eval:doc.script_type==='API'", @@ -88,7 +88,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-03 22:42:02.708148", + "modified": "2021-01-03 18:50:14.767595", "modified_by": "Administrator", "module": "Core", "name": "Server Script", diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index 4dc4f12b34..12a8fa47fa 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -6,6 +6,7 @@ import frappe EVENT_MAP = { 'before_insert': 'Before Insert', 'after_insert': 'After Insert', + 'before_validate': 'Before Validate', 'validate': 'Before Save', 'on_update': 'After Save', 'before_submit': 'Before Submit', diff --git a/frappe/core/doctype/server_script/test_server_script.py b/frappe/core/doctype/server_script/test_server_script.py index 957cbbf72d..aac8b3deed 100644 --- a/frappe/core/doctype/server_script/test_server_script.py +++ b/frappe/core/doctype/server_script/test_server_script.py @@ -81,6 +81,7 @@ class TestServerScript(unittest.TestCase): def tearDownClass(cls): frappe.db.commit() frappe.db.sql('truncate `tabServer Script`') + frappe.cache().delete_value('server_script_map') def setUp(self): frappe.cache().delete_value('server_script_map') diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json index 278ec3e981..13dbc32620 100644 --- a/frappe/core/doctype/system_settings/system_settings.json +++ b/frappe/core/doctype/system_settings/system_settings.json @@ -358,7 +358,7 @@ "collapsible": 1, "fieldname": "email", "fieldtype": "Section Break", - "label": "EMail" + "label": "Email" }, { "description": "Your organization name and address for the email footer.", @@ -486,7 +486,7 @@ "icon": "fa fa-cog", "issingle": 1, "links": [], - "modified": "2020-11-30 18:52:22.161391", + "modified": "2020-12-30 18:52:22.161391", "modified_by": "Administrator", "module": "Core", "name": "System Settings", @@ -504,4 +504,4 @@ "sort_field": "modified", "sort_order": "ASC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js index a6b753c880..3548b4c913 100644 --- a/frappe/core/doctype/user/user.js +++ b/frappe/core/doctype/user/user.js @@ -37,6 +37,25 @@ frappe.ui.form.on('User', { } }, + module_profile: function(frm) { + if (frm.doc.module_profile) { + frappe.call({ + "method": "frappe.core.doctype.user.user.get_module_profile", + args: { + module_profile: frm.doc.module_profile + }, + callback: function(data) { + frm.set_value("block_modules", []); + $.each(data.message || [], function(i, v) { + let d = frm.add_child("block_modules"); + d.module = v.module; + }); + frm.module_editor && frm.module_editor.refresh(); + } + }); + } + }, + onload: function(frm) { frm.can_edit_roles = has_access_to_edit_user(); @@ -255,43 +274,3 @@ function get_roles_for_editing_user() { .filter(perm => perm.permlevel >= 1 && perm.write) .map(perm => perm.role) || ['System Manager']; } - -frappe.ModuleEditor = Class.extend({ - init: function(frm, wrapper) { - this.wrapper = $('
    ').appendTo(wrapper); - this.frm = frm; - this.make(); - }, - make: function() { - var me = this; - this.frm.doc.__onload.all_modules.forEach(function(m) { - $(repl('
    \ -
    ', {module: m})).appendTo(me.wrapper); - }); - this.bind(); - }, - refresh: function() { - var me = this; - this.wrapper.find(".block-module-check").prop("checked", true); - $.each(this.frm.doc.block_modules, function(i, d) { - me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false); - }); - }, - bind: function() { - var me = this; - this.wrapper.on("change", ".block-module-check", function() { - var module = $(this).attr('data-module'); - if($(this).prop("checked")) { - // remove from block_modules - me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) { - if (d.module != module) { - return d; - } - }); - } else { - me.frm.add_child("block_modules", {"module": module}); - } - }); - } -}); diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json index 0e684a3dd4..747ace5de6 100644 --- a/frappe/core/doctype/user/user.json +++ b/frappe/core/doctype/user/user.json @@ -55,6 +55,7 @@ "allowed_in_mentions", "user_emails", "sb_allow_modules", + "module_profile", "modules_html", "block_modules", "home_settings", @@ -301,7 +302,7 @@ "no_copy": 1 }, { - "default": "0", + "default": "1", "fieldname": "logout_all_sessions", "fieldtype": "Check", "label": "Logout From All Devices After Changing Password" @@ -594,6 +595,12 @@ "fieldtype": "Select", "label": "Desk Theme", "options": "Light\nDark" + }, + { + "fieldname": "module_profile", + "fieldtype": "Link", + "label": "Module Profile", + "options": "Module Profile" } ], "icon": "fa fa-user", @@ -654,10 +661,15 @@ "group": "Activity", "link_doctype": "ToDo", "link_fieldname": "owner" + }, + { + "group": "Integrations", + "link_doctype": "Token Cache", + "link_fieldname": "user" } ], "max_attachments": 5, - "modified": "2020-12-24 19:48:49.677800", + "modified": "2021-02-01 16:11:06.037543", "modified_by": "Administrator", "module": "Core", "name": "User", diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 6f5c805a54..142cc1ee26 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -75,6 +75,7 @@ class User(Document): self.validate_user_email_inbox() ask_pass_update() self.validate_roles() + self.validate_allowed_modules() self.validate_user_image() if self.language == "Loading...": @@ -89,6 +90,15 @@ class User(Document): self.set('roles', []) self.append_roles(*[role.role for role in role_profile.roles]) + def validate_allowed_modules(self): + if self.module_profile: + module_profile = frappe.get_doc('Module Profile', self.module_profile) + self.set('block_modules', []) + for d in module_profile.get('block_modules'): + self.append('block_modules', { + 'module': d.module + }) + def validate_user_image(self): if self.user_image and len(self.user_image) > 2000: frappe.throw(_("Not a valid User Image.")) @@ -98,16 +108,17 @@ class User(Document): self.share_with_self() clear_notifications(user=self.name) frappe.clear_cache(user=self.name) + now=frappe.flags.in_test or frappe.flags.in_install self.send_password_notification(self.__new_password) frappe.enqueue( 'frappe.core.doctype.user.user.create_contact', user=self, ignore_mandatory=True, - now=frappe.flags.in_test or frappe.flags.in_install + now=now ) if self.name not in ('Administrator', 'Guest') and not self.user_image: - frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name) - + frappe.enqueue('frappe.core.doctype.user.user.update_gravatar', name=self.name, now=now) + # Set user selected timezone if self.time_zone: frappe.defaults.set_default("time_zone", self.time_zone, self.name) @@ -288,16 +299,16 @@ class User(Document): from frappe.utils.user import get_user_fullname from frappe.utils import get_url - full_name = get_user_fullname(frappe.session['user']) - if full_name == "Guest": - full_name = "Administrator" + created_by = get_user_fullname(frappe.session['user']) + if created_by == "Guest": + created_by = "Administrator" args = { 'first_name': self.first_name or self.last_name or "user", 'user': self.name, 'title': subject, 'login_url': get_url(), - 'user_fullname': full_name + 'created_by': created_by } args.update(add_args) @@ -551,6 +562,10 @@ def get_perm_info(role): @frappe.whitelist(allow_guest=True) def update_password(new_password, logout_all_sessions=0, key=None, old_password=None): + #validate key to avoid key input like ['like', '%'], '', ['in', ['']] + if key and not isinstance(key, str): + frappe.throw(_('Invalid key type')) + result = test_password_strength(new_password, key, old_password) feedback = result.get("feedback", None) @@ -1002,9 +1017,14 @@ def send_token_via_email(tmp_id,token=None): hotp = pyotp.HOTP(otpsecret) frappe.sendmail( - recipients=user_email, sender=None, subject='Verification Code', - message='

    Your verification code is {0}

    '.format(hotp.at(int(count))), - delayed=False, retry=3) + recipients=user_email, + sender=None, + subject="Verification Code", + template="verification_code", + args=dict(code=hotp.at(int(count))), + delayed=False, + retry=3 + ) return True @@ -1038,6 +1058,11 @@ def get_role_profile(role_profile): roles = frappe.get_doc('Role Profile', {'role_profile': role_profile}) return roles.roles +@frappe.whitelist() +def get_module_profile(module_profile): + module_profile = frappe.get_doc('Module Profile', {'module_profile_name': module_profile}) + return module_profile.get('block_modules') + def update_roles(role_profile): users = frappe.get_all('User', filters={'role_profile_name': role_profile}) role_profile = frappe.get_doc('Role Profile', role_profile) diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py index 82dd2ab27e..7e0b4a49c6 100644 --- a/frappe/core/doctype/user_permission/test_user_permission.py +++ b/frappe/core/doctype/user_permission/test_user_permission.py @@ -3,6 +3,7 @@ # See license.txt from __future__ import unicode_literals from frappe.core.doctype.user_permission.user_permission import add_user_permissions +from frappe.permissions import has_user_permission import frappe import unittest @@ -10,7 +11,12 @@ import unittest class TestUserPermission(unittest.TestCase): def setUp(self): frappe.db.sql("""DELETE FROM `tabUser Permission` - WHERE `user` in ('test_bulk_creation_update@example.com', 'test_user_perm1@example.com')""") + WHERE `user` in ( + 'test_bulk_creation_update@example.com', + 'test_user_perm1@example.com', + 'nested_doc_user@example.com')""") + frappe.delete_doc_if_exists("DocType", "Person") + frappe.db.sql_ddl("DROP TABLE IF EXISTS `tabPerson`") def test_default_user_permission_validation(self): user = create_user('test_default_permission@example.com') @@ -108,6 +114,45 @@ class TestUserPermission(unittest.TestCase): self.assertIsNone(removed_applicable_second) self.assertEquals(is_created, 1) + def test_user_perm_for_nested_doctype(self): + """Test if descendants' visibility is controlled for a nested DocType.""" + from frappe.core.doctype.doctype.test_doctype import new_doctype + + user = create_user("nested_doc_user@example.com", "Blogger") + if not frappe.db.exists("DocType", "Person"): + doc = new_doctype("Person", + fields=[ + { + "label": "Person Name", + "fieldname": "person_name", + "fieldtype": "Data" + } + ], unique=0) + doc.is_tree = 1 + doc.insert() + + parent_record = frappe.get_doc( + {"doctype": "Person", "person_name": "Parent", "is_group": 1} + ).insert() + + child_record = frappe.get_doc( + {"doctype": "Person", "person_name": "Child", "is_group": 0, "parent_person": parent_record.name} + ).insert() + + add_user_permissions(get_params(user, "Person", parent_record.name)) + + # check if adding perm on a group record, makes child record visible + self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) + self.assertTrue(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) + + frappe.db.set_value("User Permission", {"allow": "Person", "for_value": parent_record.name}, "hide_descendants", 1) + frappe.cache().delete_value("user_permissions") + + # check if adding perm on a group record with hide_descendants enabled, + # hides child records + self.assertTrue(has_user_permission(frappe.get_doc("Person", parent_record.name), user.name)) + self.assertFalse(has_user_permission(frappe.get_doc("Person", child_record.name), user.name)) + def create_user(email, role="System Manager"): ''' create user with role system manager ''' if frappe.db.exists('User', email): @@ -119,7 +164,7 @@ def create_user(email, role="System Manager"): user.add_roles(role) return user -def get_params(user, doctype, docname, is_default=0, applicable=None): +def get_params(user, doctype, docname, is_default=0, hide_descendants=0, applicable=None): ''' Return param to insert ''' param = { "user": user.name, @@ -127,7 +172,8 @@ def get_params(user, doctype, docname, is_default=0, applicable=None): "docname":docname, "is_default": is_default, "apply_to_all_doctypes": 1, - "applicable_doctypes": [] + "applicable_doctypes": [], + "hide_descendants": hide_descendants } if applicable: param.update({"apply_to_all_doctypes": 0}) diff --git a/frappe/core/doctype/user_permission/user_permission.js b/frappe/core/doctype/user_permission/user_permission.js index 9f824b1350..4c3f5b4eb8 100644 --- a/frappe/core/doctype/user_permission/user_permission.js +++ b/frappe/core/doctype/user_permission/user_permission.js @@ -26,11 +26,15 @@ frappe.ui.form.on('User Permission', { () => frappe.set_route('query-report', 'Permitted Documents For User', { user: frm.doc.user })); frm.trigger('set_applicable_for_constraint'); + frm.trigger('toggle_hide_descendants'); }, allow: frm => { - if(frm.doc.for_value) { - frm.set_value('for_value', null); + if (frm.doc.allow) { + if (frm.doc.for_value) { + frm.set_value('for_value', null); + } + frm.trigger('toggle_hide_descendants'); } }, @@ -43,6 +47,11 @@ frappe.ui.form.on('User Permission', { if (frm.doc.apply_to_all_doctypes) { frm.set_value('applicable_for', null); } + }, + + toggle_hide_descendants: frm => { + let show = frappe.boot.nested_set_doctypes.includes(frm.doc.allow); + frm.toggle_display('hide_descendants', show); } diff --git a/frappe/core/doctype/user_permission/user_permission.json b/frappe/core/doctype/user_permission/user_permission.json index 33a8d58bbb..9cea0856c9 100644 --- a/frappe/core/doctype/user_permission/user_permission.json +++ b/frappe/core/doctype/user_permission/user_permission.json @@ -1,330 +1,116 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, - "allow_rename": 0, - "beta": 0, "creation": "2017-07-17 14:25:27.881871", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "user", + "allow", + "column_break_3", + "for_value", + "is_default", + "advanced_control_section", + "apply_to_all_doctypes", + "applicable_for", + "column_break_9", + "hide_descendants" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "user", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "User", - "length": 0, - "no_copy": 0, "options": "User", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "allow", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "Allow", - "length": 0, - "no_copy": 0, "options": "DocType", - "permlevel": 0, - "precision": "", - "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, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "column_break_3", - "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, - "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "for_value", "fieldtype": "Dynamic Link", - "hidden": 0, "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, "in_list_view": 1, "in_standard_filter": 1, "label": "For Value", - "length": 0, - "no_copy": 0, "options": "allow", - "permlevel": 0, - "precision": "", - "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, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, + "default": "0", "fieldname": "is_default", "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": "Is Default", - "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 + "label": "Is Default" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, "fieldname": "advanced_control_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": "Advanced Control", - "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 + "label": "Advanced Control" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", - "fetch_if_empty": 0, "fieldname": "apply_to_all_doctypes", "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": "Apply To All Document Types", - "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 + "label": "Apply To All Document Types" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:!doc.apply_to_all_doctypes", - "fetch_if_empty": 0, "fieldname": "applicable_for", "fieldtype": "Link", - "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": "Applicable For", - "length": 0, - "no_copy": 0, - "options": "DocType", - "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 + "options": "DocType" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "Hide descendant records of For Value.", + "fieldname": "hide_descendants", + "fieldtype": "Check", + "hidden": 1, + "label": "Hide Descendants" } ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-04-16 19:17:23.644724", + "links": [], + "modified": "2021-01-21 18:14:10.839381", "modified_by": "Administrator", "module": "Core", "name": "User Permission", - "name_case": "", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "show_name_in_global_search": 0, "sort_field": "modified", "sort_order": "DESC", "title_field": "user", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/frappe/core/doctype/user_permission/user_permission.py b/frappe/core/doctype/user_permission/user_permission.py index de14651d50..fbc788f6bf 100644 --- a/frappe/core/doctype/user_permission/user_permission.py +++ b/frappe/core/doctype/user_permission/user_permission.py @@ -49,7 +49,8 @@ class UserPermission(Document): 'name': ['!=', self.name] }, or_filters={ 'applicable_for': cstr(self.applicable_for), - 'apply_to_all_doctypes': 1 + 'apply_to_all_doctypes': 1, + 'hide_descendants': cstr(self.hide_descendants) }, limit=1) if overlap_exists: ref_link = frappe.get_desk_link(self.doctype, overlap_exists[0].name) @@ -91,13 +92,13 @@ def get_user_permissions(user=None): try: for perm in frappe.get_all('User Permission', - fields=['allow', 'for_value', 'applicable_for', 'is_default'], + fields=['allow', 'for_value', 'applicable_for', 'is_default', 'hide_descendants'], filters=dict(user=user)): meta = frappe.get_meta(perm.allow) add_doc_to_perm(perm, perm.for_value, perm.is_default) - if meta.is_nested_set(): + if meta.is_nested_set() and not perm.hide_descendants: decendants = frappe.db.get_descendants(perm.allow, perm.for_value) for doc in decendants: add_doc_to_perm(perm, doc, False) @@ -172,8 +173,8 @@ def check_applicable_doc_perm(user, doctype, docname): "allow": doctype, "for_value":docname, }) - for d in data: - applicable.append(d.applicable_for) + for permission in data: + applicable.append(permission.applicable_for) return applicable @@ -194,7 +195,8 @@ def add_user_permissions(data): data = json.loads(data) data = frappe._dict(data) - d = check_applicable_doc_perm(data.user, data.doctype, data.docname) + # get all doctypes on whom this permission is applied + perm_applied_docs = check_applicable_doc_perm(data.user, data.doctype, data.docname) exists = frappe.db.exists("User Permission", { "user": data.user, "allow": data.doctype, @@ -202,26 +204,27 @@ def add_user_permissions(data): "apply_to_all_doctypes": 1 }) if data.apply_to_all_doctypes == 1 and not exists: - remove_applicable(d, data.user, data.doctype, data.docname) - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, apply_to_all = 1) + remove_applicable(perm_applied_docs, data.user, data.doctype, data.docname) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, apply_to_all=1) return 1 elif len(data.applicable_doctypes) > 0 and data.apply_to_all_doctypes != 1: remove_apply_to_all(data.user, data.doctype, data.docname) - update_applicable(d, data.applicable_doctypes, data.user, data.doctype, data.docname) + update_applicable(perm_applied_docs, data.applicable_doctypes, data.user, data.doctype, data.docname) for applicable in data.applicable_doctypes : - if applicable not in d: - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable) + if applicable not in perm_applied_docs: + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable) elif exists: - insert_user_perm(data.user, data.doctype, data.docname, data.is_default, applicable = applicable) + insert_user_perm(data.user, data.doctype, data.docname, data.is_default, data.hide_descendants, applicable=applicable) return 1 return 0 -def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, applicable=None): +def insert_user_perm(user, doctype, docname, is_default=0, hide_descendants=0, apply_to_all=None, applicable=None): user_perm = frappe.new_doc("User Permission") user_perm.user = user user_perm.allow = doctype user_perm.for_value = docname user_perm.is_default = is_default + user_perm.hide_descendants = hide_descendants if applicable: user_perm.applicable_for = applicable user_perm.apply_to_all_doctypes = 0 @@ -229,8 +232,8 @@ def insert_user_perm(user, doctype, docname, is_default=0, apply_to_all=None, ap user_perm.apply_to_all_doctypes = 1 user_perm.insert() -def remove_applicable(d, user, doctype, docname): - for applicable_for in d: +def remove_applicable(perm_applied_docs, user, doctype, docname): + for applicable_for in perm_applied_docs: frappe.db.sql("""DELETE FROM `tabUser Permission` WHERE `user`=%s AND `applicable_for`=%s diff --git a/frappe/core/doctype/user_permission/user_permission_list.js b/frappe/core/doctype/user_permission/user_permission_list.js index bd3cdffe96..0ce66fa8e3 100644 --- a/frappe/core/doctype/user_permission/user_permission_list.js +++ b/frappe/core/doctype/user_permission/user_permission_list.js @@ -19,6 +19,7 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("is_default", "hidden", 1); dialog.set_df_property("apply_to_all_doctypes", "hidden", 1); dialog.set_df_property("applicable_doctypes", "hidden", 1); + dialog.set_df_property("hide_descendants", "hidden", 1); } }, { @@ -54,6 +55,10 @@ frappe.listview_settings['User Permission'] = { } } }, + { + fieldtype: "Section Break", + hide_border: 1 + }, { fieldname: 'is_default', label: __('Is Default'), @@ -74,6 +79,19 @@ frappe.listview_settings['User Permission'] = { } } }, + { + fieldtype: "Column Break" + }, + { + fieldname: 'hide_descendants', + label: __('Hide Descendants'), + fieldtype: 'Check', + hidden: 1 + }, + { + fieldtype: "Section Break", + hide_border: 1 + }, { label: __("Applicable Document Types"), fieldname: "applicable_doctypes", @@ -214,6 +232,9 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("is_default", "hidden", 0); dialog.set_df_property("apply_to_all_doctypes", "hidden", 0); dialog.set_value("apply_to_all_doctypes", "checked", 1); + let show = frappe.boot.nested_set_doctypes.includes(dialog.get_value("doctype")); + dialog.set_df_property("hide_descendants", "hidden", !show); + dialog.refresh(); }, on_docname_change: function(dialog, options, applicable) { @@ -233,6 +254,7 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("applicable_doctypes", "options", options); dialog.set_df_property("applicable_doctypes", "hidden", 1); } + dialog.refresh(); }, on_apply_to_all_doctypes_change: function(dialog, options) { @@ -243,5 +265,6 @@ frappe.listview_settings['User Permission'] = { dialog.set_df_property("applicable_doctypes", "options", options); dialog.set_df_property("applicable_doctypes", "hidden", 1); } + dialog.refresh_sections(); } -}; \ No newline at end of file +}; diff --git a/frappe/core/doctype/version/version_view.html b/frappe/core/doctype/version/version_view.html index 5383be82a1..67f005ed4c 100644 --- a/frappe/core/doctype/version/version_view.html +++ b/frappe/core/doctype/version/version_view.html @@ -21,7 +21,7 @@ {{ item[1] }} {{ item[2] }} - {% endif %} + {% endfor %} {% endif %} @@ -58,7 +58,7 @@ - {% endif %} + {% endfor %} @@ -93,4 +93,4 @@ {% endfor %} {% endif %} -
    \ No newline at end of file + diff --git a/frappe/core/page/background_jobs/background_jobs.js b/frappe/core/page/background_jobs/background_jobs.js index 789784d5aa..cabe91375f 100644 --- a/frappe/core/page/background_jobs/background_jobs.js +++ b/frappe/core/page/background_jobs/background_jobs.js @@ -1,5 +1,5 @@ frappe.pages["background_jobs"].on_page_load = (wrapper) => { - background_job = new BackgroundJobs(wrapper); + const background_job = new BackgroundJobs(wrapper); $(wrapper).bind('show', () => { background_job.show(); @@ -20,10 +20,12 @@ class BackgroundJobs { this.show_failed = false; this.show_failed_button = this.page.add_inner_button(__("Show Failed Jobs"), () => { - this.show_failed = !this.show_failed - this.show_failed_button && this.show_failed_button.text( - this.show_failed ? __("Hide Failed Jobs") : __("Show Failed Jobs") - ) + this.show_failed = !this.show_failed; + if (this.show_failed_button) { + this.show_failed_button.text( + this.show_failed ? __("Hide Failed Jobs") : __("Show Failed Jobs") + ); + } }); $(frappe.render_template('background_jobs_outer')).appendTo(this.page.body); diff --git a/frappe/core/page/dashboard_view/dashboard_view.js b/frappe/core/page/dashboard_view/dashboard_view.js index 073245a1ef..686d11c6bf 100644 --- a/frappe/core/page/dashboard_view/dashboard_view.js +++ b/frappe/core/page/dashboard_view/dashboard_view.js @@ -183,7 +183,7 @@ class Dashboard { frappe.db.get_list('Dashboard').then(dashboards => { dashboards.map(dashboard => { let name = dashboard.name; - if(name != this.dashboard_name){ + if (name != this.dashboard_name) { this.page.add_menu_item(name, () => frappe.set_route("dashboard-view", name), 1); } }); diff --git a/frappe/core/page/desktop/desktop.js b/frappe/core/page/desktop/desktop.js deleted file mode 100644 index cc36a5a4e9..0000000000 --- a/frappe/core/page/desktop/desktop.js +++ /dev/null @@ -1,3 +0,0 @@ -frappe.pages['desktop'].on_page_load = function() { - frappe.utils.set_title(__("Home")); -}; \ No newline at end of file diff --git a/frappe/core/page/desktop/desktop.json b/frappe/core/page/desktop/desktop.json deleted file mode 100644 index 66bbfbfd40..0000000000 --- a/frappe/core/page/desktop/desktop.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "content": null, - "creation": "2019-01-29 13:11:48.872579", - "docstatus": 0, - "doctype": "Page", - "icon": "icon-th", - "idx": 0, - "modified": "2019-01-29 13:11:48.872579", - "modified_by": "Administrator", - "module": "Core", - "name": "desktop", - "owner": "Administrator", - "page_name": "desktop", - "roles": [ - { - "role": "All" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0, - "title": "Desktop" -} \ No newline at end of file diff --git a/frappe/core/page/permission_manager/permission_manager.js b/frappe/core/page/permission_manager/permission_manager.js index 90747b8aae..41cc900a97 100644 --- a/frappe/core/page/permission_manager/permission_manager.js +++ b/frappe/core/page/permission_manager/permission_manager.js @@ -98,7 +98,7 @@ frappe.PermissionEngine = class PermissionEngine { } reset_std_permissions(data) { - let doctype = this.get_doctype() + let doctype = this.get_doctype(); let d = frappe.confirm(__("Reset Permissions for {0}?", [doctype]), () => { return frappe.call({ module: "frappe.core", @@ -117,7 +117,7 @@ frappe.PermissionEngine = class PermissionEngine { let rights = this.rights .filter((r) => d[r]) .map((r) => { - return __(toTitle(frappe.unscrub(r))) + return __(toTitle(frappe.unscrub(r))); }); d.rights = rights.join(", "); @@ -153,16 +153,14 @@ frappe.PermissionEngine = class PermissionEngine { this.page.clear_primary_action(); if (!this.doctype_select) { - this.set_empty_message(__("Loading")) - return + return this.set_empty_message(__("Loading")); } let doctype = this.get_doctype(); let role = this.get_role(); if (!doctype && !role) { - this.set_empty_message(__("Select Document Type or Role to start.")) - return; + return this.set_empty_message(__("Select Document Type or Role to start.")); } // get permissions @@ -202,7 +200,7 @@ frappe.PermissionEngine = class PermissionEngine { [__("Level"), 40], [__("Permissions"), 350], ["", 40] - ] + ]; table_columns.forEach((col) => { $("") @@ -292,8 +290,8 @@ frappe.PermissionEngine = class PermissionEngine { } get rights() { - return ["read", "write", "create", "delete", "submit", "cancel", "amend", - "print", "email", "report", "import", "export", "set_user_permissions", "share"] + return ["select", "read", "write", "create", "delete", "submit", "cancel", "amend", + "print", "email", "report", "import", "export", "set_user_permissions", "share"]; } set_show_users(cell, role) { @@ -436,7 +434,7 @@ frappe.PermissionEngine = class PermissionEngine { d.show(); }, "small-add" - ) + ); } make_reset_button() { @@ -459,4 +457,4 @@ frappe.PermissionEngine = class PermissionEngine { return frappe.get_children("DocType", doctype, "fields", { fieldtype: "Link", options: ["not in", ["User", '[Select]']] }); } -} +}; diff --git a/frappe/core/page/permission_manager/permission_manager.py b/frappe/core/page/permission_manager/permission_manager.py index 637b526d5c..be8921e2ff 100644 --- a/frappe/core/page/permission_manager/permission_manager.py +++ b/frappe/core/page/permission_manager/permission_manager.py @@ -77,6 +77,18 @@ def add(parent, role, permlevel): @frappe.whitelist() def update(doctype, role, permlevel, ptype, value=None): + """Update role permission params + + Args: + doctype (str): Name of the DocType to update params for + role (str): Role to be updated for, eg "Website Manager". + permlevel (int): perm level the provided rule applies to + ptype (str): permission type, example "read", "delete", etc. + value (None, optional): value for ptype, None indicates False + + Returns: + str: Refresh flag is permission is updated successfully + """ frappe.only_for("System Manager") out = update_permission_property(doctype, role, permlevel, ptype, value) return 'refresh' if out else None @@ -92,7 +104,7 @@ def remove(doctype, role, permlevel): if not frappe.get_all('Custom DocPerm', dict(parent=doctype)): frappe.throw(_('There must be atleast one permission rule.'), title=_('Cannot Remove')) - validate_permissions_for_doctype(doctype, for_remove=True) + validate_permissions_for_doctype(doctype, for_remove=True, alert=True) @frappe.whitelist() def reset(doctype): diff --git a/frappe/core/page/recorder/recorder.js b/frappe/core/page/recorder/recorder.js index f38af41af0..4d6d6aa84c 100644 --- a/frappe/core/page/recorder/recorder.js +++ b/frappe/core/page/recorder/recorder.js @@ -2,7 +2,8 @@ frappe.pages['recorder'].on_page_load = function(wrapper) { frappe.ui.make_app_page({ parent: wrapper, title: 'Recorder', - single_column: true + single_column: true, + card_layout: true }); frappe.recorder = new Recorder(wrapper); diff --git a/frappe/core/page/space/space.js b/frappe/core/page/space/space.js deleted file mode 100644 index e781c56d8b..0000000000 --- a/frappe/core/page/space/space.js +++ /dev/null @@ -1,12 +0,0 @@ -frappe.pages['space'].on_page_load = function (wrapper) { - frappe.ui.make_app_page({ - parent: wrapper, - name: 'space', - title: __("Workspace"), - }); - - frappe.workspace = new frappe.views.Workspace(wrapper); - $(wrapper).bind('show', function () { - frappe.workspace.show(); - }); -} \ No newline at end of file diff --git a/frappe/core/page/space/space.json b/frappe/core/page/space/space.json deleted file mode 100644 index d5f0b4c9db..0000000000 --- a/frappe/core/page/space/space.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "content": null, - "creation": "2020-02-27 15:07:57.124916", - "docstatus": 0, - "doctype": "Page", - "icon": "icon-th", - "idx": 0, - "modified": "2020-12-16 14:22:05.591912", - "modified_by": "Administrator", - "module": "Core", - "name": "space", - "owner": "Administrator", - "page_name": "space", - "roles": [ - { - "role": "All" - } - ], - "script": null, - "standard": "Yes", - "style": null, - "system_page": 0 -} \ No newline at end of file diff --git a/frappe/core/workspace/build/build.json b/frappe/core/workspace/build/build.json new file mode 100644 index 0000000000..c4bde55d7f --- /dev/null +++ b/frappe/core/workspace/build/build.json @@ -0,0 +1,211 @@ +{ + "cards_label": "Elements", + "category": "Modules", + "charts": [], + "creation": "2021-01-02 10:51:16.579957", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Workspace", + "extends_another_page": 0, + "hide_custom": 0, + "icon": "tool", + "idx": 0, + "is_standard": 1, + "label": "Build", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Modules", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Def", + "link_to": "Module Def", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workspace", + "link_to": "Workspace", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Module Onboarding", + "link_to": "Module Onboarding", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Block Module", + "link_to": "Block Module", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Models", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "DocType", + "link_to": "DocType", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workflow", + "link_to": "Workflow", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Views", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Report", + "link_to": "Report", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Print Format", + "link_to": "Print Format", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Workspace", + "link_to": "Workspace", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Dashboard", + "link_to": "Dashboard", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Scripting", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Server Script", + "link_to": "Server Script", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Custom Script", + "link_to": "Custom Script", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Scheduled Job Type", + "link_to": "Scheduled Job Type", + "link_type": "DocType", + "onboard": 0, + "only_for": "", + "type": "Link" + } + ], + "modified": "2021-01-02 14:03:15.029699", + "modified_by": "Administrator", + "module": "Core", + "name": "Build", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "shortcuts": [ + { + "doc_view": "", + "label": "DocType", + "link_to": "DocType", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Workspace", + "link_to": "Workspace", + "type": "DocType" + }, + { + "doc_view": "", + "label": "Report", + "link_to": "Report", + "type": "DocType" + } + ] +} \ No newline at end of file diff --git a/frappe/custom/doctype/custom_script/custom_script.js b/frappe/custom/doctype/custom_script/custom_script.js index dc449c506a..711e7d1796 100644 --- a/frappe/custom/doctype/custom_script/custom_script.js +++ b/frappe/custom/doctype/custom_script/custom_script.js @@ -33,6 +33,7 @@ frappe.ui.form.on('Custom Script', { } ], primary_action: ({ cdt }) => { + cdt = d.get_field('cdt').value; frm.events.add_script_for_doctype(frm, cdt); d.hide(); } diff --git a/frappe/custom/doctype/customize_form/customize_form.js b/frappe/custom/doctype/customize_form/customize_form.js index 17343573ed..79978a49d7 100644 --- a/frappe/custom/doctype/customize_form/customize_form.js +++ b/frappe/custom/doctype/customize_form/customize_form.js @@ -4,16 +4,35 @@ frappe.provide("frappe.customize_form"); frappe.ui.form.on("Customize Form", { + setup: function(frm) { + // save the last setting if refreshing + window.addEventListener("beforeunload", () => { + if (frm.doc.doc_type && frm.doc.doc_type != "undefined") { + localStorage["customize_doctype"] = frm.doc.doc_type; + } + }); + }, + onload: function(frm) { frm.disable_save(); frm.set_query("doc_type", function() { return { translate_values: false, filters: [ - ['DocType', 'issingle', '=', 0], - ['DocType', 'custom', '=', 0], - ['DocType', 'name', 'not in', frappe.model.core_doctypes_list], - ['DocType', 'restrict_to_domain', 'in', frappe.boot.active_domains] + ["DocType", "issingle", "=", 0], + ["DocType", "custom", "=", 0], + [ + "DocType", + "name", + "not in", + frappe.model.core_doctypes_list + ], + [ + "DocType", + "restrict_to_domain", + "in", + frappe.boot.active_domains + ] ] }; }); @@ -21,15 +40,15 @@ frappe.ui.form.on("Customize Form", { frm.set_query("default_print_format", function() { return { filters: { - 'print_format_type': ['!=', 'JS'], - 'doc_type': ['=', frm.doc.doc_type] + print_format_type: ["!=", "JS"], + doc_type: ["=", frm.doc.doc_type] } - } + }; }); $(frm.wrapper).on("grid-row-render", function(e, grid_row) { - if (grid_row.doc && grid_row.doc.fieldtype=="Section Break") { - $(grid_row.row).css({"font-weight": "bold"}); + if (grid_row.doc && grid_row.doc.fieldtype == "Section Break") { + $(grid_row.row).css({ "font-weight": "bold" }); } }); @@ -40,12 +59,6 @@ frappe.ui.form.on("Customize Form", { $(frm.wrapper).on("grid-move-row", function(e, frm) { frm.trigger("setup_sortable"); }); - - if (localStorage['customize_doctype']) { - // set default value from customize form - frm.set_value('doc_type', localStorage['customize_doctype']); - } - }, doc_type: function(frm) { @@ -59,7 +72,6 @@ frappe.ui.form.on("Customize Form", { if (r._server_messages && r._server_messages.length) { frm.set_value("doc_type", ""); } else { - localStorage['customize_doctype'] = frm.doc.doc_type; frm.refresh(); frm.trigger("setup_sortable"); } @@ -72,9 +84,11 @@ frappe.ui.form.on("Customize Form", { }, setup_sortable: function(frm) { - frm.page.body.find('.highlight').removeClass('highlight'); + frm.page.body.find(".highlight").removeClass("highlight"); frm.doc.fields.forEach(function(f, i) { - var data_row = frm.page.body.find('[data-fieldname="fields"] [data-idx="'+ f.idx +'"] .data-row'); + var data_row = frm.page.body.find( + '[data-fieldname="fields"] [data-idx="' + f.idx + '"] .data-row' + ); if (f.is_custom_field) { data_row.addClass("highlight"); @@ -82,9 +96,13 @@ frappe.ui.form.on("Customize Form", { f._sortable = false; } if (f.fieldtype == "Table") { - frm.add_custom_button(f.options, function() { - frm.set_value('doc_type', f.options); - }, __('Customize Child Table')); + frm.add_custom_button( + f.options, + function() { + frm.set_value("doc_type", f.options); + }, + __("Customize Child Table") + ); } }); frm.fields_dict.fields.grid.refresh(); @@ -97,36 +115,91 @@ frappe.ui.form.on("Customize Form", { if (frm.doc.doc_type) { frappe.customize_form.set_primary_action(frm); - frm.add_custom_button(__('Go to {0} List', [frm.doc.doc_type]), function() { - frappe.set_route('List', frm.doc.doc_type); - }, __('Actions')); + frm.add_custom_button( + __("Go to {0} List", [frm.doc.doc_type]), + function() { + frappe.set_route("List", frm.doc.doc_type); + }, + __("Actions") + ); - frm.add_custom_button(__('Reload'), function() { - frm.script_manager.trigger("doc_type"); - }, __('Actions')); + frm.add_custom_button( + __("Reload"), + function() { + frm.script_manager.trigger("doc_type"); + }, + __("Actions") + ); - frm.add_custom_button(__('Reset to defaults'), function() { - frappe.customize_form.confirm(__('Remove all customizations?'), frm); - }, __('Actions')); + frm.add_custom_button( + __("Reset to defaults"), + function() { + frappe.customize_form.confirm( + __("Remove all customizations?"), + frm + ); + }, + __("Actions") + ); - frm.add_custom_button(__('Set Permissions'), function() { - frappe.set_route('permission-manager', frm.doc.doc_type); - }, __('Actions')); + frm.add_custom_button( + __("Set Permissions"), + function() { + frappe.set_route("permission-manager", frm.doc.doc_type); + }, + __("Actions") + ); + } - if (frappe.boot.developer_mode) { - frm.add_custom_button(__('Export Customizations'), function() { + frm.events.setup_export(frm); + frm.events.setup_sort_order(frm); + frm.events.set_default_doc_type(frm); + }, + + set_default_doc_type(frm) { + let doc_type; + if (frappe.route_options && frappe.route_options.doc_type) { + doc_type = frappe.route_options.doc_type; + frappe.route_options = null; + localStorage.removeItem("customize_doctype"); + } + if (!doc_type) { + doc_type = localStorage.getItem("customize_doctype"); + } + if (doc_type) { + setTimeout(() => frm.set_value("doc_type", doc_type), 1000); + } + }, + + setup_export(frm) { + if (frappe.boot.developer_mode) { + frm.add_custom_button( + __("Export Customizations"), + function() { frappe.prompt( [ - {fieldtype:'Link', fieldname:'module', options:'Module Def', - label: __('Module to Export')}, - {fieldtype:'Check', fieldname:'sync_on_migrate', - label: __('Sync on Migrate'), 'default': 1}, - {fieldtype:'Check', fieldname:'with_permissions', - label: __('Export Custom Permissions'), 'default': 1}, + { + fieldtype: "Link", + fieldname: "module", + options: "Module Def", + label: __("Module to Export") + }, + { + fieldtype: "Check", + fieldname: "sync_on_migrate", + label: __("Sync on Migrate"), + default: 1 + }, + { + fieldtype: "Check", + fieldname: "with_permissions", + label: __("Export Custom Permissions"), + default: 1 + } ], function(data) { frappe.call({ - method: 'frappe.modules.utils.export_customizations', + method: "frappe.modules.utils.export_customizations", args: { doctype: frm.doc.doc_type, module: data.module, @@ -135,27 +208,25 @@ frappe.ui.form.on("Customize Form", { } }); }, - __("Select Module")); - }, __('Actions')); - } + __("Select Module") + ); + }, + __("Actions") + ); } + }, + setup_sort_order(frm) { // sort order select if (frm.doc.doc_type) { - var fields = $.map(frm.doc.fields, - function(df) { - return frappe.model.is_value_type(df.fieldtype) ? df.fieldname : null; - }); + var fields = $.map(frm.doc.fields, function(df) { + return frappe.model.is_value_type(df.fieldtype) + ? df.fieldname + : null; + }); fields = ["", "name", "modified"].concat(fields); frm.set_df_property("sort_field", "options", fields); } - - if (frappe.route_options && frappe.route_options.doc_type) { - setTimeout(function() { - frm.set_value("doc_type", frappe.route_options.doc_type); - frappe.route_options = null; - }, 1000); - } } }); diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index 82513783c7..50acab46b5 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -455,11 +455,15 @@ class CustomizeForm(Document): self.fetch_to_customize() def reset_customization(doctype): - frappe.db.sql(""" - DELETE FROM `tabProperty Setter` WHERE doc_type=%s - and `field_name`!='naming_series' - and `property`!='options' - """, doctype) + setters = frappe.get_all("Property Setter", filters={ + 'doc_type': doctype, + 'field_name': ['!=', 'naming_series'], + 'property': ['!=', 'options'] + }, pluck='name') + + for setter in setters: + frappe.delete_doc("Property Setter", setter) + frappe.clear_cache(doctype=doctype) doctype_properties = { diff --git a/frappe/custom/doctype/doctype_layout/doctype_layout.py b/frappe/custom/doctype/doctype_layout/doctype_layout.py index b580ac8f56..a4fe9a9bce 100644 --- a/frappe/custom/doctype/doctype_layout/doctype_layout.py +++ b/frappe/custom/doctype/doctype_layout/doctype_layout.py @@ -4,12 +4,11 @@ from __future__ import unicode_literals -import frappe from frappe.model.document import Document -from frappe.desk.utils import get_doctype_route +from frappe.desk.utils import slug class DocTypeLayout(Document): def validate(self): if not self.route: - self.route = get_doctype_route(self.name) + self.route = slug(self.name) diff --git a/frappe/desk/doctype/desk_chart/__init__.py b/frappe/custom/doctype/test_rename_new/__init__.py similarity index 100% rename from frappe/desk/doctype/desk_chart/__init__.py rename to frappe/custom/doctype/test_rename_new/__init__.py diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.js b/frappe/custom/doctype/test_rename_new/test_rename_new.js new file mode 100644 index 0000000000..f38f9486f9 --- /dev/null +++ b/frappe/custom/doctype/test_rename_new/test_rename_new.js @@ -0,0 +1,8 @@ +// Copyright (c) 2021, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Test rename new', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.json b/frappe/custom/doctype/test_rename_new/test_rename_new.json new file mode 100644 index 0000000000..0b089091a1 --- /dev/null +++ b/frappe/custom/doctype/test_rename_new/test_rename_new.json @@ -0,0 +1,42 @@ +{ + "actions": [], + "creation": "2021-01-13 12:47:03.572640", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "random" + ], + "fields": [ + { + "fieldname": "random", + "fieldtype": "Data", + "label": "random" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-01-13 12:47:03.572640", + "modified_by": "Administrator", + "module": "Custom", + "name": "Test rename new", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "route": "test-rename", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/custom/doctype/test_rename_new/test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_rename_new.py new file mode 100644 index 0000000000..aa5984e466 --- /dev/null +++ b/frappe/custom/doctype/test_rename_new/test_rename_new.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class Testrenamenew(Document): + pass diff --git a/frappe/custom/doctype/test_rename_new/test_test_rename_new.py b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py new file mode 100644 index 0000000000..554efbae45 --- /dev/null +++ b/frappe/custom/doctype/test_rename_new/test_test_rename_new.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class Testrenamenew(unittest.TestCase): + pass diff --git a/frappe/desk/calendar.py b/frappe/desk/calendar.py index ce9fb7f177..064d870092 100644 --- a/frappe/desk/calendar.py +++ b/frappe/desk/calendar.py @@ -29,6 +29,7 @@ def get_event_conditions(doctype, filters=None): def get_events(doctype, start, end, field_map, filters=None, fields=None): field_map = frappe._dict(json.loads(field_map)) + fields = frappe.parse_json(fields) doc_meta = frappe.get_meta(doctype) for d in doc_meta.fields: diff --git a/frappe/desk/desktop.py b/frappe/desk/desktop.py index 1fa3f61752..0ded8e0717 100644 --- a/frappe/desk/desktop.py +++ b/frappe/desk/desktop.py @@ -108,9 +108,18 @@ class Workspace: 'extends': self.page_name, 'for_user': frappe.session.user } - pages = frappe.get_all("Workspace", filters=filters, limit=1) - if pages: - return frappe.get_cached_doc("Workspace", pages[0]) + user_pages = frappe.get_all("Workspace", filters=filters, limit=1) + if user_pages: + return frappe.get_cached_doc("Workspace", user_pages[0]) + + filters = { + 'extends_another_page': 1, + 'extends': self.page_name, + 'is_default': 1 + } + default_page = frappe.get_all("Workspace", filters=filters, limit=1) + if default_page: + return frappe.get_cached_doc("Workspace", default_page[0]) self.get_pages_to_extend() return frappe.get_cached_doc("Workspace", self.page_name) @@ -361,57 +370,39 @@ def get_desktop_page(page): } @frappe.whitelist() -def get_desk_sidebar_items(flatten=False, cache=True): - """Get list of sidebar items for desk - """ +def get_desk_sidebar_items(): + """Get list of sidebar items for desk""" + + # don't get domain restricted pages + blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() + + filters = { + 'restrict_to_domain': ['in', frappe.get_active_domains()], + 'extends_another_page': 0, + 'for_user': '', + 'module': ['not in', blocked_modules] + } + + if not frappe.local.conf.developer_mode: + filters['developer_mode_only'] = '0' + + # pages sorted based on pinned to top and then by name + order_by = "pin_to_top desc, pin_to_bottom asc, name asc" + all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"], + filters=filters, order_by=order_by, ignore_permissions=True) pages = [] - _cache = frappe.cache() - if cache: - pages = _cache.get_value("desk_sidebar_items", user=frappe.session.user) - if not pages or not cache: - # don't get domain restricted pages - blocked_modules = frappe.get_doc('User', frappe.session.user).get_blocked_modules() + # Filter Page based on Permission + for page in all_pages: + try: + wspace = Workspace(page.get('name'), True) + if wspace.is_page_allowed(): + pages.append(page) + page['label'] = _(page.get('name')) + except frappe.PermissionError: + pass - filters = { - 'restrict_to_domain': ['in', frappe.get_active_domains()], - 'extends_another_page': 0, - 'for_user': '', - 'module': ['not in', blocked_modules] - } - - if not frappe.local.conf.developer_mode: - filters['developer_mode_only'] = '0' - - # pages sorted based on pinned to top and then by name - order_by = "pin_to_top desc, pin_to_bottom asc, name asc" - all_pages = frappe.get_all("Workspace", fields=["name", "category", "icon", "module"], - filters=filters, order_by=order_by, ignore_permissions=True) - pages = [] - - # Filter Page based on Permission - for page in all_pages: - try: - wspace = Workspace(page.get('name'), True) - if wspace.is_page_allowed(): - pages.append(page) - except frappe.PermissionError: - pass - - _cache.set_value("desk_sidebar_items", pages, frappe.session.user) - - if flatten: - return pages - - from collections import defaultdict - sidebar_items = defaultdict(list) - - # The order will be maintained while categorizing - for page in pages: - # Translate label - page['label'] = _(page.get('name')) - sidebar_items[page["category"]].append(page) - return sidebar_items + return pages def get_table_with_counts(): counts = frappe.cache().get_value("information_schema:counts") @@ -490,7 +481,7 @@ def get_custom_workspace_for_user(page): @frappe.whitelist() def save_customization(page, config): - """Save customizations as a separate doctype in Desk page per user + """Save customizations as a separate doctype in Workspace per user Args: page (string): Name of the page to be edited @@ -516,9 +507,9 @@ def save_customization(page, config): config = _dict(loads(config)) if config.charts: - page_doc.charts = prepare_widget(config.charts, "Desk Chart", "charts") + page_doc.charts = prepare_widget(config.charts, "Workspace Chart", "charts") if config.shortcuts: - page_doc.shortcuts = prepare_widget(config.shortcuts, "Desk Shortcut", "shortcuts") + page_doc.shortcuts = prepare_widget(config.shortcuts, "Workspace Shortcut", "shortcuts") if config.cards: page_doc.build_links_table_from_cards(config.cards) @@ -596,12 +587,11 @@ def update_onboarding_step(name, field, value): @frappe.whitelist() def reset_customization(page): - """Reset desk page customizations for a user + """Reset workspace customizations for a user Args: page (string): Name of the page to be reset """ - original_page = frappe.get_doc("Workspace", page) page_doc = get_custom_workspace_for_user(page) page_doc.delete() diff --git a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py index 2fa36b5514..b19f6cf9f0 100644 --- a/frappe/desk/doctype/dashboard_chart/dashboard_chart.py +++ b/frappe/desk/doctype/dashboard_chart/dashboard_chart.py @@ -73,7 +73,7 @@ def has_permission(doc, ptype, user): if doc.report_name in allowed_reports: return True else: - allowed_doctypes = [frappe.permissions.get_doctypes_with_read()] + allowed_doctypes = frappe.permissions.get_doctypes_with_read() if doc.document_type in allowed_doctypes: return True diff --git a/frappe/desk/doctype/number_card/number_card.py b/frappe/desk/doctype/number_card/number_card.py index 6bddd09fc7..7d1a697f6b 100644 --- a/frappe/desk/doctype/number_card/number_card.py +++ b/frappe/desk/doctype/number_card/number_card.py @@ -86,7 +86,7 @@ def get_result(doc, filters, to_date=None): filters = frappe.parse_json(filters) if not filters: - filters = [] + filters = [] if to_date: filters.append([doc.document_type, 'creation', '<', to_date]) @@ -107,9 +107,13 @@ def get_percentage_difference(doc, filters, result): return previous_result = calculate_previous_result(doc, filters) - difference = (result - previous_result)/100.0 - - return difference + if previous_result == 0: + return None + else: + if result == previous_result: + return 0 + else: + return ((result/previous_result)-1)*100.0 def calculate_previous_result(doc, filters): @@ -197,4 +201,4 @@ def add_card_to_dashboard(args): card.save() dashboard.append('cards', dashboard_link) - dashboard.save() \ No newline at end of file + dashboard.save() diff --git a/frappe/desk/doctype/workspace/workspace.js b/frappe/desk/doctype/workspace/workspace.js index bca1ee1738..19d429f9f6 100644 --- a/frappe/desk/doctype/workspace/workspace.js +++ b/frappe/desk/doctype/workspace/workspace.js @@ -2,10 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('Workspace', { + setup: function() { + frappe.meta.get_field('Workspace Link', 'only_for').no_default = true; + }, + refresh: function(frm) { frm.enable_save(); frm.get_field("is_standard").toggle(frappe.boot.developer_mode); - frm.get_field("extends_another_page").toggle(frappe.boot.developer_mode); frm.get_field("developer_mode_only").toggle(frappe.boot.developer_mode); if (frm.doc.for_user) { diff --git a/frappe/desk/doctype/workspace/workspace.json b/frappe/desk/doctype/workspace/workspace.json index e71e6f6bfb..fff766a3bf 100644 --- a/frappe/desk/doctype/workspace/workspace.json +++ b/frappe/desk/doctype/workspace/workspace.json @@ -17,6 +17,7 @@ "onboarding", "column_break_3", "extends_another_page", + "is_default", "is_standard", "developer_mode_only", "disable_user_customization", @@ -42,6 +43,7 @@ }, { "collapsible": 1, + "collapsible_depends_on": "charts", "fieldname": "section_break_2", "fieldtype": "Section Break", "label": "Dashboards" @@ -50,14 +52,14 @@ "fieldname": "charts", "fieldtype": "Table", "label": "Charts", - "options": "Desk Chart" + "options": "Workspace Chart" }, { "depends_on": "eval:!doc.extends_another_page || !doc.is_standard || frappe.boot.developer_mode", "fieldname": "shortcuts", "fieldtype": "Table", "label": "Shortcuts", - "options": "Desk Shortcut" + "options": "Workspace Shortcut" }, { "fieldname": "restrict_to_domain", @@ -139,12 +141,14 @@ }, { "collapsible": 1, + "collapsible_depends_on": "shortcuts", "fieldname": "section_break_15", "fieldtype": "Section Break", "label": "Shortcuts" }, { "collapsible": 1, + "collapsible_depends_on": "links", "fieldname": "section_break_18", "fieldtype": "Section Break", "label": "Link Cards" @@ -202,11 +206,19 @@ "fieldname": "links", "fieldtype": "Table", "label": "Links", - "options": "Desk Link" - } + "options": "Workspace Link" + }, + { + "default": "0", + "depends_on": "extends_another_page", + "description": "Sets the current page as default for all users", + "fieldname": "is_default", + "fieldtype": "Check", + "label": "Is Default" + } ], "links": [], - "modified": "2020-12-01 13:36:26.827062", + "modified": "2021-01-21 12:09:36.156614", "modified_by": "Administrator", "module": "Desk", "name": "Workspace", diff --git a/frappe/desk/doctype/workspace/workspace.py b/frappe/desk/doctype/workspace/workspace.py index 3c7ba10bcb..0934138821 100644 --- a/frappe/desk/doctype/workspace/workspace.py +++ b/frappe/desk/doctype/workspace/workspace.py @@ -4,17 +4,25 @@ from __future__ import unicode_literals import frappe -from frappe import _, _dict -from frappe.utils.data import validate_json_string +from frappe import _ from frappe.modules.export_file import export_to_files from frappe.model.document import Document +from frappe.desk.utils import validate_route_conflict -from json import loads, dumps +from json import loads class Workspace(Document): def validate(self): if (self.is_standard and not frappe.conf.developer_mode and not disable_saving_as_standard()): frappe.throw(_("You need to be in developer mode to edit this document")) + validate_route_conflict(self.doctype, self.name) + + duplicate_exists = frappe.db.exists("Workspace", { + "name": ["!=", self.name], 'is_default': 1, 'extends': self.extends + }) + + if self.is_default and self.name and duplicate_exists: + frappe.throw(_("You can only have one default page that extends a particular standard page.")) def on_update(self): if disable_saving_as_standard(): diff --git a/frappe/desk/doctype/desk_link/__init__.py b/frappe/desk/doctype/workspace_chart/__init__.py similarity index 100% rename from frappe/desk/doctype/desk_link/__init__.py rename to frappe/desk/doctype/workspace_chart/__init__.py diff --git a/frappe/desk/doctype/desk_chart/desk_chart.json b/frappe/desk/doctype/workspace_chart/workspace_chart.json similarity index 90% rename from frappe/desk/doctype/desk_chart/desk_chart.json rename to frappe/desk/doctype/workspace_chart/workspace_chart.json index 09deefd59d..0d800496af 100644 --- a/frappe/desk/doctype/desk_chart/desk_chart.json +++ b/frappe/desk/doctype/workspace_chart/workspace_chart.json @@ -26,10 +26,10 @@ ], "istable": 1, "links": [], - "modified": "2020-03-31 13:33:13.128804", + "modified": "2021-01-12 13:13:25.781925", "modified_by": "Administrator", "module": "Desk", - "name": "Desk Chart", + "name": "Workspace Chart", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/frappe/desk/doctype/workspace_chart/workspace_chart.py b/frappe/desk/doctype/workspace_chart/workspace_chart.py new file mode 100644 index 0000000000..0bb6194d2e --- /dev/null +++ b/frappe/desk/doctype/workspace_chart/workspace_chart.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WorkspaceChart(Document): + pass diff --git a/frappe/desk/doctype/desk_shortcut/__init__.py b/frappe/desk/doctype/workspace_link/__init__.py similarity index 100% rename from frappe/desk/doctype/desk_shortcut/__init__.py rename to frappe/desk/doctype/workspace_link/__init__.py diff --git a/frappe/desk/doctype/desk_link/desk_link.json b/frappe/desk/doctype/workspace_link/workspace_link.json similarity index 97% rename from frappe/desk/doctype/desk_link/desk_link.json rename to frappe/desk/doctype/workspace_link/workspace_link.json index c041aa23e0..010fb3f316 100644 --- a/frappe/desk/doctype/desk_link/desk_link.json +++ b/frappe/desk/doctype/workspace_link/workspace_link.json @@ -76,6 +76,11 @@ "fieldname": "column_break_7", "fieldtype": "Column Break" }, + { + "fieldname": "dependencies", + "fieldtype": "Data", + "label": "Dependencies" + }, { "fieldname": "only_for", "fieldtype": "Link", @@ -94,20 +99,15 @@ "fieldname": "is_query_report", "fieldtype": "Check", "label": "Is Query Report" - }, - { - "fieldname": "dependencies", - "fieldtype": "Data", - "label": "Dependencies" } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-11-30 19:12:50.067888", + "modified": "2021-01-12 13:13:12.379443", "modified_by": "Administrator", "module": "Desk", - "name": "Desk Link", + "name": "Workspace Link", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/frappe/desk/doctype/workspace_link/workspace_link.py b/frappe/desk/doctype/workspace_link/workspace_link.py new file mode 100644 index 0000000000..8a139077a6 --- /dev/null +++ b/frappe/desk/doctype/workspace_link/workspace_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WorkspaceLink(Document): + pass diff --git a/frappe/desk/doctype/workspace_shortcut/__init__.py b/frappe/desk/doctype/workspace_shortcut/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json similarity index 92% rename from frappe/desk/doctype/desk_shortcut/desk_shortcut.json rename to frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json index a8b5b6aa32..8673e93cf7 100644 --- a/frappe/desk/doctype/desk_shortcut/desk_shortcut.json +++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.json @@ -44,6 +44,36 @@ "label": "DocType View", "options": "\nList\nReport Builder\nDashboard\nTree\nNew\nCalendar" }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + }, + { + "depends_on": "eval:frappe.boot.developer_mode", + "fieldname": "icon", + "fieldtype": "Data", + "label": "Icon" + }, + { + "depends_on": "eval:frappe.boot.developer_mode", + "fieldname": "restrict_to_domain", + "fieldtype": "Link", + "label": "Restrict to Domain", + "options": "Domain" + }, + { + "depends_on": "eval:doc.type == \"DocType\" && frappe.boot.developer_mode", + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Count Filter" + }, { "fieldname": "stats_filter", "fieldtype": "Code", @@ -54,56 +84,25 @@ "fieldname": "column_break_3", "fieldtype": "Column Break" }, + { + "fieldname": "color", + "fieldtype": "Color", + "label": "Color" + }, { "description": "For example: {} Open", "fieldname": "format", "fieldtype": "Data", "label": "Format" - }, - { - "depends_on": "eval:doc.type == \"DocType\" && frappe.boot.developer_mode", - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "label": "Count Filter" - }, - { - "fieldname": "color", - "fieldtype": "Select", - "label": "Color", - "options": "Grey\nGreen\nRed\nOrange\nPink\nYellow\nBlue\nCyan\nTeal" - }, - { - "depends_on": "eval:frappe.boot.developer_mode", - "fieldname": "icon", - "fieldtype": "Data", - "label": "Icon" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:frappe.boot.developer_mode", - "fieldname": "restrict_to_domain", - "fieldtype": "Link", - "label": "Restrict to Domain", - "options": "Domain" - }, - { - "fieldname": "label", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Label", - "reqd": 1 } ], "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-08-12 14:11:55.080390", + "modified": "2021-01-12 13:13:17.571324", "modified_by": "Administrator", "module": "Desk", - "name": "Desk Shortcut", + "name": "Workspace Shortcut", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py new file mode 100644 index 0000000000..d676f08b73 --- /dev/null +++ b/frappe/desk/doctype/workspace_shortcut/workspace_shortcut.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class WorkspaceShortcut(Document): + pass diff --git a/frappe/desk/form/save.py b/frappe/desk/form/save.py index 5219a98cbd..da43b14fce 100644 --- a/frappe/desk/form/save.py +++ b/frappe/desk/form/save.py @@ -42,7 +42,6 @@ def cancel(doctype=None, name=None, workflow_state_fieldname=None, workflow_stat except Exception: frappe.errprint(frappe.utils.get_traceback()) - frappe.msgprint(frappe._("Did not cancel")) raise def send_updated_docs(doc): diff --git a/frappe/desk/page/leaderboard/leaderboard.js b/frappe/desk/page/leaderboard/leaderboard.js index aaba36f25f..825e1d959b 100644 --- a/frappe/desk/page/leaderboard/leaderboard.js +++ b/frappe/desk/page/leaderboard/leaderboard.js @@ -81,7 +81,7 @@ class Leaderboard { this.$graph_area = this.$container.find(".leaderboard-graph"); this.doctypes.map(doctype => { - const icon = this.leaderboard_config[doctype].icon + const icon = this.leaderboard_config[doctype].icon; this.get_sidebar_item(doctype, icon).appendTo(this.$sidebar_list); }); @@ -148,7 +148,7 @@ class Leaderboard { df: { fieldtype: 'DateRange', fieldname: 'selected_date_range', - placeholder: "Date Range", + placeholder: __("Date Range"), default: [frappe.datetime.month_start(), frappe.datetime.now_date()], input_class: 'input-xs', reqd: 1, @@ -196,7 +196,7 @@ class Leaderboard { this.$search_box = $(``); $(this.parent).find(".page-form").append(this.$search_box); @@ -290,7 +290,7 @@ class Leaderboard { .map(i => frappe.model.unscrub(i)); const fields = ["rank", "name", this.options.selected_filter_item]; const filters = fields.map(filter => { - const col = frappe.model.unscrub(filter); + const col = __(frappe.model.unscrub(filter)); return ( `
    opts.currency) + )).sort()); slide.get_input("timezone").empty() .add_options([""].concat(data.all_timezones)); @@ -563,7 +564,7 @@ frappe.setup.utils = { args: { language: lang }, - callback: function (r) { + callback: function () { frappe.setup._from_load_messages = true; frappe.wizard.refresh_slides(); } diff --git a/frappe/desk/page/translation_tool/translation_tool.js b/frappe/desk/page/translation_tool/translation_tool.js index 6d20c42225..b3f0c032e3 100644 --- a/frappe/desk/page/translation_tool/translation_tool.js +++ b/frappe/desk/page/translation_tool/translation_tool.js @@ -348,7 +348,7 @@ class TranslationTool { ) .then(() => { frappe.dom.unfreeze(); - frappe.show_alert({ message:__('Successfully Submitted!'), indicator: 'success'}); + frappe.show_alert({ message: __('Successfully Submitted!'), indicator: 'success'}); this.edited_translations = {}; this.update_header(); this.fetch_messages_then_render(); diff --git a/frappe/desk/page/user_profile/user_profile_controller.js b/frappe/desk/page/user_profile/user_profile_controller.js index 133a95bdc4..c1a89f316e 100644 --- a/frappe/desk/page/user_profile/user_profile_controller.js +++ b/frappe/desk/page/user_profile/user_profile_controller.js @@ -149,33 +149,32 @@ class UserProfile { }); } + // eslint-disable-next-line no-unused-vars render_percentage_chart(field, title) { - // REDESIGN-TODO: chart seems to be broken. Enable this once fixed. - this.wrapper.find('.percentage-chart-container').hide(); - // frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data', { - // user: this.user_id, - // field: field - // }).then(chart => { - // if (chart.labels.length) { - // this.percentage_chart = new frappe.Chart('.performance-percentage-chart', { - // type: 'percentage', - // data: { - // labels: chart.labels, - // datasets: chart.datasets - // }, - // truncateLegends: 1, - // barOptions: { - // height: 11, - // depth: 1 - // }, - // height: 200, - // maxSlices: 8, - // colors: ['purple', 'blue', 'cyan', 'teal', 'pink', 'red', 'orange', 'yellow'], - // }); - // } else { - // this.wrapper.find('.percentage-chart-container').hide(); - // } - // }); + frappe.xcall('frappe.desk.page.user_profile.user_profile.get_energy_points_percentage_chart_data', { + user: this.user_id, + field: field + }).then(chart => { + if (chart.labels.length) { + this.percentage_chart = new frappe.Chart('.performance-percentage-chart', { + type: 'percentage', + data: { + labels: chart.labels, + datasets: chart.datasets + }, + truncateLegends: 1, + barOptions: { + height: 11, + depth: 1 + }, + height: 200, + maxSlices: 8, + colors: ['purple', 'blue', 'cyan', 'teal', 'pink', 'red', 'orange', 'yellow'], + }); + } else { + this.wrapper.find('.percentage-chart-container').hide(); + } + }); } create_line_chart_filters() { @@ -356,13 +355,13 @@ class UserProfile { const _get_stat_dom = (value, label, icon) => { return `
    - ${frappe.utils.icon(icon, "lg", "no-stroke")} -
    -
    ${value}
    -
    ${label}
    -
    -
    ` - } + ${frappe.utils.icon(icon, "lg", "no-stroke")} +
    +
    ${value}
    +
    ${label}
    +
    +
    `; + }; this.get_user_rank().then(() => { this.get_user_points().then(() => { @@ -451,4 +450,4 @@ class UserProfileTimeline extends BaseTimeline { } frappe.provide('frappe.ui'); -frappe.ui.UserProfile = UserProfile; \ No newline at end of file +frappe.ui.UserProfile = UserProfile; diff --git a/frappe/desk/reportview.py b/frappe/desk/reportview.py index 6cd1d24626..3003385601 100644 --- a/frappe/desk/reportview.py +++ b/frappe/desk/reportview.py @@ -54,6 +54,12 @@ def get_form_params(): fields = data["fields"] + if ((isinstance(fields, string_types) and fields == "*") + or (isinstance(fields, (list, tuple)) and len(fields) == 1 and fields[0] == "*")): + parenttype = data.doctype + data["fields"] = frappe.db.get_table_columns(parenttype) + fields = data["fields"] + for field in fields: key = field.split(" as ")[0] @@ -61,21 +67,24 @@ def get_form_params(): if key.startswith('sum('): continue if key.startswith('avg('): continue - if "." in key: - parenttype, fieldname = key.split(".")[0][4:-1], key.split(".")[1].strip("`") - else: - parenttype = data.doctype - fieldname = field.strip("`") + parenttype, fieldname = get_parent_dt_and_field(key, data) - df = frappe.get_meta(parenttype).get_field(fieldname) + if fieldname == "*": + # * inside list is not allowed with other fields + fields.remove(field) + + meta = frappe.get_meta(parenttype) + df = meta.get_field(fieldname) - fieldname = df.fieldname if df else None report_hide = df.report_hide if df else None # remove the field from the query if the report hide flag is set and current view is Report if report_hide and is_report: fields.remove(field) + if df and fieldname in [df.fieldname for df in meta.get_high_permlevel_fields()]: + if df.get('permlevel') not in meta.get_permlevel_access(parenttype=data.doctype) and field in fields: + fields.remove(field) # queries must always be server side data.query = None @@ -83,6 +92,16 @@ def get_form_params(): return data +def get_parent_dt_and_field(field, data): + if "." in field: + parenttype, fieldname = field.split(".")[0][4:-1], field.split(".")[1].strip("`") + else: + parenttype = data.doctype + fieldname = field.strip("`") + + return parenttype, fieldname + + def compress(data, args = {}): """separate keys and values""" from frappe.desk.query_report import add_total_row diff --git a/frappe/desk/search.py b/frappe/desk/search.py index f249c36746..f4e6543844 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -150,7 +150,8 @@ def search_widget(doctype, txt, query=None, searchfield=None, start=0, # 2 is the index of _relevance column order_by = "_relevance, {0}, `tab{1}`.idx desc".format(order_by_based_on_meta, doctype) - ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype)) + ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' + ignore_permissions = True if doctype == "DocType" else (cint(ignore_user_permissions) and has_permission(doctype, ptype=ptype)) if doctype in UNTRANSLATED_DOCTYPES: page_length = None diff --git a/frappe/desk/utils.py b/frappe/desk/utils.py index a3de00cd54..01b47ac106 100644 --- a/frappe/desk/utils.py +++ b/frappe/desk/utils.py @@ -3,5 +3,21 @@ import frappe -def get_doctype_route(name): +def validate_route_conflict(doctype, name): + ''' + Raises exception if name clashes with routes from other documents for /app routing + ''' + + all_names = [] + for _doctype in ['Page', 'Workspace', 'DocType']: + try: + all_names.extend([slug(d) for d in frappe.get_all(_doctype, pluck='name') if (doctype != _doctype and d != name)]) + except frappe.db.TableMissingError: + pass + + if slug(name) in all_names: + frappe.msgprint(frappe._('Name already taken, please set a new name')) + raise frappe.NameError + +def slug(name): return name.lower().replace(' ', '-') \ No newline at end of file diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.js b/frappe/email/doctype/auto_email_report/auto_email_report.js index 1b91e7a38c..3423c3ccba 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.js +++ b/frappe/email/doctype/auto_email_report/auto_email_report.js @@ -97,9 +97,18 @@ frappe.ui.form.on('Auto Email Report', { }) report_filters = report_filters_list; - report_filters.forEach(function(f) { - $('' + f.label + ''+ frappe.format(filters[f.fieldname], f) +'') - .appendTo(table.find('tbody')); + const mandatory_css = { + "background-color": "var(--error-bg)", + "font-weight": "bold" + }; + + report_filters.forEach(f => { + const css = f.reqd ? mandatory_css : {}; + const row = $("").appendTo(table.find("tbody")); + $("" + f.label + "").appendTo(row); + $("" + frappe.format(filters[f.fieldname], f) +"") + .css(css) + .appendTo(row); }); table.on('click', function() { diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.json b/frappe/email/doctype/auto_email_report/auto_email_report.json index 8067566ece..211e2e9662 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.json +++ b/frappe/email/doctype/auto_email_report/auto_email_report.json @@ -147,6 +147,7 @@ "label": "Email Settings" }, { + "description": "For multiple addresses, enter the address on different line. e.g. test@test.com \u23ce test1@test.com", "fieldname": "email_to", "fieldtype": "Small Text", "label": "Email To", @@ -200,7 +201,7 @@ "read_only": 1 } ], - "modified": "2019-05-09 22:38:27.570890", + "modified": "2021-01-28 15:59:43.151995", "modified_by": "Administrator", "module": "Email", "name": "Auto Email Report", diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py index 539f6c9db8..d82caa7bd4 100644 --- a/frappe/email/doctype/auto_email_report/auto_email_report.py +++ b/frappe/email/doctype/auto_email_report/auto_email_report.py @@ -29,6 +29,7 @@ class AutoEmailReport(Document): self.validate_report_count() self.validate_emails() self.validate_report_format() + self.validate_mandatory_fields() def validate_emails(self): '''Cleanup list of emails''' @@ -56,6 +57,21 @@ class AutoEmailReport(Document): frappe.throw(_("{0} is not a valid report format. Report format should one of the following {1}") .format(frappe.bold(self.format), frappe.bold(", ".join(valid_report_formats)))) + def validate_mandatory_fields(self): + # Check if all Mandatory Report Filters are filled by the User + filters = frappe.parse_json(self.filters) if self.filters else {} + filter_meta = frappe.parse_json(self.filter_meta) if self.filter_meta else {} + throw_list = [] + for meta in filter_meta: + if meta.get("reqd") and not filters.get(meta["fieldname"]): + throw_list.append(meta['label']) + if throw_list: + frappe.throw( + title= _('Missing Filters Required'), + msg= _('Following Report Filters have missing values:') + + '

    • ' + '
    • '.join(throw_list) + '
    ', + ) + def get_report_content(self): '''Returns file in for the report in given format''' report = frappe.get_doc('Report', self.report) @@ -81,7 +97,7 @@ class AutoEmailReport(Document): if self.format == 'HTML': columns, data = make_links(columns, data) - + columns = update_field_types(columns) return self.get_html_table(columns, data) elif self.format == 'XLSX': @@ -236,5 +252,14 @@ def make_links(columns, data): elif col.fieldtype == "Dynamic Link": if col.options and row.get(col.fieldname) and row.get(col.options): row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname]) + elif col.fieldtype == "Currency": + row[col.fieldname] = frappe.format_value(row[col.fieldname], col) return columns, data + +def update_field_types(columns): + for col in columns: + if col.fieldtype in ("Link", "Dynamic Link", "Currency") and col.options != "Currency": + col.fieldtype = "Data" + col.options = "" + return columns \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.json b/frappe/email/doctype/email_account/email_account.json index 057638697a..6d811b801f 100644 --- a/frappe/email/doctype/email_account/email_account.json +++ b/frappe/email/doctype/email_account/email_account.json @@ -56,6 +56,7 @@ "auto_reply_message", "set_footer", "footer", + "brand_logo", "uidvalidity", "uidnext", "no_remaining", @@ -65,6 +66,8 @@ { "fieldname": "email_id", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "in_global_search": 1, "in_list_view": 1, "label": "Email Address", @@ -75,46 +78,62 @@ "default": "0", "fieldname": "login_id_is_different", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Use Different Email Login ID" }, { "depends_on": "login_id_is_different", "fieldname": "login_id", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Email Login ID" }, { "fieldname": "password", "fieldtype": "Password", + "hide_days": 1, + "hide_seconds": 1, "label": "Password" }, { "default": "0", "fieldname": "awaiting_password", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Awaiting password" }, { "default": "0", "fieldname": "ascii_encode_password", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Use ASCII encoding for password" }, { "description": "e.g. \"Support\", \"Sales\", \"Jerry Yang\"", "fieldname": "email_account_name", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Email Account Name", "unique": 1 }, { "fieldname": "email_settings", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "depends_on": "eval:!doc.service", "fieldname": "domain", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_list_view": 1, "in_standard_filter": 1, "label": "Domain", @@ -124,18 +143,24 @@ "depends_on": "eval:!doc.domain", "fieldname": "service", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Service", "options": "\nGMail\nSendgrid\nSparkPost\nYahoo Mail\nOutlook.com\nYandex.Mail" }, { "fieldname": "mailbox_settings", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "description": "Check this to pull emails from your mailbox", "fieldname": "enable_incoming", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Enable Incoming" }, { @@ -144,6 +169,8 @@ "fetch_from": "domain.use_imap", "fieldname": "use_imap", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Use IMAP" }, { @@ -152,6 +179,8 @@ "fetch_from": "domain.email_server", "fieldname": "email_server", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Email Server" }, { @@ -160,6 +189,8 @@ "fetch_from": "domain.use_ssl", "fieldname": "use_ssl", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Use SSL" }, { @@ -169,6 +200,8 @@ "fetch_from": "domain.attachment_limit", "fieldname": "attachment_limit", "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, "label": "Attachment Limit (MB)" }, { @@ -176,6 +209,8 @@ "description": "Append as communication against this DocType (must have fields, \"Status\", \"Subject\")", "fieldname": "append_to", "fieldtype": "Link", + "hide_days": 1, + "hide_seconds": 1, "in_standard_filter": 1, "label": "Append To", "options": "DocType" @@ -186,6 +221,8 @@ "description": "e.g. replies@yourcomany.com. All replies will come to this inbox.", "fieldname": "default_incoming", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Default Incoming" }, { @@ -193,6 +230,8 @@ "depends_on": "eval: doc.enable_incoming", "fieldname": "email_sync_option", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Email Sync Option", "options": "ALL\nUNSEEN" }, @@ -201,18 +240,24 @@ "description": "Total number of emails to sync in initial sync process ", "fieldname": "initial_sync_count", "fieldtype": "Select", + "hide_days": 1, + "hide_seconds": 1, "label": "Initial Sync Count", "options": "100\n250\n500" }, { "depends_on": "enable_incoming", "fieldname": "section_break_13", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fieldname": "notify_if_unreplied", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Notify if unreplied" }, { @@ -220,6 +265,8 @@ "depends_on": "notify_if_unreplied", "fieldname": "unreplied_for_mins", "fieldtype": "Int", + "hide_days": 1, + "hide_seconds": 1, "label": "Notify if unreplied for (in mins)" }, { @@ -227,17 +274,23 @@ "description": "Email Addresses", "fieldname": "send_notification_to", "fieldtype": "Small Text", + "hide_days": 1, + "hide_seconds": 1, "label": "Send Notification to" }, { "fieldname": "outgoing_mail_settings", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "description": "SMTP Settings for outgoing emails", "fieldname": "enable_outgoing", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Enable Outgoing" }, { @@ -246,6 +299,8 @@ "fetch_from": "domain.smtp_server", "fieldname": "smtp_server", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "SMTP Server" }, { @@ -254,6 +309,8 @@ "fetch_from": "domain.use_tls", "fieldname": "use_tls", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Use TLS" }, { @@ -262,6 +319,8 @@ "fetch_from": "domain.smtp_port", "fieldname": "smtp_port", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Port" }, { @@ -270,6 +329,8 @@ "description": "Notifications and bulk mails will be sent from this outgoing server.", "fieldname": "default_outgoing", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Default Outgoing" }, { @@ -278,6 +339,8 @@ "description": "Uses the Email Address mentioned in this Account as the Sender for all emails sent using this Account. ", "fieldname": "always_use_account_email_id_as_sender", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Always use Account's Email Address as Sender" }, { @@ -286,12 +349,16 @@ "description": "Uses the Email Address Name mentioned in this Account as the Sender's Name for all emails sent using this Account.", "fieldname": "always_use_account_name_as_sender_name", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Always use Account's Name as Sender's Name" }, { "default": "1", "fieldname": "send_unsubscribe_message", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Send unsubscribe message in email" }, { @@ -299,38 +366,52 @@ "description": "Track if your email has been opened by the recipient.\n
    \nNote: If you're sending to multiple recipients, even if 1 recipient reads the email, it'll be considered \"Opened\"", "fieldname": "track_email_status", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Track Email Status" }, { "default": "0", "fieldname": "no_smtp_authentication", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Disable SMTP server authentication" }, { "fieldname": "signature_section", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fieldname": "add_signature", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Add Signature" }, { "depends_on": "add_signature", "fieldname": "signature", "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, "label": "Signature" }, { "fieldname": "auto_reply", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "fieldname": "enable_auto_reply", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Enable Auto Reply" }, { @@ -338,21 +419,29 @@ "description": "ProTip: Add Reference: {{ reference_doctype }} {{ reference_name }} to send document reference", "fieldname": "auto_reply_message", "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, "label": "Auto Reply Message" }, { "fieldname": "set_footer", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "fieldname": "footer", "fieldtype": "Text Editor", + "hide_days": 1, + "hide_seconds": 1, "label": "Footer" }, { "fieldname": "uidvalidity", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "UIDVALIDITY", "no_copy": 1 }, @@ -360,6 +449,8 @@ "fieldname": "uidnext", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "UIDNEXT", "no_copy": 1 }, @@ -367,6 +458,8 @@ "fieldname": "no_remaining", "fieldtype": "Data", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "No of emails remaining to be synced", "no_copy": 1 }, @@ -374,19 +467,25 @@ "fieldname": "no_failed", "fieldtype": "Int", "hidden": 1, + "hide_days": 1, + "hide_seconds": 1, "label": "no failed attempts", "no_copy": 1, "read_only": 1 }, { "fieldname": "section_break_12", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "hide_days": 1, + "hide_seconds": 1 }, { "default": "0", "description": "For more information, click here.", "fieldname": "enable_automatic_linking", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Enable Automatic Linking in Documents" }, { @@ -394,6 +493,8 @@ "description": "If non-standard port (e.g. POP3: 995/110, IMAP: 993/143)", "fieldname": "incoming_port", "fieldtype": "Data", + "hide_days": 1, + "hide_seconds": 1, "label": "Port" }, { @@ -401,6 +502,8 @@ "depends_on": "eval:!doc.domain && doc.enable_outgoing", "fieldname": "append_emails_to_sent_folder", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Append Emails to Sent Folder" }, { @@ -408,18 +511,28 @@ "depends_on": "eval:!doc.domain && doc.enable_outgoing", "fieldname": "use_ssl_for_outgoing", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Use SSL for Outgoing" }, { "default": "1", "fieldname": "create_contact", "fieldtype": "Check", + "hide_days": 1, + "hide_seconds": 1, "label": "Create Contacts from Incoming Emails" + }, + { + "fieldname": "brand_logo", + "fieldtype": "Attach Image", + "label": "Brand Logo" } ], "icon": "fa fa-inbox", + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-05-11 15:18:43.931499", + "modified": "2021-01-21 10:05:24.820597", "modified_by": "Administrator", "module": "Email", "name": "Email Account", @@ -441,4 +554,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/frappe/email/doctype/email_account/email_account.py b/frappe/email/doctype/email_account/email_account.py index 343141c66d..ca4dbb83e2 100755 --- a/frappe/email/doctype/email_account/email_account.py +++ b/frappe/email/doctype/email_account/email_account.py @@ -210,7 +210,7 @@ class EmailAccount(Document): elif not in_receive and any(map(lambda t: t in message, auth_error_codes)): self.throw_invalid_credentials_exception() else: - frappe.throw(e) + frappe.throw(cstr(e)) except socket.error: if in_receive: diff --git a/frappe/email/doctype/email_account/email_account_list.js b/frappe/email/doctype/email_account/email_account_list.js index 8cc8b02c52..5ec56fb3db 100644 --- a/frappe/email/doctype/email_account/email_account_list.js +++ b/frappe/email/doctype/email_account/email_account_list.js @@ -6,15 +6,15 @@ frappe.listview_settings["Email Account"] = { return [__("Default Sending and Inbox"), color, "default_incoming,=,Yes|default_outgoing,=,Yes"] } else if(doc.default_incoming) { - var color = doc.enable_incoming ? "blue" : "gray"; + color = doc.enable_incoming ? "blue" : "gray"; return [__("Default Inbox"), color, "default_incoming,=,Yes"]; } else if(doc.default_outgoing) { - var color = doc.enable_outgoing ? "blue" : "gray"; + color = doc.enable_outgoing ? "blue" : "gray"; return [__("Default Sending"), color, "default_outgoing,=,Yes"]; } else { - var color = doc.enable_incoming ? "blue" : "gray"; + color = doc.enable_incoming ? "blue" : "gray"; return [__("Inbox"), color, "is_global,=,No|is_default=No"]; } } diff --git a/frappe/email/doctype/newsletter/test_newsletter.py b/frappe/email/doctype/newsletter/test_newsletter.py index ee7f123b7e..bd8fadc29c 100644 --- a/frappe/email/doctype/newsletter/test_newsletter.py +++ b/frappe/email/doctype/newsletter/test_newsletter.py @@ -2,58 +2,66 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe, unittest -from frappe.utils import getdate, add_days +import unittest +from random import choice -from frappe.email.doctype.newsletter.newsletter import confirmed_unsubscribe, send_scheduled_email -from six.moves.urllib.parse import unquote +import frappe +from frappe.email.doctype.newsletter.newsletter import ( + confirmed_unsubscribe, + send_scheduled_email, +) +from frappe.email.doctype.newsletter.newsletter import get_newsletter_list +from frappe.email.queue import flush +from frappe.utils import add_days, getdate test_dependencies = ["Email Group"] +emails = [ + "test_subscriber1@example.com", + "test_subscriber2@example.com", + "test_subscriber3@example.com", + "test1@example.com", +] -emails = ["test_subscriber1@example.com", "test_subscriber2@example.com", - "test_subscriber3@example.com", "test1@example.com"] class TestNewsletter(unittest.TestCase): def setUp(self): frappe.set_user("Administrator") - frappe.db.sql('delete from `tabEmail Group Member`') + frappe.db.sql("delete from `tabEmail Group Member`") + + if not frappe.db.exists("Email Group", "_Test Email Group"): + frappe.get_doc({"doctype": "Email Group", "title": "_Test Email Group"}).insert() - group_exist=frappe.db.exists("Email Group", "_Test Email Group") - if len(group_exist) == 0: - frappe.get_doc({ - "doctype": "Email Group", - "title": "_Test Email Group" - }).insert() for email in emails: - frappe.get_doc({ - "doctype": "Email Group Member", - "email": email, - "email_group": "_Test Email Group" - }).insert() + frappe.get_doc({ + "doctype": "Email Group Member", + "email": email, + "email_group": "_Test Email Group" + }).insert() def test_send(self): - name = self.send_newsletter() + self.send_newsletter() - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + email_queue_list = [frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue")] self.assertEqual(len(email_queue_list), 4) - recipients = [e.recipients[0].recipient for e in email_queue_list] - for email in emails: - self.assertTrue(email in recipients) + + recipients = set([e.recipients[0].recipient for e in email_queue_list]) + self.assertTrue(set(emails).issubset(recipients)) def test_unsubscribe(self): - # test unsubscribe name = self.send_newsletter() - from frappe.email.queue import flush + to_unsubscribe = choice(emails) + group = frappe.get_all("Newsletter Email Group", filters={"parent": name}, fields=["email_group"]) + flush(from_test=True) - to_unsubscribe = unquote(frappe.local.flags.signed_query_string.split("email=")[1].split("&")[0]) - group = frappe.get_all("Newsletter Email Group", filters={"parent" : name}, fields=["email_group"]) confirmed_unsubscribe(to_unsubscribe, group[0].email_group) name = self.send_newsletter() - - email_queue_list = [frappe.get_doc('Email Queue', e.name) for e in frappe.get_all("Email Queue")] + email_queue_list = [ + frappe.get_doc("Email Queue", e.name) for e in frappe.get_all("Email Queue") + ] self.assertEqual(len(email_queue_list), 3) recipients = [e.recipients[0].recipient for e in email_queue_list] + for email in emails: if email != to_unsubscribe: self.assertTrue(email in recipients) @@ -86,7 +94,6 @@ class TestNewsletter(unittest.TestCase): def test_portal(self): self.send_newsletter(1) frappe.set_user("test1@example.com") - from frappe.email.doctype.newsletter.newsletter import get_newsletter_list newsletters = get_newsletter_list("Newsletter", None, None, 0) self.assertEqual(len(newsletters), 1) @@ -106,4 +113,4 @@ class TestNewsletter(unittest.TestCase): self.assertEqual(len(email_queue_list), 4) recipients = [e.recipients[0].recipient for e in email_queue_list] for email in emails: - self.assertTrue(email in recipients) \ No newline at end of file + self.assertTrue(email in recipients) diff --git a/frappe/email/email_body.py b/frappe/email/email_body.py index 8ac071fa61..3fb1dfa0da 100755 --- a/frappe/email/email_body.py +++ b/frappe/email/email_body.py @@ -13,7 +13,6 @@ from email.mime.multipart import MIMEMultipart from email.header import Header from email import policy - def get_email(recipients, sender='', msg='', subject='[No Subject]', text_content = None, footer=None, print_html=None, formatted=None, attachments=None, content=None, reply_to=None, cc=[], bcc=[], email_account=None, expose_recipients=None, @@ -249,11 +248,14 @@ class EMail: return self.msg_root.as_string(policy=policy.SMTPUTF8) def get_formatted_html(subject, message, footer=None, print_html=None, - email_account=None, header=None, unsubscribe_link=None, sender=None): + email_account=None, header=None, unsubscribe_link=None, sender=None, with_container=False): if not email_account: email_account = get_outgoing_email_account(False, sender=sender) rendered_email = frappe.get_template("templates/emails/standard.html").render({ + "brand_logo": get_brand_logo(email_account) if with_container or header else None, + "with_container": with_container, + "site_url": get_url(), "header": get_header(header), "content": message, "signature": get_signature(email_account), @@ -272,14 +274,14 @@ def get_formatted_html(subject, message, footer=None, print_html=None, return html @frappe.whitelist() -def get_email_html(template, args, subject, header=None): +def get_email_html(template, args, subject, header=None, with_container=False): import json - + with_container = cint(with_container) args = json.loads(args) if header and header.startswith('['): header = json.loads(header) email = frappe.utils.jinja.get_email_from_template(template, args) - return get_formatted_html(subject, email[0], header=header) + return get_formatted_html(subject, email[0], header=header, with_container=with_container) def inline_style_in_html(html): ''' Convert email.css and html to inline-styled html @@ -288,11 +290,16 @@ def inline_style_in_html(html): apps = frappe.get_installed_apps() - css_files = [] + # add frappe email css file + css_files = ['assets/css/email.css'] + if 'frappe' in apps: + apps.remove('frappe') + for app in apps: path = 'assets/{0}/css/email.css'.format(app) - if os.path.exists(os.path.abspath(path)): - css_files.append(path) + css_files.append(path) + + css_files = [css_file for css_file in css_files if os.path.exists(os.path.abspath(css_file))] p = Premailer(html=html, external_styles=css_files, strip_important=False) @@ -353,7 +360,7 @@ def get_message_id(): def get_signature(email_account): if email_account and email_account.add_signature and email_account.signature: - return "

    " + email_account.signature + return "
    " + email_account.signature else: return "" @@ -366,10 +373,10 @@ def get_footer(email_account, footer=None): if email_account and email_account.footer: args.update({'email_account_footer': email_account.footer}) - company_address = frappe.db.get_default("email_footer_address") + sender_address = frappe.db.get_default("email_footer_address") - if company_address: - args.update({'company_address': company_address}) + if sender_address: + args.update({'sender_address': sender_address}) if not cint(frappe.db.get_default("disable_standard_email_footer")): args.update({'default_mail_footer': frappe.get_hooks('default_mail_footer')}) @@ -467,3 +474,6 @@ def get_header(header=None): def sanitize_email_header(str): return str.replace('\r', '').replace('\n', '') + +def get_brand_logo(email_account): + return email_account.get('brand_logo') \ No newline at end of file diff --git a/frappe/email/queue.py b/frappe/email/queue.py index f780aebdc1..2aff04edc9 100755 --- a/frappe/email/queue.py +++ b/frappe/email/queue.py @@ -24,7 +24,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= attachments=None, reply_to=None, cc=None, bcc=None, message_id=None, in_reply_to=None, send_after=None, expose_recipients=None, send_priority=1, communication=None, now=False, read_receipt=None, queue_separately=False, is_notification=False, add_unsubscribe_link=1, inline_images=None, - header=None, print_letterhead=False): + header=None, print_letterhead=False, with_container=False): """Add email to sending queue (Email Queue) :param recipients: List of recipients. @@ -48,6 +48,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= :param add_unsubscribe_link: Send unsubscribe link in the footer of the Email, default 1. :param inline_images: List of inline images as {"filename", "filecontent"}. All src properties will be replaced with random Content-Id :param header: Append header in email (boolean) + :param with_container: Wraps email inside styled container """ if not unsubscribe_method: unsubscribe_method = "/api/method/frappe.email.queue.unsubscribe" @@ -130,7 +131,7 @@ def send(recipients=None, sender=None, subject=None, message=None, text_content= email_content = get_formatted_html(subject, message, email_account=email_account, header=header, - unsubscribe_link=unsubscribe_link) + unsubscribe_link=unsubscribe_link, with_container=with_container) # add to queue add(recipients, sender, subject, diff --git a/frappe/email/test_email_body.py b/frappe/email/test_email_body.py index 705a853bc6..9b0b5e41d7 100644 --- a/frappe/email/test_email_body.py +++ b/frappe/email/test_email_body.py @@ -138,7 +138,7 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> ''' transformed_html = '''

    Hi John

    -

    This is a test email

    +

    This is a test email

    ''' self.assertTrue(transformed_html in inline_style_in_html(html)) @@ -154,10 +154,8 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> content=email_html, header=['Email Title', 'green'] ).as_string().replace("\r\n", "\n") - - self.assertTrue('''''' in email_string) + # REDESIGN-TODO: Add style for indicators in email + self.assertTrue('''''' in email_string) self.assertTrue('Email Title' in email_string) def test_get_email_header(self): diff --git a/frappe/event_streaming/doctype/event_producer/event_producer.py b/frappe/event_streaming/doctype/event_producer/event_producer.py index d8a6a55510..e43b4d131c 100644 --- a/frappe/event_streaming/doctype/event_producer/event_producer.py +++ b/frappe/event_streaming/doctype/event_producer/event_producer.py @@ -295,7 +295,7 @@ def set_update(update, producer_site): if data.changed: local_doc.update(data.changed) if data.removed: - update_row_removed(local_doc, data.removed) + local_doc = update_row_removed(local_doc, data.removed) if data.row_changed: update_row_changed(local_doc, data.row_changed) if data.added: @@ -318,7 +318,17 @@ def update_row_removed(local_doc, removed): for tablename, rownames in iteritems(removed): table = local_doc.get_table_field_doctype(tablename) for row in rownames: - frappe.db.delete(table, row) + table_rows = local_doc.get(tablename) + child_table_row = get_child_table_row(table_rows, row) + table_rows.remove(child_table_row) + local_doc.set(tablename, table_rows) + return local_doc + + +def get_child_table_row(table_rows, row): + for entry in table_rows: + if entry.get('name') == row: + return entry def update_row_changed(local_doc, changed): diff --git a/frappe/geo/country_info.json b/frappe/geo/country_info.json index e4c4e278b0..1e0ae161bc 100644 --- a/frappe/geo/country_info.json +++ b/frappe/geo/country_info.json @@ -2729,11 +2729,11 @@ }, "Zimbabwe": { "code": "zw", - "currency": "ZWD", - "currency_fraction": "Thebe", + "currency": "ZWL", + "currency_fraction": "Cent", "currency_fraction_units": 100, "currency_name": "Zimbabwe Dollar", - "currency_symbol": "P", + "currency_symbol": "ZWL$", "number_format": "# ###.##", "timezones": [ "Africa/Harare" diff --git a/frappe/geo/utils.py b/frappe/geo/utils.py new file mode 100644 index 0000000000..d94a13ea41 --- /dev/null +++ b/frappe/geo/utils.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals + +import frappe + +from pymysql import InternalError + + +@frappe.whitelist() +def get_coords(doctype, filters, type): + '''Get a geojson dict representing a doctype.''' + filters_sql = get_coords_conditions(doctype, filters)[4:] + + coords = None + if type == 'location_field': + coords = return_location(doctype, filters_sql) + elif type == 'coordinates': + coords = return_coordinates(doctype, filters_sql) + + out = convert_to_geojson(type, coords) + return out + +def convert_to_geojson(type, coords): + '''Converts GPS coordinates to geoJSON string.''' + geojson = {"type": "FeatureCollection", "features": None} + + if type == 'location_field': + geojson['features'] = merge_location_features_in_one(coords) + elif type == 'coordinates': + geojson['features'] = create_gps_markers(coords) + + return geojson + + +def merge_location_features_in_one(coords): + '''Merging all features from location field.''' + geojson_dict = [] + for element in coords: + geojson_loc = frappe.parse_json(element['location']) + if not geojson_loc: + continue + for coord in geojson_loc['features']: + coord['properties']['name'] = element['name'] + geojson_dict.append(coord.copy()) + + return geojson_dict + + +def create_gps_markers(coords): + '''Build Marker based on latitude and longitude.''' + geojson_dict = [] + for i in coords: + node = {"type": "Feature", "properties": {}, "geometry": {"type": "Point", "coordinates": None}} + node['properties']['name'] = i.name + node['geometry']['coordinates'] = [i.latitude, i.longitude] + geojson_dict.append(node.copy()) + + return geojson_dict + + +def return_location(doctype, filters_sql): + '''Get name and location fields for Doctype.''' + if filters_sql: + try: + coords = frappe.db.sql('''SELECT name, location FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) + except InternalError: + frappe.msgprint(frappe._('This Doctype does not contain location fields'), raise_exception=True) + return + else: + coords = frappe.get_all(doctype, fields=['name', 'location']) + return coords + + +def return_coordinates(doctype, filters_sql): + '''Get name, latitude and longitude fields for Doctype.''' + if filters_sql: + try: + coords = frappe.db.sql('''SELECT name, latitude, longitude FROM `tab{}` WHERE {}'''.format(doctype, filters_sql), as_dict=True) + except InternalError: + frappe.msgprint(frappe._('This Doctype does not contain latitude and longitude fields'), raise_exception=True) + return + else: + coords = frappe.get_all(doctype, fields=['name', 'latitude', 'longitude']) + return coords + + +def get_coords_conditions(doctype, filters=None): + '''Returns SQL conditions with user permissions and filters for event queries.''' + from frappe.desk.reportview import get_filters_cond + if not frappe.has_permission(doctype): + frappe.throw(frappe._("Not Permitted"), frappe.PermissionError) + + return get_filters_cond(doctype, filters, [], with_match_conditions=True) diff --git a/frappe/hooks.py b/frappe/hooks.py index d024cb7929..3e206f0ad3 100644 --- a/frappe/hooks.py +++ b/frappe/hooks.py @@ -18,7 +18,7 @@ app_email = "info@frappe.io" docs_app = "frappe_io" -translator_url = "https://translatev2.erpnext.com" +translator_url = "https://translate.erpnext.com" before_install = "frappe.utils.install.before_install" after_install = "frappe.utils.install.after_install" @@ -58,6 +58,11 @@ website_route_rules = [ {"from_route": "/kb/", "to_route": "Help Article"}, {"from_route": "/newsletters", "to_route": "Newsletter"}, {"from_route": "/profile", "to_route": "me"}, + {"from_route": "/app/", "to_route": "app"}, +] + +website_redirects = [ + {"source": r"/desk(.*)", "target": r"/app\1"}, ] base_template = "templates/base.html" diff --git a/frappe/integrations/doctype/connected_app/__init__.py b/frappe/integrations/doctype/connected_app/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/connected_app/connected_app.js b/frappe/integrations/doctype/connected_app/connected_app.js new file mode 100644 index 0000000000..4d20f65559 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.js @@ -0,0 +1,38 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Connected App', { + refresh: frm => { + frm.add_custom_button(__('Get OpenID Configuration'), async () => { + if (!frm.doc.openid_configuration) { + frappe.msgprint(__('Please enter OpenID Configuration URL')); + } else { + try { + const response = await fetch(frm.doc.openid_configuration); + const oidc = await response.json(); + frm.set_value('authorization_uri', oidc.authorization_endpoint); + frm.set_value('token_uri', oidc.token_endpoint); + frm.set_value('userinfo_uri', oidc.userinfo_endpoint); + frm.set_value('introspection_uri', oidc.introspection_endpoint); + frm.set_value('revocation_uri', oidc.revocation_endpoint); + } catch (error) { + frappe.msgprint(__('Please check OpenID Configuration URL')); + } + } + }); + + if (!frm.is_new()) { + frm.add_custom_button(__('Connect to {}', [frm.doc.provider_name]), async () => { + frappe.call({ + method: 'initiate_web_application_flow', + doc: frm.doc, + callback: function(r) { + window.open(r.message, '_blank'); + } + }); + }); + } + + frm.toggle_display('sb_client_credentials_section', !frm.is_new()); + } +}); diff --git a/frappe/integrations/doctype/connected_app/connected_app.json b/frappe/integrations/doctype/connected_app/connected_app.json new file mode 100644 index 0000000000..e5dbb0472a --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.json @@ -0,0 +1,166 @@ +{ + "actions": [], + "beta": 1, + "creation": "2019-01-24 15:51:06.362222", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "provider_name", + "cb_00", + "openid_configuration", + "sb_client_credentials_section", + "client_id", + "redirect_uri", + "cb_01", + "client_secret", + "sb_scope_section", + "scopes", + "sb_endpoints_section", + "authorization_uri", + "token_uri", + "revocation_uri", + "cb_02", + "userinfo_uri", + "introspection_uri", + "section_break_18", + "query_parameters" + ], + "fields": [ + { + "fieldname": "provider_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Provider Name", + "reqd": 1 + }, + { + "fieldname": "cb_00", + "fieldtype": "Column Break" + }, + { + "fieldname": "openid_configuration", + "fieldtype": "Data", + "label": "OpenID Configuration" + }, + { + "collapsible": 1, + "fieldname": "sb_client_credentials_section", + "fieldtype": "Section Break", + "label": "Client Credentials" + }, + { + "fieldname": "client_id", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Client Id" + }, + { + "fieldname": "redirect_uri", + "fieldtype": "Data", + "label": "Redirect URI", + "read_only": 1 + }, + { + "fieldname": "cb_01", + "fieldtype": "Column Break" + }, + { + "fieldname": "client_secret", + "fieldtype": "Password", + "label": "Client Secret" + }, + { + "collapsible": 1, + "fieldname": "sb_scope_section", + "fieldtype": "Section Break", + "label": "Scopes" + }, + { + "collapsible": 1, + "fieldname": "sb_endpoints_section", + "fieldtype": "Section Break", + "label": "Endpoints" + }, + { + "fieldname": "cb_02", + "fieldtype": "Column Break" + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope" + }, + { + "fieldname": "authorization_uri", + "fieldtype": "Data", + "label": "Authorization URI" + }, + { + "fieldname": "token_uri", + "fieldtype": "Data", + "label": "Token URI" + }, + { + "fieldname": "revocation_uri", + "fieldtype": "Data", + "label": "Revocation URI" + }, + { + "fieldname": "userinfo_uri", + "fieldtype": "Data", + "label": "Userinfo URI" + }, + { + "fieldname": "introspection_uri", + "fieldtype": "Data", + "label": "Introspection URI" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break", + "label": "Extra Parameters" + }, + { + "fieldname": "query_parameters", + "fieldtype": "Table", + "label": "Query Parameters", + "options": "Query Parameters" + } + ], + "links": [ + { + "link_doctype": "Token Cache", + "link_fieldname": "connected_app" + } + ], + "modified": "2020-11-16 16:29:50.277405", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Connected App", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "provider_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/connected_app/connected_app.py b/frappe/integrations/doctype/connected_app/connected_app.py new file mode 100644 index 0000000000..ec08f8e4be --- /dev/null +++ b/frappe/integrations/doctype/connected_app/connected_app.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +import os +from urllib.parse import urljoin +from urllib.parse import urlencode + +import frappe +from frappe import _ +from frappe.model.document import Document +from requests_oauthlib import OAuth2Session + +if any((os.getenv('CI'), frappe.conf.developer_mode, frappe.conf.allow_tests)): + # Disable mandatory TLS in developer mode and tests + os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1' + +class ConnectedApp(Document): + """Connect to a remote oAuth Server. Retrieve and store user's access token + in a Token Cache. + """ + + def validate(self): + base_url = frappe.utils.get_url() + callback_path = '/api/method/frappe.integrations.doctype.connected_app.connected_app.callback/' + self.name + self.redirect_uri = urljoin(base_url, callback_path) + + def get_oauth2_session(self, user=None, init=False): + token = None + token_updater = None + + if not init: + user = user or frappe.session.user + token_cache = self.get_user_token(user) + token = token_cache.get_json() + token_updater = token_cache.update_data + + return OAuth2Session( + client_id=self.client_id, + token=token, + token_updater=token_updater, + auto_refresh_url=self.token_uri, + redirect_uri=self.redirect_uri, + scope=self.get_scopes() + ) + + def initiate_web_application_flow(self, user=None, success_uri=None): + """Return an authorization URL for the user. Save state in Token Cache.""" + user = user or frappe.session.user + oauth = self.get_oauth2_session(init=True) + query_params = self.get_query_params() + authorization_url, state = oauth.authorization_url(self.authorization_uri, **query_params) + token_cache = self.get_token_cache(user) + + if not token_cache: + token_cache = frappe.new_doc('Token Cache') + token_cache.user = user + token_cache.connected_app = self.name + + token_cache.success_uri = success_uri + token_cache.state = state + token_cache.save(ignore_permissions=True) + frappe.db.commit() + + return authorization_url + + def get_user_token(self, user=None, success_uri=None): + """Return an existing user token or initiate a Web Application Flow.""" + user = user or frappe.session.user + token_cache = self.get_token_cache(user) + + if token_cache: + return token_cache + + redirect = self.initiate_web_application_flow(user, success_uri) + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = redirect + return redirect + + def get_token_cache(self, user): + token_cache = None + token_cache_name = self.name + '-' + user + + if frappe.db.exists('Token Cache', token_cache_name): + token_cache = frappe.get_doc('Token Cache', token_cache_name) + + return token_cache + + def get_scopes(self): + return [row.scope for row in self.scopes] + + def get_query_params(self): + return {param.key: param.value for param in self.query_parameters} + + +@frappe.whitelist(allow_guest=True) +def callback(code=None, state=None): + """Handle client's code. + + Called during the oauthorization flow by the remote oAuth2 server to + transmit a code that can be used by the local server to obtain an access + token. + """ + if frappe.request.method != 'GET': + frappe.throw(_('Invalid request method: {}').format(frappe.request.method)) + + if frappe.session.user == 'Guest': + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = '/login?' + urlencode({'redirect-to': frappe.request.url}) + return + + path = frappe.request.path[1:].split('/') + if len(path) != 4 or not path[3]: + frappe.throw(_('Invalid Parameters.')) + + connected_app = frappe.get_doc('Connected App', path[3]) + token_cache = frappe.get_doc('Token Cache', connected_app.name + '-' + frappe.session.user) + + if state != token_cache.state: + frappe.throw(_('Invalid state.')) + + oauth_session = connected_app.get_oauth2_session(init=True) + query_params = connected_app.get_query_params() + token = oauth_session.fetch_token(connected_app.token_uri, + code=code, + client_secret=connected_app.get_password('client_secret'), + include_client_id=True, + **query_params + ) + token_cache.update_data(token) + + frappe.local.response['type'] = 'redirect' + frappe.local.response['location'] = token_cache.get('success_uri') or connected_app.get_url() diff --git a/frappe/integrations/doctype/connected_app/test_connected_app.py b/frappe/integrations/doctype/connected_app/test_connected_app.py new file mode 100644 index 0000000000..6faa542a60 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_connected_app.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# See license.txt +from __future__ import unicode_literals + +import unittest +import requests +from urllib.parse import urljoin + +import frappe +from frappe.integrations.doctype.social_login_key.test_social_login_key import create_or_update_social_login_key + + +def get_user(usr, pwd): + user = frappe.new_doc('User') + user.email = usr + user.enabled = 1 + user.first_name = "_Test" + user.new_password = pwd + user.roles = [] + user.append('roles', { + 'doctype': 'Has Role', + 'parentfield': 'roles', + 'role': 'System Manager' + }) + user.insert() + + return user + + +def get_connected_app(): + doctype = 'Connected App' + connected_app = frappe.new_doc(doctype) + connected_app.provider_name = 'frappe' + connected_app.scopes = [] + connected_app.append('scopes', {'scope': 'all'}) + connected_app.insert() + + return connected_app + + +def get_oauth_client(): + oauth_client = frappe.new_doc('OAuth Client') + oauth_client.app_name = '_Test Connected App' + oauth_client.redirect_uris = 'to be replaced' + oauth_client.default_redirect_uri = 'to be replaced' + oauth_client.grant_type = 'Authorization Code' + oauth_client.response_type = 'Code' + oauth_client.skip_authorization = 1 + oauth_client.insert() + + return oauth_client + + +class TestConnectedApp(unittest.TestCase): + + def setUp(self): + """Set up a Connected App that connects to our own oAuth provider. + + Frappe comes with it's own oAuth2 provider that we can test against. The + client credentials can be obtained from an "OAuth Client". All depends + on "Social Login Key" so we create one as well. + + The redirect URIs from "Connected App" and "OAuth Client" have to match. + Frappe's "Authorization URL" and "Access Token URL" (actually they're + just endpoints) are stored in "Social Login Key" so we get them from + there. + """ + self.user_name = 'test-connected-app@example.com' + self.user_password = 'Eastern_43A1W' + + self.user = get_user(self.user_name, self.user_password) + self.connected_app = get_connected_app() + self.oauth_client = get_oauth_client() + social_login_key = create_or_update_social_login_key() + self.base_url = social_login_key.get('base_url') + + frappe.db.commit() + self.connected_app.reload() + self.oauth_client.reload() + + redirect_uri = self.connected_app.get('redirect_uri') + self.oauth_client.update({ + 'redirect_uris': redirect_uri, + 'default_redirect_uri': redirect_uri + }) + self.oauth_client.save() + + self.connected_app.update({ + 'authorization_uri': urljoin(self.base_url, social_login_key.get('authorize_url')), + 'client_id': self.oauth_client.get('client_id'), + 'client_secret': self.oauth_client.get('client_secret'), + 'token_uri': urljoin(self.base_url, social_login_key.get('access_token_url')) + }) + self.connected_app.save() + + frappe.db.commit() + self.connected_app.reload() + self.oauth_client.reload() + + def test_web_application_flow(self): + """Simulate a logged in user who opens the authorization URL.""" + def login(): + return session.get(urljoin(self.base_url, '/api/method/login'), params={ + 'usr': self.user_name, + 'pwd': self.user_password + }) + + session = requests.Session() + + # first login of a new user on a new site fails with "401 UNAUTHORIZED" + # when anybody fixes that, the two lines below can be removed + first_login = login() + self.assertEqual(first_login.status_code, 401) + + second_login = login() + self.assertEqual(second_login.status_code, 200) + + authorization_url = self.connected_app.initiate_web_application_flow(user=self.user_name) + + auth_response = session.get(authorization_url) + self.assertEqual(auth_response.status_code, 200) + + callback_response = session.get(auth_response.url) + self.assertEqual(callback_response.status_code, 200) + + self.token_cache = self.connected_app.get_token_cache(self.user_name) + token = self.token_cache.get_password('access_token') + self.assertNotEqual(token, None) + + oauth2_session = self.connected_app.get_oauth2_session(self.user_name) + resp = oauth2_session.get(urljoin(self.base_url, '/api/method/frappe.auth.get_logged_user')) + self.assertEqual(resp.json().get('message'), self.user_name) + + def tearDown(self): + def delete_if_exists(attribute): + doc = getattr(self, attribute, None) + if doc: + doc.delete() + + delete_if_exists('token_cache') + delete_if_exists('connected_app') + + if getattr(self, 'oauth_client', None): + tokens = frappe.get_all('OAuth Bearer Token', filters={ + 'client': self.oauth_client.name + }) + for token in tokens: + doc = frappe.get_doc('OAuth Bearer Token', token.name) + doc.delete() + + codes = frappe.get_all('OAuth Authorization Code', filters={ + 'client': self.oauth_client.name + }) + for code in codes: + doc = frappe.get_doc('OAuth Authorization Code', code.name) + doc.delete() + + delete_if_exists('user') + delete_if_exists('oauth_client') + + frappe.db.commit() diff --git a/frappe/integrations/doctype/connected_app/test_records.json b/frappe/integrations/doctype/connected_app/test_records.json new file mode 100644 index 0000000000..4d19369248 --- /dev/null +++ b/frappe/integrations/doctype/connected_app/test_records.json @@ -0,0 +1,13 @@ +[ + { + "doctype": "Connected App", + "provider_name": "frappe", + "client_id": "test_client_id", + "client_secret": "test_client_secret", + "scopes": [ + { + "scope": "all" + } + ] + } +] diff --git a/frappe/integrations/doctype/oauth_client/test_records.json b/frappe/integrations/doctype/oauth_client/test_records.json index cff06457c5..11e6338a87 100644 --- a/frappe/integrations/doctype/oauth_client/test_records.json +++ b/frappe/integrations/doctype/oauth_client/test_records.json @@ -1,7 +1,6 @@ [ { - "app_name": "_Test OAuth Client", - "client_id": "test_client_id", + "app_name": "_Test OAuth Client", "client_secret": "test_client_secret", "default_redirect_uri": "http://localhost", "docstatus": 0, diff --git a/frappe/integrations/doctype/oauth_scope/__init__.py b/frappe/integrations/doctype/oauth_scope/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/oauth_scope/oauth_scope.json b/frappe/integrations/doctype/oauth_scope/oauth_scope.json new file mode 100644 index 0000000000..3a6e528999 --- /dev/null +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "creation": "2020-07-15 22:08:14.616585", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "scope" + ], + "fields": [ + { + "fieldname": "scope", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Scope" + } + ], + "istable": 1, + "links": [], + "modified": "2020-07-15 22:15:18.930632", + "modified_by": "Administrator", + "module": "Integrations", + "name": "OAuth Scope", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/desk/doctype/desk_link/desk_link.py b/frappe/integrations/doctype/oauth_scope/oauth_scope.py similarity index 89% rename from frappe/desk/doctype/desk_link/desk_link.py rename to frappe/integrations/doctype/oauth_scope/oauth_scope.py index 700f55e194..a5dfe7e1ce 100644 --- a/frappe/desk/doctype/desk_link/desk_link.py +++ b/frappe/integrations/doctype/oauth_scope/oauth_scope.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class DeskLink(Document): +class OAuthScope(Document): pass diff --git a/frappe/integrations/doctype/query_parameters/__init__.py b/frappe/integrations/doctype/query_parameters/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/query_parameters/query_parameters.json b/frappe/integrations/doctype/query_parameters/query_parameters.json new file mode 100644 index 0000000000..de31c28df7 --- /dev/null +++ b/frappe/integrations/doctype/query_parameters/query_parameters.json @@ -0,0 +1,37 @@ +{ + "actions": [], + "creation": "2020-11-16 14:54:37.226914", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "key", + "value" + ], + "fields": [ + { + "fieldname": "key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Key", + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Value", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-11-16 15:18:35.887149", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Query Parameters", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/frappe/desk/doctype/desk_chart/desk_chart.py b/frappe/integrations/doctype/query_parameters/query_parameters.py similarity index 87% rename from frappe/desk/doctype/desk_chart/desk_chart.py rename to frappe/integrations/doctype/query_parameters/query_parameters.py index dbbfae6cd7..bfb8eae0b6 100644 --- a/frappe/desk/doctype/desk_chart/desk_chart.py +++ b/frappe/integrations/doctype/query_parameters/query_parameters.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document -class DeskChart(Document): +class QueryParameters(Document): pass diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py index 58bd48d64a..e0b99ad391 100644 --- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py +++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py @@ -22,3 +22,17 @@ def make_social_login_key(**kwargs): kwargs["provider_name"] = "Test OAuth2 Provider" doc = frappe.get_doc(kwargs) return doc + +def create_or_update_social_login_key(): + # used in other tests (connected app, oauth20) + try: + social_login_key = frappe.get_doc("Social Login Key", "frappe") + except frappe.DoesNotExistError: + social_login_key = frappe.new_doc("Social Login Key") + social_login_key.get_social_login_provider("Frappe", initialize=True) + social_login_key.base_url = frappe.utils.get_url() + social_login_key.enable_social_login = 0 + social_login_key.save() + frappe.db.commit() + + return social_login_key diff --git a/frappe/integrations/doctype/token_cache/__init__.py b/frappe/integrations/doctype/token_cache/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frappe/integrations/doctype/token_cache/test_records.json b/frappe/integrations/doctype/token_cache/test_records.json new file mode 100644 index 0000000000..05840221a6 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_records.json @@ -0,0 +1,18 @@ +[ + { + "doctype": "Token Cache", + "user": "test@example.com", + "access_token": "test-access-token", + "refresh_token": "test-refresh-token", + "token_type": "Bearer", + "expires_in": 1000, + "scopes": [ + { + "scope": "all" + }, + { + "scope": "openid" + } + ] + } +] \ No newline at end of file diff --git a/frappe/integrations/doctype/token_cache/test_token_cache.py b/frappe/integrations/doctype/token_cache/test_token_cache.py new file mode 100644 index 0000000000..73c9f38fce --- /dev/null +++ b/frappe/integrations/doctype/token_cache/test_token_cache.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# See license.txt +from __future__ import unicode_literals + +import unittest +import frappe + +test_dependencies = ['User', 'Connected App', 'Token Cache'] + +class TestTokenCache(unittest.TestCase): + + def setUp(self): + self.token_cache = frappe.get_last_doc('Token Cache') + self.token_cache.update({'connected_app': frappe.get_last_doc('Connected App').name}) + self.token_cache.save() + + def test_get_auth_header(self): + self.token_cache.get_auth_header() + + def test_update_data(self): + self.token_cache.update_data({ + 'access_token': 'new-access-token', + 'refresh_token': 'new-refresh-token', + 'token_type': 'bearer', + 'expires_in': 2000, + 'scope': 'new scope' + }) + + def test_get_expires_in(self): + self.token_cache.get_expires_in() + + def test_is_expired(self): + self.token_cache.is_expired() + + def get_json(self): + self.token_cache.get_json() diff --git a/frappe/integrations/doctype/token_cache/token_cache.js b/frappe/integrations/doctype/token_cache/token_cache.js new file mode 100644 index 0000000000..b7cac9b804 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.js @@ -0,0 +1,8 @@ +// Copyright (c) 2019, Frappe Technologies and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Token Cache', { + // refresh: function(frm) { + + // } +}); diff --git a/frappe/integrations/doctype/token_cache/token_cache.json b/frappe/integrations/doctype/token_cache/token_cache.json new file mode 100644 index 0000000000..c016405031 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.json @@ -0,0 +1,110 @@ +{ + "actions": [], + "autoname": "format:{connected_app}-{user}", + "beta": 1, + "creation": "2019-01-24 16:56:55.631096", + "doctype": "DocType", + "document_type": "System", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "user", + "connected_app", + "provider_name", + "access_token", + "refresh_token", + "expires_in", + "state", + "scopes", + "success_uri", + "token_type" + ], + "fields": [ + { + "fieldname": "user", + "fieldtype": "Link", + "label": "User", + "options": "User", + "read_only": 1 + }, + { + "fieldname": "connected_app", + "fieldtype": "Link", + "label": "Connected App", + "options": "Connected App", + "read_only": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Password", + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "refresh_token", + "fieldtype": "Password", + "label": "Refresh Token", + "read_only": 1 + }, + { + "fieldname": "expires_in", + "fieldtype": "Int", + "label": "Expires In", + "read_only": 1 + }, + { + "fieldname": "state", + "fieldtype": "Data", + "label": "State", + "read_only": 1 + }, + { + "fieldname": "scopes", + "fieldtype": "Table", + "label": "Scopes", + "options": "OAuth Scope", + "read_only": 1 + }, + { + "fieldname": "success_uri", + "fieldtype": "Data", + "label": "Success URI", + "read_only": 1 + }, + { + "fieldname": "token_type", + "fieldtype": "Data", + "label": "Token Type", + "read_only": 1 + }, + { + "fetch_from": "connected_app.provider_name", + "fieldname": "provider_name", + "fieldtype": "Data", + "label": "Provider Name", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-11-13 13:35:53.714352", + "modified_by": "Administrator", + "module": "Integrations", + "name": "Token Cache", + "owner": "Administrator", + "permissions": [ + { + "delete": 1, + "read": 1, + "role": "System Manager" + }, + { + "delete": 1, + "if_owner": 1, + "read": 1, + "role": "All" + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/frappe/integrations/doctype/token_cache/token_cache.py b/frappe/integrations/doctype/token_cache/token_cache.py new file mode 100644 index 0000000000..7cac58fae0 --- /dev/null +++ b/frappe/integrations/doctype/token_cache/token_cache.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2019, Frappe Technologies and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +from datetime import datetime, timedelta + +import frappe +from frappe import _ +from frappe.utils import cstr, cint +from frappe.model.document import Document + +class TokenCache(Document): + + def get_auth_header(self): + if self.access_token: + headers = {'Authorization': 'Bearer ' + self.get_password('access_token')} + return headers + + raise frappe.exceptions.DoesNotExistError + + def update_data(self, data): + """ + Store data returned by authorization flow. + + Params: + data - Dict with access_token, refresh_token, expires_in and scope. + """ + token_type = cstr(data.get('token_type', '')).lower() + if token_type not in ['bearer', 'mac']: + frappe.throw(_('Received an invalid token type.')) + # 'Bearer' or 'MAC' + token_type = token_type.title() if token_type == 'bearer' else token_type.upper() + + self.token_type = token_type + self.access_token = cstr(data.get('access_token', '')) + self.refresh_token = cstr(data.get('refresh_token', '')) + self.expires_in = cint(data.get('expires_in', 0)) + + new_scopes = data.get('scope') + if new_scopes: + if isinstance(new_scopes, str): + new_scopes = new_scopes.split(' ') + if isinstance(new_scopes, list): + self.scopes = None + for scope in new_scopes: + self.append('scopes', {'scope': scope}) + + self.state = None + self.save(ignore_permissions=True) + frappe.db.commit() + return self + + def get_expires_in(self): + expiry_time = frappe.utils.get_datetime(self.modified) + timedelta(self.expires_in) + return (datetime.now() - expiry_time).total_seconds() + + def is_expired(self): + return self.get_expires_in() < 0 + + def get_json(self): + return { + 'access_token': self.get_password('access_token', ''), + 'refresh_token': self.get_password('refresh_token', ''), + 'expires_in': self.get_expires_in(), + 'token_type': self.token_type + } diff --git a/frappe/integrations/doctype/webhook/webhook.py b/frappe/integrations/doctype/webhook/webhook.py index f1556aa661..ad64d9f714 100644 --- a/frappe/integrations/doctype/webhook/webhook.py +++ b/frappe/integrations/doctype/webhook/webhook.py @@ -85,7 +85,7 @@ def enqueue_webhook(doc, webhook): for i in range(3): try: - r = requests.post(webhook.request_url, data=json.dumps(data), headers=headers, timeout=5) + r = requests.post(webhook.request_url, data=json.dumps(data, default=str), headers=headers, timeout=5) r.raise_for_status() frappe.logger().debug({"webhook_success": r.text}) break diff --git a/frappe/integrations/oauth2.py b/frappe/integrations/oauth2.py index a750c8328c..07db778a2d 100644 --- a/frappe/integrations/oauth2.py +++ b/frappe/integrations/oauth2.py @@ -20,6 +20,7 @@ def get_oauth_server(): return frappe.local.oauth_server def sanitize_kwargs(param_kwargs): + """Remove 'data' and 'cmd' keys, if present.""" arguments = param_kwargs arguments.pop('data', None) arguments.pop('cmd', None) diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py index 5d86b3bac8..7a90ecaca5 100644 --- a/frappe/model/base_document.py +++ b/frappe/model/base_document.py @@ -48,7 +48,7 @@ def get_controller(doctype): else: class_overrides = frappe.get_hooks('override_doctype_class') if class_overrides and class_overrides.get(doctype): - import_path = frappe.get_hooks('override_doctype_class').get(doctype)[-1] + import_path = class_overrides[doctype][-1] module_path, classname = import_path.rsplit('.', 1) module = frappe.get_module(module_path) if not hasattr(module, classname): @@ -69,10 +69,13 @@ def get_controller(doctype): if frappe.local.dev_server: return _get_controller() - - key = '{}:doctype_classes'.format(frappe.local.site) - return frappe.cache().hget(key, doctype, generator=_get_controller, shared=True) - + + site_controllers = frappe.controllers.setdefault(frappe.local.site, {}) + if doctype not in site_controllers: + site_controllers[doctype] = _get_controller() + + return site_controllers[doctype] + class BaseDocument(object): ignore_in_getter = ("doctype", "_meta", "meta", "_table_fields", "_valid_columns") diff --git a/frappe/model/db_query.py b/frappe/model/db_query.py index ee4b1dde2a..8eac75eb65 100644 --- a/frappe/model/db_query.py +++ b/frappe/model/db_query.py @@ -40,7 +40,10 @@ class DatabaseQuery(object): ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, update=None, add_total_row=None, user_settings=None, reference_doctype=None, return_query=False, strict=True, pluck=None, ignore_ddl=False): - if not ignore_permissions and not frappe.has_permission(self.doctype, "read", user=user): + if not ignore_permissions and \ + not frappe.has_permission(self.doctype, "select", user=user) and \ + not frappe.has_permission(self.doctype, "read", user=user): + frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) raise frappe.PermissionError(self.doctype) @@ -315,7 +318,10 @@ class DatabaseQuery(object): def append_table(self, table_name): self.tables.append(table_name) doctype = table_name[4:-1] - if (not self.flags.ignore_permissions) and (not frappe.has_permission(doctype)): + ptype = 'select' if frappe.only_has_select_perm(doctype) else 'read' + + if (not self.flags.ignore_permissions) and\ + (not frappe.has_permission(doctype, ptype=ptype)): frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(doctype)) raise frappe.PermissionError(doctype) @@ -576,7 +582,7 @@ class DatabaseQuery(object): self.shared = frappe.share.get_shared(self.doctype, self.user) if (not meta.istable and - not role_permissions.get("read") and + not (role_permissions.get("select") or role_permissions.get("read")) and not self.flags.ignore_permissions and not has_any_user_permission_for_doctype(self.doctype, self.user, self.reference_doctype)): only_if_shared = True @@ -591,7 +597,7 @@ class DatabaseQuery(object): self.match_conditions.append("`tab{0}`.`owner` = {1}".format(self.doctype, frappe.db.escape(self.user, percent=False))) # add user permission only if role has read perm - elif role_permissions.get("read"): + elif role_permissions.get("read") or role_permissions.get("select"): # get user permissions user_permissions = frappe.permissions.get_user_permissions(self.user) self.add_user_permissions(user_permissions) diff --git a/frappe/model/delete_doc.py b/frappe/model/delete_doc.py index f3710de39b..7b29692ad1 100644 --- a/frappe/model/delete_doc.py +++ b/frappe/model/delete_doc.py @@ -76,7 +76,12 @@ def delete_doc(doctype=None, name=None, force=0, ignore_doctypes=None, for_reloa delete_from_table(doctype, name, ignore_doctypes, None) - if not (for_reload or frappe.flags.in_migrate or frappe.flags.in_install or frappe.flags.in_uninstall or frappe.flags.in_test): + if frappe.conf.developer_mode and not doc.custom and not ( + for_reload + or frappe.flags.in_migrate + or frappe.flags.in_install + or frappe.flags.in_uninstall + ): try: delete_controllers(name, doc.module) except (FileNotFoundError, OSError, KeyError): diff --git a/frappe/model/document.py b/frappe/model/document.py index d17025538b..1cd981ead8 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -939,15 +939,17 @@ class Document(BaseDocument): self.load_doc_before_save() self.reset_seen() + # before_validate method should be executed before ignoring validations + if self._action in ("save", "submit"): + self.run_method("before_validate") + if self.flags.ignore_validate: return if self._action=="save": - self.run_method("before_validate") self.run_method("validate") self.run_method("before_save") elif self._action=="submit": - self.run_method("before_validate") self.run_method("validate") self.run_method("before_submit") elif self._action=="cancel": diff --git a/frappe/model/meta.py b/frappe/model/meta.py index c740d495c1..5dc7ca2d4d 100644 --- a/frappe/model/meta.py +++ b/frappe/model/meta.py @@ -68,7 +68,7 @@ def load_doctype_from_file(doctype): class Meta(Document): _metaclass = True default_fields = list(default_fields)[1:] - special_doctypes = ("DocField", "DocPerm", "Role", "DocType", "Module Def", 'DocType Action', 'DocType Link') + special_doctypes = ("DocField", "DocPerm", "DocType", "Module Def", 'DocType Action', 'DocType Link') def __init__(self, doctype): self._fields = {} @@ -450,6 +450,25 @@ class Meta(Document): return self.high_permlevel_fields + def get_permlevel_access(self, permission_type='read', parenttype=None): + has_access_to = [] + roles = frappe.get_roles() + for perm in self.get_permissions(parenttype): + if perm.role in roles and perm.permlevel > 0 and perm.get(permission_type): + if perm.permlevel not in has_access_to: + has_access_to.append(perm.permlevel) + + return has_access_to + + def get_permissions(self, parenttype=None): + if self.istable and parenttype: + # use parent permissions + permissions = frappe.get_meta(parenttype).permissions + else: + permissions = self.get('permissions', []) + + return permissions + def get_dashboard_data(self): '''Returns dashboard setup related to this doctype. @@ -484,6 +503,8 @@ class Meta(Document): if not data.transactions: # init groups data.transactions = [] + + if not data.non_standard_fieldnames: data.non_standard_fieldnames = {} for link in dashboard_links: diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py index 35fbf94dc6..2c9dc5d823 100644 --- a/frappe/model/rename_doc.py +++ b/frappe/model/rename_doc.py @@ -21,8 +21,16 @@ def update_document_title(doctype, docname, title_field=None, old_title=None, ne docname = rename_doc(doctype=doctype, old=docname, new=new_name, merge=merge) if old_title and new_title and not old_title == new_title: - frappe.db.set_value(doctype, docname, title_field, new_title) - frappe.msgprint(_('Saved'), alert=True, indicator='green') + try: + frappe.db.set_value(doctype, docname, title_field, new_title) + frappe.msgprint(_('Saved'), alert=True, indicator='green') + except Exception as e: + if frappe.db.is_duplicate_entry(e): + frappe.throw( + _("{0} {1} already exists").format(doctype, frappe.bold(docname)), + title=_("Duplicate Name"), + exc=frappe.DuplicateEntryError + ) return docname @@ -49,9 +57,7 @@ def rename_doc(doctype, old, new, force=False, merge=False, ignore_permissions=F old_doc = frappe.get_doc(doctype, old) out = old_doc.run_method("before_rename", old, new, merge) or {} new = (out.get("new") or new) if isinstance(out, dict) else (out or new) - - if doctype != "DocType": - new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) + new = validate_rename(doctype, new, meta, merge, force, ignore_permissions) if not merge: rename_parent_and_child(doctype, old, new, meta) @@ -250,6 +256,7 @@ def update_link_field_values(link_fields, old, new, doctype): pass else: parent = field['parent'] + docfield = field["fieldname"] # Handles the case where one of the link fields belongs to # the DocType being renamed. @@ -261,11 +268,8 @@ def update_link_field_values(link_fields, old, new, doctype): if parent == new and doctype == "DocType": parent = old - frappe.db.sql(""" - update `tab{table_name}` set `{fieldname}`=%s - where `{fieldname}`=%s""".format( - table_name=parent, - fieldname=field['fieldname']), (new, old)) + frappe.db.set_value(parent, {docfield: old}, docfield, new) + # update cached link_fields as per new if doctype=='DocType' and field['parent'] == old: field['parent'] = new diff --git a/frappe/model/sync.py b/frappe/model/sync.py index 6f29cc2bb0..61983d322c 100644 --- a/frappe/model/sync.py +++ b/frappe/model/sync.py @@ -52,23 +52,22 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe ("desk", "onboarding_step"), ("desk", "onboarding_step_map"), ("desk", "module_onboarding"), - ("desk", "desk_link"), - ("desk", "desk_chart"), - ("desk", "desk_shortcut"), + ("desk", "workspace_link"), + ("desk", "workspace_chart"), + ("desk", "workspace_shortcut"), ("desk", "workspace")): files.append(os.path.join(frappe.get_app_path("frappe"), d[0], "doctype", d[1], d[1] + ".json")) for module_name in frappe.local.app_modules.get(app_name) or []: folder = os.path.dirname(frappe.get_module(app_name + "." + module_name).__file__) - get_doc_files(files, folder, force, sync_everything, verbose=verbose) + get_doc_files(files, folder) l = len(files) if l: for i, doc_path in enumerate(files): import_file_by_path(doc_path, force=force, ignore_version=True, reset_permissions=reset_permissions, for_sync=True) - #print module_name + ' | ' + doctype + ' | ' + name frappe.db.commit() @@ -78,7 +77,7 @@ def sync_for(app_name, force=0, sync_everything = False, verbose=False, reset_pe # print each progress bar on new line print() -def get_doc_files(files, start_path, force=0, sync_everything = False, verbose=False): +def get_doc_files(files, start_path): """walk and sync all doctypes and pages""" # load in sequence - warning for devs diff --git a/frappe/model/workflow.py b/frappe/model/workflow.py index 72ce8c9ce4..3e8125f9b1 100644 --- a/frappe/model/workflow.py +++ b/frappe/model/workflow.py @@ -53,14 +53,17 @@ def get_transitions(doc, workflow = None, raise_exception=False): return transitions def get_workflow_safe_globals(): - # access to frappe.db.get_value and frappe.db.get_list + # access to frappe.db.get_value, frappe.db.get_list, and date time utils. return dict( frappe=frappe._dict( - db=frappe._dict( - get_value=frappe.db.get_value, - get_list=frappe.db.get_list + db=frappe._dict(get_value=frappe.db.get_value, get_list=frappe.db.get_list), + session=frappe.session, + utils=frappe._dict( + now_datetime=frappe.utils.now_datetime, + add_to_date=frappe.utils.add_to_date, + get_datetime=frappe.utils.get_datetime, + now=frappe.utils.now, ), - session=frappe.session ) ) @@ -117,9 +120,8 @@ def apply_workflow(doc, action): return doc @frappe.whitelist() -def can_cancel_document(doc): - doc = frappe.get_doc(frappe.parse_json(doc)) - workflow = get_workflow(doc.doctype) +def can_cancel_document(doctype): + workflow = get_workflow(doctype) for state_doc in workflow.states: if state_doc.doc_status == '2': for transition in workflow.transitions: diff --git a/frappe/patches.txt b/frappe/patches.txt index 08b30be90a..0f37946398 100644 --- a/frappe/patches.txt +++ b/frappe/patches.txt @@ -209,7 +209,7 @@ frappe.patches.v9_1.resave_domain_settings frappe.patches.v9_1.revert_domain_settings frappe.patches.v9_1.move_feed_to_activity_log execute:frappe.delete_doc('Page', 'data-import-tool', ignore_missing=True) -frappe.patches.v10_0.reload_countries_and_currencies # 14-10-2020 +frappe.patches.v10_0.reload_countries_and_currencies # 2021-02-03 frappe.patches.v10_0.refactor_social_login_keys frappe.patches.v10_0.enable_chat_by_default_within_system_settings frappe.patches.v10_0.remove_custom_field_for_disabled_domain @@ -316,6 +316,8 @@ frappe.patches.v13_0.delete_event_producer_and_consumer_keys frappe.patches.v13_0.web_template_set_module #2020-10-05 frappe.patches.v13_0.remove_custom_link execute:frappe.delete_doc("DocType", "Footer Item") +execute:frappe.reload_doctype('user') +execute:frappe.reload_doctype('docperm') frappe.patches.v13_0.replace_field_target_with_open_in_new_tab frappe.core.doctype.role.patches.v13_set_default_desk_properties frappe.patches.v13_0.add_switch_theme_to_navbar_settings @@ -323,3 +325,7 @@ frappe.patches.v13_0.update_icons_in_customized_desk_pages execute:frappe.db.set_default('desktop:home_page', 'space') execute:frappe.delete_doc_if_exists('Page', 'workspace') execute:frappe.delete_doc_if_exists('Page', 'dashboard', force=1) +frappe.core.doctype.page.patches.drop_unused_pages +execute:frappe.get_doc('Role', 'Guest').save() # remove desk access +frappe.patches.v13_0.rename_desk_page_to_workspace # 02.02.2021 +frappe.patches.v13_0.delete_package_publish_tool diff --git a/frappe/patches/v13_0/delete_package_publish_tool.py b/frappe/patches/v13_0/delete_package_publish_tool.py new file mode 100644 index 0000000000..25024f58dd --- /dev/null +++ b/frappe/patches/v13_0/delete_package_publish_tool.py @@ -0,0 +1,11 @@ +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +from __future__ import unicode_literals +import frappe + + +def execute(): + frappe.delete_doc("DocType", "Package Publish Tool", ignore_missing=True) + frappe.delete_doc("DocType", "Package Document Type", ignore_missing=True) + frappe.delete_doc("DocType", "Package Publish Target", ignore_missing=True) diff --git a/frappe/patches/v13_0/rename_desk_page_to_workspace.py b/frappe/patches/v13_0/rename_desk_page_to_workspace.py index 457e5c066e..6483fc380c 100644 --- a/frappe/patches/v13_0/rename_desk_page_to_workspace.py +++ b/frappe/patches/v13_0/rename_desk_page_to_workspace.py @@ -2,6 +2,20 @@ import frappe from frappe.model.rename_doc import rename_doc def execute(): - if frappe.db.exists("Doctype", "Desk Page"): - rename_doc('DocType', 'Desk Page', 'Workspace') - frappe.reload_doc('desk', 'doctype', 'workspace') + if frappe.db.exists("DocType", "Desk Page"): + if frappe.db.exists('DocType', 'Workspace'): + # this patch was not added initially, so this page might still exist + frappe.delete_doc('DocType', 'Desk Page') + else: + frappe.flags.ignore_route_conflict_validation = True + rename_doc('DocType', 'Desk Page', 'Workspace') + frappe.flags.ignore_route_conflict_validation = False + + rename_doc('DocType', 'Desk Chart', 'Workspace Chart', ignore_if_exists=True) + rename_doc('DocType', 'Desk Shortcut', 'Workspace Shortcut', ignore_if_exists=True) + rename_doc('DocType', 'Desk Link', 'Workspace Link', ignore_if_exists=True) + + frappe.reload_doc('desk', 'doctype', 'workspace', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_link', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_chart', force=True) + frappe.reload_doc('desk', 'doctype', 'workspace_shortcut', force=True) diff --git a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py index da7d054682..93bf5c766e 100644 --- a/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py +++ b/frappe/patches/v13_0/update_icons_in_customized_desk_pages.py @@ -2,6 +2,8 @@ from __future__ import unicode_literals import frappe def execute(): + if not frappe.db.exists('Desk Page'): return + pages = frappe.get_all("Desk Page", filters={ "is_standard": False }, fields=["name", "extends", "for_user"]) default_icon = {} for page in pages: diff --git a/frappe/patches/v13_0/website_theme_custom_scss.py b/frappe/patches/v13_0/website_theme_custom_scss.py index 0035283428..a5f08324e8 100644 --- a/frappe/patches/v13_0/website_theme_custom_scss.py +++ b/frappe/patches/v13_0/website_theme_custom_scss.py @@ -2,9 +2,23 @@ import frappe def execute(): frappe.reload_doctype('Website Theme') + frappe.reload_doc('website', 'doctype', 'website_theme_ignore_app') + frappe.reload_doc('website', 'doctype', 'color') + for theme in frappe.get_all('Website Theme'): doc = frappe.get_doc('Website Theme', theme.name) if not doc.get('custom_scss') and doc.theme_scss: # move old theme to new theme doc.custom_scss = doc.theme_scss + + if doc.background_color: + setup_color_record(doc.background_color) + doc.save() + +def setup_color_record(color): + frappe.get_doc({ + "doctype": "Color", + "__newname": color, + "color": color, + }).save() diff --git a/frappe/permissions.py b/frappe/permissions.py index 0d766aec8d..abb1f6653a 100644 --- a/frappe/permissions.py +++ b/frappe/permissions.py @@ -7,7 +7,7 @@ import frappe, copy, json from frappe import _, msgprint from frappe.utils import cint import frappe.share -rights = ("read", "write", "create", "delete", "submit", "cancel", "amend", +rights = ("select", "read", "write", "create", "delete", "submit", "cancel", "amend", "print", "email", "report", "import", "export", "set_user_permissions", "share") # TODO: @@ -73,6 +73,7 @@ def has_permission(doctype, ptype="read", doc=None, verbose=False, user=None, ra role_permissions = get_role_permissions(meta, user=user) perm = role_permissions.get(ptype) + if not perm: push_perm_check_log(_('User {0} does not have doctype access via role permission for document {1}').format(frappe.bold(user), frappe.bold(doctype))) @@ -192,9 +193,9 @@ def get_role_permissions(doctype_meta, user=None): and ptype != 'create'): perms['if_owner'][ptype] = 1 # has no access if not owner - # only provide read access so that user is able to at-least access list + # only provide select or read access so that user is able to at-least access list # (and the documents will be filtered based on owner sin further checks) - perms[ptype] = 1 if ptype == 'read' else 0 + perms[ptype] = 1 if ptype in ['select', 'read'] else 0 frappe.local.role_permissions[cache_key] = perms @@ -397,7 +398,8 @@ def set_user_permission_if_allowed(doctype, name, user, with_message=False): if get_role_permissions(frappe.get_meta(doctype), user).set_user_permissions!=1: add_user_permission(doctype, name, user) -def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, is_default=0): +def add_user_permission(doctype, name, user, ignore_permissions=False, applicable_for=None, + is_default=0, hide_descendants=0): '''Add user permission''' from frappe.core.doctype.user_permission.user_permission import user_permission_exists @@ -412,6 +414,7 @@ def add_user_permission(doctype, name, user, ignore_permissions=False, applicabl for_value=name, is_default=is_default, applicable_for=applicable_for, + hide_descendants=hide_descendants, )).insert(ignore_permissions=ignore_permissions) def remove_user_permission(doctype, name, user): diff --git a/frappe/printing/doctype/print_format/print_format.js b/frappe/printing/doctype/print_format/print_format.js index 9ef5652dda..786f8f97ab 100644 --- a/frappe/printing/doctype/print_format/print_format.js +++ b/frappe/printing/doctype/print_format/print_format.js @@ -66,7 +66,7 @@ frappe.ui.form.on("Print Format", { hide_absolute_value_field: function (frm) { // TODO: make it work with frm.doc.doc_type // Problem: frm isn't updated in some random cases - const doctype = locals[frm.doc.doctype][frm.doc.name]; + const doctype = locals[frm.doc.doctype][frm.doc.name].doc_type; if (doctype) { frappe.model.with_doctype(doctype, () => { const meta = frappe.get_meta(doctype); diff --git a/frappe/printing/doctype/print_format/print_format.json b/frappe/printing/doctype/print_format/print_format.json index 6e51ef0018..92d4a67d14 100644 --- a/frappe/printing/doctype/print_format/print_format.json +++ b/frappe/printing/doctype/print_format/print_format.json @@ -201,17 +201,17 @@ { "default": "0", "depends_on": "doc_type", - "description": "If checked, negative numberic values of Currency, Quantity or Count would be shown as positive", + "description": "If checked, negative numeric values of Currency, Quantity or Count would be shown as positive", "fieldname": "absolute_value", "fieldtype": "Check", - "label": "Show absolute values" + "label": "Show Absolute Values" } ], "icon": "fa fa-print", "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2020-12-10 18:58:55.598269", + "modified": "2020-12-14 11:38:49.132061", "modified_by": "Administrator", "module": "Printing", "name": "Print Format", diff --git a/frappe/printing/page/print/print.js b/frappe/printing/page/print/print.js index 82cfd6adf5..7e1db1eddb 100644 --- a/frappe/printing/page/print/print.js +++ b/frappe/printing/page/print/print.js @@ -157,7 +157,7 @@ frappe.ui.form.PrintView = class { label: value, value: value, disabled: true - } + }; } setup_menu() { @@ -291,6 +291,7 @@ frappe.ui.form.PrintView = class { // } setup_customize_dialog() { + let print_format = this.get_print_format(); $(document).on('new-print-format', (e) => { this.refresh_print_options(); if (e.print_format) { @@ -429,7 +430,7 @@ frappe.ui.form.PrintView = class { }); setTimeout(() => { - $print_format.height(this.$print_format_body.find('.print-format').outerHeight()) + $print_format.height(this.$print_format_body.find('.print-format').outerHeight()); }, 500); } @@ -474,7 +475,7 @@ frappe.ui.form.PrintView = class { no_letterhead: me.with_letterhead(), letterhead: this.get_letterhead(), }, - callback: function(data) {}, + callback: function() {}, }); } else if (me.get_mapped_printer().length === 1) { // printer is already mapped in localstorage (applies for both raw and pdf ) diff --git a/frappe/public/build.json b/frappe/public/build.json index b50ce4ce7e..35e9d62436 100755 --- a/frappe/public/build.json +++ b/frappe/public/build.json @@ -136,6 +136,7 @@ "public/js/frappe/router_history.js", "public/js/frappe/defaults.js", "public/js/frappe/roles_editor.js", + "public/js/frappe/module_editor.js", "public/js/frappe/microtemplate.js", "public/js/frappe/ui/page.html", @@ -250,7 +251,7 @@ "public/js/frappe/list/list_view.js", "public/js/frappe/list/list_factory.js", - "public/js/frappe/list/views.js", + "public/js/frappe/list/list_view_select.js", "public/js/frappe/list/list_sidebar.js", "public/js/frappe/list/list_sidebar.html", "public/js/frappe/list/list_sidebar_stat.html", @@ -261,6 +262,7 @@ "public/js/frappe/views/calendar/calendar.js", "public/js/frappe/views/dashboard/dashboard_view.js", "public/js/frappe/views/image/image_view.js", + "public/js/frappe/views/map/map_view.js", "public/js/frappe/views/kanban/kanban_view.js", "public/js/frappe/views/inbox/inbox_view.js", "public/js/frappe/views/file/file_view.js", @@ -300,7 +302,7 @@ "public/less/controls.less", "node_modules/frappe-datatable/dist/frappe-datatable.css" ], - "css/email.css": "public/less/email.less", + "css/email.css": "public/scss/email.scss", "js/barcode_scanner.min.js": "public/js/frappe/barcode_scanner/quagga.js", "js/user_profile_controller.min.js": "desk/page/user_profile/user_profile_controller.js", "css/login.css": "public/scss/login.scss" diff --git a/frappe/public/css/email.css b/frappe/public/css/email.css deleted file mode 100644 index 62f0c6ebec..0000000000 --- a/frappe/public/css/email.css +++ /dev/null @@ -1,181 +0,0 @@ -/* csslint ignore:start */ -/* palette colors*/ -body { - line-height: 1.5; - color: #12283A; -} -p { - margin: 1em 0 !important; -} -.ql-editor { - white-space: normal; -} -.ql-editor p { - margin: 0 !important; -} -hr { - border-top: 1px solid #ECEEF0; -} -.body-table { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -} -.body-table td { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; -} -.email-header, -.email-body, -.email-footer { - width: 100% !important; - min-width: 100% !important; -} -.email-body { - font-size: 14px; -} -.email-footer { - border-top: 1px solid #ECEEF0; - font-size: 12px; -} -.email-header { - border: 1px solid #ECEEF0; - border-radius: 4px 4px 0 0; -} -.email-header .brand-image { - width: 24px; - height: 24px; - display: block; -} -.email-header-title { - font-weight: bold; -} -.body-table.has-header .email-body { - border: 1px solid #ECEEF0; - border-radius: 0 0 4px 4px; - border-top: none; -} -.body-table.has-header .email-footer { - border-top: none; -} -.email-footer-container { - margin-top: 30px; -} -.email-footer-container > div:not(:last-child) { - margin-bottom: 5px; -} -.email-unsubscribe a { - color: #8d99a6; - text-decoration: underline; -} -.btn { - text-decoration: none; - padding: 7px 10px; - font-size: 12px; - border: 1px solid; - border-radius: 3px; -} -.btn.btn-default { - color: #fff; - background-color: #f0f4f7; - border-color: transparent; -} -.btn.btn-primary { - color: #fff; - background-color: #2996F1; - border-color: #444bff; -} -.table { - width: 100%; - border-collapse: collapse; -} -.table td, -.table th { - padding: 8px; - line-height: 1.42857143; - vertical-align: top; - border-top: 1px solid #ECEEF0; - text-align: left; -} -.table th { - font-weight: bold; -} -.table > thead > tr > th { - vertical-align: middle; - border-bottom: 2px solid #ECEEF0; -} -.table > thead:first-child > tr:first-child > th { - border-top: none; -} -.table.table-bordered { - border: 1px solid #ECEEF0; -} -.table.table-bordered td, -.table.table-bordered th { - border: 1px solid #ECEEF0; -} -.more-info { - font-size: 80% !important; - color: #8d99a6 !important; - border-top: 1px solid #ebeff2; - padding-top: 10px; -} -.text-right { - text-align: right !important; -} -.text-center { - text-align: center !important; -} -.text-muted { - color: #8d99a6 !important; -} -.text-extra-muted { - color: #ECEEF0 !important; -} -.text-regular { - font-size: 14px; -} -.text-medium { - font-size: 12px; -} -.text-small { - font-size: 10px; -} -.text-bold { - font-weight: bold; -} -.indicator { - width: 8px; - height: 8px; - border-radius: 8px; - background-color: #b8c2cc; - display: inline-block; - margin-right: 5px; -} -.indicator.indicator-blue { - background-color: #5e64ff; -} -.indicator.indicator-green { - background-color: #98d85b; -} -.indicator.indicator-orange { - background-color: #ffa00a; -} -.indicator.indicator-red { - background-color: #ff5858; -} -.indicator.indicator-yellow { - background-color: #feef72; -} -.screenshot { - box-shadow: 0px 3px 6px rgba(0, 0, 0, 0.1); - border: 1px solid #ECEEF0; - margin: 8px 0; - max-width: 100%; -} -.list-unstyled { - list-style-type: none; - padding: 0; -} -/* auto email report */ -.report-title { - margin-bottom: 20px; -} -/* csslint ignore:end */ diff --git a/frappe/public/css/list.css b/frappe/public/css/list.css index 5ae77c73ca..88ad147d33 100644 --- a/frappe/public/css/list.css +++ b/frappe/public/css/list.css @@ -401,6 +401,13 @@ input.list-row-checkbox { .pswp__more-item img { max-height: 100%; } +.map-view-container { + display: flex; + flex-wrap: wrap; + width: 100%; + height: calc(100vh - 284px); + z-index: 0; +} .list-paging-area .gantt-view-mode { margin-left: 15px; margin-right: 15px; diff --git a/frappe/public/icons/timeless/symbol-defs.svg b/frappe/public/icons/timeless/symbol-defs.svg index 56e345e74f..d6852c620f 100644 --- a/frappe/public/icons/timeless/symbol-defs.svg +++ b/frappe/public/icons/timeless/symbol-defs.svg @@ -663,4 +663,18 @@ + + + + + + + + + + + + + + diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js index 1674a348b8..813205ecd0 100644 --- a/frappe/public/js/frappe/chat.js +++ b/frappe/public/js/frappe/chat.js @@ -192,7 +192,7 @@ frappe.quick_edit = (doctype, docname, fn) => { }) const dialog = new frappe.ui.Dialog({ - title: __('Edit') + `${doctype} (${docname})`, + title: __('Edit') + `${doctype} (${docname})`, fields: required, action: { primary: { @@ -1305,8 +1305,6 @@ class { this.set_wrapper(selector ? selector : "body") this.set_options(options) - // Load Emojis. - frappe.chat.emoji() } /** @@ -2402,11 +2400,11 @@ class extends Component { return ( h("div",{class:`chat-bubble ${props.groupable ? "chat-groupable" : ""} chat-bubble-${me ? "r" : "l"}`, onclick: this.onclick}, - props.room_type === "Group" && !me? + props.room_type === "Group" && !me ? h("div",{class:"chat-bubble-author"}, - h("a", { onclick: () => { frappe.set_route('Form', 'User', props.user) } }, - frappe.user.full_name(props.user) - ) + h("a", { onclick: () => { frappe.set_route('Form', 'User', props.user) } }, + frappe.user.full_name(props.user) + ) ) : null, h("div",{class:"chat-bubble-content"}, h("small","", diff --git a/frappe/public/js/frappe/color_picker/color_picker.js b/frappe/public/js/frappe/color_picker/color_picker.js index e66d693d85..aa26bb1c90 100644 --- a/frappe/public/js/frappe/color_picker/color_picker.js +++ b/frappe/public/js/frappe/color_picker/color_picker.js @@ -119,6 +119,7 @@ class Picker { } setup_hue_event() { + // eslint-disable-next-line no-unused-vars let on_drag = (x, y) => { this.hue_selector_position.x = x; this.hue = Math.round(x * 360 / this.hue_map.offsetWidth); @@ -148,7 +149,9 @@ class Picker { } get_pointer_coords() { + // eslint-disable-next-line no-unused-vars let h, s, v; + // eslint-disable-next-line no-unused-vars [h, s, v] = utils.get_hsv(this.get_color()); let width = this.color_map.offsetWidth; let height = this.color_map.offsetHeight; diff --git a/frappe/public/js/frappe/desk.js b/frappe/public/js/frappe/desk.js index 16f54c1b13..cac2e65885 100644 --- a/frappe/public/js/frappe/desk.js +++ b/frappe/public/js/frappe/desk.js @@ -64,7 +64,7 @@ frappe.Application = Class.extend({ frappe.theme_switcher = new frappe.ui.ThemeSwitcher(); frappe.theme_switcher.show(); } - }) + }); this.set_rtl(); @@ -164,6 +164,7 @@ frappe.Application = Class.extend({ }, set_route() { + frappe.flags.setting_original_route = true; if (frappe.boot && localStorage.getItem("session_last_route")) { frappe.set_route(localStorage.getItem("session_last_route")); localStorage.removeItem("session_last_route"); @@ -171,6 +172,7 @@ frappe.Application = Class.extend({ // route to home page frappe.router.route(); } + frappe.after_ajax(() => frappe.flags.setting_original_route = false); }, setup_frappe_vue() { @@ -253,10 +255,7 @@ frappe.Application = Class.extend({ }, load_bootinfo: function() { if(frappe.boot) { - frappe.modules = {}; - (frappe.boot.allowed_workspaces || []).forEach(function(m) { - frappe.modules[m.module]=m; - }); + this.setup_workspaces(); frappe.model.sync(frappe.boot.docs); $.extend(frappe._messages, frappe.boot.__messages); this.check_metadata_cache_status(); @@ -278,6 +277,19 @@ frappe.Application = Class.extend({ } }, + setup_workspaces() { + frappe.modules = {}; + frappe.workspaces = {}; + for (let page of frappe.boot.allowed_workspaces || []) { + frappe.modules[page.module]=page; + frappe.workspaces[frappe.router.slug(page.name)] = page; + } + if (!frappe.workspaces['home']) { + // default workspace is settings for Frappe + frappe.workspaces['home'] = frappe.workspaces['build']; + } + }, + load_user_permissions: function() { frappe.defaults.update_user_permissions(); diff --git a/frappe/public/js/frappe/file_uploader/FileBrowser.vue b/frappe/public/js/frappe/file_uploader/FileBrowser.vue index 46bd61f120..f3017261ee 100644 --- a/frappe/public/js/frappe/file_uploader/FileBrowser.vue +++ b/frappe/public/js/frappe/file_uploader/FileBrowser.vue @@ -5,23 +5,26 @@ href="" class="text-muted text-medium" @click.prevent="$emit('hide-browser')" - >{{ __('← Back to upload files') }} + > + {{ __("← Back to upload files") }} + -
    +
    + v-model="search_text" + @input="search_by_name" + />
    @@ -30,16 +33,18 @@ import TreeNode from "./TreeNode.vue"; export default { - name: 'FileBrowser', + name: "FileBrowser", components: { TreeNode }, data() { return { node: { - label: __('Home'), - value: 'Home', + label: __("Home"), + value: "Home", children: [], + children_start: 0, + children_loading: false, is_leaf: false, fetching: false, fetched: false, @@ -47,8 +52,9 @@ export default { filtered: true }, selected_node: {}, - filter_text: '' - } + search_text: "", + page_length: 10 + }; }, mounted() { this.toggle_node(this.node); @@ -57,27 +63,51 @@ export default { toggle_node(node) { if (!node.fetched && !node.is_leaf) { node.fetching = true; - this.get_files_in_folder(node.value) - .then(files => { + node.children_start = 0; + node.children_loading = false; + this.get_files_in_folder(node.value, 0).then( + ({ files, has_more }) => { node.open = true; node.children = files; node.fetched = true; node.fetching = false; - }); + node.children_start += this.page_length; + node.has_more_children = has_more; + } + ); } else { node.open = !node.open; this.select_node(node); } }, + load_more(node) { + if (node.has_more_children) { + let start = node.children_start; + node.children_loading = true; + this.get_files_in_folder(node.value, start).then( + ({ files, has_more }) => { + node.children = node.children.concat(files); + node.children_start += this.page_length; + node.has_more_children = has_more; + node.children_loading = false; + } + ); + } + }, select_node(node) { if (node.is_leaf) { this.selected_node = node; } }, - get_files_in_folder(folder) { - return frappe.call('frappe.core.doctype.file.file.get_files_in_folder', { folder }) + get_files_in_folder(folder, start) { + return frappe + .call("frappe.core.doctype.file.file.get_files_in_folder", { + folder, + start, + page_length: this.page_length + }) .then(r => { - let files = r.message || []; + let { files = [], has_more = false } = r.message || {}; files.sort((a, b) => { if (a.is_folder && b.is_folder) { return a.modified < b.modified ? -1 : 1; @@ -90,47 +120,77 @@ export default { } return 0; }); - return files.map(file => { - let filename = file.file_name || file.name; - return { - label: frappe.utils.file_name_ellipsis(filename, 40), - filename: filename, - file_url: file.file_url, - value: file.name, - is_leaf: !file.is_folder, - fetched: !file.is_folder, // fetched if node is leaf - children: [], - open: false, - fetching: false, - filtered: true - } - }); + files = files.map(file => this.make_file_node(file)); + return { files, has_more }; }); }, - apply_filter: frappe.utils.debounce(function() { - let filter_text = this.filter_text.toLowerCase(); - let apply_filter = (node) => { - let search_string = node.filename.toLowerCase(); - if (node.is_leaf) { - node.filtered = search_string.includes(filter_text); - } else { - node.children.forEach(apply_filter); - } + search_by_name: frappe.utils.debounce(function() { + if (this.search_text === "") { + this.node = this.folder_node; + return; } - this.node.children.forEach(apply_filter); - }, 300) + if (this.search_text.length < 3) return; + frappe + .call( + "frappe.core.doctype.file.file.get_files_by_search_text", + { + text: this.search_text + } + ) + .then(r => { + let files = r.message || []; + files = files.map(file => this.make_file_node(file)); + if (!this.folder_node) { + this.folder_node = this.node; + } + this.node = { + label: __("Search Results"), + value: "", + children: files, + by_search: true, + open: true, + filtered: true + }; + }); + }, 300), + make_file_node(file) { + let filename = file.file_name || file.name; + let label = frappe.utils.file_name_ellipsis(filename, 40); + return { + label: label, + filename: filename, + file_url: file.file_url, + value: file.name, + is_leaf: !file.is_folder, + fetched: !file.is_folder, // fetched if node is leaf + children: [], + children_loading: false, + children_start: 0, + open: false, + fetching: false, + filtered: true + }; + } } -} +}; diff --git a/frappe/public/js/frappe/file_uploader/TreeNode.vue b/frappe/public/js/frappe/file_uploader/TreeNode.vue index ef3dbd8990..b494e8dc6c 100644 --- a/frappe/public/js/frappe/file_uploader/TreeNode.vue +++ b/frappe/public/js/frappe/file_uploader/TreeNode.vue @@ -1,14 +1,13 @@ + diff --git a/frappe/public/js/frappe/file_uploader/WebLink.vue b/frappe/public/js/frappe/file_uploader/WebLink.vue index b0527f7d9f..f5fc841b5d 100644 --- a/frappe/public/js/frappe/file_uploader/WebLink.vue +++ b/frappe/public/js/frappe/file_uploader/WebLink.vue @@ -6,9 +6,6 @@ {{ __('← Back to upload files') }}
    -
    - {{ __('Web Link') }} -
    this.upload_files(), secondary_action_label: __('Toggle Private'), diff --git a/frappe/public/js/frappe/form/controls/base_control.js b/frappe/public/js/frappe/form/controls/base_control.js index 319aa067cc..d7f873bee0 100644 --- a/frappe/public/js/frappe/form/controls/base_control.js +++ b/frappe/public/js/frappe/form/controls/base_control.js @@ -40,23 +40,31 @@ frappe.ui.form.Control = Class.extend({ return this.df.get_status(this); } - if((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form') { + if ((!this.doctype && !this.docname) || this.df.parenttype === 'Web Form' || this.df.is_web_form) { // like in case of a dialog box if (cint(this.df.hidden)) { // eslint-disable-next-line - if(explain) console.log("By Hidden: None"); + if (explain) console.log("By Hidden: None"); // eslint-disable-line no-console return "None"; } else if (cint(this.df.hidden_due_to_dependency)) { // eslint-disable-next-line - if(explain) console.log("By Hidden Dependency: None"); + if(explain) console.log("By Hidden Dependency: None"); // eslint-disable-line no-console return "None"; } else if (cint(this.df.read_only)) { // eslint-disable-next-line - if(explain) console.log("By Read Only: Read"); + if (explain) console.log("By Read Only: Read"); // eslint-disable-line no-console return "Read"; + } else if ((this.grid && + this.grid.display_status == 'Read') || + (this.layout && + this.layout.grid && + this.layout.grid.display_status == 'Read')) { + // parent grid is read + if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console + return "Read"; } return "Write"; @@ -65,13 +73,22 @@ frappe.ui.form.Control = Class.extend({ var status = frappe.perm.get_field_display_status(this.df, frappe.model.get_doc(this.doctype, this.docname), this.perm || (this.frm && this.frm.perm), explain); + // Match parent grid controls read only status + if (status === 'Write' && (this.grid || (this.layout && this.layout.grid))) { + var grid = this.grid || this.layout.grid; + if (grid.display_status == 'Read') { + status = 'Read'; + if (explain) console.log("By Parent Grid Read-only: Read"); // eslint-disable-line no-console + } + } + // hide if no value if (this.doctype && status==="Read" && !this.only_input && is_null(frappe.model.get_value(this.doctype, this.docname, this.df.fieldname)) && !in_list(["HTML", "Image", "Button"], this.df.fieldtype)) { // eslint-disable-next-line - if(explain) console.log("By Hide Read-only, null fields: None"); + if (explain) console.log("By Hide Read-only, null fields: None"); // eslint-disable-line no-console status = "None"; } diff --git a/frappe/public/js/frappe/form/controls/base_input.js b/frappe/public/js/frappe/form/controls/base_input.js index 471825f193..46ab62b717 100644 --- a/frappe/public/js/frappe/form/controls/base_input.js +++ b/frappe/public/js/frappe/form/controls/base_input.js @@ -20,7 +20,7 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({
    \
    \ \ - \ +

    \
    \
    \ ').appendTo(this.parent); @@ -127,13 +127,6 @@ frappe.ui.form.ControlInput = frappe.ui.form.Control.extend({ let display_value = frappe.format(value, this.df, { no_icon: true, inline: true }, doc); this.disp_area && $(this.disp_area).html(display_value); }, - - bind_change_event: function() { - var me = this; - this.$input && this.$input.on("change", this.change || function(e) { - me.parse_validate_and_set_in_model(me.get_input_value(), e); - }); - }, set_label: function(label) { if(label) this.df.label = label; diff --git a/frappe/public/js/frappe/form/controls/code.js b/frappe/public/js/frappe/form/controls/code.js index f3c51e0232..eec450b390 100644 --- a/frappe/public/js/frappe/form/controls/code.js +++ b/frappe/public/js/frappe/form/controls/code.js @@ -10,7 +10,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ .appendTo(this.input_area); this.expanded = false; - this.$expand_button = $(``).click(() => { + this.$expand_button = $(``).click(() => { this.expanded = !this.expanded; this.refresh_height(); this.toggle_label(); @@ -23,6 +23,7 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ const ace = window.ace; this.editor = ace.edit(this.ace_editor_target.get(0)); this.editor.setTheme('ace/theme/tomorrow'); + this.editor.setOption("showPrintMargin", false); this.set_language(); // events @@ -38,8 +39,11 @@ frappe.ui.form.ControlCode = frappe.ui.form.ControlText.extend({ }, toggle_label() { - const button_label = this.expanded ? __('Collapse') : __('Expand'); - this.$expand_button && this.$expand_button.text(button_label); + this.$expand_button && this.$expand_button.text(this.get_button_label()); + }, + + get_button_label() { + return this.expanded ? __('Collapse', null, 'Shrink code field.') : __('Expand', null, 'Enlarge code field.'); }, set_language() { diff --git a/frappe/public/js/frappe/form/controls/comment.js b/frappe/public/js/frappe/form/controls/comment.js index 07e3ab6605..59b53bf59e 100644 --- a/frappe/public/js/frappe/form/controls/comment.js +++ b/frappe/public/js/frappe/form/controls/comment.js @@ -122,5 +122,15 @@ frappe.ui.form.ControlComment = frappe.ui.form.ControlTextEditor.extend({ clear() { this.quill.setText(''); + }, + + disable() { + this.quill.disable(); + this.button.prop('disabled', true); + }, + + enable() { + this.quill.enable(); + this.button.prop('disabled', false); } }); diff --git a/frappe/public/js/frappe/form/controls/data.js b/frappe/public/js/frappe/form/controls/data.js index 401de2ed5d..48b4d9da35 100644 --- a/frappe/public/js/frappe/form/controls/data.js +++ b/frappe/public/js/frappe/form/controls/data.js @@ -3,6 +3,7 @@ frappe.provide('frappe.phone_call'); frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ html_element: "input", input_type: "text", + trigger_change_on_input_event: true, make_input: function() { if(this.$input) return; @@ -22,8 +23,20 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlInput.extend({ this.has_input = true; this.bind_change_event(); this.setup_autoname_check(); - // somehow this event does not bubble up to document - // after v7, if you can debug, remove this + }, + bind_change_event: function() { + const change_handler = e => { + if (this.change) this.change(e); + else { + let value = this.get_input_value(); + this.parse_validate_and_set_in_model(value, e); + } + }; + this.$input.on("change", change_handler); + if (this.trigger_change_on_input_event) { + // debounce to avoid repeated validations on value change + this.$input.on("input", frappe.utils.debounce(change_handler, 500)); + } }, setup_autoname_check: function() { if (!this.df.parent) return; diff --git a/frappe/public/js/frappe/form/controls/date.js b/frappe/public/js/frappe/form/controls/date.js index da214029be..ca214ca0fa 100644 --- a/frappe/public/js/frappe/form/controls/date.js +++ b/frappe/public/js/frappe/form/controls/date.js @@ -1,5 +1,5 @@ - frappe.ui.form.ControlDate = frappe.ui.form.ControlData.extend({ + trigger_change_on_input_event: false, make_input: function() { this._super(); this.make_picker(); diff --git a/frappe/public/js/frappe/form/controls/geolocation.js b/frappe/public/js/frappe/form/controls/geolocation.js index 9dfad09299..dfd0f4d174 100644 --- a/frappe/public/js/frappe/form/controls/geolocation.js +++ b/frappe/public/js/frappe/form/controls/geolocation.js @@ -1,3 +1,5 @@ +frappe.provide('frappe.utils.utils'); + frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ horizontal: false, @@ -15,7 +17,7 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ this.map_area.prependTo($input_wrapper); this.$wrapper.find('.control-input').addClass("hidden"); - if ($input_wrapper.is(':visible')) { + if (this.frm) { this.make_map(); } else { $(document).on('frappe.ui.Dialog:shown', () => { @@ -90,11 +92,11 @@ frappe.ui.form.ControlGeolocation = frappe.ui.form.ControlData.extend({ }); L.Icon.Default.imagePath = '/assets/frappe/images/leaflet/'; - this.map = L.map(this.map_id).setView([19.0800, 72.8961], 13); + this.map = L.map(this.map_id).setView(frappe.utils.map_defaults.center, + frappe.utils.map_defaults.zoom); - L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { - attribution: '© OpenStreetMap contributors' - }).addTo(this.map); + L.tileLayer(frappe.utils.map_defaults.tiles, + frappe.utils.map_defaults.options).addTo(this.map); }, bind_leaflet_locate_control() { diff --git a/frappe/public/js/frappe/form/controls/html.js b/frappe/public/js/frappe/form/controls/html.js index 751b784f44..f8a3645705 100644 --- a/frappe/public/js/frappe/form/controls/html.js +++ b/frappe/public/js/frappe/form/controls/html.js @@ -9,6 +9,7 @@ frappe.ui.form.ControlHTML = frappe.ui.form.Control.extend({ }, get_content: function() { var content = this.df.options || ""; + content = __(content); try { return frappe.render(content, this); } catch (e) { diff --git a/frappe/public/js/frappe/form/controls/link.js b/frappe/public/js/frappe/form/controls/link.js index ce1e2ae79d..4ed0c40d33 100644 --- a/frappe/public/js/frappe/form/controls/link.js +++ b/frappe/public/js/frappe/form/controls/link.js @@ -9,6 +9,7 @@ import Awesomplete from 'awesomplete'; frappe.ui.form.recent_link_validations = {}; frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ + trigger_change_on_input_event: false, make_input: function() { var me = this; // line-height: 1 is for Mozilla 51, shows extra padding otherwise @@ -51,6 +52,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ this.translate_values = true; this.setup_buttons(); this.setup_awesomeplete(); + this.bind_change_event(); }, get_options: function() { return this.df.options; @@ -182,7 +184,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ let filter_string = me.get_filter_description(args.filters); if (filter_string) { r.results.push({ - html: `${filter_string}`, + html: `${filter_string}`, value: '', action: () => {} }); @@ -217,6 +219,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ } me.$input.cache[doctype][term] = r.results; me.awesomplete.list = me.$input.cache[doctype][term]; + me.toggle_href(doctype); } }); }, 500)); @@ -303,6 +306,15 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ // returns [{value: 'Manufacturer 1', 'description': 'mobile part 1, mobile part 2'}] }, + toggle_href(doctype) { + if (frappe.model.can_select(doctype) && !frappe.model.can_read(doctype)) { + // remove href from link field as user has only select perm + this.$input_area.find(".link-btn").addClass('hide'); + } else { + this.$input_area.find(".link-btn").removeClass('hide'); + } + }, + get_filter_description(filters) { let doctype = this.get_options(); let filter_array = []; @@ -458,7 +470,7 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ fetch_and_validate_link(resolve, df, doctype, docname, value, fetch) { frappe.call({ - method:'frappe.desk.form.utils.validate_link', + method: 'frappe.desk.form.utils.validate_link', type: "GET", args: { 'value': value, @@ -467,8 +479,8 @@ frappe.ui.form.ControlLink = frappe.ui.form.ControlData.extend({ }, no_spinner: true, callback: (r) => { - if(r.message=='Ok') { - if(r.fetch_values && docname) { + if (r.message=='Ok') { + if (r.fetch_values && docname) { this.set_fetch_values(df, docname, r.fetch_values); } resolve(r.valid_value); diff --git a/frappe/public/js/frappe/form/controls/rating.js b/frappe/public/js/frappe/form/controls/rating.js index 34e890d45c..191db35538 100644 --- a/frappe/public/js/frappe/form/controls/rating.js +++ b/frappe/public/js/frappe/form/controls/rating.js @@ -47,7 +47,7 @@ frappe.ui.form.ControlRating = frappe.ui.form.ControlInt.extend({ }); }, get_value() { - return cint(this.value); + return cint(this.value, null); }, set_formatted_input(value) { let el = $(this.input_area).find('i'); diff --git a/frappe/public/js/frappe/form/controls/table.js b/frappe/public/js/frappe/form/controls/table.js index 5c4aed250b..bde08e4cee 100644 --- a/frappe/public/js/frappe/form/controls/table.js +++ b/frappe/public/js/frappe/form/controls/table.js @@ -9,7 +9,8 @@ frappe.ui.form.ControlTable = frappe.ui.form.Control.extend({ frm: this.frm, df: this.df, perm: this.perm || (this.frm && this.frm.perm) || this.df.perm, - parent: this.wrapper + parent: this.wrapper, + control: this }); if(this.frm) { this.frm.grids[this.frm.grids.length] = this; diff --git a/frappe/public/js/frappe/form/dashboard.js b/frappe/public/js/frappe/form/dashboard.js index f28161a80d..4dca1e4daf 100644 --- a/frappe/public/js/frappe/form/dashboard.js +++ b/frappe/public/js/frappe/form/dashboard.js @@ -335,7 +335,7 @@ frappe.ui.form.Dashboard = class FormDashboard { } } else if (this.data.fieldname) { frappe.route_options = this.get_document_filter(doctype); - if (show_open) { + if (show_open && frappe.ui.notifications) { frappe.ui.notifications.show_open_count_list(doctype); } } @@ -602,7 +602,7 @@ class Section { this.df = options || {}; this.make(); - if (this.df.title && this.df.collapsible) { + if (this.df.title && this.df.collapsible && localStorage.getItem(options.css_class + '-closed')) { this.collapse(); } this.refresh(); @@ -657,6 +657,7 @@ class Section { this.collapse_link = this.head.on("click", () => { this.collapse(); }); + this.set_icon(); this.indicator.show(); } } @@ -677,9 +678,15 @@ class Section { this.body.toggleClass("hide", hide); this.head && this.head.toggleClass("collapsed", hide); - let indicator_icon = hide ? 'down' : 'up-line'; + this.set_icon(hide); - this.indicator & this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); + // save state for next reload ('' is falsy) + localStorage.setItem(this.df.css_class + '-closed', hide ? '1' : ''); + } + + set_icon(hide) { + let indicator_icon = hide ? 'down' : 'up-line'; + this.indicator && this.indicator.html(frappe.utils.icon(indicator_icon, 'sm', 'mb-1')); } is_collapsed() { diff --git a/frappe/public/js/frappe/form/footer/base_timeline.js b/frappe/public/js/frappe/form/footer/base_timeline.js index fa429f52d4..ab4ad95a81 100644 --- a/frappe/public/js/frappe/form/footer/base_timeline.js +++ b/frappe/public/js/frappe/form/footer/base_timeline.js @@ -9,6 +9,7 @@ class BaseTimeline { make() { this.timeline_wrapper = $(`
    `); + this.wrapper = this.timeline_wrapper; this.timeline_items_wrapper = $(`
    `); this.timeline_actions_wrapper = $(`
    @@ -28,9 +29,13 @@ class BaseTimeline { this.render_timeline_items(); } - add_action_button(label, action) { + add_action_button(label, action, icon=null, btn_class=null) { + let icon_element = icon ? frappe.utils.icon(icon, 'xs') : null; this.timeline_actions_wrapper.show(); - let action_btn = $(``); + let action_btn = $(``); action_btn.click(action); this.timeline_actions_wrapper.append(action_btn); return action_btn; @@ -75,7 +80,10 @@ class BaseTimeline { // timeline_badge, icon, icon_size, // hide_timestamp, is_card const timeline_item = $(`
    `); - + timeline_item.attr({ + "data-doctype": item.doctype, + "data-name": item.name, + }); if (item.icon) { timeline_item.append(`
    diff --git a/frappe/public/js/frappe/form/footer/footer.js b/frappe/public/js/frappe/form/footer/footer.js index 9dca796e49..a1dabedff0 100644 --- a/frappe/public/js/frappe/form/footer/footer.js +++ b/frappe/public/js/frappe/form/footer/footer.js @@ -31,17 +31,21 @@ frappe.ui.form.Footer = Class.extend({ }, on_submit: (comment) => { if (strip_html(comment).trim() != "") { + this.frm.comment_box.disable(); frappe.xcall("frappe.desk.form.utils.add_comment", { reference_doctype: this.frm.doctype, reference_name: this.frm.docname, content: comment, comment_email: frappe.session.user, comment_by: frappe.session.user_fullname - }).then(() => { + }).then((comment) => { + let comment_item = this.frm.timeline.get_comment_timeline_item(comment); this.frm.comment_box.set_value(''); frappe.utils.play_sound("click"); - this.frm.timeline.refresh(); + this.frm.timeline.add_timeline_item(comment_item); this.frm.sidebar.refresh_comments_count && this.frm.sidebar.refresh_comments_count(); + }).finally(() => { + this.frm.comment_box.enable(); }); } } diff --git a/frappe/public/js/frappe/form/footer/form_timeline.js b/frappe/public/js/frappe/form/footer/form_timeline.js index 18bc7cd06d..5b3dd29125 100644 --- a/frappe/public/js/frappe/form/footer/form_timeline.js +++ b/frappe/public/js/frappe/form/footer/form_timeline.js @@ -7,14 +7,19 @@ class FormTimeline extends BaseTimeline { make() { super.make(); - this.setup_document_email_link(); this.setup_timeline_actions(); this.render_timeline_items(); this.setup_activity_toggle(); } + refresh() { + super.refresh(); + this.frm.trigger('timeline_refresh'); + this.setup_document_email_link(); + } + setup_timeline_actions() { - this.add_action_button(__('New Email'), () => this.compose_mail()); + this.add_action_button(__('New Email'), () => this.compose_mail(), 'mail', 'btn-secondary-dark'); this.setup_new_event_button(); } @@ -29,7 +34,7 @@ class FormTimeline extends BaseTimeline { }; return new frappe.views.InteractionComposer(args); }; - this.add_action_button(__('New Event'), create_event); + this.add_action_button(__('New Event'), create_event, 'calendar'); } } @@ -65,9 +70,11 @@ class FormTimeline extends BaseTimeline { setup_document_email_link() { let doc_info = this.doc_info || this.frm.get_docinfo(); + this.document_email_link_wrapper && this.document_email_link_wrapper.remove(); + if (doc_info.document_email) { const link = `${doc_info.document_email}`; - const message = __("Send an email to {0} to link it here", [link.bold()]); + const message = __("Add to this activity by mailing to {0}", [link.bold()]); this.document_email_link_wrapper = $(` `); - this.timeline_wrapper.prepend(this.document_email_link_wrapper); + this.timeline_wrapper.append(this.document_email_link_wrapper); this.document_email_link_wrapper .find('.document-email-link') @@ -166,6 +173,8 @@ class FormTimeline extends BaseTimeline { creation: communication.creation, is_card: true, content: this.get_communication_timeline_content(communication), + doctype: "Communication", + name: communication.name }); }); return communication_timeline_contents; @@ -202,16 +211,22 @@ class FormTimeline extends BaseTimeline { get_comment_timeline_contents() { let comment_timeline_contents = []; (this.doc_info.comments || []).forEach(comment => { - comment_timeline_contents.push({ - icon: 'small-message', - creation: comment.creation, - is_card: true, - content: this.get_comment_timeline_content(comment), - }); + comment_timeline_contents.push(this.get_comment_timeline_item(comment)); }); return comment_timeline_contents; } + get_comment_timeline_item(comment) { + return { + icon: 'small-message', + creation: comment.creation, + is_card: true, + doctype: "Comment", + name: comment.name, + content: this.get_comment_timeline_content(comment), + }; + } + get_comment_timeline_content(doc) { const comment_content = $(frappe.render_template('timeline_message_box', { doc })); this.setup_comment_actions(comment_content, doc); @@ -343,7 +358,7 @@ class FormTimeline extends BaseTimeline { const args = { doc: this.frm.doc, frm: this.frm, - recipients: this.get_recipient(), + recipients: communication_doc ? communication_doc.sender : this.get_recipient(), is_a_reply: Boolean(communication_doc), title: communication_doc ? __('Reply') : null, last_email: communication_doc diff --git a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js index ae8156b6d3..0f57998475 100644 --- a/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js +++ b/frappe/public/js/frappe/form/footer/version_timeline_content_builder.js @@ -124,26 +124,26 @@ function get_version_timeline_content(version_doc, frm) { return p; }); if (parts.length) { - out.push(get_version_comment(version_doc, __("{0} rows for {1}", - [__(key), parts.join(', ')]))); + let message = ''; + + if (key === 'added') { + message = __("added rows for {0}", [parts.join(', ')]); + } else if (key === 'removed') { + message = __("removed rows for {0}", [parts.join(', ')]); + } + + let version_comment = get_version_comment(version_doc, message); + let user_link = get_user_link(version_doc); + out.push(`${user_link} ${version_comment}`); } } }); - // if (data.creation && data.created_by) { - // // created via automation - // if (updater_reference_link) { - // out.push(get_version_comment(version_doc, __('{0} created {1}', [get_user_link(version_doc), updater_reference_link]))); - // } else { - // out.push(get_version_comment(version_doc, __('{0} created', [get_user_link(version_doc)]))); - // } - // } - return out; } -function get_version_comment(version_doc, text, version_type=null) { +function get_version_comment(version_doc, text) { return frappe.utils.get_form_link('Version', version_doc.name, true, text); } diff --git a/frappe/public/js/frappe/form/form.js b/frappe/public/js/frappe/form/form.js index 6930873893..a70797e295 100644 --- a/frappe/public/js/frappe/form/form.js +++ b/frappe/public/js/frappe/form/form.js @@ -93,7 +93,7 @@ frappe.ui.form.Form = class FrappeForm { this.script_manager.setup(); this.watch_model_updates(); - if(!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { + if (!this.meta.hide_toolbar && frappe.boot.desk_settings.timeline) { this.footer = new frappe.ui.form.Footer({ frm: this, parent: $('
    ').appendTo(this.page.main.parent()) @@ -102,7 +102,6 @@ frappe.ui.form.Form = class FrappeForm { } this.setup_file_drop(); this.setup_doctype_actions(); - this.setup_docinfo_change_listener(); this.setup_notify_on_rename(); this.setup_done = true; @@ -201,7 +200,7 @@ frappe.ui.form.Form = class FrappeForm { setup_notify_on_rename() { $(document).on('rename', (ev, dt, old_name, new_name) => { - if(dt==this.doctype) + if (dt==this.doctype) this.rename_notify(dt, old_name, new_name); }); } @@ -355,20 +354,15 @@ frappe.ui.form.Form = class FrappeForm { } switch_doc(docname) { - // record switch - // if(this.docname != docname && (!this.meta.in_dialog || this.in_form) && !this.meta.istable) { - // if (this.print_preview) { - // this.print_preview.hide(); - // } - // } // reset visible columns, since column headings can change in different docs this.grids.forEach(grid_obj => { grid_obj.grid.visible_columns = null // reset page number to 1 - grid_obj.grid.grid_pagination.go_to_page(1); + grid_obj.grid.grid_pagination.go_to_page(1, true); }); frappe.ui.form.close_grid_form(); this.docname = docname; + this.setup_docinfo_change_listener(); } check_reload() { @@ -477,7 +471,7 @@ frappe.ui.form.Form = class FrappeForm { focus_on_first_input() { let first = this.form_wrapper.find('.form-layout :input:visible:first'); - if(!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { + if (!in_list(["Date", "Datetime"], first.attr("data-fieldtype"))) { first.focus(); } } @@ -570,13 +564,8 @@ frappe.ui.form.Form = class FrappeForm { let me = this; return new Promise((resolve, reject) => { btn && $(btn).prop("disabled", true); - $(document.activeElement).blur(); - frappe.ui.form.close_grid_form(); - // let any pending js process finish - setTimeout(function() { - me.validate_and_save(save_action, callback, btn, on_error, resolve, reject); - }, 100); + me.validate_and_save(save_action, callback, btn, on_error, resolve, reject); }).then(() => { me.show_success_action(); }).catch((e) => { @@ -936,7 +925,7 @@ frappe.ui.form.Form = class FrappeForm { } add_web_link(path, label) { - label = label || "See on Website"; + label = __(label) || __("See on Website"); this.web_link = this.sidebar.add_user_action(__(label), function() {}).attr("href", path || this.doc.route).attr("target", "_blank"); } @@ -1005,7 +994,7 @@ frappe.ui.form.Form = class FrappeForm { print_doc() { frappe.route_options = { frm: this, - } + }; frappe.set_route('print', this.doctype, this.doc.name); } @@ -1264,7 +1253,10 @@ frappe.ui.form.Form = class FrappeForm { } if (df && df[property] != value) { df[property] = value; - this.refresh_field(fieldname); + if (!docname || !table_field) { + // do not refresh childtable fields since `this.fields_dict` doesn't have child table fields + this.refresh_field(fieldname); + } } } @@ -1506,7 +1498,7 @@ frappe.ui.form.Form = class FrappeForm { const escaped_name = encodeURIComponent(value); - return `${label}'` + return `${label}'`; } else { return ''; } @@ -1647,10 +1639,15 @@ frappe.ui.form.Form = class FrappeForm { } setup_docinfo_change_listener() { - frappe.realtime.on(`update_docinfo_for_${this.doctype}_${this.docname}`, ({doc, key, action='update'}) => { - let doc_list = (frappe.model.docinfo[this.doctype][this.docname][key] || []); + let doctype = this.doctype; + let docname = this.docname; + let listener_name = `update_docinfo_for_${doctype}_${docname}`; + // to avoid duplicates + frappe.realtime.off(listener_name); + frappe.realtime.on(listener_name, ({doc, key, action='update'}) => { + let doc_list = (frappe.model.docinfo[doctype][docname][key] || []); if (action === 'add') { - frappe.model.docinfo[this.doctype][this.docname][key].push(doc); + frappe.model.docinfo[doctype][docname][key].push(doc); } let docindex = doc_list.findIndex(old_doc => { @@ -1659,13 +1656,17 @@ frappe.ui.form.Form = class FrappeForm { if (docindex > -1) { if (action === 'update') { - frappe.model.docinfo[this.doctype][this.docname][key].splice(docindex, 1, doc); + frappe.model.docinfo[doctype][docname][key].splice(docindex, 1, doc); } if (action === 'delete') { - frappe.model.docinfo[this.doctype][this.docname][key].splice(docindex, 1); + frappe.model.docinfo[doctype][docname][key].splice(docindex, 1); } } - this.timeline && this.timeline.refresh(); + // no need to update timeline of owner of comment + // gets handled via comment submit code + if (!(['add', 'update'].includes(action) && doc.doctype === 'Comment' && doc.owner === frappe.session.user)) { + this.timeline && this.timeline.refresh(); + } }); } diff --git a/frappe/public/js/frappe/form/form_viewers.js b/frappe/public/js/frappe/form/form_viewers.js index 3d488e4729..d9d5ba6e68 100644 --- a/frappe/public/js/frappe/form/form_viewers.js +++ b/frappe/public/js/frappe/form/form_viewers.js @@ -6,10 +6,11 @@ frappe.ui.form.FormViewers = class FormViewers { } refresh() { - let users = this.frm.get_docinfo()['viewers']; - let currently_viewing = users.current.filter(user => user != frappe.session.user); - let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true}); - this.parent.empty().append(avatar_group); + // REDESIGN-TODO: fix this + // let users = this.frm.get_docinfo()['viewers']; + // let currently_viewing = users.current.filter(user => user != frappe.session.user); + // let avatar_group = frappe.avatar_group(currently_viewing, 5, {'align': 'left', 'overlap': true}); + this.parent.empty(); //.append(avatar_group); } }; diff --git a/frappe/public/js/frappe/form/formatters.js b/frappe/public/js/frappe/form/formatters.js index 27b3b869c5..30ed7f15e4 100644 --- a/frappe/public/js/frappe/form/formatters.js +++ b/frappe/public/js/frappe/form/formatters.js @@ -50,7 +50,15 @@ frappe.form.formatters = { return frappe.form.formatters._right(value==null ? "" : cint(value), options) }, Percent: function(value, docfield, options) { - return frappe.form.formatters._right(flt(value, 2) + "%", options) + const precision = ( + docfield.precision + || cint( + frappe.boot.sysdefaults + && frappe.boot.sysdefaults.float_precision + ) + || 2 + ); + return frappe.form.formatters._right(flt(value, precision) + "%", options); }, Rating: function(value) { return ` @@ -120,11 +128,15 @@ frappe.form.formatters = { return repl('%(value)s', {onclick: docfield.link_onclick.replace(/"/g, '"'), value:value}); } else if(docfield && doctype) { - return ` - ${__(options && options.label || value)}` + if (!frappe.model.can_select(doctype) && frappe.model.can_read(doctype)) { + return ` + ${__(options && options.label || value)}`; + } else { + return value; + } } else { return value; } diff --git a/frappe/public/js/frappe/form/grid.js b/frappe/public/js/frappe/form/grid.js index c05868964e..921ad1cdb5 100644 --- a/frappe/public/js/frappe/form/grid.js +++ b/frappe/public/js/frappe/form/grid.js @@ -201,7 +201,7 @@ export default class Grid { this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0); if (selected_children.length == this.grid_pagination.page_length) { - frappe.utils.scroll_to(this.wrapper); + this.scroll_to_top(); } } @@ -213,9 +213,12 @@ export default class Grid { this.wrapper.find('.grid-heading-row .grid-row-check:checked:first').prop('checked', 0); this.refresh(); - frappe.utils.scroll_to(this.wrapper); + this.scroll_to_top(); }); + } + scroll_to_top() { + frappe.utils.scroll_to(this.wrapper); } select_row(name) { @@ -265,6 +268,8 @@ export default class Grid { } refresh(force) { + if (this.frm.setting_dependency) return; + this.data = this.get_data(); !this.wrapper && this.make(); @@ -275,6 +280,8 @@ export default class Grid { if (this.frm) { this.display_status = frappe.perm.get_field_display_status(this.df, this.frm.doc, this.perm); + } else if (this.df.is_web_form && this.control) { + this.display_status = this.control.get_status(); } else { // not in form this.display_status = 'Write'; diff --git a/frappe/public/js/frappe/form/grid_pagination.js b/frappe/public/js/frappe/form/grid_pagination.js index 0920bf5981..35daafe89d 100644 --- a/frappe/public/js/frappe/form/grid_pagination.js +++ b/frappe/public/js/frappe/form/grid_pagination.js @@ -95,7 +95,7 @@ export default class GridPagination { } } - go_to_page(index) { + go_to_page(index, from_refresh) { if (!index) { index = this.page_index; } else { @@ -108,6 +108,9 @@ export default class GridPagination { } this.update_page_numbers(); + if (!from_refresh) { + this.grid.scroll_to_top(); + } } go_to_last_page_to_add_row() { diff --git a/frappe/public/js/frappe/form/grid_row.js b/frappe/public/js/frappe/form/grid_row.js index 433caecd67..675cb6f77c 100644 --- a/frappe/public/js/frappe/form/grid_row.js +++ b/frappe/public/js/frappe/form/grid_row.js @@ -232,8 +232,10 @@ export default class GridRow {
    `) - .appendTo($('
    ').appendTo(this.row)) - .on('click', function() { me.toggle_view(); return false; }); + .appendTo($('
    ').appendTo(this.row)) + .on('click', function() { + me.toggle_view(); return false; + }); if(this.is_too_small()) { // narrow @@ -573,13 +575,15 @@ export default class GridRow { this.wrapper.removeClass("grid-row-open"); } open_prev() { - if(this.grid.grid_rows[this.doc.idx-2]) { - this.grid.grid_rows[this.doc.idx-2].toggle_view(true); + const row_index = this.wrapper.index(); + if (this.grid.grid_rows[row_index - 1]) { + this.grid.grid_rows[row_index - 1].toggle_view(true); } } open_next() { - if(this.grid.grid_rows[this.doc.idx]) { - this.grid.grid_rows[this.doc.idx].toggle_view(true); + const row_index = this.wrapper.index(); + if (this.grid.grid_rows[row_index + 1]) { + this.grid.grid_rows[row_index + 1].toggle_view(true); } else { this.grid.add_new_row(null, null, true); } diff --git a/frappe/public/js/frappe/form/grid_row_form.js b/frappe/public/js/frappe/form/grid_row_form.js index ee6735d3ac..399f233c54 100644 --- a/frappe/public/js/frappe/form/grid_row_form.js +++ b/frappe/public/js/frappe/form/grid_row_form.js @@ -16,6 +16,9 @@ export default class GridRowForm { body: this.form_area, no_submit_on_enter: true, frm: this.row.frm, + grid: this.row.grid, + grid_row: this.row, + grid_row_form: this, }); this.layout.make(); diff --git a/frappe/public/js/frappe/form/layout.js b/frappe/public/js/frappe/form/layout.js index 1d0f1f8ffd..aad3740ab0 100644 --- a/frappe/public/js/frappe/form/layout.js +++ b/frappe/public/js/frappe/form/layout.js @@ -1,7 +1,7 @@ import '../class'; frappe.ui.form.Layout = Class.extend({ - init: function(opts) { + init: function (opts) { this.views = {}; this.pages = []; this.sections = []; @@ -33,7 +33,7 @@ frappe.ui.form.Layout = Class.extend({ this.get_new_name_field() ]; if (this.doctype_layout) { - fields = fields.concat(this.get_fields_from_layout()) + fields = fields.concat(this.get_fields_from_layout()); } else { fields = fields.concat(frappe.meta.sort_docfields(frappe.meta.docfield_map[this.doctype])); } @@ -87,7 +87,7 @@ frappe.ui.form.Layout = Class.extend({ this.message.empty().addClass('hidden'); } }, - render: function(new_fields) { + render: function (new_fields) { var me = this; var fields = new_fields || this.fields; @@ -101,8 +101,8 @@ frappe.ui.form.Layout = Class.extend({ if (this.no_opening_section()) { this.make_section(); } - $.each(fields, function(i, df) { - switch(df.fieldtype) { + $.each(fields, function (i, df) { + switch (df.fieldtype) { case "Fold": me.make_page(df); break; @@ -119,17 +119,17 @@ frappe.ui.form.Layout = Class.extend({ }, - no_opening_section: function() { - return (this.fields[0] && this.fields[0].fieldtype!="Section Break") || !this.fields.length; + no_opening_section: function () { + return (this.fields[0] && this.fields[0].fieldtype != "Section Break") || !this.fields.length; }, - setup_dashboard_section: function() { + setup_dashboard_section: function () { if (this.no_opening_section()) { this.fields.unshift({fieldtype: 'Section Break'}); } }, - replace_field: function(fieldname, df, render) { + replace_field: function (fieldname, df, render) { df.fieldname = fieldname; // change of fieldname is avoided if (this.fields_dict[fieldname] && this.fields_dict[fieldname].df) { const fieldobj = this.init_field(df, render); @@ -145,7 +145,7 @@ frappe.ui.form.Layout = Class.extend({ } }, - make_field: function(df, colspan, render) { + make_field: function (df, colspan, render) { !this.section && this.make_section(); !this.column && this.make_column(); @@ -161,29 +161,30 @@ frappe.ui.form.Layout = Class.extend({ fieldobj.section = this.section; }, - init_field: function(df, render = false) { + init_field: function (df, render = false) { const fieldobj = frappe.ui.form.make_control({ df: df, doctype: this.doctype, parent: this.column.wrapper.get(0), frm: this.frm, render_input: render, - doc: this.doc + doc: this.doc, + layout: this }); fieldobj.layout = this; return fieldobj; }, - make_page: function(df) { + make_page: function (df) { // eslint-disable-line no-unused-vars var me = this, head = $('').appendTo(this.wrapper); this.page = $('
    ').appendTo(this.wrapper); - this.fold_btn = head.find(".btn-fold").on("click", function() { + this.fold_btn = head.find(".btn-fold").on("click", function () { var page = $(this).parent().next(); if (page.hasClass("hide")) { $(this).removeClass("btn-fold").html(__("Hide details")); @@ -201,11 +202,11 @@ frappe.ui.form.Layout = Class.extend({ this.folded = true; }, - unfold: function() { + unfold: function () { this.fold_btn.trigger('click'); }, - make_section: function(df) { + make_section: function (df) { this.section = new frappe.ui.form.Section(this, df); // append to layout fields @@ -217,14 +218,14 @@ frappe.ui.form.Layout = Class.extend({ this.column = null; }, - make_column: function(df) { + make_column: function (df) { this.column = new frappe.ui.form.Column(this.section, df); if (df && df.fieldname) { this.fields_list.push(this.column); } }, - refresh: function(doc) { + refresh: function (doc) { var me = this; if (doc) this.doc = doc; @@ -267,7 +268,7 @@ frappe.ui.form.Layout = Class.extend({ }, - refresh_fields: function(fields) { + refresh_fields: function (fields) { let fieldnames = fields.map((field) => { if (field.fieldname) return field.fieldname; }); @@ -282,15 +283,15 @@ frappe.ui.form.Layout = Class.extend({ }); }, - add_fields: function(fields) { + add_fields: function (fields) { this.render(fields); this.refresh_fields(fields); }, - refresh_section_collapse: function() { + refresh_section_collapse: function () { if (!this.doc) return; - for (var i=0; i=0;i--) { + for (var i = me.fields_list.length - 1; i >= 0; i--) { var f = me.fields_list[i]; f.guardian_has_value = true; if (f.df.depends_on) { @@ -498,25 +499,29 @@ frappe.ui.form.Layout = Class.extend({ this.refresh_section_count(); }, - set_dependant_property: function(condition, fieldname, property) { + set_dependant_property: function (condition, fieldname, property) { let set_property = this.evaluate_depends_on_value(condition); let value = set_property ? 1 : 0; let form_obj; if (this.frm) { form_obj = this.frm; - } else if (this.is_dialog) { + } else if (this.is_dialog || this.doctype === 'Web Form') { form_obj = this; } if (form_obj) { if (this.doc && this.doc.parent) { + form_obj.setting_dependency = true; form_obj.set_df_property(this.doc.parentfield, property, value, this.doc.parent, fieldname); + form_obj.setting_dependency = false; + // refresh child fields + this.fields_dict[fieldname] && this.fields_dict[fieldname].refresh(); } else { form_obj.set_df_property(fieldname, property, value); } } }, - evaluate_depends_on_value: function(expression) { + evaluate_depends_on_value: function (expression) { var out = null; var doc = this.doc; @@ -530,23 +535,23 @@ frappe.ui.form.Layout = Class.extend({ var parent = this.frm ? this.frm.doc : this.doc || null; - if (typeof(expression) === 'boolean') { + if (typeof (expression) === 'boolean') { out = expression; - } else if (typeof(expression) === 'function') { + } else if (typeof (expression) === 'function') { out = expression(doc); - } else if (expression.substr(0,5)=='eval:') { + } else if (expression.substr(0, 5)=='eval:') { try { out = eval(expression.substr(5)); if (parent && parent.istable && expression.includes('is_submittable')) { out = true; } - } catch(e) { + } catch (e) { frappe.throw(__('Invalid "depends_on" expression')); } - } else if (expression.substr(0,3)=='fn:' && this.frm) { + } else if (expression.substr(0, 3)=='fn:' && this.frm) { out = this.frm.script_manager.trigger(expression.substr(3), this.doctype, this.docname); } else { var value = doc[expression]; @@ -638,7 +643,7 @@ frappe.ui.form.Section = Class.extend({ this.wrapper.toggleClass("hide-control", !!hide); }, - collapse: function(hide) { + collapse: function (hide) { // unknown edge case if (!(this.head && this.body)) { return; @@ -657,7 +662,7 @@ frappe.ui.form.Section = Class.extend({ // refresh signature fields this.fields_list.forEach((f) => { - if (f.df.fieldtype=='Signature') { + if (f.df.fieldtype == 'Signature') { f.refresh(); } }); @@ -667,11 +672,11 @@ frappe.ui.form.Section = Class.extend({ return this.body.hasClass('hide'); }, - has_missing_mandatory: function() { + has_missing_mandatory: function () { var missing_mandatory = false; - for (var j=0, l=this.fields_list.length; j < l; j++) { + for (var j = 0, l = this.fields_list.length; j < l; j++) { var section_df = this.fields_list[j].df; - if (section_df.reqd && this.layout.doc[section_df.fieldname]==null) { + if (section_df.reqd && this.layout.doc[section_df.fieldname] == null) { missing_mandatory = true; break; } @@ -689,13 +694,13 @@ frappe.ui.form.Column = Class.extend({ this.make(); this.resize_all_columns(); }, - make: function() { + make: function () { this.wrapper = $('
    \
    \
    \
    ').appendTo(this.section.body) .find("form") - .on("submit", function() { + .on("submit", function () { return false; }); @@ -704,7 +709,7 @@ frappe.ui.form.Column = Class.extend({ + '').appendTo(this.wrapper); } }, - resize_all_columns: function() { + resize_all_columns: function () { // distribute all columns equally var colspan = cint(12 / this.section.wrapper.find(".form-column").length); @@ -713,7 +718,7 @@ frappe.ui.form.Column = Class.extend({ .addClass("col-sm-" + colspan); }, - refresh: function() { + refresh: function () { this.section.refresh(); } }); diff --git a/frappe/public/js/frappe/form/link_selector.js b/frappe/public/js/frappe/form/link_selector.js index c72e74cafc..07cd6864c5 100644 --- a/frappe/public/js/frappe/form/link_selector.js +++ b/frappe/public/js/frappe/form/link_selector.js @@ -152,9 +152,13 @@ frappe.ui.form.LinkSelector = Class.extend({ d = me.target.add_new_row(); }, () => frappe.timeout(0.1), - () => frappe.model.set_value(d.doctype, d.name, me.fieldname, value), - () => frappe.timeout(0.5), - () => frappe.model.set_value(d.doctype, d.name, me.qty_fieldname, data.qty), + () => { + let args = {}; + args[me.fieldname] = value; + args[me.qty_fieldname] = data.qty; + + return frappe.model.set_value(d.doctype, d.name, args); + }, () => frappe.show_alert(__("Added {0} ({1})", [value, data.qty])) ]); } diff --git a/frappe/public/js/frappe/form/multi_select_dialog.js b/frappe/public/js/frappe/form/multi_select_dialog.js index 0eae43e2f1..26baee05ea 100644 --- a/frappe/public/js/frappe/form/multi_select_dialog.js +++ b/frappe/public/js/frappe/form/multi_select_dialog.js @@ -42,7 +42,7 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { title: __("Select {0}", [(this.doctype == '[Select]') ? __("value") : __(doctype_plural)]), fields: fields, primary_action_label: this.primary_action_label || __("Get Items"), - secondary_action_label: __("Make {0}", [me.doctype]), + secondary_action_label: __("Make {0}", [__(me.doctype)]), primary_action: function () { let filters_data = me.get_custom_filters(); me.action(me.get_checked_values(), cur_dialog.get_values(), me.args, filters_data); @@ -356,4 +356,4 @@ frappe.ui.form.MultiSelectDialog = class MultiSelectDialog { } }); } -}; \ No newline at end of file +}; diff --git a/frappe/public/js/frappe/form/print_utils.js b/frappe/public/js/frappe/form/print_utils.js index 7a095b8cfe..9f2e6e77a1 100644 --- a/frappe/public/js/frappe/form/print_utils.js +++ b/frappe/public/js/frappe/form/print_utils.js @@ -1,38 +1,48 @@ -frappe.ui.get_print_settings = function (pdf, callback, letter_head, pick_columns) { +frappe.ui.get_print_settings = function( + pdf, + callback, + letter_head, + pick_columns +) { var print_settings = locals[":Print Settings"]["Print Settings"]; - var default_letter_head = locals[":Company"] && frappe.defaults.get_default('company') - ? locals[":Company"][frappe.defaults.get_default('company')]["default_letter_head"] - : ''; + var default_letter_head = + locals[":Company"] && frappe.defaults.get_default("company") + ? locals[":Company"][frappe.defaults.get_default("company")]["default_letter_head"] + : ""; - var columns = [{ - fieldtype: "Check", - fieldname: "with_letter_head", - label: __("With Letter head") - }, { - fieldtype: "Select", - fieldname: "letter_head", - label: __("Letter Head"), - depends_on: "with_letter_head", - options: $.map(frappe.boot.letter_heads, function (i, d) { return d }), - default: letter_head || default_letter_head - }, { - fieldtype: "Select", - fieldname: "orientation", - label: __("Orientation"), - options: [ - { "value": "Landscape", "label": __("Landscape") }, - { "value": "Portrait", "label": __("Portrait") } - ], - default: "Landscape" - }]; + var columns = [ + { + fieldtype: "Check", + fieldname: "with_letter_head", + label: __("With Letter head") + }, + { + fieldtype: "Select", + fieldname: "letter_head", + label: __("Letter Head"), + depends_on: "with_letter_head", + options: Object.keys(frappe.boot.letter_heads), + default: letter_head || default_letter_head + }, + { + fieldtype: "Select", + fieldname: "orientation", + label: __("Orientation"), + options: [ + { value: "Landscape", label: __("Landscape") }, + { value: "Portrait", label: __("Portrait") } + ], + default: "Landscape" + } + ]; if (pick_columns) { columns.push( { label: __("Pick Columns"), fieldtype: "Check", - fieldname: "pick_columns", + fieldname: "pick_columns" }, { label: __("Select Columns"), @@ -48,18 +58,22 @@ frappe.ui.get_print_settings = function (pdf, callback, letter_head, pick_column ); } - return frappe.prompt(columns, function (data) { - var data = $.extend(print_settings, data); - if (!data.with_letter_head) { - data.letter_head = null; - } - if (data.letter_head) { - data.letter_head = frappe.boot.letter_heads[print_settings.letter_head]; - } - callback(data); - }, __("Print Settings")); -} - + return frappe.prompt( + columns, + function(data) { + data = $.extend(print_settings, data); + if (!data.with_letter_head) { + data.letter_head = null; + } + if (data.letter_head) { + data.letter_head = + frappe.boot.letter_heads[print_settings.letter_head]; + } + callback(data); + }, + __("Print Settings") + ); +}; // qz tray connection wrapper // - allows active and inactive connections to resolve regardless @@ -67,62 +81,87 @@ frappe.ui.get_print_settings = function (pdf, callback, letter_head, pick_column // - if connection fails, catch the reject, fire the mimetype launcher // - after mimetype launcher is fired, try to connect 3 more times // - display success/fail message to user -frappe.ui.form.qz_connect = function () { - return new Promise(function (resolve, reject) { +frappe.ui.form.qz_connect = function() { + return new Promise(function(resolve, reject) { frappe.ui.form.qz_init().then(() => { - if (qz.websocket.isActive()) { // if already active, resolve immediately + if (qz.websocket.isActive()) { + // if already active, resolve immediately // frappe.show_alert({message: __('QZ Tray Connection Active!'), indicator: 'green'}); resolve(); } else { // try to connect once before firing the mimetype launcher frappe.show_alert({ - message: __('Attempting Connection to QZ Tray...'), - indicator: 'blue' + message: __("Attempting Connection to QZ Tray..."), + indicator: "blue" }); - qz.websocket.connect().then(() => { - frappe.show_alert({ - message: __('Connected to QZ Tray!'), - indicator: 'green' - }); - resolve(); - }, function retry(err) { - if (err.message === 'Unable to establish connection with QZ') { - // if a connect was not successful, launch the mimetype, try 3 more times + qz.websocket.connect().then( + () => { frappe.show_alert({ - message: __('Attempting to launch QZ Tray...'), - indicator: 'blue' - }, 14); - window.location.assign("qz:launch"); - qz.websocket.connect({ - retries: 3, - delay: 1 - }).then(() => { - frappe.show_alert({ - message: __('Connected to QZ Tray!'), - indicator: 'green' - }); - resolve(); - }, - () => { - frappe.throw(__('Error connecting to QZ Tray Application...

    You need to have QZ Tray application installed and running, to use the Raw Print feature.

    Click here to Download and install QZ Tray.
    Click here to learn more about Raw Printing.')); - reject(); + message: __("Connected to QZ Tray!"), + indicator: "green" }); - } else { - frappe.show_alert({ - message: 'QZ Tray ' + err.toString(), - indicator: 'red' - }, 14); - reject(); + resolve(); + }, + function retry(err) { + if ( + err.message === + "Unable to establish connection with QZ" + ) { + // if a connect was not successful, launch the mimetype, try 3 more times + frappe.show_alert( + { + message: __( + "Attempting to launch QZ Tray..." + ), + indicator: "blue" + }, + 14 + ); + window.location.assign("qz:launch"); + qz.websocket + .connect({ + retries: 3, + delay: 1 + }) + .then( + () => { + frappe.show_alert({ + message: __( + "Connected to QZ Tray!" + ), + indicator: "green" + }); + resolve(); + }, + () => { + frappe.throw( + __( + 'Error connecting to QZ Tray Application...

    You need to have QZ Tray application installed and running, to use the Raw Print feature.

    Click here to Download and install QZ Tray.
    Click here to learn more about Raw Printing.' + ) + ); + reject(); + } + ); + } else { + frappe.show_alert( + { + message: "QZ Tray " + err.toString(), + indicator: "red" + }, + 14 + ); + reject(); + } } - }); + ); } }); }); -} +}; -frappe.ui.form.qz_init = function () { +frappe.ui.form.qz_init = function() { // Initializing qz tray library - return new Promise((resolve) => { + return new Promise(resolve => { if (typeof qz === "object" && typeof qz.version === "string") { // resolve immediately if already Initialized resolve(); @@ -131,11 +170,11 @@ frappe.ui.form.qz_init = function () { "/assets/frappe/node_modules/js-sha256/build/sha256.min.js", "/assets/frappe/node_modules/qz-tray/qz-tray.js" ]; - frappe.require(qz_required_assets,() => { + frappe.require(qz_required_assets, () => { qz.api.setPromiseType(function promise(resolver) { return new Promise(resolver); }); - qz.api.setSha256Type(function (data) { + qz.api.setSha256Type(function(data) { // Codacy fix /*global sha256*/ return sha256(data); @@ -144,33 +183,39 @@ frappe.ui.form.qz_init = function () { }); // note 'frappe.require' does not have callback on fail. Hence, any failure cannot be communicated to the user. } - }); -} +}; -frappe.ui.form.qz_get_printer_list = function () { +frappe.ui.form.qz_get_printer_list = function() { // returns the list of printers that are available to the QZ Tray - return frappe.ui.form.qz_connect().then(function () { - return qz.printers.find(); - }).then((data) => { - return data; - }).catch((err) => { - frappe.ui.form.qz_fail(err); - }); -} + return frappe.ui.form + .qz_connect() + .then(function() { + return qz.printers.find(); + }) + .then(data => { + return data; + }) + .catch(err => { + frappe.ui.form.qz_fail(err); + }); +}; -frappe.ui.form.qz_success = function () { +frappe.ui.form.qz_success = function() { // notify qz successful print frappe.show_alert({ - message: __('Print Sent to the printer!'), - indicator: 'green' + message: __("Print Sent to the printer!"), + indicator: "green" }); -} +}; -frappe.ui.form.qz_fail = function (e) { +frappe.ui.form.qz_fail = function(e) { // notify qz errors - frappe.show_alert({ - message: __("QZ Tray Failed: ") + e.toString(), - indicator: 'red' - }, 20); -} + frappe.show_alert( + { + message: __("QZ Tray Failed: ") + e.toString(), + indicator: "red" + }, + 20 + ); +}; diff --git a/frappe/public/js/frappe/form/quick_entry.js b/frappe/public/js/frappe/form/quick_entry.js index d80aaf5107..4692e66140 100644 --- a/frappe/public/js/frappe/form/quick_entry.js +++ b/frappe/public/js/frappe/form/quick_entry.js @@ -36,9 +36,14 @@ frappe.ui.form.QuickEntryForm = Class.extend({ this.render_dialog(); resolve(this); } else { + // no quick entry, open full form frappe.quick_entry = null; frappe.set_route('Form', this.doctype, this.doc.name) .then(() => resolve(this)); + // call init_callback for consistency + if (this.init_callback) { + this.init_callback(this.doc); + } } }); }); diff --git a/frappe/public/js/frappe/form/script_helpers.js b/frappe/public/js/frappe/form/script_helpers.js index 83ba191d4d..0465624975 100644 --- a/frappe/public/js/frappe/form/script_helpers.js +++ b/frappe/public/js/frappe/form/script_helpers.js @@ -16,17 +16,19 @@ window.refresh_field = function(n, docname, table_field) { if(typeof n==typeof []) refresh_many(n, docname, table_field); - if (n && typeof n==='string' && table_field){ + if (n && typeof n==='string' && table_field) { var grid = cur_frm.fields_dict[table_field].grid, - field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}); - if (field && field.length){ + field = frappe.utils.filter_dict(grid.docfields, {fieldname: n}), + grid_row = grid.grid_rows_by_docname[docname]; + + if (field && field.length) { field = field[0]; var meta = frappe.meta.get_docfield(field.parent, field.fieldname, docname); $.extend(field, meta); - if (docname){ - cur_frm.fields_dict[table_field].grid.grid_rows_by_docname[docname].refresh_field(n); + if (grid_row) { + grid_row.refresh_field(n); } else { - cur_frm.fields_dict[table_field].grid.refresh(); + grid.refresh(); } } } else if(cur_frm) { diff --git a/frappe/public/js/frappe/form/script_manager.js b/frappe/public/js/frappe/form/script_manager.js index 55152304f4..7ac3673b08 100644 --- a/frappe/public/js/frappe/form/script_manager.js +++ b/frappe/public/js/frappe/form/script_manager.js @@ -171,7 +171,7 @@ frappe.ui.form.ScriptManager = Class.extend({ eval(client_script); } - if(!this.frm.doctype_layout && doctype.__custom_js) { + if (!this.frm.doctype_layout && doctype.__custom_js) { try { eval(doctype.__custom_js); } catch(e) { diff --git a/frappe/public/js/frappe/form/sidebar/attachments.js b/frappe/public/js/frappe/form/sidebar/attachments.js index 042f420939..9e1ea30c6e 100644 --- a/frappe/public/js/frappe/form/sidebar/attachments.js +++ b/frappe/public/js/frappe/form/sidebar/attachments.js @@ -74,7 +74,7 @@ frappe.ui.form.Attachments = Class.extend({ let remove_action = null; if (frappe.model.can_write(this.frm.doctype, this.frm.name)) { - remove_action = function(target_id, wrapper) { + remove_action = function(target_id) { frappe.confirm(__("Are you sure you want to delete the attachment?"), function() { me.remove_attachment(target_id); diff --git a/frappe/public/js/frappe/form/sidebar/form_sidebar.js b/frappe/public/js/frappe/form/sidebar/form_sidebar.js index c3f4336a35..40f9e65bb0 100644 --- a/frappe/public/js/frappe/form/sidebar/form_sidebar.js +++ b/frappe/public/js/frappe/form/sidebar/form_sidebar.js @@ -93,7 +93,7 @@ frappe.ui.form.Sidebar = class { __("{0} edited this {1}", [ frappe.user.full_name(this.frm.doc.modified_by).bold(), "
    " + comment_when(this.frm.doc.modified), - ]) + ], "For example, 'Jon Doe edited this 5 minutes ago'.") ); this.sidebar .find(".created-by") @@ -101,7 +101,7 @@ frappe.ui.form.Sidebar = class { __("{0} created this {1}", [ frappe.user.full_name(this.frm.doc.owner).bold(), "
    " + comment_when(this.frm.doc.creation), - ]) + ], "For example, 'Jon Doe created this 5 minutes ago'.") ); this.refresh_like(); @@ -139,7 +139,7 @@ frappe.ui.form.Sidebar = class { return; } - let tags_parent = this.sidebar.find(".form-tags") + let tags_parent = this.sidebar.find(".form-tags"); this.frm.tags = new frappe.ui.TagEditor({ parent: tags_parent, @@ -200,7 +200,7 @@ frappe.ui.form.Sidebar = class { 'doc_name': this.frm.doc.name, 'following': !is_followed }).then(() => { - frappe.model.set_docinfo(this.frm.doctype, this.frm.doc.name, "is_document_followed", !is_followed) + frappe.model.set_docinfo(this.frm.doctype, this.frm.doc.name, "is_document_followed", !is_followed); this.refresh_follow(!is_followed); }); }); diff --git a/frappe/public/js/frappe/form/success_action.js b/frappe/public/js/frappe/form/success_action.js index 312d83ef0f..91bfec2aae 100644 --- a/frappe/public/js/frappe/form/success_action.js +++ b/frappe/public/js/frappe/form/success_action.js @@ -37,7 +37,7 @@ frappe.ui.form.SuccessAction = class SuccessAction { setting.message; const $buttons = this.get_actions().map(action => { - const $btn = $(``); + const $btn = $(``); $btn.click(() => action.action(this.form)); return $btn; }); diff --git a/frappe/public/js/frappe/form/templates/timeline_message_box.html b/frappe/public/js/frappe/form/templates/timeline_message_box.html index 2fb70a613c..5cd24973c9 100644 --- a/frappe/public/js/frappe/form/templates/timeline_message_box.html +++ b/frappe/public/js/frappe/form/templates/timeline_message_box.html @@ -1,4 +1,4 @@ -
    +
    {% if (doc.comment_type && doc.comment_type == "Comment") { %} @@ -23,7 +23,8 @@ {% if (doc._doc_status && doc._doc_status_indicator) { %} + title="{%= __(doc._doc_status) %}" + style="order: -1"> diff --git a/frappe/public/js/frappe/form/toolbar.js b/frappe/public/js/frappe/form/toolbar.js index af3223cc9d..7e2502e58a 100644 --- a/frappe/public/js/frappe/form/toolbar.js +++ b/frappe/public/js/frappe/form/toolbar.js @@ -222,11 +222,11 @@ frappe.ui.form.Toolbar = class Toolbar { if (!this.frm.is_new() && !this.frm.meta.issingle) { this.page.add_action_icon("left", () => { this.frm.navigate_records(1); - }, 'prev-doc', __("Previous")); + }, 'prev-doc', __("Previous Document")); this.page.add_action_icon("right", ()=> { this.frm.navigate_records(0); - }, 'next-doc', __("Next")); - } + }, 'next-doc', __("Next Document")); + } } make_menu_items() { @@ -234,9 +234,9 @@ frappe.ui.form.Toolbar = class Toolbar { const me = this; const p = this.frm.perm[0]; const docstatus = cint(this.frm.doc.docstatus); - const is_submittable = frappe.model.is_submittable(this.frm.doc.doctype) + const is_submittable = frappe.model.is_submittable(this.frm.doc.doctype); - const print_settings = frappe.model.get_doc(":Print Settings", "Print Settings") + const print_settings = frappe.model.get_doc(":Print Settings", "Print Settings"); const allow_print_for_draft = cint(print_settings.allow_print_for_draft); const allow_print_for_cancelled = cint(print_settings.allow_print_for_cancelled); @@ -249,7 +249,7 @@ frappe.ui.form.Toolbar = class Toolbar { }, true); this.print_icon = this.page.add_action_icon("printer", function() { me.frm.print_doc(); - },'', __("Print")); + }, '', __("Print")); } } @@ -372,7 +372,7 @@ frappe.ui.form.Toolbar = class Toolbar { return this.get_docstatus()===1 && !this.frm.doc.__islocal && this.frm.perm[0].submit - && this.frm.doc.__unsaved + && this.frm.doc.__unsaved; } can_cancel() { return this.get_docstatus()===1 @@ -470,9 +470,22 @@ frappe.ui.form.Toolbar = class Toolbar { me.frm.page.set_view('main'); }, 'edit'); } else if(status === "Cancel") { - this.page.set_secondary_action(__(status), function() { - me.frm.savecancel(this); - }); + let add_cancel_button = () => { + this.page.set_secondary_action(__(status), function() { + me.frm.savecancel(this); + }); + }; + if (this.has_workflow()) { + frappe.xcall('frappe.model.workflow.can_cancel_document', { + 'doctype': this.frm.doc.doctype, + }).then((can_cancel) => { + if (can_cancel) { + add_cancel_button(); + } + }); + } else { + add_cancel_button(); + } } else { var click = { "Save": function() { @@ -553,4 +566,4 @@ frappe.ui.form.Toolbar = class Toolbar { dialog.show(); } -} +}; diff --git a/frappe/public/js/frappe/form/workflow.js b/frappe/public/js/frappe/form/workflow.js index 4c59e8219b..16d9f8676b 100644 --- a/frappe/public/js/frappe/form/workflow.js +++ b/frappe/public/js/frappe/form/workflow.js @@ -85,7 +85,7 @@ frappe.ui.form.States = Class.extend({ frappe.workflow.get_transitions(this.frm.doc).then(transitions => { this.frm.page.clear_actions_menu(); transitions.forEach(d => { - if(frappe.user_roles.includes(d.allowed) && has_approval_access(d)) { + if (frappe.user_roles.includes(d.allowed) && has_approval_access(d)) { added = true; me.frm.page.add_action_item(__(d.action), function() { // set the workflow_action for use in form scripts @@ -103,17 +103,8 @@ frappe.ui.form.States = Class.extend({ }); } }); - if (!added) { - //call function and clear cancel button if Cancel doc state is defined in the workfloe - frappe.xcall('frappe.model.workflow.can_cancel_document', {doc: this.frm.doc}).then((can_cancel) => { - if (!can_cancel) { - this.frm.page.clear_secondary_action(); - } - }); - } else { - this.setup_btn(added); - } + this.setup_btn(added); }); }, diff --git a/frappe/public/js/frappe/list/base_list.js b/frappe/public/js/frappe/list/base_list.js index 96b06aac95..24e14ffc38 100644 --- a/frappe/public/js/frappe/list/base_list.js +++ b/frappe/public/js/frappe/list/base_list.js @@ -154,7 +154,7 @@ frappe.views.BaseList = class BaseList { this.page = this.parent.page; this.$page = $(this.parent); !this.hide_card_layout && this.page.main.addClass('frappe-card'); - this.page.page_form.removeClass("row").addClass("flex justify-between"); + this.page.page_form.removeClass("row").addClass("flex"); this.hide_page_form && this.page.page_form.hide(); this.hide_sidebar && this.$page.addClass('no-list-sidebar'); this.setup_page_head(); @@ -180,11 +180,12 @@ frappe.views.BaseList = class BaseList { 'Gantt': 'gantt', 'Kanban': 'kanban', 'Dashboard': 'dashboard' - } + }; if (frappe.boot.desk_settings.view_switcher) { - this.views_menu = this.page.add_custom_button_group(__(`{0} View`, [this.view_name]), icon_map[this.view_name] || 'list'); - this.views_list = new frappe.views.Views({ + this.views_menu = this.page.add_custom_button_group(__('{0} View', [this.view_name]), + icon_map[this.view_name] || 'list'); + this.views_list = new frappe.views.ListViewSelect({ doctype: this.doctype, parent: this.views_menu, page: this.page, @@ -209,13 +210,13 @@ frappe.views.BaseList = class BaseList { } } else { this.refresh_button = this.page.add_action_icon("refresh", () => { - this.refresh() + this.refresh(); }); } } set_menu_items() { - this.set_default_secondary_action() + this.set_default_secondary_action(); this.menu_items && this.menu_items.map((item) => { if (item.condition && item.condition() === false) { @@ -819,6 +820,7 @@ frappe.views.view_modes = [ "Calendar", "Image", "Inbox", + "Tree", ]; frappe.views.is_valid = (view_mode) => frappe.views.view_modes.includes(view_mode); diff --git a/frappe/public/js/frappe/list/list_filter.js b/frappe/public/js/frappe/list/list_filter.js index c02755d50c..4c5a1da319 100644 --- a/frappe/public/js/frappe/list/list_filter.js +++ b/frappe/public/js/frappe/list/list_filter.js @@ -75,7 +75,7 @@ export default class ListFilter { } bind_toggle_saved_filters() { - this.wrapper.find('.saved-filters-preview').click((e) => { + this.wrapper.find('.saved-filters-preview').click(() => { this.toggle_saved_filters(this.saved_filters_hidden); }); } diff --git a/frappe/public/js/frappe/list/list_settings.js b/frappe/public/js/frappe/list/list_settings.js index f2045c9c34..4877d6fbb7 100644 --- a/frappe/public/js/frappe/list/list_settings.js +++ b/frappe/public/js/frappe/list/list_settings.js @@ -1,7 +1,7 @@ export default class ListSettings { constructor({ listview, doctype, meta, settings }) { if (!doctype) { - frappe.throw(__('Doctype required')); + frappe.throw('DocType required'); } this.listview = listview; diff --git a/frappe/public/js/frappe/list/list_sidebar.js b/frappe/public/js/frappe/list/list_sidebar.js index 68a462e53e..5b92119807 100644 --- a/frappe/public/js/frappe/list/list_sidebar.js +++ b/frappe/public/js/frappe/list/list_sidebar.js @@ -39,13 +39,105 @@ frappe.views.ListSidebar = class ListSidebar { } - setup_list_group_by() { - this.list_group_by = new frappe.views.ListGroupBy({ - doctype: this.doctype, - sidebar: this, - list_view: this.list_view, - page: this.page - }); + setup_views() { + var show_list_link = false; + + if (frappe.views.calendar[this.doctype]) { + this.sidebar.find('.list-link[data-view="Calendar"]').removeClass("hide"); + this.sidebar.find('.list-link[data-view="Gantt"]').removeClass('hide'); + show_list_link = true; + } + //show link for kanban view + this.sidebar.find('.list-link[data-view="Kanban"]').removeClass('hide'); + if (this.doctype === "Communication" && frappe.boot.email_accounts.length) { + this.sidebar.find('.list-link[data-view="Inbox"]').removeClass('hide'); + show_list_link = true; + } + + if (frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree) { + this.sidebar.find(".tree-link").removeClass("hide"); + } + + this.current_view = 'List'; + var route = frappe.get_route(); + if (route.length > 2 && frappe.views.view_modes.includes(route[2])) { + this.current_view = route[2]; + + if (this.current_view === 'Kanban') { + this.kanban_board = route[3]; + } else if (this.current_view === 'Inbox') { + this.email_account = route[3]; + } + } + + // disable link for current view + this.sidebar.find('.list-link[data-view="' + this.current_view + '"] a') + .attr('disabled', 'disabled').addClass('disabled'); + + //enable link for Kanban view + this.sidebar.find('.list-link[data-view="Kanban"] a, .list-link[data-view="Inbox"] a') + .attr('disabled', null).removeClass('disabled'); + + // show image link if image_view + if (this.list_view.meta.image_field) { + this.sidebar.find('.list-link[data-view="Image"]').removeClass('hide'); + show_list_link = true; + } + + if (this.list_view.settings.get_coords_method || + (this.list_view.meta.fields.find(i => i.fieldname === "latitude") && + this.list_view.meta.fields.find(i => i.fieldname === "longitude")) || + (this.list_view.meta.fields.find(i => i.fieldname === 'location' && i.fieldtype == 'Geolocation'))) { + this.sidebar.find('.list-link[data-view="Map"]').removeClass('hide'); + show_list_link = true; + } + + if (show_list_link) { + this.sidebar.find('.list-link[data-view="List"]').removeClass('hide'); + } + } + + setup_reports() { + // add reports linked to this doctype to the dropdown + var me = this; + var added = []; + var dropdown = this.page.sidebar.find('.reports-dropdown'); + var divider = false; + + var add_reports = function(reports) { + $.each(reports, function(name, r) { + if (!r.ref_doctype || r.ref_doctype == me.doctype) { + var report_type = r.report_type === 'Report Builder' ? + `List/${r.ref_doctype}/Report` : 'query-report'; + + var route = r.route || report_type + '/' + (r.title || r.name); + + if (added.indexOf(route) === -1) { + // don't repeat + added.push(route); + + if (!divider) { + me.get_divider().appendTo(dropdown); + divider = true; + } + + $('
  • ' + + __(r.title || r.name) + '
  • ').appendTo(dropdown); + } + } + }); + }; + + // from reference doctype + if (this.list_view.settings.reports) { + add_reports(this.list_view.settings.reports); + } + + // Sort reports alphabetically + var reports = Object.values(frappe.boot.user.all_reports).sort((a,b) => a.title.localeCompare(b.title)) || []; + + // from specially tagged reports + add_reports(reports); } setup_list_filter() { @@ -56,6 +148,29 @@ frappe.views.ListSidebar = class ListSidebar { }); } + setup_kanban_boards() { + const $dropdown = this.page.sidebar.find('.kanban-dropdown'); + frappe.views.KanbanView.setup_dropdown_in_sidebar(this.doctype, $dropdown); + } + + + setup_keyboard_shortcuts() { + this.sidebar.find('.list-link > a, .list-link > .btn-group > a').each((i, el) => { + frappe.ui.keys + .get_shortcut_group(this.page) + .add($(el)); + }); + } + + setup_list_group_by() { + this.list_group_by = new frappe.views.ListGroupBy({ + doctype: this.doctype, + sidebar: this, + list_view: this.list_view, + page: this.page + }); + } + get_stats() { var me = this; frappe.call({ diff --git a/frappe/public/js/frappe/list/list_sidebar_group_by.js b/frappe/public/js/frappe/list/list_sidebar_group_by.js index 1ac295d7fe..df3dea35d1 100644 --- a/frappe/public/js/frappe/list/list_sidebar_group_by.js +++ b/frappe/public/js/frappe/list/list_sidebar_group_by.js @@ -24,7 +24,7 @@ frappe.views.ListGroupBy = class ListGroupBy { fields: this.get_group_by_dropdown_fields(), }); - d.set_primary_action('Save', ({ group_by_fields }) => { + d.set_primary_action(__("Save"), ({ group_by_fields }) => { frappe.model.user_settings.save( this.doctype, 'group_by_fields', @@ -38,11 +38,13 @@ frappe.views.ListGroupBy = class ListGroupBy { d.hide(); }); - d.$body.prepend(``); + d.$body.prepend(` + + `); this.page.sidebar.find('.add-list-group-by a').on('click', () => { frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area'); @@ -99,7 +101,7 @@ frappe.views.ListGroupBy = class ListGroupBy { setup_dropdown() { this.$wrapper.find('.group-by-field').on('show.bs.dropdown', (e) => { let $dropdown = $(e.currentTarget).find('.group-by-dropdown'); - this.set_loading_state($dropdown) + this.set_loading_state($dropdown); let fieldname = $(e.currentTarget).find('a') .attr('data-fieldname'); let fieldtype = $(e.currentTarget) @@ -203,9 +205,11 @@ frappe.views.ListGroupBy = class ListGroupBy { render_dropdown_items(fields, fieldtype, $dropdown, applied_filter) { let standard_html = ` `; let applied_filter_html=''; @@ -241,7 +245,7 @@ frappe.views.ListGroupBy = class ListGroupBy { ${field.count} `; - }; + } setup_filter_by() { this.$wrapper.on('click', '.group-by-item', (e) => { diff --git a/frappe/public/js/frappe/list/list_view.js b/frappe/public/js/frappe/list/list_view.js index 8483bf2ee0..6e8bd63676 100644 --- a/frappe/public/js/frappe/list/list_view.js +++ b/frappe/public/js/frappe/list/list_view.js @@ -167,9 +167,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { `` - ) - .click(() => this.show_restrictions(match_rules_list)) - .appendTo(this.page.page_form); + ).click(() => this.show_restrictions(match_rules_list)).appendTo(this.page.page_form); } } @@ -504,7 +502,10 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } } - render_header() { + render_header(refresh_header=false) { + if (refresh_header) { + this.$result.find('.list-row-head').remove(); + } if (this.$result.find(".list-row-head").length === 0) { // append header once this.$result.prepend(this.get_header_html()); @@ -692,7 +693,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (df.fieldtype === "Code") { return value; } else if (df.fieldtype === "Percent") { - return `
    + return `
    @@ -761,7 +762,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } return ` + title="${__(label)}: ${frappe.utils.escape_html(_value)}"> ${html} `; }; @@ -1111,7 +1112,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { } // link, let the event be handled via set_route - if ($target.is("a")) { return; } + if ($target.is("a")) return; // clicked on the row, open form const $row = $(e.currentTarget); @@ -1485,7 +1486,7 @@ frappe.views.ListView = class ListView extends frappe.views.BaseList { if (frappe.user.has_role("System Manager")) { items.push({ - label: __("Settings"), + label: __("List Settings"), action: () => this.show_list_settings(), standard: true, }); diff --git a/frappe/public/js/frappe/list/list_view_select.js b/frappe/public/js/frappe/list/list_view_select.js new file mode 100644 index 0000000000..646efcfbfe --- /dev/null +++ b/frappe/public/js/frappe/list/list_view_select.js @@ -0,0 +1,329 @@ +frappe.provide("frappe.views"); + +frappe.views.ListViewSelect = class ListViewSelect { + constructor(opts) { + $.extend(this, opts); + this.set_current_view(); + this.setup_views(); + } + + add_view_to_menu(view, action) { + let $el = this.page.add_custom_menu_item( + this.parent, + view, + action, + true, + null, + this.icon_map[view] || "list" + ); + $el.parent().attr("data-view", view); + } + + set_current_view() { + this.current_view = "List"; + const route = frappe.get_route(); + const view_name = frappe.utils.to_title_case(route[2] || ""); + if (route.length > 2 && frappe.views.view_modes.includes(view_name)) { + this.current_view = view_name; + + if (this.current_view === "Kanban") { + this.kanban_board = route[3]; + } else if (this.current_view === "Inbox") { + this.email_account = route[3]; + } + } + } + + set_route(view, calendar_name) { + const route = [this.slug(), "view", view]; + if (calendar_name) route.push(calendar_name); + frappe.set_route(route); + } + + setup_views() { + const views = { + List: { + condition: true, + action: () => this.set_route("list") + }, + Report: { + condition: true, + action: () => this.set_route("report"), + current_view_handler: () => { + const reports = this.get_reports(); + this.setup_dropdown_in_sidebar("Report", reports, { + label: __("Report Builder"), + action: () => this.set_route("report") + }); + } + }, + Dashboard: { + condition: true, + action: () => this.set_route("dashboard") + }, + Calendar: { + condition: frappe.views.calendar[this.doctype], + action: () => this.set_route("calendar", "default"), + current_view_handler: () => { + this.get_calendars().then(calendars => { + this.setup_dropdown_in_sidebar("Calendar", calendars); + }); + } + }, + Gantt: { + condition: frappe.views.calendar[this.doctype], + action: () => this.set_route("gantt") + }, + Inbox: { + condition: + this.doctype === "Communication" && + frappe.boot.email_accounts.length, + action: () => this.set_route("inbox"), + current_view_handler: () => { + const accounts = this.get_email_accounts(); + let default_action; + if ( + has_common(frappe.user_roles, [ + "System Manager", + "Administrator" + ]) + ) { + default_action = { + label: __("New Email Account"), + action: () => frappe.new_doc("Email Account") + }; + } + this.setup_dropdown_in_sidebar( + "Inbox", + accounts, + default_action + ); + } + }, + Image: { + condition: this.list_view.meta.image_field, + action: () => this.set_route("image") + }, + Tree: { + condition: + frappe.treeview_settings[this.doctype] || + frappe.get_meta(this.doctype).is_tree, + action: () => this.set_route("tree") + }, + Kanban: { + condition: true, + action: () => this.setup_kanban_boards(), + current_view_handler: () => { + frappe.views.KanbanView.get_kanbans(this.doctype).then( + kanbans => this.setup_kanban_switcher(kanbans) + ); + } + } + }; + + frappe.views.view_modes.forEach(view => { + if (this.current_view !== view && views[view].condition) { + this.add_view_to_menu(view, views[view].action); + } + + if (this.current_view == view) { + views[view].current_view_handler && + views[view].current_view_handler(); + } + }); + } + + setup_dropdown_in_sidebar(view, items, default_action) { + if (!this.sidebar) return; + const views_wrapper = this.sidebar.sidebar.find(".views-section"); + views_wrapper.find(".sidebar-label").html(`${__(view)}`); + const $dropdown = views_wrapper.find(".views-dropdown"); + + let placeholder = `Select ${view}`; + let html = ``; + + if (!items || !items.length) { + html = `
    + ${__("No {} Found", [view])} +
    `; + } else { + items.map(item => { + if (item.name == this.get_page_name()) { + placeholder = item.name; + } + html += `
  • ${ + item.name + }
  • `; + }); + } + + views_wrapper.find(".selected-view").html(placeholder); + + if (default_action) { + views_wrapper.find(".sidebar-action a").html(default_action.label); + views_wrapper + .find(".sidebar-action a") + .click(() => default_action.action()); + } + + $dropdown.html(html); + + views_wrapper.removeClass("hide"); + } + + setup_kanban_switcher(kanbans) { + const kanban_switcher = this.page.add_custom_button_group( + __("Select Kanban"), + null, + this.list_view.$filter_section + ); + + kanbans.map(k => { + this.page.add_custom_menu_item( + kanban_switcher, + k.name, + () => this.set_route("kanban", k.name), + false + ); + }); + + this.page.add_custom_menu_item( + kanban_switcher, + __("Create New Kanban Board"), + () => frappe.views.KanbanView.show_kanban_dialog(this.doctype), + true + ); + } + + get_page_name() { + return frappe.utils.to_title_case( + frappe.get_route().slice(-1)[0] || "" + ); + } + + get_reports() { + // add reports linked to this doctype to the dropdown + let added = []; + let reports_to_add = []; + + let add_reports = reports => { + reports.map(r => { + if (!r.ref_doctype || r.ref_doctype == this.doctype) { + const report_type = + r.report_type === "Report Builder" + ? `/app/list/${r.ref_doctype}/report` + : "query-report"; + + const route = + r.route || report_type + "/" + (r.title || r.name); + + if (added.indexOf(route) === -1) { + // don't repeat + added.push(route); + reports_to_add.push({ + name: r.title || r.name, + route: route + }); + } + } + }); + }; + + // from reference doctype + if (this.list_view.settings.reports) { + add_reports(this.list_view.settings.reports); + } + + // Sort reports alphabetically + var reports = + Object.values(frappe.boot.user.all_reports).sort((a, b) => + a.title.localeCompare(b.title) + ) || []; + + // from specially tagged reports + add_reports(reports); + + return reports_to_add; + } + + setup_kanban_boards() { + const last_opened_kanban = + frappe.model.user_settings[this.doctype]["Kanban"] && + frappe.model.user_settings[this.doctype]["Kanban"] + .last_kanban_board; + if (last_opened_kanban) { + frappe.set_route( + "list", + this.doctype, + "kanban", + last_opened_kanban + ); + } else { + frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); + } + } + + get_calendars() { + const doctype = this.doctype; + let calendars = []; + + return frappe.db + .get_list("Calendar View", { + filters: { + reference_doctype: doctype + } + }) + .then(result => { + if (!(result && Array.isArray(result) && result.length)) return; + + if (frappe.views.calendar[this.doctype]) { + // has standard calendar view + calendars.push({ + name: "Default", + route: `/app/${this.slug()}/view/calendar/default` + }); + } + result.map(calendar => { + calendars.push({ + name: calendar.name, + route: `/app/${this.slug()}/view/calendar/${ + calendar.name + }` + }); + }); + + return calendars; + }); + } + + get_email_accounts() { + let accounts_to_add = []; + let accounts = frappe.boot.email_accounts; + accounts.forEach(account => { + let email_account = + account.email_id == "All Accounts" + ? "All Accounts" + : account.email_account; + let route = `/app/communication/inbox/${email_account}`; + let display_name = [ + "All Accounts", + "Sent Mail", + "Spam", + "Trash" + ].includes(account.email_id) + ? __(account.email_id) + : account.email_account; + + accounts_to_add.push({ + name: display_name, + route: route + }); + }); + + return accounts_to_add; + } + + slug() { + return frappe.router.slug(frappe.router.doctype_layout || this.doctype); + } +}; diff --git a/frappe/public/js/frappe/list/views.js b/frappe/public/js/frappe/list/views.js deleted file mode 100644 index 7eada35d42..0000000000 --- a/frappe/public/js/frappe/list/views.js +++ /dev/null @@ -1,269 +0,0 @@ -frappe.provide('frappe.views'); - -frappe.views.Views = class Views { - constructor(opts) { - $.extend(this, opts); - this.set_current_view(); - this.setup_views(); - } - - add_view_to_menu(view, action) { - let $el = this.page.add_custom_menu_item( - this.parent, - view, - action, - true, - null, - this.icon_map[view] || 'list' - ); - $el.parent().attr('data-view', view) - } - - set_current_view() { - this.current_view = 'List'; - const route = frappe.get_route(); - const view_name = frappe.utils.to_title_case(route[2] || ''); - if (route.length > 2 && frappe.views.view_modes.includes(view_name)) { - this.current_view = view_name; - - if (this.current_view === 'Kanban') { - this.kanban_board = route[3]; - } else if (this.current_view === 'Inbox') { - this.email_account = route[3]; - } - } - } - - set_route(view, calendar_name) { - const route = [this.get_doctype_route(), 'view', view]; - if (calendar_name) route.push(calendar_name); - frappe.set_route(route); - } - - setup_views() { - const views = { - 'List': { - condition: true, - action: () => this.set_route('list') - }, - 'Report': { - condition: true, - action: () => this.set_route('report'), - current_view_handler: () => { - const reports = this.get_reports(); - this.setup_dropdown_in_sidebar( - 'Report', - reports, - { - label: __('Report Builder'), - action: () => this.set_route('report') - } - ); - } - }, - 'Dashboard': { - condition: true, - action: () => this.set_route('dashboard') - }, - 'Calendar': { - condition: frappe.views.calendar[this.doctype], - action: () => this.set_route('calendar', 'default'), - current_view_handler: () => { - this.get_calendars().then(calendars => { - this.setup_dropdown_in_sidebar( - 'Calendar', - calendars, - ); - }); - } - }, - 'Gantt': { - condition: frappe.views.calendar[this.doctype], - action: () => this.set_route('gantt') - }, - 'Inbox': { - condition: this.doctype === "Communication" && frappe.boot.email_accounts.length, - action: () => this.set_route('inbox'), - current_view_handler: () => { - const accounts = this.get_email_accounts(); - let default_action; - if (has_common(frappe.user_roles, ["System Manager", "Administrator"])) { - default_action = { - label: __('New Email Account'), - action: () => frappe.new_doc("Email Account") - } - } - this.setup_dropdown_in_sidebar( - 'Inbox', - accounts, - default_action, - ); - } - }, - 'Image': { - condition: this.list_view.meta.image_field, - action: () => this.set_route('image') - }, - 'Tree': { - condition: frappe.treeview_settings[this.doctype] || frappe.get_meta(this.doctype).is_tree, - action: () => this.set_route('tree') - }, - 'Kanban': { - condition: true, - action: () => this.setup_kanban_boards(), - current_view_handler: () => { - frappe.views.KanbanView.get_kanbans(this.doctype).then((kanbans) => { - this.setup_dropdown_in_sidebar( - 'Kanban', - kanbans, - { - label: __('New Kanban Board'), - action: () => frappe.views.KanbanView.show_kanban_dialog(this.doctype) - } - ); - }); - } - }, - } - - frappe.views.view_modes.forEach(view => { - if (this.current_view !== view && views[view].condition) { - this.add_view_to_menu(view, views[view].action); - } - - if (this.current_view == view) { - views[view].current_view_handler && views[view].current_view_handler(); - } - }); - } - - - setup_dropdown_in_sidebar(view, items, default_action) { - if (!this.sidebar) return; - const views_wrapper = this.sidebar.sidebar.find('.views-section'); - views_wrapper.find('.sidebar-label').html(`${__(view)}`); - const $dropdown = views_wrapper.find('.views-dropdown'); - - let placeholder = `Select ${view}`; - let html = ``; - - if (!items || !items.length) { - html = `
    - ${__('No {} Found', [view])} -
    `; - } else { - items.map(item => { - if (item.name == frappe.utils.to_title_case(frappe.get_route().slice(-1)[0] || '')) { - placeholder = item.name; - } - html += `
  • ${item.name}
  • `; - }); - } - - views_wrapper.find('.selected-view').html(placeholder); - - if (default_action) { - views_wrapper.find('.sidebar-action a').html(default_action.label); - views_wrapper.find('.sidebar-action a').click(() => default_action.action()); - } - - $dropdown.html(html); - - views_wrapper.removeClass('hide'); - } - - get_reports() { - // add reports linked to this doctype to the dropdown - let added = []; - let reports_to_add = []; - - let add_reports = (reports) => { - reports.map((r) => { - if (!r.ref_doctype || r.ref_doctype == this.doctype) { - const report_type = r.report_type === 'Report Builder' ? - `/app/list/${r.ref_doctype}/report` : 'query-report'; - - const route = r.route || report_type + '/' + (r.title || r.name); - - if (added.indexOf(route) === -1) { - // don't repeat - added.push(route); - reports_to_add.push({name: r.title || r.name, route: route}); - } - } - }); - }; - - // from reference doctype - if (this.list_view.settings.reports) { - add_reports(this.list_view.settings.reports); - } - - // Sort reports alphabetically - var reports = Object.values(frappe.boot.user.all_reports).sort((a,b) => a.title.localeCompare(b.title)) || []; - - // from specially tagged reports - add_reports(reports); - - return reports_to_add; - } - - setup_kanban_boards() { - const last_opened_kanban = frappe.model.user_settings[this.doctype]['Kanban'] - && frappe.model.user_settings[this.doctype]['Kanban'].last_kanban_board; - if (last_opened_kanban) { - frappe.set_route('list', this.doctype, 'kanban', last_opened_kanban); - } else { - frappe.views.KanbanView.show_kanban_dialog(this.doctype, true); - } - } - - get_calendars() { - const doctype = this.doctype; - let calendars = []; - - return frappe.db.get_list('Calendar View', { - filters: { - reference_doctype: doctype - } - }).then(result => { - if (!(result && Array.isArray(result) && result.length)) return; - - if (frappe.views.calendar[this.doctype]) { - // has standard calendar view - calendars.push({ - name: 'Default', - route: `/app/${this.get_doctype_route()}/view/calendar/default` - }); - } - result.map(calendar => { - calendars.push({name: calendar.name, route: `/app/${this.get_doctype_route()}/view/calendar/${calendar.name}`}); - }); - - return calendars; - }); - } - - get_email_accounts() { - let accounts_to_add = []; - let accounts = frappe.boot.email_accounts; - accounts.forEach(account => { - let email_account = (account.email_id == "All Accounts") ? "All Accounts" : account.email_account; - let route = `/app/communication/inbox/${email_account}`; - let display_name = ["All Accounts", "Sent Mail", "Spam", "Trash"].includes(account.email_id) - ? __(account.email_id) - : account.email_account; - - accounts_to_add.push({ - name: display_name, - route: route - }); - }); - - return accounts_to_add; - } - - get_doctype_route() { - return frappe.router.slug(frappe.router.doctype_layout || this.doctype); - } -} \ No newline at end of file diff --git a/frappe/public/js/frappe/microtemplate.js b/frappe/public/js/frappe/microtemplate.js index d233a47893..7b45db952e 100644 --- a/frappe/public/js/frappe/microtemplate.js +++ b/frappe/public/js/frappe/microtemplate.js @@ -89,11 +89,19 @@ frappe.render_template = function(name, data) { } frappe.render_grid = function(opts) { // build context - if(opts.grid) { + if (opts.grid) { opts.columns = opts.grid.getColumns(); opts.data = opts.grid.getData().getItems(); } + if ( + opts.print_settings && + opts.print_settings.orientation && + opts.print_settings.orientation.toLowerCase() === "landscape" + ) { + opts.landscape = true; + } + // show landscape view if columns more than 10 if (opts.landscape == null) { if(opts.columns && opts.columns.length > 10) { diff --git a/frappe/public/js/frappe/model/create_new.js b/frappe/public/js/frappe/model/create_new.js index 210a4ae5d3..dc6ee56fca 100644 --- a/frappe/public/js/frappe/model/create_new.js +++ b/frappe/public/js/frappe/model/create_new.js @@ -7,7 +7,12 @@ $.extend(frappe.model, { new_names: {}, new_name_count: {}, - get_new_doc: function(doctype, parent_doc, parentfield, with_mandatory_children) { + get_new_doc: function( + doctype, + parent_doc, + parentfield, + with_mandatory_children + ) { frappe.provide("locals." + doctype); var doc = { docstatus: 0, @@ -19,13 +24,13 @@ $.extend(frappe.model, { }; frappe.model.set_default_values(doc, parent_doc); - if(parent_doc) { + if (parent_doc) { $.extend(doc, { parent: parent_doc.name, parentfield: parentfield, - parenttype: parent_doc.doctype, + parenttype: parent_doc.doctype }); - if(!parent_doc[parentfield]) parent_doc[parentfield] = []; + if (!parent_doc[parentfield]) parent_doc[parentfield] = []; doc.idx = parent_doc[parentfield].length + 1; parent_doc[parentfield].push(doc); } else { @@ -34,7 +39,7 @@ $.extend(frappe.model, { frappe.model.add_to_locals(doc); - if(with_mandatory_children) { + if (with_mandatory_children) { frappe.model.create_mandatory_children(doc); } @@ -43,26 +48,31 @@ $.extend(frappe.model, { } // set the name if called from a link field - if(frappe.route_options && frappe.route_options.name_field) { - + if (frappe.route_options && frappe.route_options.name_field) { var meta = frappe.get_meta(doctype); // set title field / name as name - if(meta.autoname && meta.autoname.indexOf("field:")!==-1) { + if (meta.autoname && meta.autoname.indexOf("field:") !== -1) { doc[meta.autoname.substr(6)] = frappe.route_options.name_field; - } else if(meta.title_field) { + } else if (meta.title_field) { doc[meta.title_field] = frappe.route_options.name_field; } - delete frappe.route_options.name_field; } // set route options - if(frappe.route_options && !doc.parent) { + if (frappe.route_options && !doc.parent) { $.each(frappe.route_options, function(fieldname, value) { var df = frappe.meta.has_field(doctype, fieldname); - if(df && in_list(['Link', 'Data', 'Select', 'Dynamic Link'], df.fieldtype) && !df.no_copy) { - doc[fieldname]=value; + if ( + df && + in_list( + ["Link", "Data", "Select", "Dynamic Link"], + df.fieldtype + ) && + !df.no_copy + ) { + doc[fieldname] = value; } }); frappe.route_options = null; @@ -72,13 +82,17 @@ $.extend(frappe.model, { }, make_new_doc_and_get_name: function(doctype, with_mandatory_children) { - return frappe.model.get_new_doc(doctype, null, null, with_mandatory_children).name; + return frappe.model.get_new_doc( + doctype, + null, + null, + with_mandatory_children + ).name; }, get_new_name: function(doctype) { - var cnt = frappe.model.new_name_count - if(!cnt[doctype]) - cnt[doctype] = 0; + var cnt = frappe.model.new_name_count; + if (!cnt[doctype]) cnt[doctype] = 0; cnt[doctype]++; return frappe.router.slug(`new-${doctype}-${cnt[doctype]}`); }, @@ -87,21 +101,27 @@ $.extend(frappe.model, { var doctype = doc.doctype; var docfields = frappe.meta.get_docfields(doctype); var updated = []; - for(var fid=0;fid 0) { - var ref_fieldname = df["default"].slice(1).toLowerCase().replace(" ", "_"); - var ref_value = parent_doc ? - parent_doc[ref_fieldname] : - frappe.defaults.get_user_default(ref_fieldname); - var ref_doc = ref_value ? frappe.get_doc(df["default"], ref_value) : null; + if (frappe.get_list(df["default"]).length > 0) { + var ref_fieldname = df["default"] + .slice(1) + .toLowerCase() + .replace(" ", "_"); + var ref_value = parent_doc + ? parent_doc[ref_fieldname] + : frappe.defaults.get_user_default(ref_fieldname); + var ref_doc = ref_value + ? frappe.get_doc(df["default"], ref_value) + : null; - if(ref_doc && ref_doc[df.fieldname]) { + if (ref_doc && ref_doc[df.fieldname]) { return ref_doc[df.fieldname]; } } @@ -232,9 +273,10 @@ $.extend(frappe.model, { add_child: function(parent_doc, doctype, parentfield, idx) { // if given doc, fieldname only - if(arguments.length===2) { + if (arguments.length === 2) { parentfield = doctype; - doctype = frappe.meta.get_field(parent_doc.doctype, parentfield).options; + doctype = frappe.meta.get_field(parent_doc.doctype, parentfield) + .options; } // create row doc @@ -244,9 +286,11 @@ $.extend(frappe.model, { child.idx = idx; // renum for fraction - if(idx !== cint(idx)) { - var sorted = parent_doc[parentfield].sort(function(a, b) { return a.idx - b.idx; }); - for(var i=0, j=sorted.length; i { - if(opts && $.isPlainObject(opts)) { + if (opts && $.isPlainObject(opts)) { frappe.route_options = opts; } frappe.model.with_doctype(doctype, function() { - if(frappe.create_routes[doctype]) { - frappe.set_route(frappe.create_routes[doctype]) + if (frappe.create_routes[doctype]) { + frappe + .set_route(frappe.create_routes[doctype]) .then(() => resolve()); } else { - frappe.ui.form.make_quick_entry(doctype, null, init_callback) + frappe.ui.form + .make_quick_entry(doctype, null, init_callback) .then(() => resolve()); } }); - }); -} +}; diff --git a/frappe/public/js/frappe/model/indicator.js b/frappe/public/js/frappe/model/indicator.js index 94d9241d67..575ab35b29 100644 --- a/frappe/public/js/frappe/model/indicator.js +++ b/frappe/public/js/frappe/model/indicator.js @@ -45,7 +45,7 @@ frappe.get_indicator = function(doc, doctype) { "Info": "light-blue", }[locals["Workflow State"][value].style]; } - if(!colour) colour = "gray"; + if (!colour) colour = "gray"; return [__(value), colour, workflow_fieldname + ',=,' + value]; } diff --git a/frappe/public/js/frappe/model/model.js b/frappe/public/js/frappe/model/model.js index 630081fd80..a2e872085e 100644 --- a/frappe/public/js/frappe/model/model.js +++ b/frappe/public/js/frappe/model/model.js @@ -135,8 +135,8 @@ $.extend(frappe.model, { let cached_timestamp = null; let cached_doc = null; - let cached_docs = frappe.model.get_from_localstorage(doctype) - + let cached_docs = frappe.model.get_from_localstorage(doctype); + if (cached_docs) { cached_doc = cached_docs.filter(doc => doc.name === doctype)[0]; if(cached_doc) { @@ -252,6 +252,10 @@ $.extend(frappe.model, { return frappe.boot.user.can_create.indexOf(doctype)!==-1; }, + can_select: function(doctype) { + return frappe.boot.user.can_select.indexOf(doctype)!==-1; + }, + can_read: function(doctype) { return frappe.boot.user.can_read.indexOf(doctype)!==-1; }, diff --git a/frappe/public/js/frappe/module_editor.js b/frappe/public/js/frappe/module_editor.js new file mode 100644 index 0000000000..35037a3e62 --- /dev/null +++ b/frappe/public/js/frappe/module_editor.js @@ -0,0 +1,39 @@ +frappe.ModuleEditor = Class.extend({ + init: function(frm, wrapper) { + this.wrapper = $('
    ').appendTo(wrapper); + this.frm = frm; + this.make(); + }, + make: function() { + var me = this; + this.frm.doc.__onload.all_modules.forEach(function(m) { + $(repl('
    \ +
    ', {module: m})).appendTo(me.wrapper); + }); + this.bind(); + }, + refresh: function() { + var me = this; + this.wrapper.find(".block-module-check").prop("checked", true); + $.each(this.frm.doc.block_modules, function(i, d) { + me.wrapper.find(".block-module-check[data-module='"+ d.module +"']").prop("checked", false); + }); + }, + bind: function() { + var me = this; + this.wrapper.on("change", ".block-module-check", function() { + var module = $(this).attr('data-module'); + if ($(this).prop("checked")) { + // remove from block_modules + me.frm.doc.block_modules = $.map(me.frm.doc.block_modules || [], function(d) { + if (d.module != module) { + return d; + } + }); + } else { + me.frm.add_child("block_modules", {"module": module}); + } + }); + } +}); \ No newline at end of file diff --git a/frappe/public/js/frappe/recorder/RecorderDetail.vue b/frappe/public/js/frappe/recorder/RecorderDetail.vue index 53b3c8720b..57e63a0233 100644 --- a/frappe/public/js/frappe/recorder/RecorderDetail.vue +++ b/frappe/public/js/frappe/recorder/RecorderDetail.vue @@ -1,11 +1,6 @@